ywb-office-preview 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 杨文彬
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # Vue Office 文件预览组件
2
+
3
+ 一个基于 Vue 2 的 Office 文件在线预览组件,支持 PDF、Word、Excel、PowerPoint 等常见办公文档的在线预览。
4
+
5
+ ## 目标
6
+ `@vue-office`是一个非常方便的文件预览组件,但是依赖也很大,如果直接在项目中依赖,则增加了很多额外依赖,相对应带来的问题:
7
+ - 浏览器加载需要优化,如何分包,如何懒加载
8
+ - `webpack` 或 `vite` 需要配置对应的文件,相对会降低开发时的构建工具的处理速度
9
+
10
+ ## 解决方案
11
+ 采用 iframe 隔离方案:
12
+ 1. 将 `@vue-office` 的代码编译到 iframe 中,打包到 `office-preview` 目录下
13
+ 2. `vue` 组件通过 `iframe` 和 `office-preview` 下的代码交互
14
+ 3. 使用 `copy-webpack-plugin` 或 `vite-plugin-static-copy` 实现开发时动态代理`iframe`
15
+
16
+ ## 架构设计
17
+
18
+ ```mermaid
19
+ graph TD
20
+ A[Vue 主应用] -->|postMessage| B[iframe 容器]
21
+ B -->|加载| C[office-preview 应用]
22
+ C -->|文件类型判断| D[文件类型判断模块]
23
+ D -->|PDF| E[PDF 预览]
24
+ D -->|Word| F[Word 预览]
25
+ D -->|Excel| G[Excel 预览]
26
+ D -->|PPT| H[PPT 预览]
27
+
28
+ subgraph 主应用
29
+ A
30
+ end
31
+
32
+ subgraph 预览应用
33
+ B
34
+ C
35
+ D
36
+ E
37
+ F
38
+ G
39
+ H
40
+ end
41
+ ```
42
+
43
+ ### 核心模块说明
44
+
45
+ 1. **主应用模块**
46
+ - Vue 组件封装
47
+ - iframe 通信管理
48
+ - 文件 URL 处理
49
+
50
+ 2. **预览应用模块**
51
+ - 文件类型判断
52
+ - 预览组件加载
53
+ - 文件内容渲染
54
+
55
+ 3. **通信模块**
56
+ - postMessage 消息处理
57
+ - 状态同步
58
+ - 错误处理
59
+
60
+ ## 功能特点
61
+ - 📄 支持多种文件格式预览
62
+ - PDF 文件(.pdf)
63
+ - Word 文档(.doc, .docx)
64
+ - Excel 表格(.xls, .xlsx)
65
+ - PowerPoint 演示文稿(.ppt, .pptx)
66
+ - 🔍 智能文件类型识别
67
+ - 通过文件扩展名快速判断
68
+ - 通过文件内容(magic number)精确判断
69
+ - 支持新旧版 Office 文件格式
70
+ - 🎨 美观的界面设计
71
+ - 自适应容器大小
72
+ - 响应式布局
73
+ - 优雅的错误提示
74
+ - ⚡ 高性能实现
75
+ - 按需加载预览组件
76
+ - 优化的文件类型判断逻辑
77
+ - 完善的错误处理机制
78
+
79
+ ## 技术栈
80
+
81
+ - Vue 2
82
+ - TypeScript
83
+ - vue-office 系列组件
84
+ - @vue-office/pdf
85
+ - @vue-office/docx
86
+ - @vue-office/excel
87
+ - @vue-office/pptx
88
+
89
+ ## 安装使用
90
+
91
+ 1. 安装依赖
92
+ ```bash
93
+ npm install ywb-office-preview
94
+ ```
95
+
96
+ 2. 引入组件
97
+ ```vue
98
+ <template>
99
+ <file-preview :src="fileUrl" />
100
+ </template>
101
+
102
+ <script setup>
103
+ import FilePreview from './components/FilePreview.vue'
104
+ </script>
105
+ ```
106
+
107
+ ## 组件属性
108
+
109
+ | 属性名 | 类型 | 必填 | 默认值 | 说明 |
110
+ |--------|--------|------|--------|----------------|
111
+ | fileUrl | String | 是 | - | 文件访问地址 |
112
+ | fileType | String | 否 | `null` | 文件类型 |
113
+ | frameUrl | String | 否 | `./office-preview/index.html` | 预览iframe的地址 |
114
+
115
+
116
+ ## 文件类型判断逻辑
117
+
118
+ 1. 扩展名判断(快速)
119
+ - 通过文件 URL 后缀快速判断文件类型
120
+ - 支持常见文件扩展名
121
+
122
+ 2. 内容判断(精确)
123
+ - PDF: `%PDF` 文件头
124
+ - DOCX/XLSX/PPTX: ZIP 文件头 + 内容类型判断
125
+ - 旧版 Office 文件: 复合文档格式判断
126
+
127
+ ## 示例代码
128
+
129
+ ```vue
130
+ <template>
131
+ <div class="preview-container">
132
+ <file-preview
133
+ :src="fileUrl"
134
+ width="800px"
135
+ height="600px"
136
+ />
137
+ </div>
138
+ </template>
139
+
140
+ <script setup>
141
+ import { ref } from 'vue'
142
+ import FilePreview from './components/FilePreview.vue'
143
+
144
+ const fileUrl = ref('https://example.com/document.pdf')
145
+ </script>
146
+
147
+ <style scoped>
148
+ .preview-container {
149
+ width: 100%;
150
+ height: 800px;
151
+ }
152
+ </style>
153
+ ```
154
+
155
+ ## 开发指南
156
+
157
+ ### 本地开发
158
+ 1. 克隆项目
159
+ ```bash
160
+ git clone https://gitee.com/as1314/office-preview.git
161
+ ```
162
+
163
+ 2. 安装依赖
164
+ ```bash
165
+ npm install
166
+ ```
167
+
168
+ 3. 启动开发服务器
169
+ ```bash
170
+ npm run dev
171
+ ```
172
+
173
+ ### 构建部署
174
+ 1. 构建全部(库 + 预览应用)
175
+ ```bash
176
+ npm run build
177
+ ```
178
+
179
+ 2. 仅构建库(输出 lib/)
180
+ ```bash
181
+ npm run build:lib
182
+ ```
183
+
184
+ 3. 仅构建预览应用(输出 office-preview/)
185
+ ```bash
186
+ npm run build:html
187
+ ```
188
+
189
+ ## 贡献指南
190
+
191
+ 欢迎提交 Issue 和 Pull Request 来帮助改进这个项目。
192
+
193
+ ## 许可证
194
+
195
+ MIT License
196
+
197
+
198
+
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const vue = require("vue");
4
+ const uuidv4 = () => {
5
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
6
+ var r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8;
7
+ return v.toString(16);
8
+ });
9
+ };
10
+ class FrameChannel {
11
+ constructor(iframe) {
12
+ this.callbackMap = /* @__PURE__ */ new Map();
13
+ this.eventMap = /* @__PURE__ */ new Map();
14
+ this.win = (iframe == null ? void 0 : iframe.contentWindow) || window;
15
+ this.onMessage = this.onMessage.bind(this);
16
+ this.win.addEventListener("message", this.onMessage);
17
+ }
18
+ emit(event, ...args) {
19
+ return new Promise((resolve) => {
20
+ const callbackId = uuidv4();
21
+ this.callbackMap.set(callbackId, resolve);
22
+ this.win.postMessage(
23
+ {
24
+ event,
25
+ args,
26
+ callbackId
27
+ },
28
+ "*"
29
+ );
30
+ });
31
+ }
32
+ addEventListener(event, callback) {
33
+ this.eventMap.set(event, callback);
34
+ }
35
+ onMessage(event) {
36
+ const { event: eventName, args, callbackId } = event.data;
37
+ if (/^callback:/.test(eventName)) {
38
+ const backId = eventName.replace(/^callback:/, "");
39
+ const callback = this.callbackMap.get(backId);
40
+ if (callback) {
41
+ callback(args);
42
+ this.callbackMap.delete(backId);
43
+ return;
44
+ }
45
+ }
46
+ const cb = this.eventMap.get(eventName);
47
+ if (cb) {
48
+ const resp = (results) => {
49
+ this.win.postMessage(
50
+ {
51
+ event: `callback:${callbackId}`,
52
+ args: results
53
+ },
54
+ "*"
55
+ );
56
+ };
57
+ cb(resp, ...args);
58
+ }
59
+ }
60
+ }
61
+ const SupportedFileType = ["excel", "pdf", "ppt", "word"];
62
+ const SupportedFileExtension = [
63
+ {
64
+ type: "excel",
65
+ extension: [".xls", ".xlsx"]
66
+ },
67
+ {
68
+ type: "pdf",
69
+ extension: [".pdf"]
70
+ },
71
+ {
72
+ type: "word",
73
+ extension: [".doc", ".docx"]
74
+ },
75
+ {
76
+ type: "ppt",
77
+ extension: [".ppt", ".pptx"]
78
+ }
79
+ ];
80
+ class PreviewFrameChannel extends FrameChannel {
81
+ }
82
+ class OfficePreviewChannel extends FrameChannel {
83
+ }
84
+ const OfficePreview = vue.defineComponent({
85
+ name: "OfficePreview",
86
+ props: {
87
+ fileUrl: {
88
+ type: String
89
+ },
90
+ frameUrl: {
91
+ type: String,
92
+ default: "./office-preview/index.html"
93
+ },
94
+ fileType: {
95
+ type: String,
96
+ default: null
97
+ }
98
+ },
99
+ emits: {},
100
+ setup(props, context) {
101
+ const frameRef = vue.ref();
102
+ const channelRef = vue.ref();
103
+ vue.onMounted(() => {
104
+ const channel = new OfficePreviewChannel(frameRef.value);
105
+ channel.addEventListener("loaded", () => {
106
+ channelRef.value = channel;
107
+ context.emit("loaded", channel);
108
+ updateFile();
109
+ });
110
+ });
111
+ const updateFile = () => {
112
+ var _a;
113
+ if (props.fileUrl) {
114
+ (_a = channelRef.value) == null ? void 0 : _a.emit("loadFile", props.fileUrl, props.fileType);
115
+ }
116
+ };
117
+ vue.watch(() => props.fileUrl, updateFile);
118
+ vue.watch(() => props.fileType, updateFile);
119
+ return () => {
120
+ return vue.h("iframe", {
121
+ "ref": frameRef,
122
+ "attrs": {
123
+ "src": props.frameUrl
124
+ },
125
+ "class": "office-preview"
126
+ });
127
+ };
128
+ }
129
+ });
130
+ exports.FrameChannel = FrameChannel;
131
+ exports.OfficePreview = OfficePreview;
132
+ exports.OfficePreviewChannel = OfficePreviewChannel;
133
+ exports.PreviewFrameChannel = PreviewFrameChannel;
134
+ exports.SupportedFileExtension = SupportedFileExtension;
135
+ exports.SupportedFileType = SupportedFileType;
package/lib/index.css ADDED
@@ -0,0 +1,5 @@
1
+ .office-preview {
2
+ width: 100%;
3
+ height: 100%;
4
+ border: none;
5
+ }
package/lib/index.js ADDED
@@ -0,0 +1,135 @@
1
+ import { defineComponent, ref, onMounted, watch, h } from "vue";
2
+ const uuidv4 = () => {
3
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
4
+ var r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8;
5
+ return v.toString(16);
6
+ });
7
+ };
8
+ class FrameChannel {
9
+ constructor(iframe) {
10
+ this.callbackMap = /* @__PURE__ */ new Map();
11
+ this.eventMap = /* @__PURE__ */ new Map();
12
+ this.win = (iframe == null ? void 0 : iframe.contentWindow) || window;
13
+ this.onMessage = this.onMessage.bind(this);
14
+ this.win.addEventListener("message", this.onMessage);
15
+ }
16
+ emit(event, ...args) {
17
+ return new Promise((resolve) => {
18
+ const callbackId = uuidv4();
19
+ this.callbackMap.set(callbackId, resolve);
20
+ this.win.postMessage(
21
+ {
22
+ event,
23
+ args,
24
+ callbackId
25
+ },
26
+ "*"
27
+ );
28
+ });
29
+ }
30
+ addEventListener(event, callback) {
31
+ this.eventMap.set(event, callback);
32
+ }
33
+ onMessage(event) {
34
+ const { event: eventName, args, callbackId } = event.data;
35
+ if (/^callback:/.test(eventName)) {
36
+ const backId = eventName.replace(/^callback:/, "");
37
+ const callback = this.callbackMap.get(backId);
38
+ if (callback) {
39
+ callback(args);
40
+ this.callbackMap.delete(backId);
41
+ return;
42
+ }
43
+ }
44
+ const cb = this.eventMap.get(eventName);
45
+ if (cb) {
46
+ const resp = (results) => {
47
+ this.win.postMessage(
48
+ {
49
+ event: `callback:${callbackId}`,
50
+ args: results
51
+ },
52
+ "*"
53
+ );
54
+ };
55
+ cb(resp, ...args);
56
+ }
57
+ }
58
+ }
59
+ const SupportedFileType = ["excel", "pdf", "ppt", "word"];
60
+ const SupportedFileExtension = [
61
+ {
62
+ type: "excel",
63
+ extension: [".xls", ".xlsx"]
64
+ },
65
+ {
66
+ type: "pdf",
67
+ extension: [".pdf"]
68
+ },
69
+ {
70
+ type: "word",
71
+ extension: [".doc", ".docx"]
72
+ },
73
+ {
74
+ type: "ppt",
75
+ extension: [".ppt", ".pptx"]
76
+ }
77
+ ];
78
+ class PreviewFrameChannel extends FrameChannel {
79
+ }
80
+ class OfficePreviewChannel extends FrameChannel {
81
+ }
82
+ const OfficePreview = defineComponent({
83
+ name: "OfficePreview",
84
+ props: {
85
+ fileUrl: {
86
+ type: String
87
+ },
88
+ frameUrl: {
89
+ type: String,
90
+ default: "./office-preview/index.html"
91
+ },
92
+ fileType: {
93
+ type: String,
94
+ default: null
95
+ }
96
+ },
97
+ emits: {},
98
+ setup(props, context) {
99
+ const frameRef = ref();
100
+ const channelRef = ref();
101
+ onMounted(() => {
102
+ const channel = new OfficePreviewChannel(frameRef.value);
103
+ channel.addEventListener("loaded", () => {
104
+ channelRef.value = channel;
105
+ context.emit("loaded", channel);
106
+ updateFile();
107
+ });
108
+ });
109
+ const updateFile = () => {
110
+ var _a;
111
+ if (props.fileUrl) {
112
+ (_a = channelRef.value) == null ? void 0 : _a.emit("loadFile", props.fileUrl, props.fileType);
113
+ }
114
+ };
115
+ watch(() => props.fileUrl, updateFile);
116
+ watch(() => props.fileType, updateFile);
117
+ return () => {
118
+ return h("iframe", {
119
+ "ref": frameRef,
120
+ "attrs": {
121
+ "src": props.frameUrl
122
+ },
123
+ "class": "office-preview"
124
+ });
125
+ };
126
+ }
127
+ });
128
+ export {
129
+ FrameChannel,
130
+ OfficePreview,
131
+ OfficePreviewChannel,
132
+ PreviewFrameChannel,
133
+ SupportedFileExtension,
134
+ SupportedFileType
135
+ };
@@ -0,0 +1,88 @@
1
+ import { ComponentOptionsMixin } from 'vue';
2
+ import { DefineComponent } from 'vue';
3
+ import { ExtractPropTypes } from 'vue';
4
+ import { PropType } from 'vue';
5
+
6
+ declare type EventArgs<EventMap, EventName extends EventNames<EventMap>> = EventMap[EventName] extends (...args: any[]) => any ? Parameters<EventMap[EventName]> : any[];
7
+
8
+ declare type EventCallback<EventMap, EventName extends EventNames<EventMap>> = (args: EventResponse<EventMap, EventName>) => void;
9
+
10
+ declare type EventNames<EventMap> = EventMap extends {
11
+ [key: string]: any;
12
+ } ? keyof EventMap : any;
13
+
14
+ declare type EventResponse<EventMap, EventName extends EventNames<EventMap>> = EventMap[EventName] extends (...args: any[]) => any ? ReturnType<EventMap[EventName]> : never;
15
+
16
+ /**
17
+ * iframe 通信channel
18
+ * @template EventsType 支持发送的事件类型
19
+ * @template ListenersType 支持监听的事件类型
20
+ */
21
+ export declare class FrameChannel<EventsType = string | symbol, ListenersType = string | symbol> {
22
+ private win;
23
+ private callbackMap;
24
+ private eventMap;
25
+ constructor(iframe?: HTMLIFrameElement);
26
+ emit<T extends EventNames<EventsType>>(event: T, ...args: EventArgs<EventsType, T>): Promise<EventResponse<EventsType, T>>;
27
+ addEventListener<T extends EventNames<ListenersType>>(event: T, callback: (resolve: EventCallback<ListenersType, T>, ...args: EventArgs<ListenersType, T>) => void): void;
28
+ private onMessage;
29
+ }
30
+
31
+ export declare const OfficePreview: DefineComponent< {
32
+ fileUrl: {
33
+ type: StringConstructor;
34
+ };
35
+ frameUrl: {
36
+ type: StringConstructor;
37
+ default: string;
38
+ };
39
+ fileType: {
40
+ type: PropType<SupportedFileTypes>;
41
+ default: null;
42
+ };
43
+ }, () => JSX.Element, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
44
+ loaded: (channel: OfficePreviewChannel) => any;
45
+ }, string, Readonly<ExtractPropTypes< {
46
+ fileUrl: {
47
+ type: StringConstructor;
48
+ };
49
+ frameUrl: {
50
+ type: StringConstructor;
51
+ default: string;
52
+ };
53
+ fileType: {
54
+ type: PropType<SupportedFileTypes>;
55
+ default: null;
56
+ };
57
+ }>>, {
58
+ frameUrl: string;
59
+ fileType: SupportedFileTypes;
60
+ }>;
61
+
62
+ export declare class OfficePreviewChannel extends FrameChannel<PreviewComponentEvents, PreviewFrameEvents> {
63
+ }
64
+
65
+ export declare interface PreviewComponentEvents {
66
+ loadFile?: (fileUrl: string, fileType?: SupportedFileTypes) => void;
67
+ }
68
+
69
+ export declare class PreviewFrameChannel extends FrameChannel<PreviewFrameEvents, PreviewComponentEvents> {
70
+ }
71
+
72
+ export declare interface PreviewFrameEvents {
73
+ loaded?: () => void;
74
+ }
75
+
76
+ /**
77
+ * 文件类型映射表
78
+ */
79
+ export declare const SupportedFileExtension: {
80
+ type: SupportedFileTypes;
81
+ extension: string[];
82
+ }[];
83
+
84
+ export declare const SupportedFileType: SupportedFileTypes[];
85
+
86
+ declare type SupportedFileTypes = "word" | "excel" | "ppt" | "pdf";
87
+
88
+ export { }