JS之文件预览功能的实现
需求:JS实现文件预览功能,可预览图片、Word文档、Excel表格、PDF文件、TXT文件。
Word预览:
需要安装docx-preview
Excel预览:
需要安装xlsx
PDF预览:
需要安装pdfh5
npm i pdfh5 -D
package.json
"pdfh5": "1.4.2",
package-lock.json
"node_modules/pdfh5": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/pdfh5/-/pdfh5-1.4.2.tgz", "integrity": "sha512-1BL8HIx/EEZowRPBgas7/WokbGEv1gxKNRmmHSimG113178mKxIBH4pxWBc0tj6d25Sy+EwnlQwv9cUUmQa42w==" },
TXT预览:
txtToUtf8.js
export const txtToUtf8 = (file) => { return new Promise(async (resolve, reject) => { const codeType = await getUnicodeType(file); if (codeType === 'utf-8') { return resolve(file); }; let newBlob = null let render = new FileReader() render.readAsText(file, 'gb2312') render.onload = (res) => { newBlob = new Blob([res.target.result], { type: "text/plain" }) let newFile = new File([newBlob], file.name, { type: newBlob.type }) resolve(newFile) } render.onerror = (err) => { reject(err) } }) } // 获取txt文件编码类型 const getUnicodeType = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function (e) { var v8 = new Uint8Array(e.target.result); if (isUTF8(v8)) { resolve("utf-8") } else { resolve("gbk") } }; reader.onerror = e => { reject(e); }; reader.readAsArrayBuffer(file); }) } // 判断是否为UTF8编码类型的文件 const isUTF8 = (bytes) => { var i = 0; while (i < bytes.length) { if (( // ASCII bytes[i] == 0x09 || bytes[i] == 0x0A || bytes[i] == 0x0D || (0x20 <= bytes[i] && bytes[i] <= 0x7E) )) { i += 1; continue; } if (( // non-overlong 2-byte (0xC2 <= bytes[i] && bytes[i] <= 0xDF) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) )) { i += 2; continue; } if (( // excluding overlongs bytes[i] == 0xE0 && (0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) ) || ( // straight 3-byte ((0xE1 <= bytes[i] && bytes[i] <= 0xEC) || bytes[i] == 0xEE || bytes[i] == 0xEF) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) ) || ( // excluding surrogates bytes[i] == 0xED && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x9F) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) ) ) { i += 3; continue; } if (( // planes 1-3 bytes[i] == 0xF0 && (0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) ) || ( // planes 4-15 (0xF1 <= bytes[i] && bytes[i] <= 0xF3) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) ) || ( // plane 16 bytes[i] == 0xF4 && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) ) ) { i += 4; continue; } return false; } return true; }
预览相关的全部代码示例如下:
UploadFile.vue
<template>
<div :class="subFormItem ? 'sub-comp' : 'custom-comp'">
<van-field
:label="item.label"
:required="item.isConfigRequired ?? item.isRequired"
placeholder="点击上传"
readonly
>
<template #input>
<div class="uploader">
<van-row>
<van-col
v-for="(file, index) in formData[item.prop]"
:key="file.objectName"
span="24"
>
<div class="doc-file-item">
<img
class="file-icon"
src="@/assets/file/png-ext.png"
v-if="getFileType(file.fileName) === 'png'"
/>
<img
class="file-icon"
src="@/assets/file/jpg-ext.png"
v-if="
getFileType(file.fileName) === 'jpg' ||
getFileType(file.fileName) === 'jpeg'
"
/>
<img
class="file-icon"
src="@/assets/file/gif-ext.png"
v-if="getFileType(file.fileName) === 'gif'"
/>
<img
class="file-icon"
src="@/assets/file/ppt-ext.png"
v-if="
getFileType(file.fileName) === 'ppt' ||
getFileType(file.fileName) === 'pptx'
"
/>
<img
class="file-icon"
src="@/assets/file/pdf-ext.png"
v-if="getFileType(file.fileName) === 'pdf'"
/>
<img
class="file-icon"
src="@/assets/file/word-ext.png"
v-if="
getFileType(file.fileName) === 'docx' ||
getFileType(file.fileName) === 'doc'
"
/>
<img
class="file-icon"
src="@/assets/file/excel-ext.png"
v-if="
getFileType(file.fileName) === 'xlsx' ||
getFileType(file.fileName) === 'xls'
"
/>
<img
class="file-icon"
src="@/assets/file/zip-ext.png"
v-if="getFileType(file.fileName) === 'zip'"
/>
<img
class="file-icon"
src="@/assets/file/txt-ext.png"
v-if="getFileType(file.fileName) === 'txt'"
/>
<img
class="file-icon"
src="@/assets/file/unknown-ext.png"
v-if="getFileType(file.fileName) === ''"
/>
<span @click="previewFile(file)">{{ file.fileName }}</span>
<div
class="file-delete"
@click="deleteFile(formData, index, item)"
v-show="!isReadOnly"
>
<van-icon name="cross" size="12" />
</div>
</div>
</van-col>
<div v-if="!formData[item.prop] && isReadOnly">无附件</div>
</van-row>
<van-uploader
:accept="acceptFileTypes.join(',')"
:multiple="true"
:preview-image="false"
:after-read="(file) => uploaderAfter(file, 'file')"
v-show="!isReadOnly"
>
<van-button icon="plus" type="primary">点击上传</van-button>
</van-uploader>
</div>
</template>
</van-field>
</div>
<UploadProgress
:loading="uploadLoading"
:rate="uploadRate"
:show-progress="showProgress"
/>
<van-popup
v-model:show="previewVisible"
round
closeable
position="bottom"
:style="{ height: '90%', padding: '16px' }"
>
<FilePreview :file="currentFile" />
</van-popup>
</template>
<script setup>
import {
onMounted,
ref,
toRefs,
defineAsyncComponent,
computed,
watch,
} from "vue";
import { Toast, Dialog } from "vant";
import ActivitiApi from "@/api/activiti";
import { checkMobileModel } from "@/utils/androidOrIos";
import {
hasOptionsComp,
isUploaderComp,
PROCESS_ORDER_STATUS,
acceptFileTypes,
deleteFile,
sensitiveInfoSalt,
reportUsageRecord,
} from "@/utils/common";
import compressorImage from "@/utils/compressor";
import dayjs from "dayjs";
import UploadProgress from "@/components/uploadProgress/uploadProgress.vue";
import { useRoute, useRouter } from "vue-router";
import { txtToUtf8 } from "@/utils/txtToUtf8";
const FilePreview = defineAsyncComponent(() =>
import("@/components/filePreview/FilePreview.vue")
);
const props = defineProps({
formData: Object,
item: Object,
isReadOnly: Object,
compList: Array,
reportType: String,
wholeFormData: Object,
subFormItem: Object,
subFormIdx: Number,
});
const {
formData,
item,
isReadOnly,
compList,
reportType,
wholeFormData,
subFormItem,
subFormIdx,
} = toRefs(props);
watch(
() => formData.value[item.value.prop],
(val) => {
if (wholeFormData.value) {
const resArr = JSON.parse(
wholeFormData.value[subFormItem.value.prop] ?? "[]"
);
if (!resArr[subFormIdx.value]) {
resArr[subFormIdx.value] = {};
}
resArr[subFormIdx.value][item.value.prop] = val;
wholeFormData.value[subFormItem.value.prop] = JSON.stringify(resArr);
sessionStorage.formState = JSON.stringify(wholeFormData.value);
}
}
);
const router = useRouter();
const route = useRoute();
const previewVisible = ref(false);
const currentFile = ref();
const previewFile = (file) => {
if (
getFileType(file.fileName) == "ppt" ||
getFileType(file.fileName) == "pptx"
) {
Toast.fail("ppt文件暂不支持预览");
} else {
reportDownLoad();
const params = {
env: "internet",
fileName: file.objectName,
};
ActivitiApi.getBatchFileUrl(params)
.then((res) => {
const data = res.data;
if (data.code === 200) {
previewVisible.value = true;
file.fileUrl = data.data;
currentFile.value = file;
} else {
throw new Error(data.message);
}
})
.catch((err) => {
if (err.message) {
Toast.fail(err.message);
} else {
Toast.fail("上传失败");
}
});
}
};
const reportDownLoad = () => {
const data = {
landingPageClass: "unknown",
landingPageSubclass: "FileDownload",
landingPageRelateName:
route.query.workConfCode ?? sessionStorage.workConfCode,
quantity: 1,
accessSource: route.query.workConfCode ? "sso" : "frontEndRoute",
};
if (reportType.value) {
data.landingPageClass = reportType.value;
}
if (sessionStorage.eAppInfo) {
data.landingPageClass = "eApp";
data.landingPageRelateName = JSON.parse(
sessionStorage.eAppInfo ?? "{}"
).code;
}
reportUsageRecord(data);
};
const uploadLoading = ref(false);
const uploadRate = ref(0);
const showProgress = ref(false);
const uploaderAfter = async (file, type) => {
if (!file) {
return;
}
uploadLoading.value = true;
const uploadFormData = new FormData();
if (file instanceof Array) {
let fileList = file;
if (type === "image") {
fileList = await compressorImage(fileList);
fileList.forEach((item) => {
uploadFormData.append("fileList", item.file);
});
} else {
const txts = [];
const tasks = [];
file.forEach((item) => {
if (getFileType(item.file.name) === "txt") {
txts.push(item.file);
} else {
uploadFormData.append("fileList", item.file);
}
});
txts.forEach((txt) => {
tasks.push(txtToUtf8(txt));
});
await Promise.all(tasks)
.then((txtList) => {
txtList.forEach((txt) => {
uploadFormData.append("fileList", txt);
});
})
.catch((err) => {
txts.forEach((txt) => {
uploadFormData.append("fileList", txt);
});
});
}
}
if (file.constructor === Object) {
if (type === "image") {
let fileObj = file;
fileObj = await compressorImage(fileObj);
uploadFormData.append("fileList", fileObj.file);
} else {
if (getFileType(file.file.name) === "txt") {
await txtToUtf8(file.file)
.then((txt) => {
uploadFormData.append("fileList", txt);
})
.catch((err) => {
uploadFormData.append("fileList", file.file);
});
} else {
uploadFormData.append("fileList", file.file);
}
}
}
showProgress.value = true;
uploadFile(uploadFormData)
.then((res) => {
if (formData.value[item.value.prop]) {
formData.value[item.value.prop] =
formData.value[item.value.prop].concat(res);
} else {
formData.value[item.value.prop] = res;
}
})
.finally(() => {
uploadRate.value = 0;
uploadLoading.value = false;
showProgress.value = false;
});
};
const uploadFile = (data) => {
return new Promise((resolve, reject) => {
ActivitiApi.uploadBatchFile(data, (progress) => {
uploadRate.value = Math.round((progress.loaded / progress.total) * 100);
})
.then((res) => {
const data = res.data;
if (data.code === 200) {
Toast.success("上传成功");
resolve(data.data);
} else {
throw new Error(data.message);
}
})
.catch((err) => {
if (err.message) {
Toast.fail(err.message);
} else {
Toast.fail("上传失败");
}
reject(err);
});
});
};
const uploadImage = (data) => {
return new Promise((resolve, reject) => {
ActivitiApi.uploadBatchImage(data, (progress) => {
uploadRate.value = Math.round((progress.loaded / progress.total) * 100);
})
.then((res) => {
const data = res.data;
if (data.code === 200) {
Toast.success("上传成功");
resolve(data.data);
} else {
throw new Error(data.message);
}
})
.catch((err) => {
if (err.message) {
Toast.fail(err.message);
} else {
Toast.fail("上传失败");
}
reject(err);
});
});
};
const openUpload = (event, data) => {
if (data.waterMarkConfigComps?.length) {
const result = data.waterMarkConfigComps.find((prop) => {
return !formData.value[prop];
});
const target = compList.value.find((comp) => {
return result === comp.prop;
});
if (target) {
if (target.type !== "title" && target.type !== "noticeBar") {
Toast.fail(target.label + "不能为空");
event.preventDefault();
}
}
}
};
const onOversize = (file) => {
Toast.fail("文件大小不能超过50MB");
};
const getFileType = (fileName) => {
if (!fileName) {
return "";
}
let flag = fileName.split(".");
return flag[flag.length - 1];
};
</script>
<style scoped lang="less">
.custom-comp {
margin: 0px 20px;
width: 90%;
}
.component {
.custom-comp {
margin: 0;
width: 100%;
}
}
.uploader {
width: 100%;
display: flex;
flex-direction: column;
}
.doc-file-item {
width: 100%;
margin-bottom: 18px;
display: flex;
flex-shrink: 0;
align-items: center;
}
.doc-file-item span {
width: 100%;
margin: 0 8px;
font-size: 12px;
line-height: 18px;
word-break: break-all;
}
.file-delete {
width: 16px;
height: 16px;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #ffffff;
background: rgba(0, 0, 0, 0.6);
}
.file-icon {
width: 30px;
height: auto;
}
</style>
FilePreview.vue
<script setup> import { ref, onMounted, getCurrentInstance, reactive, toRefs, watch, nextTick, } from "vue"; import * as docx from "docx-preview"; import * as XLSX from "xlsx"; import Pdfh5 from "pdfh5"; import "pdfh5/css/pdfh5.css"; import request from "../../utils/request"; import { Toast } from "vant"; const { proxy } = getCurrentInstance(); const typeName = ref(""); const imgUrl = ref(""); const srcList = ref(); const loading = ref(false); const pdfUrl = ref(""); const pdfh5 = ref(null); const pptUrl = ref(""); const txtUrl = ref(""); const docFile = ref(null); const txtText = ref(""); const emits = defineEmits(); const emptyTips = ref("暂无内容"); const fullscreen = ref(false); const data = reactive({ excel: { // 数据 workbook: {}, // 表名称集合 sheetNames: [], // 激活项 sheetNameActive: "", // 当前激活表格 SheetActiveTable: "", }, }); const props = defineProps({ showTime: { type: Boolean, default: false, }, file: { type: Object, default: {}, }, clientHeight: { type: Number, default: 600, }, }); const { excel } = toRefs(data); const wordUrl = ref(""); onMounted(() => { loading.value = true; let fileType = props.file.fileName.split("."); fileType = fileType[fileType.length - 1]; init(fileType); }); // 前一个页面调用的init 我在前一个页面根据文件名字后缀已经判断是什么类型的文件了 const init = (type) => { typeName.value = type; if ( type.startsWith("JPG") || type.startsWith("JPEG") || type.startsWith("PNG") || type.startsWith("jpg") || type.startsWith("jpeg") || type.startsWith("png") ) { request({ method: "GET", url: props.file.fileUrl, responseType: "blob", //告诉服务器想到的响应格式 headers: { Accept: "application/octet-stream", }, }) .then((res) => { if (res) { let blob = new Blob([res.data], { type: "image/jpg" }); const imageUrl = URL.createObjectURL(blob); imgUrl.value = imageUrl; (srcList.value = [imageUrl]), (loading.value = false); } else { loading.value = false; } }) .catch(function (error) { console.log(error); loading.value = false; }); } else if (type == "pdf") { request({ method: "GET", url: props.file.fileUrl, responseType: "blob", //告诉服务器想到的响应格式 headers: { "Content-Type": "application/octet-stream", }, }) .then((res) => { if (res) { let blob = new Blob([res.data], { type: "application/pdf" }); const url = URL.createObjectURL(blob); loading.value = false; pdfUrl.value = url; pdfh5.value = new Pdfh5("#pdf-preview", { pdfurl: pdfUrl.value, }); } else { loading.value = false; } }) .catch(function (error) { console.log(error); loading.value = false; }); } else if (type == "ppt" || type == "pptx") { pptUrl.value = "https://view.xdocin.com/view?src=" + props.file.fileUrl; } else if (type == "xlsx" || type == "xls") { //表格 request({ method: "GET", url: props.file.fileUrl, responseType: "arraybuffer", //告诉服务器想到的响应格式 headers: { "Content-Type": "application/vnd.ms-excel;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, }) .then((res) => { console.log(res, "xls"); loading.value = false; if (res) { const workbook = XLSX.read(new Uint8Array(res.data), { type: "array", }); const sheetNames = workbook.SheetNames; // 工作表名称集合 excel.value.workbook = workbook; excel.value.sheetNames = sheetNames; excel.value.sheetNameActive = sheetNames[0]; console.log("excel", excel); getSheetNameTable(sheetNames[0]); } }) .catch(function (error) { console.log(error); loading.value = false; }); } else if (type == "docx" || type == "doc") { request({ method: "GET", url: props.file.fileUrl, responseType: "blob", //告诉服务器想到的响应格式 }) .then((res) => { console.log("DOC", res); loading.value = false; if (res) { // let docx = require("docx-preview"); docx.renderAsync(res.data, docFile.value, null, { className: "docx", // 默认和文档样式类的类名/前缀 inWrapper: true, // 启用围绕文档内容渲染包装器 ignoreWidth: false, // 禁止页面渲染宽度 ignoreHeight: false, // 禁止页面渲染高度 ignoreFonts: false, // 禁止字体渲染 breakPages: true, // 在分页符上启用分页 ignoreLastRenderedPageBreak: true, //禁用lastRenderedPageBreak元素的分页 experimental: false, //启用实验性功能(制表符停止计算) trimXmlDeclaration: true, //如果为真,xml声明将在解析之前从xml文档中删除 debug: false, // 启用额外的日志记录 }); // mammoth.convertToHtml({ arrayBuffer: new Uint8Array(xhr.response) }).then((resultObject) => { // nextTick(() => { // wordText.value = resultObject.value; // });p // }); } }) .catch(function (error) { loading.value = false; }); } else if (type === "txt") { request({ method: "GET", url: props.file.fileUrl, }) .then((res) => { txtText.value = res.data; loading.value = false; }) .catch(function (error) { console.log(error); loading.value = false; }); } else { Toast.fail("暂不支持预览该文件类型"); } }; const getSheetNameTable = (sheetName) => { try { // 获取当前工作表的数据 const worksheet = excel.value.workbook.Sheets[sheetName]; // 转换为数据 1.json数据有些问题,2.如果是html那么样式需修改 let htmlData = XLSX.utils.sheet_to_html(worksheet, { header: "", footer: "", }); htmlData = htmlData === "" ? htmlData : htmlData.replace( /<table/, '<table class="default-table" border="1px solid #ccc" cellpadding="0" cellspacing="0"' ); // 第一行进行改颜色 htmlData = htmlData === "" ? htmlData : htmlData.replace(/<tr/, '<tr style="background:#b4c9e8"'); excel.value.SheetActiveTable = htmlData; } catch (e) { // 如果工作表没有数据则到这里来处理 excel.value.SheetActiveTable = '<h4 style="text-align: center">' + emptyTips.value + "</h4>"; } }; watch( () => props.file, (newVal, oldVal) => { loading.value = true; let fileType = props.file.fileName.split("."); fileType = fileType[fileType.length - 1]; console.log(props.file, "watch"); imgUrl.value = ""; pdfUrl.value = ""; init(fileType); } ); defineExpose({ init, }); </script> <template> <div class="viewItemFile"> <van-loading v-if="typeName !== 'pdf' && loading" /> <div class="image" v-if=" typeName.startsWith('JPG') || typeName.startsWith('jpg') || typeName.startsWith('JPEG') || typeName.startsWith('jpeg') || typeName.startsWith('PNG') || typeName.startsWith('png') " > <div> <img style="display: block; max-width: 100%; margin: 24px auto" :src="imgUrl" alt="" /> </div> </div> <div class="docWrap" v-if="typeName == 'docx' || typeName == 'doc'"> <!-- 预览文件的地方(用于渲染) --> <div ref="docFile" class="docPre"></div> </div> <div class="xlxs-pre" v-if="typeName == 'xlsx' || typeName == 'xls'"> <div class="tab"> <a-radio-group size="small" v-model:value="excel.sheetNameActive" @change="getSheetNameTable(excel.sheetNameActive)" > <a-radio-button v-for="(item, index) in excel.sheetNames" :key="index" :value="item" >{{ item }}</a-radio-button > </a-radio-group> </div> <div style=" margin-top: 5px; border: 1px solid #a0a0a0; overflow-x: auto; overflow-y: scroll; " > <div v-html="excel.SheetActiveTable" style="padding: 10px 15px"></div> </div> </div> <div v-if="typeName == 'pdf'" style="height: 100%"> <div id="pdf-preview" /> </div> <div v-if="typeName == 'ppt' || typeName == 'pptx'" style="height: 100%"> <iframe id="ppt-preview" width="100%" height="auto" :src="pptUrl" /> </div> <div class="text-plain" v-if="typeName === 'txt'"> <textarea readonly :value="txtText"></textarea> </div> </div> </template> <style lang="less" scoped> .viewItemFile { height: 100%; .image { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; div { height: 600px; width: 600px; } } .divContent { display: flex; align-items: center; justify-content: center; } } .viewItemFile { #pdf-preview .pinch-zoom-container { height: 100% !important; } .xlxs-pre { height: calc(100vh - 40px); padding: 20px; .table-html-wrap :deep(table) { border-right: 1px solid #e8eaec; border-bottom: 1px solid #e8eaec; border-collapse: collapse; margin: auto; } .table-html-wrap :deep(table td) { border-left: 1px solid #e8eaec; border-top: 1px solid #e8eaec; white-space: wrap; text-align: left; min-width: 100px; padding: 4px; } table { border-top: 1px solid #ebeef5; border-left: 1px solid #ebeef5; width: 100%; overflow: auto; tr { height: 44px; } td { min-width: 200px; max-width: 400px; padding: 4px 8px; border-right: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5; } } } :deep(table) { width: 100% !important; border-collapse: collapse !important; border-spacing: 0 !important; text-align: center !important; border: 0px !important; overflow-x: auto !important; overflow-y: scroll !important; } :deep(table tr td) { /* border: 1px solid gray !important; */ border-right: 1px solid gray !important; border-bottom: 1px solid gray !important; width: 300px !important; height: 33px !important; } /**整体样式 */ :deep(.excel-view-container) { background-color: #ffffff; } /**标题样式 */ :deep(.class4Title) { font-size: 22px !important; font-weight: bold !important; padding: 10px !important; } /**表格表头样式 */ :deep(.class4TableTh) { /* font-size: 14px !important; */ font-weight: bold !important; padding: 2px !important; background-color: #ccc !important; } } .docWrap { width: 100%; :deep(.docx-wrapper) { background: #ffffff !important; :deep(.docx-wrapper section) { padding: 8pt 16pt; width: 100%; } :deep(.docx-wrapper > section.docx) { box-shadow: 0; } } } html body { width: 100%; height: 100vh; margin: 0; } .text-plain { width: 100%; height: 100%; padding-top: 30px; textarea { width: 100%; height: 100%; border: none; } .visible-content { height: 100%; border: none; } } </style> <style scoped> .docWrap { width: 100%; } :deep(.docWrap .docx-wrapper) { background: #ffffff !important; padding: 16px !important; } :deep(.docWrap .docx-wrapper > section.docx) { padding: 8pt 16pt !important; width: 100% !important; overflow: scroll !important; } :deep(#pdf-preview .pinch-zoom-container) { height: auto !important; } </style>
即可。

浙公网安备 33010602011771号