OfficeViewer.vue 4.55 KB
<template>
    <view class="office-viewer">
        <view v-if="error" class="error-container">
            <IconFont name="issue" size="24" color="#ff6b6b" />
            <text class="error-text">{{ error }}</text>
            <nut-button type="primary" size="small" class="retry-btn" @click="retry">重试</nut-button>
        </view>

        <view v-else class="document-container">
            <!-- #ifdef H5 -->
            <iframe
                v-if="preview_url"
                :src="preview_url"
                frameborder="0"
                class="preview-iframe"
                :style="{ height: container_height }"
                @load="on_loaded"
            />
            <view v-else class="unsupported-container">
                <IconFont name="issue" size="24" color="#ff6b6b" />
                <text class="unsupported-text">不支持的文件类型: {{ normalized_type || '未知' }}</text>
            </view>
            <!-- #endif -->

            <!-- #ifdef WEAPP -->
            <view class="unsupported-container">
                <IconFont name="issue" size="24" color="#ff6b6b" />
                <text class="unsupported-text">小程序不支持内嵌 Office 预览</text>
            </view>
            <!-- #endif -->
        </view>

        <view v-if="loading" class="loading-overlay">
            <IconFont name="loading" size="24" class="animate-spin text-blue-600" />
            <text class="loading-text">加载中...</text>
        </view>
    </view>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
import { getTencentPreviewUrl } from '@/components/documents/DocumentPreview/utils'

const props = defineProps({
    src: {
        type: [String, ArrayBuffer],
        required: true
    },
    fileType: {
        type: String,
        default: ''
    },
    height: {
        type: String,
        default: '70vh'
    }
})

const emit = defineEmits(['rendered', 'error', 'retry'])

const loading = ref(false)
const error = ref('')

const container_height = computed(() => props.height)

const normalized_type = computed(() => {
    const raw_type = (props.fileType || '').toLowerCase()
    if (raw_type === 'doc' || raw_type === 'docx') return 'docx'
    if (raw_type === 'xls' || raw_type === 'xlsx') return 'xlsx'
    if (raw_type === 'ppt' || raw_type === 'pptx') return 'pptx'
    if (raw_type === 'pdf') return 'pdf'
    return raw_type
})

const preview_url = computed(() => {
    if (!props.src || typeof props.src !== 'string') return ''

    if (normalized_type.value === 'pdf') return props.src
    if (['docx', 'xlsx', 'pptx'].includes(normalized_type.value)) {
        return getTencentPreviewUrl(props.src)
    }

    return ''
})

const on_loaded = () => {
    loading.value = false
    emit('rendered')
}

const retry = () => {
    loading.value = true
    error.value = ''
    emit('retry')
}

watch(() => [props.src, props.fileType], () => {
    error.value = ''

    if (!props.src) {
        loading.value = false
        error.value = '文档地址不能为空'
        emit('error', new Error(error.value))
        return
    }

    loading.value = true

    if (!preview_url.value) {
        loading.value = false
        error.value = '文档类型不支持或地址格式不正确'
        emit('error', new Error(error.value))
    }
}, { immediate: true })
</script>

<style lang="less" scoped>
.office-viewer {
    position: relative;
    width: 100%;
    height: 100%;
    background: #fff;
    border-radius: 8px;
    overflow: hidden;

    .error-container,
    .unsupported-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 400rpx;
        gap: 24rpx;
        padding: 40rpx;

        .error-text,
        .unsupported-text {
            font-size: 28rpx;
            color: #666;
            text-align: center;
            line-height: 1.5;
        }

        .retry-btn {
            margin-top: 16rpx;
        }
    }

    .document-container {
        width: 100%;
        height: 100%;
    }

    .preview-iframe {
        width: 100%;
        border: none;
    }

    .loading-overlay {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 10;
        background: rgba(255, 255, 255, 0.9);
        border-radius: 16rpx;
        padding: 24rpx 32rpx;
        display: flex;
        align-items: center;
        gap: 16rpx;

        .loading-text {
            font-size: 28rpx;
            color: #333;
        }
    }
}
</style>