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

@@ -187,6 +187,7 @@ menus:
pureSwiper: Swiper Plugin
pureVirtualList: Virtual List
purePdf: PDF Preview
pureDocx: DOCX Preview
pureExcel: Export Excel
pureInfiniteScroll: Table Infinite Scroll
pureSensitive: Sensitive Filter

View File

@@ -187,6 +187,7 @@ menus:
pureSwiper: Swiper插件
pureVirtualList: 虚拟列表
purePdf: PDF预览
pureDocx: DOCX预览
pureExcel: 导出Excel
pureInfiniteScroll: 表格无限滚动
pureSensitive: 敏感词过滤

View File

@@ -70,6 +70,7 @@
"cropperjs": "^1.6.2",
"dayjs": "^1.11.20",
"deep-chat": "^2.4.2",
"docx-preview": "^0.3.7",
"echarts": "^6.0.0",
"el-table-infinite-scroll": "^3.0.8",
"element-plus": "^2.13.6",

72
pnpm-lock.yaml generated
View File

@@ -77,6 +77,9 @@ importers:
deep-chat:
specifier: ^2.4.2
version: 2.4.2
docx-preview:
specifier: ^0.3.7
version: 0.3.7
echarts:
specifier: ^6.0.0
version: 6.0.0
@@ -2919,6 +2922,9 @@ packages:
core-js@3.49.0:
resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cosmiconfig-typescript-loader@6.2.0:
resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==}
engines: {node: '>=v18'}
@@ -3136,6 +3142,9 @@ packages:
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
docx-preview@0.3.7:
resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -3843,6 +3852,9 @@ packages:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -4073,6 +4085,9 @@ packages:
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4115,6 +4130,9 @@ packages:
lie@3.1.1:
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -4523,6 +4541,9 @@ packages:
package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -4925,6 +4946,9 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -5038,6 +5062,9 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -5075,6 +5102,9 @@ packages:
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -5213,6 +5243,9 @@ packages:
resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==}
engines: {node: '>=20'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -8521,6 +8554,8 @@ snapshots:
core-js@3.49.0: {}
core-util-is@1.0.3: {}
cosmiconfig-typescript-loader@6.2.0(@types/node@20.19.37)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3):
dependencies:
'@types/node': 20.19.37
@@ -8737,6 +8772,10 @@ snapshots:
dijkstrajs@1.0.3: {}
docx-preview@0.3.7:
dependencies:
jszip: 3.10.1
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -9542,6 +9581,8 @@ snapshots:
dependencies:
is-docker: 2.2.1
isarray@1.0.0: {}
isexe@2.0.0: {}
istanbul-lib-coverage@3.2.2: {}
@@ -10014,6 +10055,13 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -10052,6 +10100,10 @@ snapshots:
dependencies:
immediate: 3.0.6
lie@3.3.0:
dependencies:
immediate: 3.0.6
lightningcss-android-arm64@1.32.0:
optional: true
@@ -10427,6 +10479,8 @@ snapshots:
package-manager-detector@1.6.0: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -10793,6 +10847,16 @@ snapshots:
react-is@17.0.2: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -10930,6 +10994,8 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
@@ -10960,6 +11026,8 @@ snapshots:
set-blocking@2.0.0: {}
setimmediate@1.0.5: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -11096,6 +11164,10 @@ snapshots:
get-east-asian-width: 1.5.0
strip-ansi: 7.2.0
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1

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 {