fix: 接诊UI
This commit is contained in:
196
apps/web-antd/src/components/file-preview/FilePreviewModal.vue
Normal file
196
apps/web-antd/src/components/file-preview/FilePreviewModal.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 通用文件预览:图片用 Ant Image;PDF fetch→blob→iframe;支持多文件切换
|
||||
* 开票中心与素材库复用,避免两套预览逻辑
|
||||
*/
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import { Button, Image, Modal, Spin, message } from 'ant-design-vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 是否显示 */
|
||||
open: boolean;
|
||||
/** 文件 URL 列表 */
|
||||
files?: string[];
|
||||
/** 弹窗标题前缀 */
|
||||
title?: string;
|
||||
}>(),
|
||||
{
|
||||
files: () => [],
|
||||
title: '文件预览',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', v: boolean): void;
|
||||
}>();
|
||||
|
||||
const currentIndex = ref(0);
|
||||
const previewIsImage = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
const previewPdfUrl = ref('');
|
||||
const previewLoading = ref(false);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: (v: boolean) => emit('update:open', v),
|
||||
});
|
||||
|
||||
const currentUrl = computed(() => props.files?.[currentIndex.value] || '');
|
||||
|
||||
const displayTitle = computed(() => {
|
||||
const name = (currentUrl.value || '').split('/').pop() || '';
|
||||
const total = props.files?.length || 0;
|
||||
if (total > 1) {
|
||||
return `${props.title}(${currentIndex.value + 1}/${total})${name ? ' - ' + name : ''}`;
|
||||
}
|
||||
return name ? `${props.title} - ${name}` : props.title;
|
||||
});
|
||||
|
||||
function isImageUrl(url: string) {
|
||||
return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(url || '');
|
||||
}
|
||||
|
||||
function revokePdfBlob() {
|
||||
if (previewPdfUrl.value && previewPdfUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewPdfUrl.value);
|
||||
}
|
||||
previewPdfUrl.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载当前文件到预览区
|
||||
*/
|
||||
async function loadCurrent() {
|
||||
const url = currentUrl.value;
|
||||
revokePdfBlob();
|
||||
if (!url) {
|
||||
message.info('暂无文件');
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
if (isImageUrl(url)) {
|
||||
previewIsImage.value = true;
|
||||
previewImageUrl.value = url;
|
||||
previewLoading.value = false;
|
||||
return;
|
||||
}
|
||||
previewIsImage.value = false;
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error('文件加载失败');
|
||||
const blob = await resp.blob();
|
||||
const pdfBlob =
|
||||
blob.type && blob.type !== 'application/octet-stream'
|
||||
? blob
|
||||
: new Blob([blob], { type: 'application/pdf' });
|
||||
previewPdfUrl.value = URL.createObjectURL(pdfBlob);
|
||||
} catch {
|
||||
message.error('预览失败,请尝试下载后查看');
|
||||
visible.value = false;
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function prevFile() {
|
||||
if (currentIndex.value > 0) {
|
||||
currentIndex.value -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
function nextFile() {
|
||||
if (props.files && currentIndex.value < props.files.length - 1) {
|
||||
currentIndex.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.open, props.files] as const,
|
||||
([open]) => {
|
||||
if (open) {
|
||||
currentIndex.value = 0;
|
||||
loadCurrent();
|
||||
} else {
|
||||
revokePdfBlob();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(currentIndex, () => {
|
||||
if (props.open) {
|
||||
loadCurrent();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokePdfBlob();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="visible"
|
||||
:title="displayTitle"
|
||||
:footer="null"
|
||||
width="860px"
|
||||
destroy-on-close
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div class="preview-wrap">
|
||||
<div v-if="(files?.length || 0) > 1" class="nav">
|
||||
<Button size="small" :disabled="currentIndex <= 0" @click="prevFile">上一份</Button>
|
||||
<Button
|
||||
size="small"
|
||||
:disabled="currentIndex >= (files?.length || 0) - 1"
|
||||
@click="nextFile"
|
||||
>
|
||||
下一份
|
||||
</Button>
|
||||
</div>
|
||||
<Spin :spinning="previewLoading">
|
||||
<div v-if="previewIsImage" class="img-box">
|
||||
<Image :src="previewImageUrl" :preview="true" style="max-width: 100%" />
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="previewPdfUrl"
|
||||
:src="previewPdfUrl"
|
||||
class="pdf-frame"
|
||||
title="pdf-preview"
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preview-wrap {
|
||||
min-height: 240px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.img-box {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--muted) / 0.25);
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
.pdf-frame {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user