# 移动端文件预览
# 集成 pdf 预览,docx 预览,图片预览
filePreview/index.vue
点击查看
<!-- 文件预览 -->
<template>
<span class="text-primary fs-26">
<span @click="handleCopyFilePath(filePath)">
<img
class="w-full h-full"
:style="{ objectFit: objectFit }"
v-if="isPicture && !showText"
:src="filePath"
/>
<span v-if="showText">
{{ text || approvalFileName(filePath) }}
</span>
</span>
<div
v-if="visible"
class="overflow-hidden fixed top-0 left-0 w-full"
:class="[supportPreview ? 'h-screen h-dvh' : 'h-0']"
style="z-index: 10"
>
<div
:id="containerId"
class="h-screen h-dvh w-full overflow-auto container"
>
<div
v-if="supportPreview"
class="fixed right-50 bottom-50 bc-primary fs-20 text-white px-20 py-20 rounded-20 w-40 h-40 flex-row items-center justify-center"
style="z-index: 20"
@click="visible = false"
>
返回
</div>
<div
v-if="supportPreview"
class="fixed left-50 bottom-50 bc-primary fs-20 text-white px-20 py-20 rounded-20 w-40 h-40 flex-row items-center justify-center"
style="z-index: 20"
@click="previewWithApp"
>
测试
</div>
<pdfH5Preview
v-if="isPdf"
ref="pdfH5PreviewRef"
:filePath="filePath"
:supportPreview="supportPreview"
@onErr="handleErr"
@onSuccess="supportPreview = true"
/>
<docxPreview
v-if="isDocx"
ref="docxPreviewRef"
:filePath="filePath"
:supportPreview="supportPreview"
@onErr="handleErr"
@onSuccess="supportPreview = true"
/>
<excelPreview
v-if="isXlsx"
ref="xlsxPreviewRef"
:filePath="filePath"
:supportPreview="supportPreview"
@onErr="handleErr"
@onSuccess="supportPreview = true"
/>
</div>
</div>
</span>
</template>
<script>
import { Dialog, ImagePreview } from "vant";
import pdfH5Preview from "./pdfH5Preview.vue";
import docxPreview from "./docxPreview.vue";
import excelPreview from "./excelPreview.vue";
import { copy, getEnv } from "@/utils/utils";
// 随机的字符串
const randomString = () => Math.random().toString(36).slice(2);
export default {
components: { pdfH5Preview, docxPreview, excelPreview },
props: {
text: String,
objectFit: {
type: String,
default: "scale-down",
},
showText: {
type: Boolean,
default: true,
},
filePath: {
type: String,
required: true,
},
},
data() {
return {
containerId: randomString(),
visible: false,
supportPreview: false,
};
},
computed: {
isPicture() {
const path = this.filePath.toLowerCase();
return /\.(jpg|jpeg|png)$/.test(path);
},
isPdf() {
const path = this.filePath.toLowerCase();
return /\.(pdf)$/.test(path);
},
isDocx() {
const path = this.filePath.toLowerCase();
return /\.(docx)$/.test(path);
},
isXlsx() {
const path = this.filePath.toLowerCase();
return /\.(xlsx)$/.test(path);
},
},
watch: {},
methods: {
approvalFileName(fileUrl = "") {
const name = fileUrl.split("/").slice(-1)[0];
return name;
},
previewWithApp() {
// 微信小程序环境调用本身的 api
if (window.uni && getEnv().isWxmp && !this.isPicture) {
const fileType = this.filePath.split(".").slice(-1)[0];
const h5Url = encodeURIComponent(window.location.href);
const filePath = encodeURIComponent(this.filePath);
window.uni.navigateTo({
url: `/pages/webview/preview?h5Url=${h5Url}&fileType=${fileType}&filePath=${filePath}`,
});
return;
}
},
async handleCopyFilePath(fileUrl) {
if (this.isPicture) {
ImagePreview([fileUrl]);
return;
}
if (this.isPdf) {
this.visible = true;
return;
}
if (this.isDocx) {
this.visible = true;
return;
}
if (this.isXlsx) {
this.visible = true;
return;
}
this.handleErr();
},
handleErr(errInfo) {
console.log(errInfo, "errInfo");
this.visible = false;
copy(this.filePath)
.then(() => {
Dialog.alert({
title: "提示",
message: "不支持在线预览,文件链接已复制至剪切板,请通过浏览器打开",
theme: "round-button",
confirmButtonColor: "#3485f8",
});
})
.catch(() => {
Dialog.alert({
title: "提示",
message: "当前环境不支持复制",
theme: "round-button",
confirmButtonColor: "#3485f8",
});
});
},
},
created() {},
mounted() {},
};
</script>
<style lang="less" scoped>
.container {
transition: all 0.3s;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# docx预览
安装依赖
npm i docx-preview easyscroller
1
docxPreview.vue
点击查看
<!-- docx文件预览 -->
<template>
<div class="bc-white h-dvh h-screen w-screen docxContent">
<div
v-if="supportPreview"
class="text-white bottom-200 right-50 fixed bc-overlay w-40 h-40 px-20 py-20 flex-row items-center justify-center rounded-20"
style="z-index: 20"
@click="backTop"
>
<van-icon size="22" name="back-top" />
</div>
<!-- <div class="zoomUp" style="z-index: 20" @click="zoomUp">
<van-icon name="plus" />
</div>
<div class="zoomDown" style="z-index: 20" @click="zoomDown">
<van-icon name="minus" />
</div> -->
<div class="docxPreview" style="width: fit-content">
<div ref="previewContainer"></div>
</div>
</div>
</template>
<script>
import { Toast } from "vant";
import { renderAsync } from "docx-preview";
import { scaleFill } from "@/utils/utils";
import { EasyScroller } from "easyscroller";
export default {
components: {},
props: {
filePath: {
type: String,
required: true,
},
supportPreview: {
type: Boolean,
default: false,
},
},
data() {
return {
scale: 1,
baseScale: 1,
scrollControl: null,
};
},
computed: {},
watch: {},
methods: {
async previewDocx(file) {
const that = this;
Toast.loading({
duration: 0,
message: "加载中...",
forbidClick: true,
loadingType: "spinner",
});
try {
let blob;
if (file.raw) {
blob = file.raw; // 本地预览
console.log(blob, "blob1");
} else {
const response = await fetch(file);
blob = await response.blob();
}
this.$nextTick(() => {
renderAsync(blob, this.$refs.previewContainer, null, {
inWrapper: true, // 启用围绕文档内容渲染包装器
ignoreWidth: false, // 禁止页面渲染宽度
ignoreHeight: false, // 禁止页面渲染高度
ignoreFonts: false, // 禁止字体渲染
breakPages: true, // 在分页符上启用分页
ignoreLastRenderedPageBreak: true, //禁用lastRenderedPageBreak元素的分页
experimental: false, //启用实验性功能(制表符停止计算)
trimXmlDeclaration: true, //如果为真,xml声明将在解析之前从xml文档中删除
}).then((res) => {
Toast.clear();
setTimeout(() => {
that.scale = scaleFill({
target: ".docxPreview",
parent: ".docxContent",
autoScale: false,
});
that.baseScale = that.scale;
const targetDom = document.querySelector(".docxPreview");
that.scrollControl = new EasyScroller(targetDom, {
scrollingX: true,
scrollingY: true,
zooming: true,
minZoom: that.baseScale,
maxZoom: 2,
});
}, 300);
that.$emit("onSuccess", res);
});
});
} catch (error) {
Toast.clear();
console.error("无法预览文件", error);
this.$emit("onErr", error);
}
},
backTop() {
const Scroller = this.scrollControl.getScroller();
Scroller.scrollTo(
Scroller.__scrollLeft,
0,
Scroller.options.snapping,
Scroller.options.__zoomLevel
);
},
zoomUp() {},
zoomDown() {},
},
created() {},
mounted() {
this.previewDocx(this.filePath);
},
destroyed() {
this.scrollControl.destroy();
},
};
</script>
<style lang="less" scoped>
::v-deep .docx-wrapper {
padding: 0 !important;
}
::v-deep .docx {
padding: 20px !important;
margin-bottom: 20px !important;
}
.zoomUp {
color: #fff;
background-color: var(--overlay-color);
width: 40px;
height: 40px;
padding: 20px;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
right: 50px;
bottom: 400px;
}
.zoomDown {
color: #fff;
background-color: var(--overlay-color);
width: 40px;
height: 40px;
padding: 20px;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
right: 50px;
bottom: 300px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# pdf预览
安装依赖
npm i pdfh5
1
pdfH5Preview.vue
点击查看
<!-- pdfH5预览 -->
<template>
<div class="h-full">
<div :id="pdfPreviewId"></div>
</div>
</template>
<script>
// 随机的字符串
const randomString = () => Math.random().toString(36).slice(2);
import Pdfh5 from "pdfh5";
import "pdfh5/css/pdfh5.css";
export default {
components: {},
props: {
filePath: {
type: String,
required: true,
},
supportPreview: {
type: Boolean,
default: false,
},
},
data() {
return {
pdfPreviewId: randomString(),
pdfh5: null,
loading: false,
};
},
computed: {},
watch: {},
methods: {
previewPdf(filePath) {
//实例化
const that = this;
this.pdfh5 = new Pdfh5(`#${this.pdfPreviewId}`, {
pdfurl: filePath,
// cMapUrl:"https://unpkg.com/pdfjs-dist@3.8.162/cmaps/",
// responseType: "blob" // blob arraybuffer
});
//监听完成事件
this.pdfh5.on("complete", function (status, msg, time) {
//禁止手势缩放
// that.pdfh5.zoomEnable(false);
if (status === "error") {
if (that.loading) return;
that.$emit("onErr", { status, msg, time });
} else {
that.loading = true;
that.$emit("onSuccess", { status, msg, time });
}
});
},
},
created() {},
mounted() {
this.previewPdf(this.filePath);
},
destroyed() {
this.pdfh5.destroy();
},
};
</script>
<style lang="css" scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# excel预览
安装依赖
npm i @vue-office/excel
1
点击查看
<!-- excel文件预览 -->
<template>
<vue-office-excel
:src="filePath"
style="height: 100%"
@rendered="renderedHandler"
@error="errorHandler"
/>
</template>
<script>
import "@vue-office/excel/lib/index.css";
import VueOfficeExcel from "@vue-office/excel";
export default {
components: { VueOfficeExcel },
props: {
filePath: {
type: String,
required: true,
},
supportPreview: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
computed: {},
watch: {},
methods: {
renderedHandler(res) {
this.$emit("onSuccess", res);
},
errorHandler(error) {
this.$emit("onErr", error);
},
},
created() {},
mounted() {},
};
</script>
<style lang="css" scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# utils
// 复制到剪切板
export const copy = (txt) => {
if (document.queryCommandSupported("copy")) {
let textarea = document.createElement("textarea");
textarea.value = txt;
textarea.readOnly = "readOnly";
document.body.appendChild(textarea);
textarea.select(); // 选中文本内容
textarea.setSelectionRange(0, txt.length);
let result = document.execCommand("copy");
textarea.remove();
return Promise.resolve("复制成功");
} else {
return Promise.reject("浏览器不支持复制");
}
};
// 元素缩放到撑满容器
function scaleFill(options) {
const { target, parent, autoScale = true } = options;
const container = document.querySelector(parent);
const el = document.querySelector(target);
console.log(el.clientWidth, "scaleFill", container.clientWidth);
if (el.clientWidth > container.clientWidth) {
const scale = container.clientWidth / el.clientWidth;
if (autoScale) {
el.style.transformOrigin = "0 0";
el.style.transform = `scale(${scale})`;
}
return scale;
}
return 1;
}
// 获取浏览器环境
const getEnv = () => {
const ua = window.navigator.userAgent.toLowerCase();
const browser = {
// android平台
isAndroid: /Android|Adr/i.test(ua),
// ios平台
isIos: /iPhone|iPod|iPad/i.test(ua),
// 微信生态
isWechat: /MicroMessenger/i.test(ua),
// 微信小程序
isWxmp:
/miniProgram/i.test(ua) || window.__wxjs_environment === "miniprogram",
// 钉钉环境
isDingding: /DingTalk/i.test(ua),
};
return browser;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54