Vue3中实现world、excel、txt、pdf文件预览,(vue-office的使用)

发布时间 2024-01-09 17:57:03作者: Felix_Openmind

依赖

        "@vue-office/docx": "^1.3.0",
        "@vue-office/excel": "^1.4.5",
        "@vue-office/pdf": "^1.5.3",

代码案例

核心代码

        <div class="preview-box">
            <a-modal class="preview-modal" v-model:visible="previewVisible" :footer="null" :width="1200"
                     style="max-height: 600px"
                     :onCancel="onPreviewModalClick">
                <div class="preview-container">
                    <vue-office-docx
                      v-if="previewType === 'word'"
                      :src="previewUrl"
                      @rendered="renderingCompleted" />
                    <vue-office-excel
                      v-if="previewType === 'excel'"
                      :src="previewUrl"
                      @rendered="renderingCompleted"
                    />
                    <vue-office-pdf
                      v-if="previewType === 'pdf'"
                      :src="previewUrl"
                      @rendered="renderingCompleted "/>
                    <img v-if="previewType === 'img'" :src="previewUrl" style="width: 960px; height: 720px; display: block; margin: 0 auto" alt=""/>
                    <div v-if="previewType === 'txt'" class="txt" style="white-space: pre-wrap;">
                        {{ textContent }}
                    </div>
                </div>
                <template #closeIcon>
                    <span class="close-modal-item">
                        <CloseOutlined/>
                    </span>
                </template>
            </a-modal>

        </div>

全部代码

