vue3和uniapp(vant4)倒计时和触底组件

vue3:

<viewDocument ref="viewDocumentRef" :if-upload="'viewDocument'" :finish-click="handlefinishClick"/>
import viewDocument from '@/components/ViewDocument/viewDocument.vue'

const viewDocumentRef = ref()
viewDocumentRef.value.open('1767591852797.pdf', 5)
//完成
let handlefinishClick = (status)=>{
  console.log(status,'完成')
  // emit('zdyFunction')
  // if(status == 'close'){}
}
<template>
    <Dialog title="" v-model="dialogVisible" width="1000" lock-scroll style="height: 90vh;max-height: 100vh;margin-top: 4vh;" :fullscreen="false" @close="cancleFn" append-to-body>
        <div id="scrollable-element" style="height: calc(90vh - 166px); overflow: auto;">
            <iframe :src="urlEle" frameborder="0" style="height: calc(90vh - 166px);" width="100%" v-if="showEle == 'txt'">
                This browser does not support PDFs. Please download the PDF to view it
            </iframe>
            <officeWj ref="officeWjRef" :type="type" :src="urlEle" :height="'calc(90vh - 166px)'" :noUbload="ifUpload" :courseProgressData="courseProgressData" @show-yxw-menu="showYxwFlog=true" @document-view="chudiFn" v-if="showEle == 'office'" />
            
            <video
                :id="playerId"
                ref="videoPlayer"
                v-show="showEle == 'video'"
                class="video-js vjs-big-play-centered"
                @ended="handleEnded"
                style="width: 100%;height: 100%;"
                preload="auto"
                >
                    <source type="video/mp4" :src="urlEleVideo"/>
            </video>
            <!-- <audio ref="audioRef" :src="urlEle" v-show="showEle == 'audio'" preload="auto" controls></audio> -->
            <img :src="urlEle" alt="" style="width: 100%; height: auto; display: block;" v-if="showEle == 'img'" />
            
        </div>
        <template #footer>
            <slot></slot>
            <div class="flex justify-center">
              <span v-show="!flag">(请阅读完文档,并且 {{curTime}} 秒后可点击完成)</span><el-button @click="nextFn" :disabled="!flag"> 完 成 </el-button>
              <el-button @click="cancleFn"> 关 闭 </el-button>
            </div>
        </template>
    </Dialog>
</template>
<script setup lang="ts">
import { courseOfflineApi } from '@/api/education/course'
import officeWj from '../../views/education/courseOffline/components/officeWj.vue'
import { config } from '@/config/axios/config'
import Videojs from 'video.js'
import 'video.js/dist/video-js.css'
import { nanoid } from 'nanoid';
defineOptions({name: 'Preview'})
const props = defineProps({
  ifUpload: {
    type: String
  },
  showDown: {
    type: Boolean,
    default: true
  },
  finishClick: {type:Function,required:false},
})
const dialogVisible = ref(false)
const type = ref()
const showEle = ref('')
const urlEle = ref('')
const urlEleVideo = ref('')
const audioName = ref('')
const imgType = ['bmp', 'jpg', 'jpeg', 'png',
        'tif', 'gif', 'pcx', 'tga', 'exif', 'fpx', 'svg',
        'psd', 'cdr', 'pcd', 'dxf', 'ufo', 'eps', 'ai',
        'raw', 'WMF', 'webp', 'avif', 'apng']
