feat(ui): add reusable file preview
This commit is contained in:
parent
44a3ea1028
commit
699e78f20b
|
|
@ -26,6 +26,11 @@
|
|||
</template>
|
||||
</el-upload>
|
||||
<span style="display:none!important"><el-input v-model="value"></el-input></span>
|
||||
<xt-file-preview
|
||||
v-model="previewVisible"
|
||||
:files="defaultFileList"
|
||||
:initial-index="previewIndex"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -53,7 +58,9 @@
|
|||
data() {
|
||||
return {
|
||||
value: "",
|
||||
defaultFileList: []
|
||||
defaultFileList: [],
|
||||
previewVisible: false,
|
||||
previewIndex: 0,
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
|
|
@ -151,7 +158,9 @@
|
|||
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
|
||||
},
|
||||
handlePreview(uploadFile){
|
||||
window.open(uploadFile.url)
|
||||
const index = this.defaultFileList.indexOf(uploadFile)
|
||||
this.previewIndex = index >= 0 ? index : 0
|
||||
this.previewVisible = true
|
||||
},
|
||||
request(param){
|
||||
var apiObj = config.apiObjFile;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
<div v-if="tip" class="el-upload__tip">{{tip}}</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<xt-file-preview
|
||||
v-model="previewVisible"
|
||||
:files="defaultFileList"
|
||||
:initial-index="previewIndex"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -54,6 +59,8 @@
|
|||
return {
|
||||
defaultFileList: [],
|
||||
uploadingCount: 0,
|
||||
previewVisible: false,
|
||||
previewIndex: 0,
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
|
|
@ -104,6 +111,9 @@
|
|||
id: item.id,
|
||||
name: item.name,
|
||||
url: item.path || item.url,
|
||||
path: item.path || item.url,
|
||||
mime: item.mime,
|
||||
type: item.type,
|
||||
status: "success",
|
||||
})
|
||||
}
|
||||
|
|
@ -128,7 +138,10 @@
|
|||
}
|
||||
file.name = res.name
|
||||
file.url = res.path
|
||||
file.path = res.path
|
||||
file.id = res.id
|
||||
file.mime = res.mime
|
||||
file.type = res.type
|
||||
},
|
||||
error(err){
|
||||
this.$notify.error({
|
||||
|
|
@ -149,7 +162,9 @@
|
|||
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
|
||||
},
|
||||
handlePreview(uploadFile){
|
||||
window.open(uploadFile.url)
|
||||
const index = this.defaultFileList.indexOf(uploadFile)
|
||||
this.previewIndex = index >= 0 ? index : 0
|
||||
this.previewVisible = true
|
||||
},
|
||||
request(param){
|
||||
var apiObj = config.apiObjFile;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
</template>
|
||||
<template #file="{ file }">
|
||||
<div class="sc-upload-list-item">
|
||||
<el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="cover" :preview-src-list="preview" :initial-index="preview.findIndex(n=>n==file.url)" hide-on-click-modal append-to-body :z-index="9999">
|
||||
<el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="cover" @click="handlePreview(file)">
|
||||
<template #placeholder>
|
||||
<div class="sc-upload-multiple-image-slot">
|
||||
Loading...
|
||||
|
|
@ -42,6 +42,11 @@
|
|||
</template>
|
||||
</el-upload>
|
||||
<span style="display:none!important"><el-input v-model="value"></el-input></span>
|
||||
<xt-file-preview
|
||||
v-model="previewVisible"
|
||||
:files="defaultFileList"
|
||||
:initial-index="previewIndex"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -69,7 +74,9 @@
|
|||
data(){
|
||||
return {
|
||||
value: "",
|
||||
defaultFileList: []
|
||||
defaultFileList: [],
|
||||
previewVisible: false,
|
||||
previewIndex: 0,
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
|
|
@ -235,7 +242,9 @@
|
|||
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
|
||||
},
|
||||
handlePreview(uploadFile){
|
||||
window.open(uploadFile.url)
|
||||
const index = this.defaultFileList.indexOf(uploadFile)
|
||||
this.previewIndex = index >= 0 ? index : 0
|
||||
this.previewVisible = true
|
||||
},
|
||||
request(param){
|
||||
var apiObj = config.apiObj;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
<template>
|
||||
<span class="xt-file-preview-trigger">
|
||||
<slot name="trigger" :open="open"></slot>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="currentFile.name || '文件预览'"
|
||||
width="90%"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="xt-file-preview">
|
||||
<div v-if="!currentFile.url" class="xt-file-preview__empty">
|
||||
文件地址不存在
|
||||
</div>
|
||||
<img
|
||||
v-else-if="previewType === 'image'"
|
||||
class="xt-file-preview__image"
|
||||
:src="currentFile.url"
|
||||
:alt="currentFile.name"
|
||||
/>
|
||||
<video
|
||||
v-else-if="previewType === 'video'"
|
||||
class="xt-file-preview__media"
|
||||
:src="currentFile.url"
|
||||
controls
|
||||
preload="metadata"
|
||||
></video>
|
||||
<audio
|
||||
v-else-if="previewType === 'audio'"
|
||||
class="xt-file-preview__audio"
|
||||
:src="currentFile.url"
|
||||
controls
|
||||
preload="metadata"
|
||||
></audio>
|
||||
<iframe
|
||||
v-else-if="previewType === 'pdf'"
|
||||
class="xt-file-preview__pdf"
|
||||
:src="currentFile.url"
|
||||
:title="currentFile.name"
|
||||
></iframe>
|
||||
<div v-else class="xt-file-preview__unsupported">
|
||||
<div class="xt-file-preview__file-name">{{ currentFile.name }}</div>
|
||||
<div>该文件格式不支持浏览器内预览,请下载源文件查看。</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="xt-file-preview__footer">
|
||||
<div>
|
||||
<el-button v-if="fileList.length > 1" :disabled="currentIndex <= 0" @click="previous">
|
||||
上一个
|
||||
</el-button>
|
||||
<span v-if="fileList.length > 1" class="xt-file-preview__counter">
|
||||
{{ currentIndex + 1 }} / {{ fileList.length }}
|
||||
</span>
|
||||
<el-button v-if="fileList.length > 1" :disabled="currentIndex >= fileList.length - 1" @click="next">
|
||||
下一个
|
||||
</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-button :disabled="!currentFile.url" @click="download">下载源文件</el-button>
|
||||
<el-button type="primary" @click="visible = false">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
|
||||
const VIDEO_EXTENSIONS = ["mp4", "webm", "ogg", "mov", "m4v"];
|
||||
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac"];
|
||||
|
||||
export default {
|
||||
name: "XtFilePreview",
|
||||
props: {
|
||||
modelValue: { type: Boolean, default: undefined },
|
||||
files: { type: Array, default: () => [] },
|
||||
initialIndex: { type: Number, default: 0 },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0,
|
||||
internalVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue === undefined ? this.internalVisible : this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.internalVisible = value;
|
||||
this.$emit("update:modelValue", value);
|
||||
},
|
||||
},
|
||||
fileList() {
|
||||
return this.files.map((file) => this.normalizeFile(file));
|
||||
},
|
||||
currentFile() {
|
||||
return this.fileList[this.currentIndex] || {};
|
||||
},
|
||||
previewType() {
|
||||
const mime = (this.currentFile.mime || "").toLowerCase();
|
||||
const extension = this.extensionOf(this.currentFile.name || this.currentFile.url || "");
|
||||
if (mime.startsWith("image/") || IMAGE_EXTENSIONS.includes(extension)) return "image";
|
||||
if (mime.startsWith("video/") || VIDEO_EXTENSIONS.includes(extension)) return "video";
|
||||
if (mime.startsWith("audio/") || AUDIO_EXTENSIONS.includes(extension)) return "audio";
|
||||
if (mime === "application/pdf" || extension === "pdf") return "pdf";
|
||||
return "unsupported";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.resetIndex();
|
||||
},
|
||||
initialIndex() {
|
||||
if (this.modelValue) this.resetIndex();
|
||||
},
|
||||
files() {
|
||||
this.resetIndex(this.currentIndex);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
normalizeFile(file) {
|
||||
const response = file?.response || {};
|
||||
return {
|
||||
id: file?.id || response.id,
|
||||
name: file?.name || response.name || "未命名文件",
|
||||
url: file?.path || file?.url || response.path || response.url || "",
|
||||
mime: file?.mime || file?.raw?.type || response.mime || "",
|
||||
};
|
||||
},
|
||||
extensionOf(value) {
|
||||
const cleanValue = value.split(/[?#]/)[0];
|
||||
const dotIndex = cleanValue.lastIndexOf(".");
|
||||
return dotIndex >= 0 ? cleanValue.slice(dotIndex + 1).toLowerCase() : "";
|
||||
},
|
||||
resetIndex(index = this.initialIndex) {
|
||||
const maxIndex = Math.max(0, this.files.length - 1);
|
||||
this.currentIndex = Math.min(Math.max(0, index), maxIndex);
|
||||
},
|
||||
open(index = this.initialIndex) {
|
||||
this.resetIndex(index);
|
||||
this.visible = true;
|
||||
},
|
||||
previous() {
|
||||
if (this.currentIndex > 0) this.currentIndex -= 1;
|
||||
},
|
||||
next() {
|
||||
if (this.currentIndex < this.fileList.length - 1) this.currentIndex += 1;
|
||||
},
|
||||
download() {
|
||||
if (!this.currentFile.url) return;
|
||||
const link = document.createElement("a");
|
||||
link.href = this.currentFile.url;
|
||||
link.download = this.currentFile.name || "download";
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.xt-file-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 420px;
|
||||
height: 70vh;
|
||||
background: var(--el-fill-color-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xt-file-preview__image,
|
||||
.xt-file-preview__media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.xt-file-preview__audio {
|
||||
width: min(600px, 90%);
|
||||
}
|
||||
|
||||
.xt-file-preview__pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.xt-file-preview__empty,
|
||||
.xt-file-preview__unsupported {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.xt-file-preview__file-name {
|
||||
margin-bottom: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.xt-file-preview__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.xt-file-preview__counter {
|
||||
display: inline-block;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -30,17 +30,23 @@
|
|||
<el-table-column label="物料类别" prop="material_cate" width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="调查表" prop="survery_form" width="100" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-link v-if="scope.row.survery_form_" type="text" :href="scope.row.survery_form_.path" target="_blank" style="color:blue" @click.stop="">下载</el-link>
|
||||
<xt-file-preview v-if="scope.row.survery_form_" :files="[scope.row.survery_form_]">
|
||||
<template #trigger="{ open }"><el-link type="primary" @click.stop="open()">预览</el-link></template>
|
||||
</xt-file-preview>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="营业执照" prop="business_license" width="100" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-link v-if="scope.row.business_license_" type="text" :href="scope.row.business_license_.path" target="_blank" style="color:blue" @click.stop="">下载</el-link>
|
||||
<xt-file-preview v-if="scope.row.business_license_" :files="[scope.row.business_license_]">
|
||||
<template #trigger="{ open }"><el-link type="primary" @click.stop="open()">预览</el-link></template>
|
||||
</xt-file-preview>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="质量证书" prop="quality_certificate" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-link v-if="scope.row.quality_certificate_" type="text" :href="scope.row.quality_certificate_.path" target="_blank" style="color:blue" @click.stop="">下载</el-link>
|
||||
<xt-file-preview v-if="scope.row.quality_certificate_" :files="[scope.row.quality_certificate_]">
|
||||
<template #trigger="{ open }"><el-link type="primary" @click.stop="open()">预览</el-link></template>
|
||||
</xt-file-preview>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</scTable>
|
||||
|
|
@ -70,4 +76,4 @@ const exportCols = [
|
|||
{ header: "物料名称", key: "material_name", wch: 20 },
|
||||
{ header: "物料类别", key: "material_cate", wch: 20 },
|
||||
]
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="文件" prop="files" >
|
||||
<template #default="scope">
|
||||
<div v-for="item in scope.row.files_" :key="item.id">
|
||||
<el-link style="font-size:12px" type="primary" :href="item.path" target="_blank">{{item.name}}</el-link>
|
||||
</div>
|
||||
<xt-file-preview :files="scope.row.files_">
|
||||
<template #trigger="{ open }">
|
||||
<div v-for="(item, index) in scope.row.files_" :key="item.id">
|
||||
<el-link style="font-size:12px" type="primary" @click.stop="open(index)">{{item.name}}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</xt-file-preview>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="操作" fixed="right" align="left" width="170">
|
||||
|
|
@ -108,4 +112,4 @@ export default {
|
|||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,14 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="文件资料" prop="name">
|
||||
<template #default="scope">
|
||||
<div v-for="item in scope.row.files_" :key="item.id">
|
||||
<el-link style="font-size:12px" type="primary" :href="item.path" target="_blank">
|
||||
{{ item.name }}</el-link>
|
||||
</div>
|
||||
<xt-file-preview :files="scope.row.files_">
|
||||
<template #trigger="{ open }">
|
||||
<div v-for="(item, index) in scope.row.files_" :key="item.id">
|
||||
<el-link style="font-size:12px" type="primary" @click.stop="open(index)">
|
||||
{{ item.name }}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</xt-file-preview>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</scTable>
|
||||
|
|
|
|||
|
|
@ -146,15 +146,15 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="文件资料" prop="name">
|
||||
<template #default="scope">
|
||||
<div v-for="item in scope.row.files_" :key="item.id">
|
||||
<el-link
|
||||
style="font-size: 12px"
|
||||
type="primary"
|
||||
:href="item.path"
|
||||
target="_blank"
|
||||
>{{ item.name }}</el-link
|
||||
>
|
||||
</div>
|
||||
<xt-file-preview :files="scope.row.files_">
|
||||
<template #trigger="{ open }">
|
||||
<div v-for="(item, index) in scope.row.files_" :key="item.id">
|
||||
<el-link style="font-size: 12px" type="primary" @click.stop="open(index)">
|
||||
{{ item.name }}
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</xt-file-preview>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import ehsSelect from './components/ehsSelect/select'
|
|||
import ehsTableSelect from './components/ehsSelect/tableSelect'
|
||||
import xtSelect from './components/xtSelect/index.vue'
|
||||
import xtScanInput from './components/xtScanInput/index.vue'
|
||||
import xtFilePreview from './components/xtFilePreview/index.vue'
|
||||
export default {
|
||||
install(app) {
|
||||
app.component('ehsUserSelect', ehsUserSelect);
|
||||
|
|
@ -12,5 +13,6 @@ export default {
|
|||
app.component('ehsTableSelect', ehsTableSelect);
|
||||
app.component('xtSelect', xtSelect);
|
||||
app.component('xtScanInput', xtScanInput);
|
||||
app.component('xtFilePreview', xtFilePreview);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue