This commit is contained in:
guochunsi
2026-01-07 18:33:03 +08:00
parent bb06c81997
commit 9e3e775b0f
11 changed files with 277 additions and 155 deletions

View File

@@ -0,0 +1,92 @@
<template>
<el-dropdown
v-if="hasVisibleItems"
trigger="click"
@command="handleCommand"
:style="dropdownStyle"
>
<el-button
:type="buttonType"
link
:style="buttonStyle"
>
<slot name="button">
{{ buttonText }}
<el-icon v-if="buttonIcon" class="el-icon--right" :style="iconStyle">
<component :is="buttonIcon" v-if="buttonIcon" />
</el-icon>
</slot>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in visibleItems"
:key="item.command"
:command="item.command"
>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span :style="item.icon ? { marginLeft: '8px' } : {}">{{ item.label }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Operation } from '@element-plus/icons-vue'
interface MenuItem {
command: string
label: string
icon?: any
visible?: boolean | (() => boolean)
}
interface Props {
items: MenuItem[]
buttonText?: string
buttonIcon?: any
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
buttonStyle?: string | Record<string, any>
dropdownStyle?: string | Record<string, any>
iconStyle?: string | Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
buttonText: '更多',
buttonIcon: Operation,
buttonType: 'primary',
buttonStyle: () => ({ whiteSpace: 'nowrap' }),
dropdownStyle: () => ({ marginLeft: '12px' }),
iconStyle: () => ({ marginLeft: '4px' })
})
const emit = defineEmits<{
command: [command: string]
}>()
// 计算可见的菜单项
const visibleItems = computed(() => {
return props.items.filter(item => {
if (item.visible === undefined) return true
if (typeof item.visible === 'boolean') return item.visible
if (typeof item.visible === 'function') return item.visible()
return false
})
})
// 是否有可见的菜单项
const hasVisibleItems = computed(() => {
return visibleItems.value.length > 0
})
// 处理命令
const handleCommand = (command: string) => {
emit('command', command)
}
</script>

View File