const message = useMessage()
const studyState = ref('0')
const courseProgressData = ref()
const audioRef = ref()
const videoPlayer = ref()
const showYxwFlog = ref(false)
const isOffice = ref(true)
const playerId = ref()
// const flag = ref(true)
let flag = computed(() => {
  return flag1.value && flag2.value
})
const flag1 = ref(false) // 倒计时
const flag2 = ref(false) // 触底
const curTime = ref('')
let times = null
const timer = (time)=>{
  times = setInterval(()=>{
    curTime.value = time--
    if(time < 0){
      clearInterval(times)
      // flag.value = false
      flag1.value = true
    }
  },1000)
}
provide('goToExam', '1')
const open = async (filePath: string, time?: any) => {
    dialogVisible.value = true
    playerId.value = `mainVideo${nanoid(8)}`
    audioName.value = ''
    studyState.value = '2'
    // courseProgressData.value = ''
    curTime.value = time
    timer(time)
    type.value = getFileType(filePath)
    if(!type.value){
        return
    }
    type.value = type.value.toLowerCase()
    urlEle.value = filePath
    await nextTick()
    const element = document.getElementById('scrollable-element')!;
    if( type.value && imgType.includes(type.value)){
        showEle.value = 'img'
        if(props.ifUpload == 'no'){
            return
        }
        element.addEventListener('scroll', function() {
            if (element.scrollHeight - element.scrollTop <= element.clientHeight + 50) {
                debounceScroll()
            }
        });
        if(element.clientHeight >= element.scrollHeight){
            showYxwFlog.value = true
        }
    }else if( type.value === "txt"){
        showEle.value = 'txt'
        if(props.ifUpload == 'no'){
            return
        }
        element.addEventListener('scroll', function() {
            if (element.scrollHeight - element.scrollTop <= element.clientHeight + 50) {
                debounceScroll()
            }
        });
        if(element.clientHeight >= element.scrollHeight){
            showYxwFlog.value = true
        }
    }else if ( type.value === "doc"
        || type.value === "docx" || type.value === "ppt" || type.value === "pptx"
        || type.value === 'xls' || type.value === 'xlsx' || type.value === "pdf"
    ){
        element.addEventListener('scroll', function() {
            console.log('gd123')
        });
        showEle.value = 'office'
        if(type.value === "pdf"){
            isOffice.value = false
        }else{
            isOffice.value = true
        }
    }else if (type.value === "mp4") {
        showEle.value = 'video'
        getVideoInfo(fileId)
    }else if (type.value === 'mp3'){
        showEle.value = 'audio'
        if(props.ifUpload == 'no'){
            return
        }
        nextTick(() => {
            audioRef.value.addEventListener('ended', function() {
                courseProgressFn()
                console.log('gd3')
            });
        })
    }else{
        message.error("当前文件暂不支持预览")
    }
}
const getVideoInfo = async (fileId: string) => {
    const video = await courseOfflineApi.videoInfo(fileId)
    if(video != null) {
        urlEleVideo.value = config.base_url + video.streamUrl;
        setTimeout(() => {
            initPlayer(video.videoId)
        },500)
    }
}

const nextFn = ()=>{
  // console.log('完成')
  // emit('zdyFunction')
  clearInterval(times)
  times = null
  handleClick()
}
//防御性检查
const handleClick = () => {
  // 检查 onItemClick 是否存在且是函数
  if (typeof props.finishClick === 'function') {
    props.finishClick('finish')
    cancleFn()
  }
}
const chudiFn = ()=>{
  // console.log('触底')
  flag2.value = true
}

const player = ref()
const lastPlayedTime = ref(0)
// 防止快进逻辑

const initPlayer = async (fileId: string) => {
    try {
        console.log(document.getElementById(playerId.value))
        player.value = Videojs(playerId.value, {
            autoplay: true,
            controls: true,
            preload: 'metadata', // 仅预加载元数据,不提前加载完整流
            html5: {
                vhs: {
                    withCredentials: false,
                    overrideNative: true,
                    // 分片请求超时(调大,适配大视频)
                    timeout: 60000,
                    // 重试次数(网络波动时重试)
                    retry: 3,
                    // 分片大小(匹配后端10MB分片)
                    chunkSize: 10 * 1024 * 1024
                },
                // 缓冲配置
                buffer: {
                    minBufferLength: 1, // 最小缓冲长度(秒)
                    maxBufferLength: 30, // 最大缓冲长度(秒)
                    bufferBehind: 5 // 保留后方缓冲(秒)
                }
            }
        });
        urlEleVideo.value = `${config.base_url}big-file/big-video/stream/${fileId}?t=${Date.now()}`
        player.value.src({
            src: urlEleVideo.value,
            type: 'video/mp4'
        });
        player.value.ready(function() {
            console.log('播放器初始化完成');
            player.value.on('loadedmetadata', () => {
                console.log('元数据加载完成,duration:', player.value.duration());
            });
            player.value.on('timeupdate', () => {
                lastPlayedTime.value = player.value.currentTime()
            });
            player.value.on('seeked', () => {
                const current = player.value.currentTime();
                if (Math.abs(current - lastPlayedTime.value) > 0.5) { // 判定为手动跳转
                    player.value.currentTime(lastPlayedTime.value);
                }
            });
            const originalCurrentTime = player.value.currentTime;
            player.value.currentTime = function(time: number) {
                if (time === undefined) {
                    return originalCurrentTime.call(this); // 允许读取当前时间
                }
                message.error("禁止手动调整播放进度")
                return originalCurrentTime.call(this); // 禁止设置时间
            };

        });
    } catch (error) {
        console.error('初始化播放器失败:', error);
        const errorMsg = error instanceof Error ? error.message : '未知错误'
        message.error(`初始化失败: ${errorMsg}`);
    }
}
const handleEnded = () => {
    courseProgressFn()
}
const emit = defineEmits<{
    'course-progress': [data: {
        endTime: number
        paperNo: string
        examState: string
        examStudentState: string
    }],
    'zdyFunction'
}>()
//执行学习状态函数
const courseProgressFn = async () => {
    const data = await courseOfflineApi.courseProgress(courseProgressData.value)
    emit('course-progress', data)
}
//防抖动
const debounce = (func: () => void, delay: number) => {
    let timerId: NodeJS.Timeout;
    return function() {
        clearTimeout(timerId);
        timerId = setTimeout(() => {
            func();
        }, delay);
    };
}
const debounceScroll = debounce(courseProgressFn, 2000)
const getFileType = (url: string) => {
    if (url) {
        if (url.lastIndexOf(".") > -1) {
            return url.slice(url.lastIndexOf(".") + 1);
        } else {
            return false
        }
    }
}
defineExpose({open})

