OfficeViewer.vue 7.43 KB
<template>
    <div class="office-viewer">
        <!-- 错误状态 -->
        <div v-if="error" class="error-container">
            <van-icon name="warning-o" size="24" color="#ff6b6b" />
            <span class="error-text">{{ error }}</span>
            <van-button
                type="primary"
                size="small"
                @click="retry"
                class="retry-btn"
            >
                重试
            </van-button>
        </div>

        <!-- 文档预览 -->
        <div v-else class="document-container">
            <!-- DOCX 文档预览 -->
            <vue-office-docx
                v-if="fileType === 'docx'"
                :src="src"
                :style="{ height: containerHeight }"
                @rendered="onRendered"
                @error="onError"
            />

            <!-- Excel 文档预览 -->
            <vue-office-excel
                v-else-if="fileType === 'excel'"
                :src="src"
                :style="{ height: containerHeight }"
                @rendered="onRendered"
                @error="onError"
            />

            <!-- PPTX 文档预览 -->
            <vue-office-pptx
                v-else-if="fileType === 'pptx'"
                :src="src"
                :style="{ height: containerHeight }"
                @rendered="onRendered"
                @error="onError"
            />

            <!-- 不支持的文件类型 -->
            <div v-else class="unsupported-container">
                <van-icon name="warning-o" size="24" color="#ff6b6b" />
                <span class="unsupported-text">不支持的文件类型: {{ fileType }}</span>
            </div>
        </div>

        <van-loading vertical v-if="loading" class="loading-overlay">
            加载中...
        </van-loading>
    </div>
</template>

<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import VueOfficeDocx from '@vue-office/docx'
import VueOfficeExcel from '@vue-office/excel'
import VueOfficePptx from '@vue-office/pptx'
import '@vue-office/docx/lib/index.css'
import '@vue-office/excel/lib/index.css'

// Props 定义
const props = defineProps({
    // 文件 URL 或 ArrayBuffer 数据
    src: {
        type: [String, ArrayBuffer],
        required: true
    },
    // 文件类型
    fileType: {
        type: String,
        required: true,
        validator: (value) => ['docx', 'excel', 'pptx'].includes(value)
    },
    // 容器高度
    height: {
        type: String,
        default: '70vh'
    }
})

// Emits 定义
const emit = defineEmits(['rendered', 'error', 'retry'])

// 响应式数据
const error = ref('')

// 计算属性
const containerHeight = computed(() => props.height)

/**
 * 文档渲染完成回调
 */
const onRendered = () => {
    console.log('Office document rendered successfully')
    loading.value = false
    error.value = ''
    emit('rendered')
}

/**
 * 文档渲染错误回调
 * @param {Error} err - 错误对象
 */
const onError = (err) => {
    console.error('Office document render error:', err)
    loading.value = false
    error.value = '文档加载失败,请检查文件格式或网络连接'
    emit('error', err)
}

/**
 * 重试加载文档
 */
const retry = () => {
    console.log('Retrying to load office document')
    loading.value = true
    error.value = ''
    emit('retry')
}

/**
 * 监听 src 变化,重新加载文档
 */
watch(() => props.src, (newSrc) => {
    console.log('Office document src changed:', newSrc)
    if (newSrc) {
        error.value = ''

        // 验证 URL 是否有效
        if (typeof newSrc === 'string') {
            // 检查是否是有效的 URL
            try {
                new URL(newSrc)
                console.log('Valid URL detected:', newSrc)
            } catch (e) {
                // 可能是相对路径,检查是否以 http 开头
                if (!newSrc.startsWith('http') && !newSrc.startsWith('/')) {
                    console.warn('Invalid URL format:', newSrc)
                    error.value = '文档地址格式不正确'
                    return
                }
            }
        }
    }
}, { immediate: true })

/**
 * 监听 fileType 变化
 */
watch(() => props.fileType, (newType) => {
    console.log('Office document fileType changed:', newType)
})

// 响应式数据
const loading = ref(false)

// 组件挂载时的初始化
onMounted(() => {
    console.log('OfficeViewer 组件已挂载')
    // 生成一个loading 效果
    loading.value = true
})
</script>

<style lang="less" scoped>
.office-viewer {
    position: relative;
    width: 100%;
    height: 100%;
    background: #fff;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

    .error-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 200px;
        gap: 12px;
        padding: 20px;

        .error-text {
            font-size: 14px;
            color: #666;
            text-align: center;
            line-height: 1.5;
        }

        .retry-btn {
            margin-top: 8px;
        }
    }

    .unsupported-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 200px;
        gap: 12px;
        padding: 20px;

        .unsupported-text {
            font-size: 14px;
            color: #666;
            text-align: center;
            line-height: 1.5;
        }
    }

    .document-container {
        width: 100%;
        height: 100%;
        overflow: auto;

        // 覆盖 vue-office 默认样式
        :deep(.vue-office-docx),
        :deep(.vue-office-excel),
        :deep(.vue-office-pptx) {
            border: none;
            border-radius: 8px;
            min-height: 100%;
        }

        // Excel 表格样式优化
        :deep(.vue-office-excel) {
            .luckysheet-cell-main {
                font-size: 12px;
            }
        }

        // DOCX 文档样式优化
        :deep(.vue-office-docx) {
            .docx-wrapper {
                padding: 16px;
                min-height: 100%;
                background: #fff;
            }
        }

        // PPTX 演示文稿样式优化
        :deep(.vue-office-pptx) {
            .pptx-wrapper {
                background: #f5f5f5;
                min-height: 100%;
            }
        }
    }

    // Loading 覆盖层样式
    .loading-overlay {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 1000;
        background: rgba(255, 255, 255, 0.9);
        border-radius: 8px;
        padding: 20px;
        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
    }
}

// 移动端适配
@media (max-width: 768px) {
    .office-viewer {
        border-radius: 0;
        box-shadow: none;

        .document-container {
            // 移动端确保滚动流畅
            -webkit-overflow-scrolling: touch;

            :deep(.vue-office-docx) {
                .docx-wrapper {
                    padding: 12px;
                    font-size: 14px;
                    line-height: 1.6;
                }
            }

            :deep(.vue-office-excel) {
                .luckysheet-cell-main {
                    font-size: 11px;
                }
            }

            :deep(.vue-office-pptx) {
                .pptx-wrapper {
                    padding: 8px;
                }
            }
        }
    }
}
</style>