vue3文件上传弹窗,图片pdf,word,结合预览kkview

index.ts

数据

TypeScript 复制代码
// 获取简化的fileType,根据文件类型和后缀名判断文件类型
export const getFileType = (fileName) => {
  const extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
  if (['png', 'jpg', 'jpeg'].includes(extension)) {
    return 'img';
  }
  if (['doc', 'docx'].includes(extension)) {
    return 'word';
  }
  if (['xls', 'xlsx'].includes(extension)) {
    return 'excel';
  }
  if (['pdf'].includes(extension)) {
    return 'pdf';
  }
  return 'other';
};
// 获取fileType对应的后端数值
export const getFileTypeNum = (fileType) => {
  if (fileType === 'img') {
    return 1;
  }
  return 4;
};

上传文件的弹窗界面FileModal.vue

javascript 复制代码
<template>
  <!--公共业务附件组件,只支持图片和文档,如有需要音视频文件,后续再扩展 -->
  <BasicModal
    :title="title"
    :width="'50vw'"
    :height="400"
    :can-fullscreen="true"
    :keyboard="false"
    v-bind="$attrs"
    :maskClosable="false"
    :show-ok-btn="!props.readOnly"
    okText="确定"
    cancelText="取消"
    @register="registerModal"
    @ok="handleOk"
    @cancel="handleCancel"
  >
    <div class="h-full" v-loading="state.loading">
      <div class="w-full" v-if="props.fileTypeBtnList && props.fileTypeBtnList.length > 0">
        <a-radio-group v-model:value="state.fileTypeBtn" button-style="solid" @change="handleChange">
          <a-radio-button :value="0" v-if="props.fileTypeBtnList.includes(0)">全部</a-radio-button>
          <a-radio-button :value="1" v-if="props.fileTypeBtnList.includes(1)">图片</a-radio-button>
          <a-radio-button :value="4" v-if="props.fileTypeBtnList.includes(4)">文档</a-radio-button>
        </a-radio-group>
      </div>
      <div class="files mt-10px">
        <div class="file-box" v-for="(i, index) in currentFileList" :key="index">
          <img :src="i.filePath" alt="" v-if="i.fileType === 'img'" />
          <img src="/@/assets/business/img/word.png" alt="" v-else-if="i.fileType === 'word'" />
          <img src="/@/assets/business/img/excel.png" alt="" v-else-if="i.fileType === 'excel'" />
          <img src="/@/assets/business/img/pdf.png" v-else-if="i.fileType === 'pdf'" />
          <a-tooltip placement="bottom">
            <template #title>
              <span>{{ i.fileName }}</span>
            </template>
            <div class="mask-box">
              <ZoomInOutlined :style="{ fontSize: '18px', color: '#fff' }" @click="previewFile(i)" />
              <VerticalAlignBottomOutlined :style="{ fontSize: '18px', color: '#fff' }" v-if="!props.readOnly" @click="downloadFile(i)" />
              <DeleteOutlined :style="{ fontSize: '18px', color: '#fff' }" v-if="!props.readOnly" @click="deleteFile(i)" />
            </div>
          </a-tooltip>
        </div>
        <a-upload action="" v-if="!props.readOnly" :accept="accept" :multiple="props.multiple" :showUploadList="false" :before-upload="beforeUpload">
          <div class="file-box add-icon"><PlusOutlined :style="{ fontSize: '36px' }" /></div>
        </a-upload>
      </div>
    </div>
  </BasicModal>
  <PreviewModal @register="registerPreviewModal" />
</template>