<template>
    <div class="apply-container">
        <a-modal
                style="position: fixed; left: 25%; top: 12%;"
                class="apply-modal"
                v-model:visible="props.isVisible"
                :title="props.disabledType ? '工作报告详情' : '工作报告上报'"
                width="900px"
                :footer="null"
                @cancel="$emit('close')"
        >

            <a-spin tip="数据加载中..." :spinning="props.spinning">
                <div class="box">
                    <a-form
                            :model="formState"
                            :label-col="{ span: 4 }"
                            :wrapper-col="{ span: 23 }"
                    >
                        <a-form-item
                                label="工作报告标题"
                                :rules="[{ required: true }]">
                            <a-input
                                    placeholder="请输入"
                                    allow-clear
                                    :disabled="props.disabledType"
                                    v-model:value="formState.title"
                            />
                        </a-form-item>
                        <a-form-item label="工作报告类型"
                                     :rules="[{ required: true }]">
                            <a-select
                                    style="width: 100%"
                                    :disabled="props.disabledType"
                                    v-model:value="formState.reportType"
                                    placeholder="请选择"
                                    allowClear
                                    :getPopupContainer="triggerNode => triggerNode.parentNode"
                            >
                                <a-select-option
                                        v-for="(selItem, index) of state.typeList"
                                        :value="selItem.code"
                                        :label="selItem.description"
                                        :key="index"
                                >
                                    {{ selItem.description }}
                                </a-select-option>
                            </a-select>
                        </a-form-item>
                        <!--                        <a-form-item label="报告内容" :rules="[{ required: true }]">-->
                        <!--                            <a-textarea-->
                        <!--                                    :rows="3"-->
                        <!--                                    :style="{ resize: 'none' }"-->
                        <!--                                    :disabled="props.disabledType"-->
                        <!--                                    v-model:value="formState.content"-->
                        <!--                            />-->
                        <!--                        </a-form-item>-->
                        <a-form-item label="附件">
                            <a-upload
                                    v-if="!props.disabledType"
                                    :disabled="props.disabledType"
                                    v-model:file-list="formState.fileList"
                                    name="file"
                                    :multiple="true"
                                    action="/empower/attachment/upload"
                                    @download="doDownload"
                                    :showUploadList="{showDownloadIcon: true}"
                                    @change="handleFileChange"
                            >
                                <a-button class="btn-color" :disabled="props.disabledType">
                                    <upload-outlined></upload-outlined>
                                    上传附件
                                </a-button>
                                <span class="btn-font">
                      <upload-outlined style="display: none"></upload-outlined>*
                      选择本地文件上传</span
                                >
                            </a-upload>
                            <div v-else style="max-height: 520px; overflow-y: scroll">
                                <div v-for="item in formState.fileList" class="echo-file-item">
                                    <a-button type="link" @click=""><span class="file-item-name">{{ item.name }}</span>
                                    </a-button>
                                    <span class="icon-box">
                                      <download-outlined class="down-icon" @click="doDownload(item)"
                                                         title="下载"></download-outlined>
                                      <eye-outlined class="eye-icon" @click="doPreview(item.response.data)"
                                                    title="预览"></eye-outlined>
                                    </span>
                                </div>
                            </div>
                        </a-form-item>
                        <div style="display: flex">
                            <!--                            <a-form-item label="报送人"-->
                            <!--                                         style="width: 100%;"-->
                            <!--                                         :labelCol=" {span: 6, offset: 2}"-->
                            <!--                                         :rules="[{ required: true }]">-->
                            <!--                                <a-input-->
                            <!--                                        :disabled="props.disabledType"-->
                            <!--                                        style="width: 100%; "-->
                            <!--                                        v-model:value="formState.reportPerson"-->
                            <!--                                />-->
                            <!--                            </a-form-item>-->
                            <!--                            <a-form-item style="width: 100%" label="报送单位"-->
                            <!--                                         :labelCol=" {span: 6, offset: 2}"-->
                            <!--                                         :rules="[{ required: true }]">-->
                            <!--                                <a-input-->
                            <!--                                        :disabled="props.disabledType"-->
                            <!--                                        style="width: 100%; "-->
                            <!--                                        v-model:value="formState.reportDept"-->
                            <!--                                />-->
                            <!--                            </a-form-item>-->
                        </div>
                        <!--                        <a-form-item label="报送时间" :rules="[{ required: true }]">-->
                        <!--                            <a-date-picker-->
                        <!--                                    :disabled="props.disabledType"-->
                        <!--                                    v-model:value="formState.reportTime"-->
                        <!--                                    show-time-->
                        <!--                                    format="YYYY-MM-DD HH:mm:ss"-->
                        <!--                                    value-format="YYYY-MM-DD HH:mm:ss"-->
                        <!--                            />-->
                        <!--                        </a-form-item>-->
                    </a-form>
                </div>
                <div class="modal-btn" v-show="!props.disabledType">
                    <a-space>
                        <a-button @click="cancel">取消</a-button>
                        <a-button type="primary" @click="submitHandle" :loading="state.loading">
                            提交
                        </a-button>
                    </a-space>
                </div>
            </a-spin>
        </a-modal>
        <div class="preview-box">
            <a-modal class="preview-modal" v-model:visible="previewVisible" :footer="null" :width="1200"
                     style="max-height: 600px"
                     :onCancel="onPreviewModalClick">
                <div class="preview-container">
                    <vue-office-docx
                      v-if="previewType === 'word'"
                      :src="previewUrl"
                      @rendered="renderingCompleted" />
                    <vue-office-excel
                      v-if="previewType === 'excel'"
                      :src="previewUrl"
                      @rendered="renderingCompleted"
                    />
                    <vue-office-pdf
                      v-if="previewType === 'pdf'"
                      :src="previewUrl"
                      @rendered="renderingCompleted "/>
                    <img v-if="previewType === 'img'" :src="previewUrl" style="width: 960px; height: 720px; display: block; margin: 0 auto" alt=""/>
                    <div v-if="previewType === 'txt'" class="txt" style="white-space: pre-wrap;">
                        {{ textContent }}
                    </div>
                </div>
                <template #closeIcon>
                    <span class="close-modal-item">
                        <CloseOutlined/>
                    </span>
                </template>
            </a-modal>

        </div>
    </div>
</template>

<script setup>
import {onMounted, reactive, ref, watch, onUnmounted, toRefs} from "vue";
import {message} from "ant-design-vue";
import mitt from "@/utils/mitt.js";
import {UploadOutlined, DownloadOutlined, EyeOutlined, CloseOutlined} from "@ant-design/icons-vue";
import {RESPONSE_STATUS} from "@/utils/bean";
import {getDictList} from "@/api/victory";
import {addWorkReport} from "@/api/workOverview";
import {nanoid} from "nanoid";
import {getUserInfo} from "@/api/user";
import VueOfficeDocx from '@vue-office/docx';
import VueOfficeExcel from '@vue-office/excel'
import VueOfficePdf from '@vue-office/pdf'
import '@vue-office/docx/lib/index.css'
import axios from "axios";



