feat: 添加preview路由用于文件预览
All checks were successful
deploy to server / build-and-deploy (push) Successful in 2m59s
All checks were successful
deploy to server / build-and-deploy (push) Successful in 2m59s
- preview路由预览文件 - FilePreviewer组件 - 删除Document卡片的下载按钮,使用单独的页面用于文件下载 - preview布局
This commit is contained in:
@ -16,13 +16,6 @@
|
|||||||
>格式:
|
>格式:
|
||||||
{{ formatFileExtension(getFileExtension(doc.filename)) }}</span
|
{{ formatFileExtension(getFileExtension(doc.filename)) }}</span
|
||||||
>
|
>
|
||||||
<el-button
|
|
||||||
class="download-button"
|
|
||||||
type="primary"
|
|
||||||
@click="handleDownload(doc.title, doc.url)"
|
|
||||||
>
|
|
||||||
下载
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
177
app/components/FilePreviewer.vue
Normal file
177
app/components/FilePreviewer.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<section class="h-screen flex flex-col">
|
||||||
|
<!-- 头部工具栏 -->
|
||||||
|
<header
|
||||||
|
v-if="showToolbar && fileMeta"
|
||||||
|
class="p-3 border-b flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h2 class="truncate font-medium" :title="fileMeta.filename_download">
|
||||||
|
{{ fileMeta.filename_download }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{{ fileMeta.type }} · {{ formatedSize }} · {{ formatedDate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 rounded border hover:bg-gray-50"
|
||||||
|
type="button"
|
||||||
|
:disabled="!fileMeta"
|
||||||
|
@click="openInNewTab"
|
||||||
|
>
|
||||||
|
在新标签打开
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
type="button"
|
||||||
|
:disabled="!fileMeta"
|
||||||
|
@click="download"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="pending" class="h-48 grid place-items-center border rounded">
|
||||||
|
正在加载...
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="errorText"
|
||||||
|
class="h-48 grid place-items-center border rounded text-red-600"
|
||||||
|
>
|
||||||
|
{{ errorText }}
|
||||||
|
</div>
|
||||||
|
<!-- 文件预览 -->
|
||||||
|
<ClientOnly v-else>
|
||||||
|
<div
|
||||||
|
v-if="fileMeta && previewable"
|
||||||
|
class="h-full w-full flex justify-center bg-gray-50"
|
||||||
|
>
|
||||||
|
<!-- 图片 -->
|
||||||
|
<el-image
|
||||||
|
v-if="isImage"
|
||||||
|
fit="contain"
|
||||||
|
class="max-w-full max-h-full select-none"
|
||||||
|
:src="src"
|
||||||
|
:alt="fileMeta.title || fileMeta.filename_download"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- PDF -->
|
||||||
|
<iframe
|
||||||
|
v-else-if="isPdf"
|
||||||
|
:src="src"
|
||||||
|
title="PDF 预览"
|
||||||
|
class="w-full h-full border-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 视频 -->
|
||||||
|
<video
|
||||||
|
v-else-if="isVideo"
|
||||||
|
:src="src"
|
||||||
|
controls
|
||||||
|
class="w-full bg-black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 文本(简单方式用 iframe;如需代码高亮可改为拉取文本并渲染 <pre>) -->
|
||||||
|
<iframe
|
||||||
|
v-else-if="isText"
|
||||||
|
:src="src"
|
||||||
|
title="文本预览"
|
||||||
|
class="w-full h-full border-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 预览的文件 ID */
|
||||||
|
fileId?: string;
|
||||||
|
file?: FileMeta;
|
||||||
|
|
||||||
|
/** 是否显示上方工具栏(文件名、大小、按钮) */
|
||||||
|
showToolbar?: boolean;
|
||||||
|
|
||||||
|
/** 下载 API 基础路径(你的后端流接口),用于“下载”按钮 */
|
||||||
|
downloadApiBase?: string;
|
||||||
|
|
||||||
|
/** 追加到 file.url 的查询(如临时 token),形如 { token: 'xxx' } */
|
||||||
|
extraQuery?: Record<string, string | number | boolean>;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
fileId: undefined,
|
||||||
|
file: undefined,
|
||||||
|
showToolbar: true,
|
||||||
|
downloadApiBase: '/api/download',
|
||||||
|
extraQuery: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, pending, error } = await useFetch<FileMeta>(
|
||||||
|
() => (props.file ? null : `/api/file/${props.fileId}`),
|
||||||
|
{ server: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorText = computed(() => error.value?.message ?? null);
|
||||||
|
const fileMeta = computed(() => props.file ?? data.value ?? null);
|
||||||
|
|
||||||
|
/** 预览源地址:支持在 file.url 上追加额外 query(如临时 token、inline) */
|
||||||
|
const src = computed<string>(() => {
|
||||||
|
if (!fileMeta.value) return '';
|
||||||
|
const url = new URL(fileMeta.value.url, window?.location?.origin);
|
||||||
|
if (props.extraQuery) {
|
||||||
|
Object.entries(props.extraQuery).forEach(([k, v]) =>
|
||||||
|
url.searchParams.set(k, String(v))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 类型判定 */
|
||||||
|
const isImage = computed(
|
||||||
|
() => fileMeta.value?.type.startsWith('image/') === true
|
||||||
|
);
|
||||||
|
const isPdf = computed(() => fileMeta.value?.type === 'application/pdf');
|
||||||
|
const isVideo = computed(
|
||||||
|
() => fileMeta.value?.type.startsWith('video/') === true
|
||||||
|
);
|
||||||
|
const isText = computed(
|
||||||
|
() => fileMeta.value?.type.startsWith('text/') === true
|
||||||
|
);
|
||||||
|
const previewable = computed(() => fileMeta.value?.previewable === true);
|
||||||
|
|
||||||
|
const formatedSize = computed(() => {
|
||||||
|
const size = fileMeta.value?.filesize ?? 0;
|
||||||
|
return formatFileSize(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatedDate = computed(() => {
|
||||||
|
if (!fileMeta.value?.uploaded_on) return '';
|
||||||
|
return new Date(fileMeta.value.uploaded_on).toLocaleDateString();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 下载动作:走你自己的流式后端,避免直链暴露(便于权限与统计) */
|
||||||
|
function download(): void {
|
||||||
|
if (!fileMeta.value) return;
|
||||||
|
const id = fileMeta.value.id;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = `${props.downloadApiBase}/${encodeURIComponent(id)}`;
|
||||||
|
a.download = fileMeta.value.filename_download;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新标签打开(直接访问直链,适合预览失败时的兜底体验) */
|
||||||
|
function openInNewTab(): void {
|
||||||
|
if (!src.value) return;
|
||||||
|
window.open(src.value, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
5
app/layouts/preview.vue
Normal file
5
app/layouts/preview.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
13
app/pages/preview/[id].vue
Normal file
13
app/pages/preview/[id].vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<file-previewer :file-id="id" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'preview',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const id = computed(() => route.params.id as string);
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user