const download = () => {
  // window.open( urlEle.value, '_blank')
  downloadPDFWithFetch(urlEle.value,audioName.value)
}
const downloadPDFWithFetch = async (url, filename = 'document.pdf') => {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const blob = await response.blob();
    const blobUrl = window.URL.createObjectURL(blob);
    
    const link = document.createElement('a');
    link.href = blobUrl;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    
    // 清理
    document.body.removeChild(link);
    window.URL.revokeObjectURL(blobUrl);
    
  } catch (error) {
    console.error('下载失败:', error);
  }
}
const officeWjRef = ref()
const yxwMenuFn = () => {
    officeWjRef.value.courseProgressFn()
}
const cancleFn = async () => {
    dialogVisible.value = false
    showEle.value = ''
    urlEle.value = ''
    audioName.value = ''
    showYxwFlog.value = false
    flag1.value = false
    flag2.value = false
    clearInterval(times)
    times = null
    if(showEle.value = 'video'){
        await nextTick()
        videoPlayer.value.pause()
    }
    if(showEle.value = 'audio'){
        await nextTick()
    }
    // player.value.dispose()
    player.value = null
}

</script>
<style lang="scss" scoped>
video.video-ele::-webkit-media-controls-timeline {
    display: none;
}
</style>

uniapp(vant4):

