240 lines
5.8 KiB
Vue
240 lines
5.8 KiB
Vue
<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>
|