|
@@ -0,0 +1,390 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="document-preview">
|
|
|
|
+ <!-- 文档列表 -->
|
|
|
|
+ <div class="document-list">
|
|
|
|
+ <div v-if="documents.length === 0" class="empty-state">
|
|
|
|
+ <FileOutlined class="empty-icon" />
|
|
|
|
+ <p>暂无文档</p>
|
|
|
|
+ </div>
|
|
|
|
+ <div v-else v-for="doc in documents" :key="doc.name" class="document-item">
|
|
|
|
+ <div class="document-info" @click="handleSelect(doc)">
|
|
|
|
+ <!-- 文档图标 -->
|
|
|
|
+ <div class="doc-icon">
|
|
|
|
+ <img
|
|
|
|
+ v-if="isImageFile(doc)"
|
|
|
|
+ :src="doc.url"
|
|
|
|
+ class="preview-thumbnail"
|
|
|
|
+ alt="preview"
|
|
|
|
+ />
|
|
|
|
+ <video
|
|
|
|
+ v-else-if="isVideoFile(doc)"
|
|
|
|
+ :src="doc.url"
|
|
|
|
+ class="preview-thumbnail"
|
|
|
|
+ preload="metadata"
|
|
|
|
+ />
|
|
|
|
+ <img
|
|
|
|
+ v-else
|
|
|
|
+ :src="getFileIcon(doc)"
|
|
|
|
+ alt="file"
|
|
|
|
+ class="file-icon"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 文档信息 -->
|
|
|
|
+ <div class="info">
|
|
|
|
+ <div class="name" :title="doc.name">{{ doc.name }}</div>
|
|
|
|
+ <div class="size">{{ doc.sizeFormatted || formatFileSize(doc.size) }}</div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 操作按钮 -->
|
|
|
|
+ <div class="actions">
|
|
|
|
+ <button class="action-btn" @click="handlePreview(doc)" title="预览">
|
|
|
|
+ <EyeOutlined />
|
|
|
|
+ </button>
|
|
|
|
+ <button class="action-btn" @click="handleDownload(doc)" title="下载">
|
|
|
|
+ <DownloadOutlined />
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 预览弹窗 -->
|
|
|
|
+ <div v-if="previewVisible" class="preview-modal" @click="closePreview">
|
|
|
|
+ <div class="preview-content" @click.stop>
|
|
|
|
+ <div class="preview-header">
|
|
|
|
+ <span>{{ currentDoc?.name }}</span>
|
|
|
|
+ <button class="close-btn" @click="closePreview">
|
|
|
|
+ <CloseOutlined />
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="preview-body">
|
|
|
|
+ <!-- 图片预览 -->
|
|
|
|
+ <img
|
|
|
|
+ v-if="isImageFile(currentDoc)"
|
|
|
|
+ :src="currentDoc?.url"
|
|
|
|
+ class="preview-image"
|
|
|
|
+ alt="preview"
|
|
|
|
+ />
|
|
|
|
+ <!-- 视频预览 -->
|
|
|
|
+ <video
|
|
|
|
+ v-else-if="isVideoFile(currentDoc)"
|
|
|
|
+ :src="currentDoc?.url"
|
|
|
|
+ controls
|
|
|
|
+ class="preview-video"
|
|
|
|
+ />
|
|
|
|
+ <!-- 其他文件预览 -->
|
|
|
|
+ <div v-else class="file-preview">
|
|
|
|
+ <FileOutlined class="large-icon" />
|
|
|
|
+ <p>该文件类型暂不支持预览</p>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script>
|
|
|
|
+import { defineComponent, ref } from 'vue';
|
|
|
|
+import {
|
|
|
|
+ FileOutlined,
|
|
|
|
+ EyeOutlined,
|
|
|
|
+ DownloadOutlined,
|
|
|
|
+ CloseOutlined
|
|
|
|
+} from '@ant-design/icons-vue';
|
|
|
|
+
|
|
|
|
+export default defineComponent({
|
|
|
|
+ name: 'DocumentPreview',
|
|
|
|
+ components: {
|
|
|
|
+ FileOutlined,
|
|
|
|
+ EyeOutlined,
|
|
|
|
+ DownloadOutlined,
|
|
|
|
+ CloseOutlined
|
|
|
|
+ },
|
|
|
|
+ props: {
|
|
|
|
+ documents: {
|
|
|
|
+ type: Array,
|
|
|
|
+ default: () => []
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ emits: ['select'],
|
|
|
|
+ setup(props, { emit }) {
|
|
|
|
+ const previewVisible = ref(false);
|
|
|
|
+ const currentDoc = ref(null);
|
|
|
|
+
|
|
|
|
+ const isImageFile = (file) => {
|
|
|
|
+ if (!file) return false;
|
|
|
|
+ const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
|
|
+ const ext = file.name?.split('.')?.pop()?.toLowerCase() || '';
|
|
|
|
+ return imageTypes.includes(ext);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const isVideoFile = (file) => {
|
|
|
|
+ if (!file) return false;
|
|
|
|
+ const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
|
|
+ const ext = file.name?.split('.')?.pop()?.toLowerCase() || '';
|
|
|
|
+ return videoTypes.includes(ext);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const getFileIcon = (file) => {
|
|
|
|
+ // 根据文件类型返回对应的图标
|
|
|
|
+ return '/src/assets/svg/word.svg'; // 默认文档图标
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const formatFileSize = (bytes) => {
|
|
|
|
+ if (!bytes || isNaN(bytes)) return '0 B';
|
|
|
|
+ const k = 1024;
|
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const handleSelect = (doc) => {
|
|
|
|
+ emit('select', doc);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const handlePreview = (doc) => {
|
|
|
|
+ currentDoc.value = doc;
|
|
|
|
+ previewVisible.value = true;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const handleDownload = (doc) => {
|
|
|
|
+ if (doc.url) {
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
+ link.href = doc.url;
|
|
|
|
+ link.download = doc.name;
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
+ link.click();
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const closePreview = () => {
|
|
|
|
+ previewVisible.value = false;
|
|
|
|
+ currentDoc.value = null;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ previewVisible,
|
|
|
|
+ currentDoc,
|
|
|
|
+ isImageFile,
|
|
|
|
+ isVideoFile,
|
|
|
|
+ getFileIcon,
|
|
|
|
+ formatFileSize,
|
|
|
|
+ handleSelect,
|
|
|
|
+ handlePreview,
|
|
|
|
+ handleDownload,
|
|
|
|
+ closePreview
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+});
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="less" scoped>
|
|
|
|
+.document-preview {
|
|
|
|
+ height: 100%;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.document-list {
|
|
|
|
+ flex: 1;
|
|
|
|
+ overflow-y: auto;
|
|
|
|
+ padding: 16px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.empty-state {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ padding: 48px 0;
|
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
|
+
|
|
|
|
+ .empty-icon {
|
|
|
|
+ font-size: 48px;
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ p {
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ margin: 0;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.document-item {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ padding: 12px;
|
|
|
|
+ border-radius: 8px;
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
+ background: #fff;
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.document-info {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ gap: 12px;
|
|
|
|
+ flex: 1;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.doc-icon {
|
|
|
|
+ width: 40px;
|
|
|
|
+ height: 40px;
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-thumbnail {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ object-fit: cover;
|
|
|
|
+ border-radius: 4px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-icon {
|
|
|
|
+ width: 24px;
|
|
|
|
+ height: 24px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.info {
|
|
|
|
+ flex: 1;
|
|
|
|
+ min-width: 0;
|
|
|
|
+
|
|
|
|
+ .name {
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ color: rgba(0, 0, 0, 0.85);
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
+ white-space: nowrap;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .size {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.actions {
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 8px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.action-btn {
|
|
|
|
+ width: 32px;
|
|
|
|
+ height: 32px;
|
|
|
|
+ border: none;
|
|
|
|
+ background: transparent;
|
|
|
|
+ border-radius: 4px;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background: rgba(0, 0, 0, 0.04);
|
|
|
|
+ color: #1677ff;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-modal {
|
|
|
|
+ position: fixed;
|
|
|
|
+ top: 0;
|
|
|
|
+ left: 0;
|
|
|
|
+ right: 0;
|
|
|
|
+ bottom: 0;
|
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ z-index: 1000;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-content {
|
|
|
|
+ background: #fff;
|
|
|
|
+ border-radius: 8px;
|
|
|
|
+ width: 90%;
|
|
|
|
+ max-width: 800px;
|
|
|
|
+ max-height: 90vh;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-header {
|
|
|
|
+ padding: 16px;
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+
|
|
|
|
+ span {
|
|
|
|
+ font-size: 16px;
|
|
|
|
+ font-weight: 500;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .close-btn {
|
|
|
|
+ border: none;
|
|
|
|
+ background: transparent;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ padding: 8px;
|
|
|
|
+ border-radius: 4px;
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background: rgba(0, 0, 0, 0.04);
|
|
|
|
+ color: rgba(0, 0, 0, 0.88);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-body {
|
|
|
|
+ flex: 1;
|
|
|
|
+ overflow: auto;
|
|
|
|
+ padding: 24px;
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-image {
|
|
|
|
+ max-width: 100%;
|
|
|
|
+ max-height: 70vh;
|
|
|
|
+ object-fit: contain;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.preview-video {
|
|
|
|
+ max-width: 100%;
|
|
|
|
+ max-height: 70vh;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-preview {
|
|
|
|
+ text-align: center;
|
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
|
+
|
|
|
|
+ .large-icon {
|
|
|
|
+ font-size: 64px;
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ p {
|
|
|
|
+ margin: 0;
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</style>
|