458 lines
11 KiB
Vue
458 lines
11 KiB
Vue
<template>
|
||
<view class="basic-file-preview">
|
||
<view class="section-title">附件</view>
|
||
<view class="file-list" v-if="hasAttachments">
|
||
<!-- 显示所有解析后的附件 -->
|
||
<view
|
||
v-for="(file, index) in parsedAttachments"
|
||
:key="(file as any).id || index"
|
||
class="file-item"
|
||
>
|
||
<view class="file-icon">
|
||
<image
|
||
:src="getFileIcon(getFileSuffix(file))"
|
||
class="icon-image"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
<view class="file-info">
|
||
<text class="file-name">{{ getFileName(file) }}</text>
|
||
</view>
|
||
<view class="file-actions">
|
||
<u-button
|
||
v-if="canPreview(getFileSuffix(file)) && !isVideo(getFileSuffix(file))"
|
||
text="预览"
|
||
size="mini"
|
||
type="primary"
|
||
@click="previewFile(file)"
|
||
/>
|
||
<u-button
|
||
v-if="isVideo(getFileSuffix(file))"
|
||
text="播放"
|
||
size="mini"
|
||
type="success"
|
||
@click="previewFile(file)"
|
||
/>
|
||
<u-button
|
||
v-if="isImage(getFileSuffix(file))"
|
||
text="查看"
|
||
size="mini"
|
||
type="warning"
|
||
@click="previewFile(file)"
|
||
/>
|
||
<u-button
|
||
text="下载"
|
||
size="mini"
|
||
type="info"
|
||
@click="downloadFileAction(file)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="no-files">
|
||
<text>暂无附件</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue';
|
||
import {
|
||
isVideo,
|
||
isImage,
|
||
canPreview,
|
||
previewFile as previewFileUtil,
|
||
previewVideo as previewVideoUtil,
|
||
previewImage as previewImageUtil,
|
||
downloadFile as downloadFileUtil
|
||
} from "@/utils/filePreview";
|
||
import { imagUrl } from "@/utils";
|
||
|
||
// 定义组件属性
|
||
interface Props {
|
||
fileUrl?: string;
|
||
fileName?: string;
|
||
fileFormat?: string;
|
||
files?: FileInfo[];
|
||
}
|
||
|
||
interface FileInfo {
|
||
id?: string;
|
||
name: string;
|
||
size: number;
|
||
url: string;
|
||
resourName?: string;
|
||
resourUrl?: string;
|
||
resSuf?: string;
|
||
}
|
||
|
||
// 定义默认属性值
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
fileUrl: '',
|
||
fileName: '',
|
||
fileFormat: '',
|
||
files: () => []
|
||
});
|
||
|
||
// 定义事件
|
||
const emit = defineEmits<{
|
||
(e: 'preview', file: FileInfo): void;
|
||
(e: 'download', file: FileInfo): void;
|
||
}>();
|
||
|
||
// 检查是否有附件
|
||
const hasAttachments = computed(() => {
|
||
const attachments = [];
|
||
|
||
// 处理files数组
|
||
if (props.files && props.files.length > 0) {
|
||
attachments.push(...props.files);
|
||
}
|
||
// 处理逗号分隔的fileUrl字符串
|
||
else if (props.fileUrl) {
|
||
const fileUrls = props.fileUrl.split(',').map(url => url.trim()).filter(url => url);
|
||
|
||
// 解析fileName字符串(如果存在)
|
||
let fileNames: string[] = [];
|
||
if (props.fileName) {
|
||
fileNames = props.fileName.split(',').map(name => name.trim()).filter(name => name);
|
||
}
|
||
|
||
// 将URL字符串转换为文件对象
|
||
fileUrls.forEach((url, index) => {
|
||
// 优先使用解析出的文件名,如果没有则从URL提取
|
||
let displayName = fileNames[index] || url.split('/').pop() || `文件${index + 1}`;
|
||
const fileFormat = url.split('.').pop() || 'unknown';
|
||
|
||
// 如果文件名包含扩展名,去掉扩展名(保持resourName不包含扩展名)
|
||
let resourName = displayName;
|
||
if (displayName.includes('.' + fileFormat)) {
|
||
resourName = displayName.replace('.' + fileFormat, '');
|
||
}
|
||
|
||
attachments.push({
|
||
id: `file_${index}`,
|
||
name: displayName,
|
||
url: url,
|
||
resourUrl: url,
|
||
resourName: resourName,
|
||
resSuf: fileFormat,
|
||
size: 0
|
||
});
|
||
});
|
||
}
|
||
|
||
return attachments.length > 0;
|
||
});
|
||
|
||
// 计算属性:解析后的附件列表
|
||
const parsedAttachments = computed(() => {
|
||
const attachments = [];
|
||
// 处理files数组
|
||
if (props.files && props.files.length > 0) {
|
||
attachments.push(...props.files);
|
||
}
|
||
// 处理逗号分隔的fileUrl字符串
|
||
else if (props.fileUrl) {
|
||
const fileUrls = props.fileUrl.split(',').map(url => url.trim()).filter(url => url);
|
||
|
||
// 解析fileName字符串(如果存在)
|
||
let fileNames: string[] = [];
|
||
if (props.fileName) {
|
||
fileNames = props.fileName.split(',').map(name => name.trim()).filter(name => name);
|
||
}
|
||
|
||
// 将URL字符串转换为文件对象
|
||
fileUrls.forEach((url, index) => {
|
||
// 优先使用解析出的文件名,如果没有则从URL提取
|
||
let displayName = fileNames[index] || url.split('/').pop() || `文件${index + 1}`;
|
||
const fileFormat = url.split('.').pop() || 'unknown';
|
||
|
||
// 如果文件名包含扩展名,去掉扩展名(保持resourName不包含扩展名)
|
||
let resourName = displayName;
|
||
if (displayName.includes('.' + fileFormat)) {
|
||
resourName = displayName.replace('.' + fileFormat, '');
|
||
}
|
||
|
||
attachments.push({
|
||
id: `file_${index}`,
|
||
name: displayName,
|
||
url: url,
|
||
resourUrl: url,
|
||
resourName: resourName,
|
||
resSuf: fileFormat,
|
||
size: 0
|
||
});
|
||
});
|
||
|
||
console.log('parsedAttachments', fileNames, fileUrls);
|
||
}
|
||
|
||
return attachments;
|
||
});
|
||
|
||
// 获取文件名
|
||
const getFileName = (file: FileInfo) => {
|
||
if (file.resourName) {
|
||
return file.resourName;
|
||
}
|
||
if (file.name) {
|
||
// 如果name包含扩展名,去掉扩展名
|
||
const lastDotIndex = file.name.lastIndexOf('.');
|
||
if (lastDotIndex > 0) {
|
||
return file.name.substring(0, lastDotIndex);
|
||
}
|
||
return file.name;
|
||
}
|
||
return '未知文件';
|
||
};
|
||
|
||
// 获取文件后缀
|
||
const getFileSuffix = (file: FileInfo) => {
|
||
if (file.resSuf) {
|
||
return file.resSuf;
|
||
}
|
||
if (file.url) {
|
||
const lastDotIndex = file.url.lastIndexOf('.');
|
||
if (lastDotIndex > 0) {
|
||
return file.url.substring(lastDotIndex + 1);
|
||
}
|
||
}
|
||
if (file.name) {
|
||
const lastDotIndex = file.name.lastIndexOf('.');
|
||
if (lastDotIndex > 0) {
|
||
return file.name.substring(lastDotIndex + 1);
|
||
}
|
||
}
|
||
return 'unknown';
|
||
};
|
||
|
||
// 获取文件图标
|
||
const getFileIcon = (fileType: string) => {
|
||
const type = fileType.toLowerCase();
|
||
switch (type) {
|
||
case 'pdf':
|
||
return '/static/base/view/pdf.png';
|
||
case 'doc':
|
||
case 'docx':
|
||
return '/static/base/view/word.png';
|
||
case 'xls':
|
||
case 'xlsx':
|
||
return '/static/base/view/excel.png';
|
||
case 'ppt':
|
||
case 'pptx':
|
||
return '/static/base/view/ppt.png';
|
||
case 'zip':
|
||
case 'rar':
|
||
case '7z':
|
||
return '/static/base/view/zip.png';
|
||
default:
|
||
return '/static/base/view/more.png';
|
||
}
|
||
};
|
||
|
||
// 文件预览
|
||
const previewFile = (file: FileInfo) => {
|
||
emit('preview', file);
|
||
|
||
// 确定文件URL和名称
|
||
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
|
||
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
|
||
const fileSuf = file.resSuf || file.name.split('.').pop() || '';
|
||
|
||
// 根据文件类型选择预览方式
|
||
if (isVideo(fileSuf)) {
|
||
handlePreviewVideo(file);
|
||
} else if (isImage(fileSuf)) {
|
||
handlePreviewImage(file);
|
||
} else if (canPreview(fileSuf)) {
|
||
handlePreviewDocument(file);
|
||
} else {
|
||
// 不支持预览的文件类型,直接下载
|
||
downloadFileAction(file);
|
||
}
|
||
};
|
||
|
||
// 文档预览
|
||
const handlePreviewDocument = (file: FileInfo) => {
|
||
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
|
||
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
|
||
const fileSuf = file.resSuf || file.name.split('.').pop() || '';
|
||
|
||
previewFileUtil(fileUrl, fileName, fileSuf)
|
||
.catch((error: any) => {
|
||
// 预览失败时尝试下载
|
||
downloadFileAction(file);
|
||
});
|
||
};
|
||
|
||
// 视频预览
|
||
const handlePreviewVideo = (file: FileInfo) => {
|
||
const videoUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
|
||
const videoName = file.resourName || file.name;
|
||
|
||
previewVideoUtil(videoUrl, videoName)
|
||
.catch((error: any) => {
|
||
// 视频预览失败时的处理
|
||
});
|
||
};
|
||
|
||
// 图片预览
|
||
const handlePreviewImage = (file: FileInfo) => {
|
||
const imageUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
|
||
|
||
previewImageUtil(imageUrl)
|
||
.catch((error: any) => {
|
||
// 图片预览失败时的处理
|
||
});
|
||
};
|
||
|
||
// 文件下载
|
||
const downloadFileAction = (file: FileInfo) => {
|
||
emit('download', file);
|
||
|
||
const finalUrl = file.resourUrl ? imagUrl(file.resourUrl) : imagUrl(file.url);
|
||
// 获取原始文件名
|
||
const originalFileName = getOriginalFileName(file);
|
||
|
||
// 调用下载工具函数
|
||
downloadFileUtil(finalUrl, originalFileName)
|
||
.then(() => {
|
||
uni.showToast({
|
||
title: "下载成功",
|
||
icon: "success",
|
||
});
|
||
})
|
||
.catch((error) => {
|
||
console.error('下载失败:', error);
|
||
uni.showToast({
|
||
title: "下载失败",
|
||
icon: "error",
|
||
});
|
||
});
|
||
};
|
||
|
||
// 获取原始文件名
|
||
const getOriginalFileName = (file: FileInfo) => {
|
||
// 优先使用resourName,如果没有则使用name,确保文件名正确
|
||
let fileName = '';
|
||
if (file.resourName) {
|
||
// 如果resourName已经包含扩展名,直接使用
|
||
if (file.resSuf && !file.resourName.includes('.' + file.resSuf)) {
|
||
fileName = `${file.resourName}.${file.resSuf}`;
|
||
} else {
|
||
fileName = file.resourName;
|
||
}
|
||
} else if (file.name) {
|
||
// 如果name已经包含扩展名,直接使用
|
||
if (file.resSuf && !file.name.includes('.' + file.resSuf)) {
|
||
fileName = `${file.name}.${file.resSuf}`;
|
||
} else {
|
||
fileName = file.name;
|
||
}
|
||
} else {
|
||
// 最后备用方案,从URL提取文件名
|
||
const urlFileName = file.url.split('/').pop() || '未知文件';
|
||
fileName = urlFileName;
|
||
}
|
||
|
||
return fileName;
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.basic-file-preview {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
border-bottom: 2px solid #007aff;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px;
|
||
border: 1px solid #eee;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
background: #fafafa;
|
||
transition: all 0.3s ease;
|
||
|
||
&:active {
|
||
transform: translateY(1px);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.file-icon {
|
||
margin-right: 12px;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.icon-image {
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
}
|
||
|
||
.file-info {
|
||
flex: 1;
|
||
|
||
.file-name {
|
||
display: block;
|
||
font-weight: 500;
|
||
margin-bottom: 4px;
|
||
color: #333;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.file-type {
|
||
font-size: 11px;
|
||
color: #999;
|
||
background: #f0f0f0;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
|
||
.u-button {
|
||
min-width: 20px;
|
||
height: 26px;
|
||
font-size: 12px;
|
||
border-radius: 13px;
|
||
padding: 0 6px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.no-files {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
</style> |