<van-button type="primary" size="mini" @click="viewDocumentFileFn()">查看</van-button>
<viewDocument :visible="viewDocumentVisible" :file-url="viewDocumentFileUrl" :file-time="viewDocumentFileTime" :finish-click="handlefinishClick" @update:visible="(val) => viewDocumentVisible = val"/>
import viewDocument from '../../../components/FilePreview/viewDocument.vue'
// 预览组件相关状态
const viewDocumentVisible = ref(false)
const viewDocumentFileUrl = ref('')
const viewDocumentFileTime = ref(0)
const viewDocumentFileFn = () => {
  // 设置预览文件信息
  viewDocumentFileUrl.value = '1767594415285.pdf'
  viewDocumentFileTime.value = 10
  // 显示预览组件
  viewDocumentVisible.value = true
}
//完成
let handlefinishClick = (status)=>{
  console.log(status,'完成')
  // if(status == 'close'){}
}
<template>
  <view class="popup-file-preview" v-if="visible">
    <!-- 遮罩层 -->
    <view class="mask" @click="closePopup"></view>

    <!-- 弹出层内容 -->
    <view class="popup-content">
      <!-- 头部标题栏 -->
      <view class="popup-header">
        <text class="file-title">{{ fileName }}</text>
        <view class="header-actions">
          <view class="action-btn" @click.stop="handleDownload">
            <van-icon name="download" size="18" color="#fff" />
          </view>
          <view class="close-btn" @click.stop="closePopup">
            <van-icon name="cross" size="20" color="#fff" />
          </view>
        </view>
      </view>

      <!-- 文件预览区域 -->
      <view class="preview-container">
        <!-- 加载状态 -->
        <view v-if="loading" class="loading-container">
          <van-loading type="spinner" color="#1989fa" />
          <text class="loading-text">加载中...</text>
        </view>

        <!-- 图片文件预览 -->
        <view v-else-if="isImageFile" class="image-preview">
          <image 
            :src="fileUrl" 
            mode="widthFix" 
            @click="handleImageClick"
            @error="onImageError"
            show-menu-by-longpress
          />
        </view>

        <!-- PDF文件预览 -->
        <view v-else-if="fileType === 'pdf'" class="pdf-preview">
          <view class="pdf-controls" v-if="pdfPages.length > 0" v-show="false">
            <van-button 
              size="small" 
              @click="prevPdfPage" 
              :disabled="currentPdfPage <= 1" 
              type="primary" 
              plain
            >
              上一页
            </van-button>
            <text class="page-info">{{ currentPdfPage }} / {{ pdfPages.length }}</text>
            <van-button 
              size="small" 
              @click="nextPdfPage" 
              :disabled="currentPdfPage >= pdfPages.length" 
              type="primary" 
              plain
            >
              下一页
            </van-button>
          </view>
          
          <!-- <view class="pdf-pages-container"> -->
            <scroll-view 
                class="pdf-pages-container" 
                ref="officeElement"
                @scroll="handleScroll"
                scroll-y
            >
            <image 
              v-for="(page, index) in pdfPages" 
              :key="index"
              :src="page" 
              mode="widthFix" 
              class="pdf-page-image"
            />
            </scroll-view>
          <!-- </view> -->
          <view class="pdf-fallback">
            <text v-show="!flag">(请阅读完文档,并且 {{curTime}} 秒后可点击完成)</text>
            <van-button type="primary" size="small" @click="nextFn" :disabled="!flag" style="margin-top: 16px;">
              完成
            </van-button>
          </view>
          <view class="pdf-fallback" v-if="pdfPages.length === 0 && !loading">
            <van-empty description="PDF预览不可用" />
            <van-button type="primary" @click="handleDownload" style="margin-top: 16px;">
              下载文件查看
            </van-button>
          </view>
        </view>

        <!-- TXT文件预览 -->
        <view v-else-if="fileType === 'txt'" class="txt-preview">
          <scroll-view class="txt-scroll" scroll-y>
            <text class="txt-content">{{ txtContent }}</text>
          </scroll-view>
        </view>

        <!-- 文档文件预览 (DOC/DOCX) -->
        <view v-else-if="isDocFile" class="doc-preview">
          <view v-if="docContent" class="doc-content">
            <rich-text :nodes="docContent"></rich-text>
          </view>
          <view v-else class="doc-fallback">
            <van-empty description="文档预览不可用" />
            <van-button type="primary" @click="handleDownload" style="margin-top: 16px;">
              下载文件查看
            </van-button>
          </view>
        </view>

        <!-- 不支持的文件类型 -->
        <view v-else class="unsupported-preview">
          <van-empty description="暂不支持该类型文件预览" />
          <van-button type="primary" @click="handleDownload" style="margin-top: 16px;">
            下载文件
          </van-button>
        </view>
      </view>

      <!-- 文件信息 -->
      <view class="file-info" v-if="fileSize">
        <text>{{ fileSize }}</text>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, watch, onMounted, computed } from 'vue';
import { showImagePreview, showToast } from 'vant';

// 定义组件属性
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  fileUrl: {
    type: String,
    required: true
  },
  fileType: {
    type: String,
    default: 'pdf'
  },
  fileName: {
    type: String,
    default: ''
  },
  fileSize: {
    type: String,
    default: ''
  },
  fileTime: {
    type: Number,
    default: 5
  },
  finishClick: {type:Function,required:false},
});

let flag = computed(() => {
  return flag1.value && flag2.value
})
const flag1 = ref(false) // 倒计时
const flag2 = ref(false) // 触底
const curTime = ref('')
let times = null
const timer = (time)=>{
  times = setInterval(()=>{
    curTime.value = time--
    if(time < 0){
      clearInterval(times)
      flag1.value = true
    }
  },1000)
}
const nextFn = ()=>{
  // console.log('完成')
  // emit('zdyFunction')
  clearInterval(times)
  times = null
  handleClick()
}
//防御性检查
const handleClick = () => {
  // 检查 onItemClick 是否存在且是函数
  if (typeof props.finishClick === 'function') {
    props.finishClick('finish')
    closePopup()
  }
}
const officeElement = ref();
const handleScroll = (e) => {
  const scrollTop = e.detail.scrollTop;
  // 获取元素高度等信息
  uni.createSelectorQuery()
    .select('.pdf-pages-container')
    .boundingClientRect((rect) => {
      if (rect) {
        const { height: clientHeight } = rect;
        // 计算是否接近底部
        uni.createSelectorQuery()
          .select('.pdf-page-image:last-child')
          .boundingClientRect((lastRect) => {
            if (lastRect) {
              const scrollHeight = scrollTop + lastRect.bottom;
              if ((scrollHeight - scrollTop) < (clientHeight + 150)) {
                  // console.log('触底')
                  flag2.value = true
              }
            }
          })
          .exec();
      }
    })
    .exec();
}

