PPTViewer.vue 6.61 KB
<template>
  <div class="ppt-viewer">
    <!-- 加载状态 -->
    <div v-if="loading" class="loading-state">
      <div class="loading-spinner">
        <i class="fas fa-spinner fa-spin"></i>
      </div>
      <div class="loading-text">正在加载PPT文件...</div>
    </div>

    <!-- 错误状态 -->
    <div v-else-if="error" class="error-state">
      <div class="error-icon">
        <i class="fas fa-exclamation-triangle"></i>
      </div>
      <div class="error-text">{{ error }}</div>
      <button class="retry-btn" @click="loadPPT">重试</button>
    </div>

    <!-- PPT预览容器 -->
    <div v-else class="ppt-container">
      <vue-office-pptx
        :src="pptUrl"
        style="height: 100vh"
        @rendered="renderedHandler"
        @error="errorHandler"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import * as filesApi from "@/api/files";
// @ts-ignore - vue-office/pptx 可能没有类型定义
import VueOfficePptx from "@vue-office/pptx";
import { fileCache } from "@/utils/fileCache";

// Props
interface Props {
  fileId: string | number;
  fileName?: string;
  isChatAnswer?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  fileName: "document.pptx",
  isChatAnswer: false,
});

// Emits
const emit = defineEmits<{
  mounted: [];
}>();

// 状态
const loading = ref(false);
const error = ref<string | null>(null);
const pptUrl = ref<string>("");
const blobUrl = ref<string>("");

// 加载PPT文件
const loadPPT = async () => {
  if (!props.fileId) {
    error.value = "缺少文件ID";
    return;
  }

  const fileId = props.fileId;
  loading.value = true;
  error.value = null;

  try {
    // 1. 尝试从持久化缓存读取
    try {
      const cached = await fileCache.getFile(fileId);
      if (cached && cached.content instanceof Blob) {
        console.log("PPT 命中持久化缓存:", fileId);
        const url = URL.createObjectURL(cached.content);
        blobUrl.value = url;
        pptUrl.value = url;
        loading.value = false;
        return;
      }
    } catch (e) {
      console.warn("读取 PPT 持久化缓存失败:", e);
    }

    console.log("开始下载PPT文件,fileId:", props.fileId, "isChatAnswer:", props.isChatAnswer);
    let response;
    if (props.isChatAnswer) {
      try {
        const cached = await fileCache.getFile(props.fileId);
        if (cached && cached.content instanceof Blob) {
          response = cached.content;
        } else {
          response = await filesApi.downloadFile(props.fileId);
        }
      } catch (error) {
        response = await filesApi.downloadFile(props.fileId);
      }
    } else {
      response = await filesApi.downloadFile(props.fileId);
    }

    // 从响应中获取 Blob 数据
    let blob = response.data || response;

    if (props.isChatAnswer && response.data?.content) {
      blob = new Blob([response.data.content], { type: "application/vnd.openxmlformats-officedocument.presentationml.presentation" });
    }

    if (!(blob instanceof Blob)) {
      throw new Error("无效的文件响应格式");
    }

    // 确保 Blob 类型为 application/vnd.openxmlformats-officedocument.presentationml.presentation
    if (
      blob.type !==
      "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    ) {
      console.log(
        "修正 Blob 类型为 application/vnd.openxmlformats-officedocument.presentationml.presentation",
      );
      blob = new Blob([blob], {
        type: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
      });
    }

    // 写入持久化缓存
    await fileCache.setFile({
      fileId,
      content: blob,
      type: "ppt",
      fileName: props.fileName || "document.pptx",
    });

    console.log("PPT数据转换完成,大小:", blob.size, "字节,类型:", blob.type);

    // 创建 Blob URL
    if (blobUrl.value) {
      URL.revokeObjectURL(blobUrl.value);
    }
    blobUrl.value = URL.createObjectURL(blob);
    pptUrl.value = blobUrl.value;

    console.log("PPT URL 创建成功:", pptUrl.value);

    // 设置 loading 为 false,等待组件渲染完成
    loading.value = false;
  } catch (err: any) {
    console.error("加载PPT失败:", err);
    error.value = `加载PPT失败: ${err.message}`;
    loading.value = false;
  }
};

// PPT渲染完成
const renderedHandler = () => {
  console.log("PPT渲染完成");
  loading.value = false;
};

// PPT渲染错误
const errorHandler = (err: any) => {
  console.error("PPT渲染失败:", err);
  error.value = `PPT渲染失败: ${err?.message || "未知错误"}`;
  loading.value = false;
};

// 清理资源
const cleanup = () => {
  if (blobUrl.value) {
    URL.revokeObjectURL(blobUrl.value);
    blobUrl.value = "";
  }
};

// 监听 fileId 变化,重新加载PPT
watch(
  () => props.fileId,
  (newFileId, oldFileId) => {
    if (newFileId !== oldFileId) {
      console.log("📊 检测到 fileId 变化,重新加载PPT:", {
        oldFileId,
        newFileId,
      });

      // 清理旧的 Blob URL
      cleanup();

      // 重新加载PPT
      loadPPT();
    }
  },
);

// 生命周期
onMounted(() => {
  loadPPT();
  emit("mounted");
});

onBeforeUnmount(() => {
  cleanup();
});
</script>

<style scoped lang="scss">
.ppt-viewer {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--color-bg, #f5f5f5);
  overflow: hidden;
}

.ppt-container {
  flex: 1;
  position: relative;
  overflow: hidden;
  background: var(--color-bg);
}

/* 加载状态 */
.loading-state {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--color-bg, #f5f5f5);
  color: var(--color-text-secondary, #666);
}

.loading-spinner {
  font-size: 32px;
  margin-bottom: 16px;
  color: var(--color-primary, #409eff);
}

.loading-text {
  font-size: 16px;
}

/* 错误状态 */
.error-state {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--color-bg, #f5f5f5);
  color: var(--color-danger, #f56c6c);
  text-align: center;
  padding: 40px;
}

.error-icon {
  font-size: 48px;
  margin-bottom: 16px;
}

.error-text {
  font-size: 16px;
  margin-bottom: 20px;
  line-height: 1.5;
}

.retry-btn {
  background: var(--color-primary, #409eff);
  color: #fff;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s;
}

.retry-btn:hover {
  background: var(--color-primary-dark, #337ecc);
}
</style>