const props = defineProps({
    spinning: {
        type: Boolean,
        default: false,
    },
    disabledType: {
        type: Boolean,
        default: false,
    },
    detailData: {
        type: Object,
        default: null,
    },
    isVisible: {
        type: Boolean,
        default: false,
    },
});
const previewVisible = ref(false);
const previewUrl = ref('');
const previewType = ref(''); // word | excel | pdf | img | oldWord | txt
const textContent = ref('');
const renderingCompleted = () => {
    console.log("渲染完成");
};

function setPreviewType(item) {
    if(item.extName === 'docx') {
        previewType.value = 'word'
        return;
    }
    if (item.extName === 'doc') {
        message.warn('暂不支持doc文件预览,请上传docx文件类型');
        previewType.value = ''
        return;
    }
    if (item.extName.includes('xls')) {
        previewType.value = 'excel'
        return;
    }
    if (item.extName.includes('pdf') || item.extName.includes('PDF')) {
        previewType.value = 'pdf'
        return;
    }
    if (item.extName === 'txt' || item.extName === 'TXT') {
        previewType.value = 'txt'
        axios.get(item.address, {
            responseType: 'text'
        }).then(res => {
            textContent.value = res.data
        })
        return;
    }
    if (item.extName.includes('jpg')
      || item.extName.includes('png')
      || item.extName.includes('jpeg')
      || item.extName.includes('svg')
      || item.extName.includes('JPEG')
      || item.extName.includes('JPG')
    ) {
        previewType.value = 'img'
        return;
    }
    if (previewType.value === '') {
        message.warn('该文件类型不支持预览');
    }
}

const doPreview = item => {
    previewUrl.value = ''
    previewType.value = ''
    previewVisible.value = false
    setPreviewType(item);
    console.log('output-> item [doPreivew] ', item, previewType.value)
    if(previewType.value !== '') {
        previewVisible.value = true;
        previewUrl.value = item.address;
    }else {
        previewVisible.value = false;
    }
}
const onPreviewModalClick = () => {
    previewVisible.value = false
}
const doDownload = item => {
    let aUrl = item.response.data.address
    let extNameIcon = item.response.data.extName
    let extName = item.response.data.name
    if (extName.includes('.')) {
        extName = extName.split('.')[0]
    }
    let xml = new XMLHttpRequest()
    xml.open('GET', aUrl, true)
    xml.responseType = 'blob'
    xml.onload = () => {
        let url = window.URL.createObjectURL(xml.response)
        let a = document.createElement('a')
        a.href = url
        a.download = `${extName}.${extNameIcon}`
        a.style.display = 'none'
        a.click()
    }
    xml.send()
}
watch(() => props.disabledType, () => {
    if (props.disabledType) {
        state.spinning = true;
        formState.value = props.detailData;
        formState.value.fileList =
            (props.detailData.attachmentVo &&
                props.detailData.attachmentVo.map(img => {
                    return {
                        uid: `vc-upload-${nanoid()}`,
                        name: `${img?.name}.${img?.extName}`,
                        type: 'image/jpeg',
                        status: 'done',
                        response: {
                            status: 0,
                            message: 'success',
                            traceId: null,
                            data: {
                                address: `${img?.address}`,
                                name: `${img?.name}`,
                                id: img?.id,
                                extName: `${img?.extName}`,
                                url: `${img?.address}`
                            }
                        }
                    }
                })) ||
            []
        state.spinning = false;
    } else {
        resetFormState();
    }
})
const state = reactive({
    loading: false,
    spinning: false,
    typeList: []
});
const emit = defineEmits(["close", "loading"]);
let formState = ref({
    title: '', // 工作标题
    reportType: undefined, // 工作报告类型
    content: '', // 报告内容
    reportPerson: '', // 报送人
    reportTime: undefined, //  报送时间
    fileList: [], // 附件
    reportDept: '', // 报送单位
});

const init = async () => {
    const res = await getDictList({pageNo: 1, pageSize: 200, parentCode: "report_type"}); // 工作报告类型
    state.typeList = res.data.data;
    await initUserInfo();
}
const initUserInfo = async () => {
    if (localStorage.getItem("userInfo") === null) {
        const info = await getUserInfo();
        let userState = info?.data?.data || {};
        localStorage.setItem("userInfo", JSON.stringify(userState));
    }
    let userState = JSON.parse(localStorage.getItem('userInfo'));
    formState.value.reportPerson = userState.name;
    formState.value.reportDept = userState.officeVos[0].officeName;
    console.log('output-> formState::: ', formState.value)
};
onMounted(() => {
    init();
});