// 定义事件
const emit = defineEmits(['update:visible']);

// 响应式数据
const loading = ref(false);
const txtContent = ref('');
const docContent = ref('');
const pdfPages = ref([]);
const currentPdfPage = ref(1);

// 计算属性
const isDocFile = computed(() => {
  return ['doc', 'docx'].includes(props.fileType?.toLowerCase());
});

const isImageFile = computed(() => {
  return ['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(props.fileType?.toLowerCase());
});

// 方法定义
const closePopup = () => {
  emit('update:visible', false);
  flag1.value = false
  flag2.value = false
  clearInterval(times)
  times = null
};

const handleDownload = () => {
  // #ifdef H5
  try {
    const link = document.createElement('a');
    link.href = props.fileUrl;
    link.download = props.fileName;
    link.target = '_blank';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  } catch (error) {
    console.error('下载文件失败:', error);
    showToast('下载失败');
  }
  // #endif
  
  // #ifndef H5
  uni.downloadFile({
    url: props.fileUrl,
    success: (res) => {
      if (res.statusCode === 200) {
        uni.saveFile({
          tempFilePath: res.tempFilePath,
          success: (saveRes) => {
            uni.openDocument({
              filePath: saveRes.savedFilePath,
              showMenu: true,
              success: () => {
                console.log('打开文档成功');
              },
              fail: (err) => {
                console.error('打开文档失败:', err);
                showToast('打开文档失败');
              }
            });
          },
          fail: (err) => {
            console.error('保存文件失败:', err);
            showToast('保存文件失败');
          }
        });
      }
    },
    fail: (err) => {
      console.error('下载文件失败:', err);
      showToast('下载文件失败');
    }
  });
  // #endif
};

const handleImageClick = () => {
  showImagePreview([props.fileUrl]);
};

const onImageError = (e) => {
  console.error('图片加载失败:', e);
  showToast('图片加载失败');
};

// TXT文件加载
const loadTxtFile = async () => {
  try {
    const res = await uni.request({
      url: props.fileUrl,
      method: 'GET',
      responseType: 'text'
    });
    
    if (res.statusCode === 200) {
      txtContent.value = res.data;
    } else {
      throw new Error(`HTTP ${res.statusCode}`);
    }
  } catch (error) {
    console.error('加载TXT文件失败:', error);
    showToast('文本加载失败');
  }
};

// DOC/DOCX文件加载
const loadDocFile = async () => {
  try {
    // #ifdef H5
    // H5环境下使用mammoth处理
    const response = await fetch(props.fileUrl);
    if (!response.ok) throw new Error('网络请求失败');
    
    const arrayBuffer = await response.arrayBuffer();
    const mammoth = await import('mammoth');
    const result = await mammoth.convertToHtml({ arrayBuffer });
    docContent.value = result.value;
    // #endif
    
    // #ifndef H5
    // 非H5环境下提示下载
    showToast('文档需下载查看');
    // #endif
  } catch (error) {
    console.error('加载文档失败:', error);
    showToast('文档加载失败');
  }
};

// PDF文件加载(使用图片方式预览)
const loadPdfFile = async () => {
  try {
    // 重置状态
    pdfPages.value = [];
    currentPdfPage.value = 1;
    
    // #ifdef H5
    // H5环境下尝试使用PDF.js处理
    const pdfjsLib = await import('pdfjs-dist');
    const pdfjsWorker = await import('pdfjs-dist/build/pdf.worker.min?url');
    pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker.default;
    
    const loadingTask = pdfjsLib.getDocument(props.fileUrl);
    const pdf = await loadingTask.promise;
    
    const pages = [];
    for (let i = 1; i <= pdf.numPages; i++) {
      const page = await pdf.getPage(i);
      const viewport = page.getViewport({ scale: 1.5 });
      
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');
      canvas.height = viewport.height;
      canvas.width = viewport.width;
      
      const renderContext = {
        canvasContext: context,
        viewport: viewport
      };
      
      await page.render(renderContext).promise;
      const dataUrl = canvas.toDataURL();
      pages.push(dataUrl);
    }
    
    pdfPages.value = pages;
    // #endif
    
    // #ifndef H5
    // 非H5环境下提示下载
    showToast('PDF需下载查看');
    // #endif
  } catch (error) {
    console.error('加载PDF失败:', error);
    showToast('PDF加载失败');
  }
};

// PDF翻页控制
const prevPdfPage = () => {
  if (currentPdfPage.value > 1) {
    currentPdfPage.value--;
  }
};

const nextPdfPage = () => {
  if (currentPdfPage.value < pdfPages.value.length) {
    currentPdfPage.value++;
  }
};

// 根据文件类型加载文件
const loadFileByType = async () => {
  loading.value = true;
  
  try {
    const type = props.fileType?.toLowerCase();
    
    if (isImageFile.value) {
      // 图片文件不需要特殊处理,直接显示
      loading.value = false;
    } else if (type === 'txt') {
      await loadTxtFile();
      loading.value = false;
    } else if (type === 'pdf') {
      await loadPdfFile();
      loading.value = false;
    } else if (isDocFile.value) {
      await loadDocFile();
      loading.value = false;
    } else {
      loading.value = false;
    }
  } catch (error) {
    console.error('文件加载失败:', error);
    loading.value = false;
    showToast('文件加载失败');
  }
};

// 监听文件URL变化
watch(() => props.fileUrl, (newUrl) => {
  if (props.visible && newUrl) {
    loadFileByType();
  }
});

// 监听弹窗显示状态变化
watch(() => props.visible, (newVal) => {
  if (newVal && props.fileUrl) {
    loadFileByType();
    curTime.value = props.fileTime
    timer(props.fileTime)
  } else {
    // 重置状态
    txtContent.value = '';
    docContent.value = '';
    pdfPages.value = [];
    currentPdfPage.value = 1;
    loading.value = false;
    flag1.value = false
    flag2.value = false
    clearInterval(times)
    times = null
  }
}, { immediate: true });

// 组件挂载时加载文件
onMounted(() => {
  if (props.visible && props.fileUrl) {
    loadFileByType();
  }
});
</script>

<style scoped>
.popup-file-preview {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
}

.mask {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  background-color: rgba(0, 0, 0, 0.6);
}

.popup-content {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 300;
  transform: translate(-50%, -50%);
  width: 90vw;
  height: 90vh;
  background-color: #fff;
  border-radius: 10px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}

.popup-header {
  height: 50px;
  background-color: #1989fa;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 15px;
  z-index: 350;
}

.file-title {
  color: #fff;
  font-size: 16px;
  font-weight: bold;
  flex: 1;
  text-align: center;
  margin-right: 30px;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.action-btn,
.close-btn {
  width: 30px;
  height: 30px;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  border-radius: 4px;
  transition: background-color 0.2s;
}

.action-btn:hover,
.close-btn:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

.preview-container {
  flex: 1;
  background-color: #f5f5f5;
  overflow: hidden;
  position: relative;
}

.loading-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

.loading-text {
  font-size: 14px;
  color: #969799;
}

/* 图片预览 */
.image-preview {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #000;
}

.image-preview image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* PDF预览 */
.pdf-preview {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.pdf-controls {
  padding: 10px;
  background-color: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  z-index: 10;
}

.page-info {
  font-size: 14px;
  color: #333;
  min-width: 80px;
  text-align: center;
}

.pdf-pages-container {
  overflow-y: auto;
  /* flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start; */
  background-color: #f0f0f0;
}

.pdf-page-image {
  width: 100%;
  display: block;
}

.pdf-fallback {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 20px;
  background-color: #fff;
}

/* TXT预览 */
.txt-preview {
  width: 100%;
  height: 100%;
  background-color: #fff;
}

.txt-scroll {
  width: 100%;
  height: 100%;
  padding: 16px;
}

.txt-content {
  font-size: 14px;
  line-height: 1.8;
  color: #333;
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* 文档预览 */
.doc-preview {
  width: 100%;
  height: 100%;
  background-color: #fff;
  overflow: auto;
}

.doc-content {
  padding: 16px;
  min-height: 100%;
}

.doc-fallback {
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 20px;
}

/* 不支持的文件类型 */
.unsupported-preview {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 20px;
  background-color: #fff;
}

/* 文件信息 */
.file-info {
  height: 40px;
  padding: 0 15px;
  display: flex;
  align-items: center;
  background-color: #f8f8f8;
  border-top: 1px solid #eee;
  color: #666;
  font-size: 14px;
}
</style>

 

posted @ 2026-01-15 16:23  峪野  阅读(26)  评论(0)    收藏  举报