<script setup>
  import { ref, reactive, computed } from 'vue';
  import { BasicModal, useModalInner, useModal } from '/@/components/Modal';
  import PreviewModal from './PreviewModal.vue';
  import { uploadJcwFileAndSaveRec, uploadJcwFileAndSaveRecBatch, jcwFilesList, deleteJcwFiles } from '/@/api/common/jcwBase';
  import { uploadBdFileAndSaveRec, uploadBdFileAndSaveRecBatch, bdFilesList, deleteBdFiles } from '/@/api/common/bdBase';
  import { downloadByOnlineUrl } from '/@/utils/file/download';
  import { message } from 'ant-design-vue';
  import { ZoomInOutlined, VerticalAlignBottomOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
  import { getFileType, getFileTypeNum } from './index';
  const emit = defineEmits(['success', 'register']);
  const props = defineProps({
    useTo: {
      type: String,
      default: 'jcw', // 'jcw' 或 'bd' ,后端是两个不同的文件系统,接触网传专业jcw,变电专业传bd,默认接触网
    },
    readOnly: {
      type: Boolean,
      default: false,
    },
    multiple: {
      type: Boolean,
      default: true,
    },
    //单文件大小限制,单位M,默认200,-1不限制
    maxSize: {
      type: Number,
      default: 200,
    },
    fileTypeBtnList: {
      type: Array,
      default: () => [0, 1, 4],
    },
  });
  const currentFileList = computed(() => {
    if (state.fileTypeBtn === 0) {
      return state.fileList;
    } else if (state.fileTypeBtn === 1) {
      return state.fileList.filter((i) => i.fileType === 'img');
    } else if (state.fileTypeBtn === 4) {
      return state.fileList.filter((i) => i.fileType !== 'img');
    }
    return [];
  });
  const accept = computed(() => {
    const acceptImg = '.png,.jpeg,.jpg';
    const acceptDoc = '.pdf,.doc,.docx,.xls,.xlsx';
    if (state.fileTypeBtn === 0) {
      return `${acceptImg},${acceptDoc}`;
    } else if (state.fileTypeBtn === 1) {
      return acceptImg;
    } else if (state.fileTypeBtn === 4) {
      return acceptDoc;
    }
    return `${acceptImg},${acceptDoc}`;
  });
  const state = reactive({
    loading: false,
    fileTypeBtn: 0, //0全部; 附件类型 1- 图片 2- 视频 3-音频 4-普通文件(目前只有1、4)
    fileList: [],
    delFileIdList: [], //
  });

  // 当前的弹窗数据
  let row = ref({});
  let title = ref('附件');
  // 注册弹窗
  const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
  const [registerModal, { closeModal }] = useModalInner(async (data) => {
    row.value = data.row;
    title.value = data?.title || '附件';
    state.fileTypeBtn = data.fileTypeBtn || 0;
    resetForm();
    getData();
  });
  const getData = async () => {
    state.loading = true;
    const getApi = props.useTo === 'jcw' ? jcwFilesList : bdFilesList;
    const res = await getApi({ bizId: row.value.id });
    if (res && res.length > 0) {
      res.forEach((i) => {
        i.fileType = getFileType(i.fileName); //转成真实的fileType
        i.isUpload = true;
        i.file = null;
      });
      state.fileList = res;
    }
    state.loading = false;
  };
  // 保存
  const handleOk = async () => {
    state.loading = true;
    // 先判断是否有需要删除的文件
    if (state.delFileIdList && state.delFileIdList.length > 0) {
      // 删除服务器上的文件
      for (let i = 0; i < state.delFileIdList.length; i++) {
        const id = state.delFileIdList[i];
        const delApi = props.useTo === 'jcw' ? deleteJcwFiles : deleteBdFiles;
        await delApi(id);
      }
    }
    const unUploadList = state.fileList.filter((i) => !i.isUpload);
    if (!unUploadList || unUploadList.length === 0) {
      // 不存在需要上传的文件,直接给假提示
      message.success('保存成功');
      state.loading = false;
      return;
    }
    //--批量上传接口start--//
    const formData = new FormData();
    unUploadList.map((i, index) => {
      formData.append('file' + index, i.file);
    });
    formData.append('bizId', row.value.id);
    const uploadApi = props.useTo === 'jcw' ? uploadJcwFileAndSaveRecBatch : uploadBdFileAndSaveRecBatch;
    uploadApi(formData)
      .then(() => {
        emit('success');
        closeModal();
        message.success('保存成功');
      })
      .catch((e) => {
        message.error('保存失败');
        console.log('上传失败', e);
      })
      .finally(() => {
        state.loading = false;
      });
    //--批量上传接口end--//
    return;
    //--单文件上传接口start--//
    Promise.all(
      unUploadList.map((i) => {
        return new Promise((resolve, reject) => {
          const uploadApi = props.useTo === 'jcw' ? uploadJcwFileAndSaveRec : uploadBdFileAndSaveRec;
          uploadApi({
            file: i.file,
            filename: i.fileName,
            data: {
              bizId: row.value.id,
              fileType: getFileTypeNum(i.fileType),
              fileName: i.fileName,
            },
          })
            .then(() => {
              resolve();
            })
            .catch(() => {
              reject({
                fileName: i.fileName,
              });
            });
        });
      })
    )
      .then(() => {
        emit('success');
        closeModal();
        message.success('保存成功');
      })
      .catch((e) => {
        message.error('保存失败');
        console.log('上传失败', e);
      })
      .finally(() => {
        state.loading = false;
      });
    //--单文件上传接口end--//
  };
  const resetForm = () => {
    state.fileList = [];
    state.delFileIdList = [];
  };
  const handleCancel = () => {
    resetForm();
  };
  // 实现文件预览功能
  const previewFile = (i) => {
    if (!i.isUpload) {
      message.info('请先点击确认按钮进行保存');
      return;
    }
    if (!i.filePath) {
      message.info('缺少文件地址,无法预览');
      return;
    }
    if (!i.fileAddr) {
      message.info('缺少文件地址,无法预览');
      return;
    }

    openPreviewModal(true, {
      filePath: i.fileAddr,
      fileName: i.fileName,
      title: i.fileName,
    });
  };
  const downloadFile = (i) => {
    if (i.isUpload) {
      if (!i.filePath) {
        message.info('缺少文件地址,无法下载');
        return;
      }
      if (!i.fileAddr) {
        message.info('缺少文件地址,无法下载');
        return;
      }
      // 下载文件
      downloadByOnlineUrl(i.filePath, i.fileName);
    } else {
      //
      message.info('当前文件未上传,请先点击确认按钮进行上传');
    }
  };
  const deleteFile = (i) => {
    if (i.isUpload) {
      // 加入待删除列表
      state.delFileIdList.push(i.id);
      state.fileList = state.fileList.filter((item) => item.id !== i.id);
    } else {
      // 直接移除本地文件
      state.fileList = state.fileList.filter((item) => item.id !== i.id);
    }
  };
  // 处理文件上传前的逻辑,获取URL并添加到列表
  const beforeUpload = async (file) => {
    const { name, size } = file;

    // 创建临时URL用于预览
    const fileUrl = URL.createObjectURL(file);
    // 简化fileType
    const fileType = getFileType(name);
    if (fileType === 'other') {
      message.error(`${name}暂不支持上传`);
      return false;
    }
    if (size / 1024 / 1024 > props.maxSize) {
      message.error(`${name}文件过大,无法上传`);
      return false;
    }
    // 添加到文件列表
    state.fileList = [
      ...state.fileList,
      {
        id: Date.now().toString(),
        bizId: row.value.id,
        filePath: fileUrl,
        fileType: fileType,
        fileExtname: '.' + name.substring(name.lastIndexOf('.') + 1),
        fileName: name,
        isUpload: false,
        file: file,
      },
    ];

    // 阻止自动上传
    return false;
  };
</script>
<style lang="less" scoped>
  .files {
    width: 100%;
    height: calc(100% - 44px);
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    overflow: auto;
    align-content: flex-start;
  }

  .file-box {
    width: 128px;
    height: 128px;
    background: #e5e5e5;
    margin-right: 12px;
    margin-bottom: 18px;
    position: relative;
    border-radius: 4px;
    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }

    .mask-box {
      opacity: 0;
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: space-evenly;
      border-radius: 4px;
      .anticon {
        cursor: pointer;
      }
    }
    &:hover {
      .mask-box {
        opacity: 1;
      }
    }
  }

  .add-icon {
    display: flex;
    align-items: center;
    justify-content: center;
  }