onUnmounted(() => {

});

const resetFormState = () => {
    let formStateObj = formState.value;
    formStateObj.title = '';
    formStateObj.reportType = undefined;
    formStateObj.content = '';
    // formStateObj.reportPerson = '';
    // formStateObj.reportDept = '';
    // formStateObj.reportTime = '';
    formStateObj.fileList = [];
}
const submitHandle = async () => {

    console.log(formState.value, " formState.value");
    if (!formState.value.title) {
        message.warn('工作报告标题不能为空')
        return;
    }
    if (!formState.value.reportType) {
        message.warn('工作报告类型不能为空')
        return;
    }
    // if (!formState.value.content) {
    //     message.warn('报告内容不能为空')
    //     return;
    // }
    // if (!formState.value.reportPerson) {
    //     message.warn('报送人不能为空')
    //     return;
    // }
    // if (!formState.value.reportDept) {
    //     message.warn('报送单位不能为空')
    //     return;
    // }
    // if (!formState.value.reportTime) {
    //     message.warn('报送时间不能为空')
    //     return;
    // }
    state.loading = true;
    console.log('output-> formState.value.fileList::: ', formState.value.fileList)
    let attachmentListVal = formState.value.fileList?.map(item => {
        if (item.response) {
            return item['response']['data'].id
        }
    })
    let payload = {
        ...formState.value,
        attachment: attachmentListVal.length ? attachmentListVal.join(',') : '',
    }
    console.log('output-> payload:::: ', payload)
    try {
        const res = await addWorkReport(payload);
        if (RESPONSE_STATUS.success === res.status) {
            state.loading = false;
            message.success('工作报告上报成功');
            mitt.emit('refreshReportTableData');
            resetFormState();
            emit("close");
        }
    } catch (e) {
        state.loading = false;
        resetFormState();
        emit("close");
    }
};
const handleFileChange = (e) => {
    let fileList = [...e.fileList]
    fileList = fileList.map(file => {
        if (file.response) {
            file.url = file.response.url
        }
        return file
    })
    formState.fileList = fileList
}


const cancel = () => {
    emit("close");
};


</script>

<style lang="scss" scoped>

.preview-container {
    max-height: 880px;
    overflow-y: scroll;
}
.close-modal-item {
  position: relative;
  font-size: 25px;
  top: -16px;
  right: -12px;
}

.echo-file-item:hover {
  background-color: #f5f5f5;
  border-radius: 9px;
}

.echo-file-item {
  display: flex;
  align-items: center;
  justify-content: space-between;

  .file-item-name {
    -webkit-line-clamp: 1;
    display: inline-block;
    max-width: 520px;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .icon-box {
    margin-right: 8px;

    .down-icon {
    }

    .eye-icon {
      margin-left: 28px;
      margin-right: 8px;
    }
  }
}


.btn-color {
  background-color: rgba(43, 121, 255, 0.1);
  color: #1f71ff;
}

.btn-font {
  margin-left: 10px;
  font-family: 'PingFang SC';
  font-style: normal;
  font-weight: 400;
  font-size: 12px;
  color: rgba(21, 22, 24, 0.48);
}

.box {
  padding-left: 50px;
}

.upload-item {
  display: flex;
  cursor: pointer;
  justify-content: space-between;
  color: #1890ff;
}

.date {
  margin-top: 20px;
}

.flow {
  display: flex;
  align-content: center;

  input {
    width: 200px;
    margin-right: 10px;
  }
}

.upload-item:hover {
  color: #1890ff;
  background-color: #fcf7f7;
}

.info-box {
  display: flex;
  justify-content: space-around;
  font-size: 14px;
  margin-bottom: 20px;

  .info-item {
    position: relative;
    left: -170px;
  }

  &-title {
    color: rgba(21, 22, 24, 0.48);
  }
}

.modal-btn {
  text-align: right;
}

.weight {
  font-weight: 600;
}

.downLoad {
  cursor: pointer;
  color: #1f71ff;
}

.form-box {
  margin-top: 10px;
  padding-left: 55px;
}

:deep(.ant-form-item-label > label) {
  font-family: "PingFang SC";
  font-weight: 600;
  color: rgba(21, 22, 24, 0.48);
  //   margin-right: 30px !important;
}
</style>