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>
相关推荐
inferno2 小时前
CSS 基础(第二部分)
前端·css
优选资源分享2 小时前
PDF to IMG v1.0:批量PDF转图片工具
pdf·实用工具
BD_Marathon2 小时前
Router_路由传参
前端·javascript·vue.js
闲云一鹤2 小时前
Cesium 去掉默认瓦片和地形,解决网络不好时地图加载缓慢的问题
前端·cesium
Dreamcatcher_AC2 小时前
前端面试高频13问
前端·javascript·vue.js
AI陪跑2 小时前
深入剖析:GrapesJS 中 addStyle() 导致拖放失效的问题
前端·javascript·react.js
登山人在路上2 小时前
Vue中导出和导入
前端·javascript·vue.js
消失的旧时光-19432 小时前
Flutter 路由从 Navigator 到 go_router:嵌套路由 / 登录守卫 / 深链一次讲透
前端·javascript·网络
掘金酱2 小时前
🏆2025 AI/Vibe Coding 对我的影响 | 年终技术征文
前端·人工智能·后端