feat: 添加download路由用于展示文档信息
All checks were successful
deploy to server / build-and-deploy (push) Successful in 5m4s
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:
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -25,6 +25,9 @@ export interface DocumentListView {
|
|||||||
/** 唯一标识符 **/
|
/** 唯一标识符 **/
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
/** 文件UUID **/
|
||||||
|
fileId: string;
|
||||||
|
|
||||||
/** 文件名 **/
|
/** 文件名 **/
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|
||||||
|
|||||||
@ -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
133
app/pages/download/[id].vue
Normal 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>
|
||||||
@ -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"
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
"support": "服务支持",
|
"support": "服务支持",
|
||||||
"about-us": "关于我们",
|
"about-us": "关于我们",
|
||||||
"contact-info": "联系信息",
|
"contact-info": "联系信息",
|
||||||
|
"downloads": "文件下载",
|
||||||
"faq": "常见问题",
|
"faq": "常见问题",
|
||||||
"documents": "文档资料",
|
"documents": "文档资料",
|
||||||
"calculator": "纸管计算工具"
|
"calculator": "纸管计算工具"
|
||||||
|
|||||||
22
server/api/download/[id].ts
Normal file
22
server/api/download/[id].ts
Normal 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
24
server/api/file/[id].ts
Normal 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
53
server/utils/file.ts
Normal 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
12
shared/types/file.ts
Normal 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
2
shared/types/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './directus';
|
||||||
|
export * from './meilisearch';
|
||||||
Reference in New Issue
Block a user