Files
cz-web/src/components/FilePreviewDialog.vue
2026-10-01 17:24:57 +08:00

240 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { AppButton } from '@/components'
import { useOssStore } from '@/stores/modules/oss'
import { PREVIEW_TEXT_MAX_BYTES, formatBytes } from '@/utils/filePreview'
const ossStore = useOssStore()
const { visible, file, kind, objectUrl, textContent, loading, error, truncated, totalBytes } = storeToRefs(ossStore)
function onClose(): void {
ossStore.closePreview()
}
function onDownload(): void {
const target = file.value
if (!target?.url) return
const link = document.createElement('a')
link.href = target.url
link.download = target.name
link.target = '_blank'
link.rel = 'noopener'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>
<template>
<Teleport to="body">
<div v-if="visible" class="preview-mask" @click.self="onClose">
<div class="preview-dialog">
<header class="preview-dialog__head">
<span class="preview-dialog__name" :title="file?.name">{{ file?.name }}</span>
<button class="preview-dialog__close" type="button" aria-label="关闭" @click="onClose">
×
</button>
</header>
<div class="preview-dialog__body">
<div v-if="loading" class="preview-dialog__center">
预览加载中…
</div>
<div v-else-if="error" class="preview-dialog__center">
<p class="preview-dialog__msg">{{ error }}</p>
<AppButton v-if="file?.url" type="primary" @click="onDownload">
下载文件
</AppButton>
</div>
<div v-else-if="kind === 'unsupported'" class="preview-dialog__center">
<p class="preview-dialog__msg">
当前格式({{ (file?.name ?? '').split('.').pop() }})不支持在线预览,请下载后查看。
</p>
<AppButton v-if="file?.url" type="primary" @click="onDownload">
下载文件
</AppButton>
</div>
<img
v-else-if="kind === 'image' && objectUrl"
:src="objectUrl"
class="preview-dialog__image"
alt="预览"
>
<iframe
v-else-if="kind === 'pdf' && objectUrl"
:src="objectUrl"
class="preview-dialog__frame"
title="预览"
/>
<video
v-else-if="kind === 'video' && objectUrl"
:src="objectUrl"
class="preview-dialog__media"
controls
/>
<audio
v-else-if="kind === 'audio' && objectUrl"
:src="objectUrl"
class="preview-dialog__media"
controls
/>
<div v-else-if="kind === 'text'" class="preview-dialog__text-wrap">
<p v-if="truncated" class="preview-dialog__hint">
<span>文件较大({{ formatBytes(totalBytes ?? 0) }}),仅预览前 {{ formatBytes(PREVIEW_TEXT_MAX_BYTES) }},完整内容请下载。</span>
<AppButton v-if="file?.url" type="primary" @click="onDownload">下载文件</AppButton>
</p>
<pre class="preview-dialog__text">{{ textContent }}</pre>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<style lang="less" scoped>
.preview-mask {
position: fixed;
inset: 0;
z-index: 2000;
.flex(row, center);
background: rgba(0, 0, 0, 0.55);
padding: @spacing-lg;
overflow: auto;
}
.preview-dialog {
width: min(960px, 100%);
max-height: calc(100vh - @spacing-xxl);
.flex(column, flex-start, stretch);
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-lg;
overflow: hidden;
&__head {
.flex(row, space-between, center);
flex-shrink: 0;
gap: @spacing-sm;
padding: @spacing-base @spacing-lg;
border-bottom: 1px solid @border-color;
}
&__name {
min-width: 0;
font-size: @font-size-base;
font-weight: 600;
color: @heading-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__close {
flex-shrink: 0;
width: 28px;
height: 28px;
border: none;
border-radius: @radius-base;
background: transparent;
color: @text-color-secondary;
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: background 0.2s, color 0.2s;
&:hover {
background: @bg-color-light;
color: @text-color;
}
}
&__body {
flex: 1;
min-height: 0;
.flex(column, flex-start, stretch);
padding: @spacing-base;
overflow: auto;
}
&__center {
.flex(column, center, center);
gap: @spacing-base;
width: 100%;
min-height: 240px;
}
&__msg {
font-size: @font-size-base;
color: @text-color-secondary;
text-align: center;
}
&__image {
max-width: 100%;
max-height: 70vh;
margin: auto;
object-fit: contain;
border-radius: @radius-base;
}
&__frame {
flex: 1;
width: 100%;
min-height: 60vh;
border: none;
}
&__media {
width: 100%;
max-height: 70vh;
margin: auto;
}
&__text-wrap {
display: flex;
flex-direction: column;
width: 100%;
min-height: 0;
}
&__hint {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: @spacing-sm;
flex-shrink: 0;
margin: 0 0 @spacing-sm;
padding: @spacing-sm @spacing-base;
border-radius: @radius-base;
background: #fff7e6;
border: 1px solid #ffd591;
color: #ad6800;
font-size: @font-size-sm;
line-height: 1.6;
}
&__text {
width: 100%;
margin: 0;
padding: @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
color: @text-color;
font-size: @font-size-sm;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
max-height: 70vh;
overflow: auto;
}
}
</style>