</style>

PreviewModal.vue,kkview预览文件和照片界面zai

TypeScript 复制代码
<template>
  <!--附件组件,只支持图片和文档,如有需要音视频文件,后续再扩展 -->
  <BasicModal
    :title="title"
    :width="'70vw'"
    :height="500"
    :default-fullscreen="true"
    :can-fullscreen="true"
    :keyboard="false"
    v-bind="$attrs"
    :maskClosable="false"
    :footer="null"
    @register="registerModal"
  >
    <div class="h-full">
      <iframe :src="previewURL" allowfullscreen="true" width="100%" height="100%"></iframe>
    </div>
  </BasicModal>
</template>

<script setup>
  import { ref, computed } from 'vue';
  import { BasicModal, useModalInner } from '/@/components/Modal';
  import { useGlobSetting } from '/@/hooks/setting';
  import { getDownloadFileUrl } from '/@/api/common/base';
  const glob = useGlobSetting();
  const previewURL = computed(() => {
    let url = '';
    const fileUrl = `${window.location.origin}${glob.apiUrl}${getDownloadFileUrl}?filePath=${filePath.value}&fullfilename=${fileName.value}`;
    url = `${glob.viewUrl}${encodeURIComponent(window.Base64.encode(fileUrl))}`;
    return url;
  });

  // 当前的弹窗数据
  let filePath = ref('');
  let fileName = ref('');
  let title = ref('附件预览');
  // 注册弹窗
  const [registerModal] = useModalInner(async (data) => {
    filePath.value = data.filePath;
    fileName.value = data.fileName;
    title.value = data?.title || '附件预览';
  });