@@ -1,150 +1,85 @@
<template>
<!-- 组件不占据任何布局空间所有预览组件都是 teleported -->
<div style="display: none;">
<!-- 图片直接使用 el-image-viewer 全屏预览 -->
<el-image-viewer
v-if="!showIframe && imageSrc && imagePreviewVisible"
:url-list="[imageSrc]"
:teleported="true"
hide-on-click-modal
@close="imagePreviewVisible = false"
/>
<!-- PDF dialog 中显示 -->
<el-dialog
<div style="width: 100%; height: 100%;">
<!-- 图片直接使用原始地址展示保持原有行为 -->
<viewer :images="[authSrc]" v-if="!showIframe">
<img
v-if="!showIframe"
ref="imgRef"
:width="imgWidth ? imgWidth : '100%;'"
:height="imgHeight ? imgHeight : '100%;'"
:src="authSrc"
/>
</viewer>
<!-- PDF通过 iframe + token 的请求展示 -->
<iframe
v-if="showIframe"
v-model="pdfDialogVisible"
:title="dialogTitle || '文件预览'"
append-to-body
width="90%"
class="pdf-preview-dialog"
>
<iframe ref="authIframeRef" :style="{ width: '100%', height: pdfIframeHeight }" />
<!-- <div class="pdf-iframe-wrapper">
<iframe
ref="authIframeRef"
:style="{
width: '100%',
height: pdfIframeHeight,
border: 'none',
display: 'block'
}"
/>
</div> -->
</el-dialog>
ref="authIframeRef"
style="width: 100%; height: 100%;"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed, onUnmounted } from 'vue';
import { ElImageViewer } from 'element-plus';
import { Session } from "/@/utils/storage";
import { ref, onMounted, nextTick } from 'vue'
import { Session } from '/@/utils/storage'
// 定义 props
const props = defineProps<{
authSrc: string;
imgWidth?: string;
imgHeight?: string;
dialogTitle?: string;
}>();
authSrc: string
imgWidth?: string
imgHeight?: string
}>()
// 定义响应式数据
const showIframe = ref(false);
const imageSrc = ref<string>('');
const imagePreviewVisible = ref(false);
const pdfDialogVisible = ref(false);
const authIframeRef = ref<HTMLIFrameElement | null>(null);
const windowHeight = ref(window.innerHeight);
const showIframe = ref(false)
const authIframeRef = ref<HTMLIFrameElement | null>(null)
const imgRef = ref<HTMLImageElement | null>(null)
// 计算 PDF iframe 的合适高度(优先使用外部传入的 imgHeight否则根据窗口高度动态计算
const pdfIframeHeight = computed(() => {
// 如果外部传入了 imgHeight优先使用
if (props.imgHeight) {
return props.imgHeight;
// 携带 token 请求 img/pdf 的 src
const getImgSrcByToken = (src?: string) => {
const targetSrc = src || props.authSrc
if (targetSrc.indexOf('.pdf') >= 0) {
// PDF通过 iframe 展示
showIframe.value = true
nextTick(() => {
const tenantId = Session.getTenant()
const iframe = authIframeRef.value
if (!iframe) return
const request = new XMLHttpRequest()
request.responseType = 'blob'
request.open('get', targetSrc, true)
request.setRequestHeader('Authorization', 'Bearer ' + Session.getToken())
request.setRequestHeader('TENANT-ID', tenantId)
request.onreadystatechange = () => {
if (request.readyState === XMLHttpRequest.DONE && request.status === 200) {
const binaryData: BlobPart[] = []
binaryData.push(request.response)
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }))
iframe.onload = () => {
URL.revokeObjectURL(iframe.src)
}
}
}
request.send(null)
})
} else {
// 图片:保持原有行为(直接使用 authSrc不做 token 转发)
showIframe.value = false
// 如需带 token 加载图片,可参考被注释的旧逻辑在此扩展
}
// 否则根据窗口高度动态计算dialog header 约 50pxpadding 约 40px留一些余量
return `${windowHeight.value - 120}px`;
});
}
// 监听窗口大小变化
const handleResize = () => {
windowHeight.value = window.innerHeight;
};
const refreshImg = (src?: string) => {
getImgSrcByToken(src)
}
onMounted(() => {
window.addEventListener('resize', handleResize);
getImgSrcByToken();
});
getImgSrcByToken()
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
// 携带token请求img的src
const getImgSrcByToken = (src?: string) => {
if (props.authSrc.indexOf(".pdf") >= 0) {
showIframe.value = true;
pdfDialogVisible.value = true;
nextTick(() => {
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片处理逻辑:加载后直接打开预览
showIframe.value = false;
pdfDialogVisible.value = false;
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
imageSrc.value = URL.createObjectURL(request.response);
imagePreviewVisible.value = true;
}
};
request.send(null);
}
};
// 刷新图片
const refreshImg = (src?: string) => {
getImgSrcByToken(src);
};
// 暴露方法供外部调用
defineExpose({
refreshImg
});
refreshImg,
})
</script>
<style scoped>
.pdf-preview-dialog :deep(.el-dialog__body) {
padding: 20px !important;
overflow-y: hidden !important;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<!-- 组件不占据任何布局空间所有预览组件都是 teleported -->
<div style="display: none;">
<!-- 图片直接使用 el-image-viewer 全屏预览 -->
<el-image-viewer
v-if="!showIframe && imageSrc && imagePreviewVisible"
:url-list="[imageSrc]"
:teleported="true"
hide-on-click-modal
@close="imagePreviewVisible = false"
/>
<!-- PDF dialog 中显示 -->
<el-dialog
v-if="showIframe"
v-model="pdfDialogVisible"
:title="dialogTitle || '文件预览'"
append-to-body
width="90%"
class="pdf-preview-dialog"
>
<iframe ref="authIframeRef" :style="{ width: '100%', height: pdfIframeHeight }" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed, onUnmounted } from 'vue';
import { ElImageViewer } from 'element-plus';
import { Session } from "/@/utils/storage";
// 定义 props
const props = defineProps<{
authSrc: string;
imgWidth?: string;
imgHeight?: string;
dialogTitle?: string;
}>();
// 定义响应式数据
const showIframe = ref(false);
const imageSrc = ref<string>('');
const imagePreviewVisible = ref(false);
const pdfDialogVisible = ref(false);
const authIframeRef = ref<HTMLIFrameElement | null>(null);
const windowHeight = ref(window.innerHeight);
// 计算 PDF iframe 的合适高度(优先使用外部传入的 imgHeight否则根据窗口高度动态计算
const pdfIframeHeight = computed(() => {
// 如果外部传入了 imgHeight优先使用
if (props.imgHeight) {
return props.imgHeight;
}
// 否则根据窗口高度动态计算dialog header 约 50pxpadding 约 40px留一些余量
return `${windowHeight.value - 120}px`;
});
// 监听窗口大小变化
const handleResize = () => {
windowHeight.value = window.innerHeight;
};
onMounted(() => {
window.addEventListener('resize', handleResize);
getImgSrcByToken();
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
// 携带token请求img的src
const getImgSrcByToken = (src?: string) => {
if (props.authSrc.indexOf(".pdf") >= 0) {
showIframe.value = true;
pdfDialogVisible.value = true;
nextTick(() => {
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片处理逻辑:加载后直接打开预览
showIframe.value = false;
pdfDialogVisible.value = false;
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
imageSrc.value = URL.createObjectURL(request.response);
imagePreviewVisible.value = true;
}
};
request.send(null);
}
};
// 刷新图片
const refreshImg = (src?: string) => {
getImgSrcByToken(src);
};
// 暴露方法供外部调用
defineExpose({
refreshImg
});
</script>
<style scoped>
.pdf-preview-dialog :deep(.el-dialog__body) {
padding: 20px !important;
overflow-y: hidden !important;
}
</style>