feat(docx-preview): 集成DOCX文件预览功能组件
Some checks failed
Lint Code / Lint Code (push) Failing after 4m20s

- 新增ReDocxPreview组件,实现基于docx-preview库的DOCX文件渲染
- 实现DOCX文件加载、错误处理及打印功能
- 知识库视图增加对DOCX文件预览支持与对应UI样式调整
- 更新本地中英文菜单配置,添加"pureDocx"菜单项
- 增加docx-preview依赖及相关包锁信息
- 优化风险评估和工单管理视图的页面内边距样式
This commit is contained in:
2026-03-30 19:56:28 +08:00
parent 919577365d
commit 4d889c9b70
8 changed files with 334 additions and 3 deletions

View File

@@ -0,0 +1,172 @@
<script setup lang="ts">
import { ref, watch, onMounted, nextTick } from "vue";
import { renderAsync } from "docx-preview";
defineOptions({
name: "ReDocxPreview"
});
const props = defineProps<{
url?: string;
file?: File | Blob;
}>();
const emit = defineEmits<{
(e: "rendered"): void;
(e: "error", error: Error): void;
}>();
const docxContainerRef = ref<HTMLDivElement>();
const loading = ref(true);
const errorMsg = ref("");
// 渲染 docx 文件
async function renderDocx() {
if (!docxContainerRef.value) return;
loading.value = true;
errorMsg.value = "";
try {
// 清空之前的内容
docxContainerRef.value.innerHTML = "";
let arrayBuffer: ArrayBuffer;
if (props.file) {
// 如果传入的是 File 或 Blob
arrayBuffer = await props.file.arrayBuffer();
} else if (props.url) {
// 从 URL 获取文件
const response = await fetch(props.url);
if (!response.ok) {
throw new Error(`加载文件失败: ${response.status}`);
}
arrayBuffer = await response.arrayBuffer();
} else {
loading.value = false;
return;
}
// 使用 docx-preview 渲染
await renderAsync(arrayBuffer, docxContainerRef.value, undefined, {
className: "docx-container",
inWrapper: true,
ignoreWidth: false,
ignoreHeight: false,
ignoreFonts: false,
breakPages: true,
ignoreLastRenderedPageBreak: true,
experimental: false,
trimXmlDeclaration: true,
useBase64URL: true,
renderHeaders: true,
renderFooters: true,
renderFootnotes: true,
renderEndnotes: true
});
emit("rendered");
} catch (error: any) {
console.error("渲染 docx 文件失败:", error);
errorMsg.value = error.message || "渲染 docx 文件失败";
emit("error", error);
} finally {
loading.value = false;
}
}
// 打印文档
function handlePrint() {
if (!docxContainerRef.value) return;
const printContent = docxContainerRef.value.innerHTML;
const printWindow = window.open("", "_blank");
if (printWindow) {
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>打印文档</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.docx-wrapper { background: white; }
@page { margin: 1cm; }
</style>
</head>
<body>${printContent}</body>
</html>
`);
printWindow.document.close();
printWindow.print();
}
}
// 暴露方法供父组件调用
defineExpose({
refresh: renderDocx,
print: handlePrint
});
// 监听 URL 或文件变化
watch(
() => [props.url, props.file],
() => {
nextTick(() => {
renderDocx();
});
}
);
onMounted(() => {
renderDocx();
});
</script>
<template>
<div class="docx-preview-component">
<div v-if="errorMsg" class="docx-error">
<el-empty :description="errorMsg">
<el-button type="primary" @click="renderDocx">重新加载</el-button>
</el-empty>
</div>
<div v-else v-loading="loading" class="docx-loading-wrapper">
<div ref="docxContainerRef" class="docx-render-container" />
</div>
</div>
</template>
<style scoped lang="scss">
.docx-preview-component {
width: 100%;
height: 100%;
}
.docx-error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
}
.docx-loading-wrapper {
width: 100%;
height: 100%;
min-height: 300px;
}
.docx-render-container {
width: 100%;
height: 100%;
:deep(.docx-wrapper) {
padding: 20px;
background: white;
section.docx {
margin-bottom: 20px;
box-shadow: 0 0 8px rgb(0 0 0 / 10%);
}
}
}
</style>

View File

@@ -3,6 +3,7 @@ import { ref, onMounted, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import VuePdfEmbed from "vue-pdf-embed";
import ReDocxPreview from "@/components/ReDocxPreview/index.vue";
import {
getDocuments,
uploadDocument,
@@ -63,6 +64,7 @@ const previewRotation = ref(0);
const previewPdfRef = ref<any>(null);
const previewAllPages = ref(false);
const previewRotations = [0, 90, 180, 270];
const previewDocxRef = ref<InstanceType<typeof ReDocxPreview> | null>(null);
// 加载项目列表
async function loadProjects() {
@@ -734,7 +736,52 @@ onMounted(() => {
</div>
</template>
<!-- Word/其他文件 - 使用iframe尝试预览 -->
<!-- Word预览 -->
<template
v-else-if="
['doc', 'docx'].includes(previewDoc.fileType?.toLowerCase())
"
>
<div class="docx-preview-container">
<div class="docx-toolbar">
<div class="docx-title">
{{ previewDoc.title }}
</div>
<div class="docx-actions">
<el-button link @click="previewDocxRef?.refresh()">
<component
:is="useRenderIcon('ri/refresh-line')"
:size="18"
/>
刷新
</el-button>
<el-button link @click="previewDocxRef?.print()">
<component
:is="useRenderIcon('ri/printer-line')"
:size="18"
/>
打印
</el-button>
<el-button link @click="handleOpenFile(previewDoc.fileUrl)">
<component
:is="useRenderIcon('ri/download-line')"
:size="18"
/>
下载
</el-button>
</div>
</div>
<el-scrollbar class="docx-scrollbar">
<ReDocxPreview
ref="previewDocxRef"
:url="previewDoc.fileUrl"
class="docx-viewer"
/>
</el-scrollbar>
</div>
</template>
<!-- 其他文件类型 -->
<template v-else>
<div class="other-preview-container">
<div class="preview-tip">
@@ -887,6 +934,43 @@ onMounted(() => {
}
}
// DOCX预览样式
.docx-preview-container {
display: flex;
flex-direction: column;
height: 70vh;
.docx-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
margin-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-lighter);
.docx-title {
font-size: 14px;
font-weight: 500;
}
.docx-actions {
display: flex;
gap: 12px;
align-items: center;
}
}
.docx-scrollbar {
flex: 1;
overflow: auto;
}
.docx-viewer {
width: 100%;
min-height: 500px;
}
}
// 其他文件预览样式
.other-preview-container {
display: flex;

View File

@@ -888,7 +888,7 @@ onUnmounted(() => {
<style scoped lang="scss">
.risk-assessment {
padding: 16px;
padding: 16px 80px 16px 16px;
.stat-card {
.stat-icon {

View File

@@ -805,7 +805,7 @@ onMounted(() => {
<style scoped lang="scss">
.workorder-management {
padding: 16px;
padding: 16px 80px 16px 16px;
.stat-card {
.stat-icon {