</script>
<style lang="less" scoped></style>

在列表主页面操作栏使用调用附件上传功能弹窗的时候

javascript 复制代码
<template>
      <BasicTable @register="registerTable">
        <template #tableTitle>
          <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd">新增</a-button>
        </template>
        <template #action="{ record }">
          <TableAction :actions="getTableAction(record)"  />
        </template>
      </BasicTable>
    </div>
  
  <FileModal @register="registerFileModal" useTo="bd" />
</template>
<script name="bd-base-device" setup>
  import FileModal from '/@/components/FileModal/FileModal.vue';
  import { useModal } from '/@/components/Modal';
  import { columns, searchFormSchema, state, getSubstationTreeData } from './device.data';
  import { listPage, deleteItem } from './device.api';
  import { useListPage } from '/@/hooks/system/useListPage';
  

  const [registerFileModal, { openModal: openFileModal }] = useModal();

  // 列表页面公共参数、方法
  const { tableContext } = useListPage({
    designScope: 'device-template',
    tableProps: {
      title: '信息',
      api: listPage,
      columns: columns,
      showIndexColumn: true,
      formConfig: {
        labelWidth: 96,
        rowProps: { gutter: 24 },
        schemas: searchFormSchema,
        autoAdvancedCol: 4,
      },
      actionColumn: {
        width: 220,
        fixed: 'right',
      },
      beforeFetch: (params) => {
        params.lineId = state.lineId;
        params.substationId = state.substationId;
      },
    },
  });
  const [registerTable, { reload }] = tableContext;

  /**
   * 新增
   */
  function handleAdd() {
    openModal(true, { type: 'add', row: null });
  }
  /**
   * 编辑
   */
  function handleEdit(record) {
    openModal(true, { type: 'edit', row: record });
  }
  /**
   * 删除事件
   */
  async function handleDelete(record) {
    await deleteItem(record.id, reload);
  }
  /**
   * 附件
   */
  function handleFile(record) {
    openFileModal(true, {
      row: record,
      title: `附件(${record.deviceName})`,
    });
  }

  /**
   * 操作栏
   */
  function getTableAction(record) {
    return [
      {
        label: '编辑',
        onClick: handleEdit.bind(null, record),
      },
      {
        label: '附件',
        onClick: handleFile.bind(null, record),
      },
      {
        label: '删除',
        popConfirm: {
          title: '是否确认删除',
          confirm: handleDelete.bind(null, record),
        },
      },
    ];
  }
 

 
</script>
相关推荐
爱上妖精的尾巴24 分钟前
8-5 WPS JS宏 match、search、replace、split支持正则表达式的字符串函数
开发语言·前端·javascript·wps·jsa
小温冲冲26 分钟前
通俗且全面精讲单例设计模式
开发语言·javascript·设计模式
意法半导体STM321 小时前
【官方原创】FDCAN数据段波特率增加后发送失败的问题分析 LAT1617
javascript·网络·stm32·单片机·嵌入式硬件·安全
为什么不问问神奇的海螺呢丶1 小时前
n9e categraf redis监控配置
前端·redis·bootstrap
云飞云共享云桌面1 小时前
推荐一些适合10个SolidWorks设计共享算力的服务器硬件配置
运维·服务器·前端·数据库·人工智能
Liu.7741 小时前
vue开发h5项目
vue.js
请为小H留灯1 小时前
Word论文 封面、目录、页码设置步骤!(2026详细版教程)
毕业设计·word·论文格式
咔咔一顿操作1 小时前
轻量无依赖!autoviwe 页面自适应组件实战:从安装到源码深度解析
javascript·arcgis·npm·css3·html5
刘联其2 小时前
.net也可以用Electron开发跨平台的桌面程序了
前端·javascript·electron
韩曙亮2 小时前
【jQuery】jQuery 选择器 ④ ( jQuery 筛选方法 | 方法分类场景 - 向下找后代、向上找祖先、同级找兄弟、范围限定查找 )
前端·javascript·jquery·jquery筛选方法