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>
|
||||
@@ -287,7 +287,7 @@ onBeforeUnmount(() => {
|
||||
<Icon icon="ant-design:inbox-outlined" class="text-4xl text-blue-500" />
|
||||
</p>
|
||||
<p class="ant-upload-text">{{ tip }}</p>
|
||||
<p class="ant-upload-hint text-gray-400">
|
||||
<p class="ant-upload-hint text-muted-foreground">
|
||||
支持拖拽或点击选择,单文件不超过 {{ maxSizeMb }}MB
|
||||
</p>
|
||||
</UploadDragger>
|
||||
@@ -296,7 +296,7 @@ onBeforeUnmount(() => {
|
||||
<div
|
||||
v-for="item in fileList"
|
||||
:key="item.uid"
|
||||
class="flex items-center gap-2 rounded border border-gray-100 px-3 py-2 text-sm"
|
||||
class="flex items-center gap-2 rounded border border-border bg-muted/20 px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<Icon
|
||||
:icon="
|
||||
@@ -311,12 +311,12 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
v-if="item.url"
|
||||
type="button"
|
||||
class="min-w-0 flex-1 truncate text-left text-blue-600"
|
||||
class="min-w-0 flex-1 truncate text-left text-primary"
|
||||
@click="previewItem(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</button>
|
||||
<span v-else class="min-w-0 flex-1 truncate text-gray-500">{{ item.name }}</span>
|
||||
<span v-else class="min-w-0 flex-1 truncate text-muted-foreground">{{ item.name }}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@@ -343,7 +343,7 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
{{ pasteListening ? '停止监听粘贴' : '开始监听粘贴' }}
|
||||
</Button>
|
||||
<span v-if="pasteListening" class="ml-2 text-xs text-orange-500">
|
||||
<span v-if="pasteListening" class="ml-2 text-xs text-orange-500 dark:text-orange-400">
|
||||
监听中:请将文件或截图 Ctrl+V 粘贴
|
||||
</span>
|
||||
</div>
|
||||
@@ -356,14 +356,14 @@ onBeforeUnmount(() => {
|
||||
destroy-on-close
|
||||
@cancel="onPreviewClose"
|
||||
>
|
||||
<div v-if="previewLoading" class="py-16 text-center text-gray-500">加载中…</div>
|
||||
<div v-else-if="previewIsImage" class="flex justify-center">
|
||||
<div v-if="previewLoading" class="py-16 text-center text-muted-foreground">加载中…</div>
|
||||
<div v-else-if="previewIsImage" class="flex justify-center rounded border border-border bg-muted/25 p-2">
|
||||
<Image :src="previewImageUrl" :preview="true" style="max-height: 70vh" />
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="previewPdfUrl"
|
||||
:src="previewPdfUrl"
|
||||
class="h-[70vh] w-full border-0"
|
||||
class="h-[70vh] w-full rounded border border-border bg-background"
|
||||
title="文件预览"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user