feat: 添加download路由用于展示文档信息
All checks were successful
deploy to server / build-and-deploy (push) Successful in 5m4s

- 添加/download/documentID路由用于文档下载
- server端添加文档元数据获取与下载API
- 将app中的types移至shared,与server共享
This commit is contained in:
2025-10-27 17:16:51 +08:00
parent 5ab72111ca
commit 4e7131b291
18 changed files with 276 additions and 0 deletions

View File

@ -4,6 +4,7 @@
v-for="(doc, index) in documents" v-for="(doc, index) in documents"
:key="index" :key="index"
class="document-card" class="document-card"
@click="handleClick(doc.fileId)"
> >
<div class="document-info"> <div class="document-info">
<h3>{{ doc.title }}</h3> <h3>{{ doc.title }}</h3>
@ -36,6 +37,15 @@
}, },
}); });
const localePath = useLocalePath();
const handleClick = (id: string) => {
// 获取路由参数
if (id) {
navigateTo(localePath(`/download/${id}`));
}
};
const handleDownload = async (fileName: string, fileUrl: string) => { const handleDownload = async (fileName: string, fileUrl: string) => {
const response = await fetch(fileUrl); const response = await fetch(fileUrl);
const blob = await response.blob(); const blob = await response.blob();
@ -59,6 +69,16 @@
width: 100%; width: 100%;
} }
.document-card {
cursor: pointer;
transition: all 0.3s ease;
}
.document-card:hover {
transform: translateY(-1px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.document-meta { .document-meta {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);

View File

@ -23,6 +23,7 @@ export function toProductDocumentView(
return { return {
id: raw.id, id: raw.id,
fileId: fileId,
filename: file.filename_download, filename: file.filename_download,
title: trans.title, title: trans.title,
url: url, url: url,
@ -68,6 +69,7 @@ export function toDocumentListView(raw: ProductDocument): DocumentListView {
return { return {
id: raw.id, id: raw.id,
fileId: fileId,
filename: file.filename_download, filename: file.filename_download,
title: trans.title, title: trans.title,
url: url, url: url,

View File

@ -25,6 +25,9 @@ export interface DocumentListView {
/** 唯一标识符 **/ /** 唯一标识符 **/
id: number; id: number;
/** 文件UUID **/
fileId: string;
/** 文件名 **/ /** 文件名 **/
filename: string; filename: string;

View File

@ -6,6 +6,9 @@ export interface ProductDocumentView {
/** 唯一标识符 **/ /** 唯一标识符 **/
id: number; id: number;
/** 文件UUID **/
fileId: string;
/** 文件名 **/ /** 文件名 **/
filename: string; filename: string;

133
app/pages/download/[id].vue Normal file
View File

@ -0,0 +1,133 @@
<template>
<div class="page-container">
<div class="page-header">
<h1 class="page-title">文件下载</h1>
<el-breadcrumb class="breadcrumb">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/products')">{{
$t('navigation.downloads')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div v-if="!pending" class="page-content">
<el-card shadow="hover" class="p-4">
<template #header>
<div class="header-content">
<el-icon class="header-icon"><ElIconDocument /></el-icon>
<span class="truncate font-medium">{{
file.filename_download
}}</span>
</div>
</template>
<dl class="text-gray-600 space-y-1 mb-6">
<div>
<dt class="font-semibold inline">类型</dt>
<dd class="inline">{{ file.type }}</dd>
</div>
<div>
<dt class="font-semibold inline">大小</dt>
<dd class="inline">{{ formatFileSize(file.filesize) }}</dd>
</div>
<div>
<dt class="font-semibold inline">上传时间</dt>
<dd class="inline">
{{ new Date(file.uploaded_on).toLocaleDateString() }}
</dd>
</div>
</dl>
<template #footer>
<div class="button-group">
<el-button type="primary" @click="handleDownload">下载</el-button>
<el-button v-if="file.previewable" @click="handlePreview"
>预览</el-button
>
</div>
</template>
</el-card>
</div>
<div v-else>
<el-skeleton :rows="6" animated />
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
// 获取路由参数
const id = computed(() => route.params.id as string);
const {
data: file,
pending,
error,
} = await useFetch<FileMeta>(`/api/file/${id.value}`);
if (error.value || !file.value) {
throw createError({
statusCode: 404,
statusMessage: '文件未找到',
});
}
function handleDownload() {
const link = document.createElement('a');
link.href = `/api/download/${id.value}`;
link.download = file.value?.filename_download ?? 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function handlePreview() {
router.push(`/preview/${id.value}`);
}
</script>
<style scoped>
.page-container {
padding: 2rem;
margin: 0 auto;
max-width: 1200px;
}
.page-header {
display: flex;
}
.page-title {
font-size: 2rem;
font-weight: bold;
color: var(--el-color-primary);
margin-bottom: 1rem;
}
.breadcrumb {
margin-left: auto;
}
.header-content {
display: flex;
align-items: center;
gap: 3px;
margin-bottom: 4px;
}
.header-icon {
font-size: 1.5rem;
margin-right: 0.5rem;
}
.button-group {
display: flex;
justify-content: flex-end;
align-items: baseline;
gap: 1rem;
}
</style>

View File

@ -34,6 +34,7 @@
"support": "Support", "support": "Support",
"about-us": "About Us", "about-us": "About Us",
"contact-info": "Contact Info", "contact-info": "Contact Info",
"downloads": "Downloads",
"faq": "FAQ", "faq": "FAQ",
"documents": "Documents", "documents": "Documents",
"calculator": "Calculator" "calculator": "Calculator"

View File

@ -34,6 +34,7 @@
"support": "服务支持", "support": "服务支持",
"about-us": "关于我们", "about-us": "关于我们",
"contact-info": "联系信息", "contact-info": "联系信息",
"downloads": "文件下载",
"faq": "常见问题", "faq": "常见问题",
"documents": "文档资料", "documents": "文档资料",
"calculator": "纸管计算工具" "calculator": "纸管计算工具"

View File

@ -0,0 +1,22 @@
import { getFileMeta } from '../../utils/file';
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
if (!id) {
throw createError({ statusCode: 400, message: '缺少文件ID' });
}
const file = await getFileMeta(id);
if (!file) {
throw createError({ statusCode: 404, message: '文件不存在' });
}
const res = await $fetch<ArrayBuffer>(file.url, {
responseType: 'arrayBuffer',
});
return new Response(res, {
headers: {
'Content-Disposition': `attachment; filename="${encodeURIComponent(file.filename_download)}"`,
'Content-Type': file.type,
},
});
});

24
server/api/file/[id].ts Normal file
View File

@ -0,0 +1,24 @@
import { getFileMeta } from '../../utils/file';
/**
* 用于处理文件相关的API请求
* 返回指定ID的文件信息
*/
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
if (!id)
throw createError({
statusCode: 400,
statusMessage: 'File ID is required',
});
const file = await getFileMeta(id);
if (!file) {
throw createError({
statusCode: 404,
statusMessage: 'File not found',
});
}
return file;
});

53
server/utils/file.ts Normal file
View File

@ -0,0 +1,53 @@
/**
* 判断文件是否支持预览
*/
export function isPreviewable(mime: string | null | undefined): boolean {
if (!mime || mime === undefined) return false;
return (
mime.startsWith('image/') ||
mime.startsWith('video/') ||
mime === 'application/pdf' ||
mime.startsWith('text/')
);
}
/**
* 从 Directus 获取文件元信息
*/
export async function getFileMeta(id: string): Promise<FileMeta | null> {
const runtimeConfig = useRuntimeConfig();
const baseUrl = runtimeConfig.public.directus.url;
const access_token = runtimeConfig.public.directus.token;
try {
const response = await $fetch<{ data: DirectusFile }>(
`${baseUrl}/files/${id}`,
{
headers: {
Authorization: access_token ? `Bearer ${access_token}` : '',
},
}
);
const file = response.data;
if (!file) return null;
return {
id: file.id,
title: file.filename_disk ?? '',
filename_download: file.filename_download ?? '',
type: file.type ?? '',
filesize: Number(file.filesize),
width: file.width ?? undefined,
height: file.height ?? undefined,
uploaded_on: file.uploaded_on ?? undefined,
url: `${baseUrl}/assets/${file.id}`,
previewable: isPreviewable(file.type),
};
} catch (error) {
if (error instanceof Error) {
console.error('Error fetching file metadata:', error.message);
}
return null;
}
}

12
shared/types/file.ts Normal file
View File

@ -0,0 +1,12 @@
export interface FileMeta {
id: string;
title: string;
filename_download: string;
type: string;
filesize: number;
width?: number;
height?: number;
uploaded_on?: string;
url: string;
previewable: boolean;
}

2
shared/types/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './directus';
export * from './meilisearch';