cloud-web-corejs 1.0.54-dev.804 → 1.0.54-dev.806
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/package.json +1 -1
- package/src/components/bizTab/BizBillActions.vue +2 -2
- package/src/components/wf/content.vue +5 -3
- package/src/components/wf/wf.js +2 -0
- package/src/components/wf/wfActionDropdown.vue +2 -2
- package/src/components/wf/wfUtil.js +6 -4
- package/src/components/xform/form-designer/form-widget/dialog/formDialog.vue +8 -5
- package/src/components/xform/form-designer/form-widget/field-widget/baseAttachment-widget.vue +14 -0
- package/src/components/xform/form-designer/setting-panel/index.vue +0 -4
- package/src/components/xform/form-designer/setting-panel/property-editor/dropdownFlag-editor.vue +4 -0
- package/src/router/modules/customer.js +4 -4
- package/src/utils/pdfUtil.js +300 -70
- package/src/views/bd/setting/bd_company_env/dialog.vue +1 -1
- package/src/views/bd/setting/table_model/edit.vue +15 -29
- package/src/views/bd/setting/table_model/mixins/edit.js +295 -149
- package/src/views/bd/setting/table_model/mixins/zdDialog.js +32 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<!-- 按钮顺序(.fr 里 DOM 次序即从左到右):
|
|
4
4
|
翻单 → 提交流程 → 业务按钮(默认插槽) → 重置 → 保存
|
|
5
5
|
#before 是留给"要排在翻单之前"的特例,一般不用。
|
|
6
|
-
另外 wfUtil
|
|
6
|
+
另外 wfUtil 的「更多」下拉 / 「修改单据」是 append 到本容器最前面的,会落在最左侧。 -->
|
|
7
7
|
<slot name="before"></slot>
|
|
8
8
|
<!-- ① 上一单 / 下一单:列表侧 openDetail 传了 navRows 才出现 -->
|
|
9
9
|
<template v-if="showBillNav">
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
* </div>
|
|
84
84
|
*
|
|
85
85
|
* 注意:根节点是 .d-header 里的 .fr,别再往外包一层,否则布局会散,
|
|
86
|
-
* 而且 wfUtil
|
|
86
|
+
* 而且 wfUtil 的「更多」下拉 / 「修改单据」就是往 .el-form .d-header > .fr 里塞的。
|
|
87
87
|
*/
|
|
88
88
|
export default {
|
|
89
89
|
name: "BizBillActions",
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
v-if="!isHideWf"
|
|
7
7
|
>
|
|
8
8
|
<template #default>
|
|
9
|
-
<!--
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
<!-- 审批按钮条:标题栏那份挂上后(headerActionsMounted)整条 v-show 藏掉。
|
|
10
|
+
用 v-show 而不是 v-if,是为了保住 addOpinionBtn——下拉里「补充意见」走的是
|
|
11
|
+
$refs.addOpinionBtn.openDialog()。开了 wfActionInHeaderEnabled 时,详情标题栏
|
|
12
|
+
另挂一份(wfActionDropdown.vue):通过/驳回/撤回平铺,其余收进「更多」下拉。 -->
|
|
12
13
|
<div
|
|
13
14
|
ref="wfActionBar"
|
|
14
15
|
style="margin: -36px 0 8px 124px; position: relative; z-index: 2; height: 28px"
|
|
15
16
|
v-if="!wfConfig.hiddenAllButton"
|
|
17
|
+
v-show="!headerActionsMounted"
|
|
16
18
|
>
|
|
17
19
|
<el-button
|
|
18
20
|
type="success"
|
package/src/components/wf/wf.js
CHANGED
|
@@ -386,6 +386,8 @@ wfContentMixin = {
|
|
|
386
386
|
candidateDatas:[],
|
|
387
387
|
wfSendTaskId: null,
|
|
388
388
|
showAddOpinionButton: false,
|
|
389
|
+
//标题栏那份审批入口挂上后为 true,流程模块按钮条据此 v-show 隐藏
|
|
390
|
+
headerActionsMounted: false,
|
|
389
391
|
|
|
390
392
|
addOpinionEnabled: false,
|
|
391
393
|
modifyCurrentCandidateEnabled: false,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="wf-action-dropdown-wrap button-sty" v-show="items.length">
|
|
3
|
-
<!-- 通过 / 驳回 / 撤回
|
|
3
|
+
<!-- 通过 / 驳回 / 撤回 挪出下拉,在标题栏直接平铺;其余操作仍收在「更多」下拉里 -->
|
|
4
4
|
<el-button
|
|
5
5
|
v-for="item in standaloneItems"
|
|
6
6
|
:key="item.key"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</el-button>
|
|
13
13
|
<el-dropdown trigger="hover" v-if="dropdownItems.length">
|
|
14
14
|
<el-button type="primary" class="button-sty">
|
|
15
|
-
{{ $t1("
|
|
15
|
+
{{ $t1("更多") }}
|
|
16
16
|
<i class="el-icon-arrow-down el-icon--right"></i>
|
|
17
17
|
</el-button>
|
|
18
18
|
<el-dropdown-menu slot="dropdown">
|
|
@@ -200,11 +200,12 @@ f = async function (option) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/* 审批操作:开了 wfActionInHeaderEnabled 时,在详情模块标题栏再挂一份 —— 通过/驳回/撤回
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
平铺成独立按钮,其余收进「更多」下拉(wfActionDropdown.vue)。挂上后把流程面板
|
|
204
|
+
headerActionsMounted 置 true,模块内原按钮条 v-show 藏掉(addOpinionBtn 还在,
|
|
205
|
+
下拉里「补充意见」才能点开)。点击走流程面板实例上的同一套方法。
|
|
205
206
|
放在「修改单据」「流程启动」之后挂——appendPrevChildDom 是前插,最后插的排最左,
|
|
206
|
-
|
|
207
|
-
hiddenAllButton / 素身详情(取不到 .fr)
|
|
207
|
+
最终顺序:通过/驳回/撤回/更多 → 修改单据 → 内建/业务按钮。
|
|
208
|
+
hiddenAllButton / 素身详情(取不到 .fr) 自动跳过,那种情况模块按钮条继续留着。
|
|
208
209
|
_hideWf 也必须跳过:那种页面 content.vue 根节点 v-if 掉了,弹框根本没渲染出来,
|
|
209
210
|
挂上去就是一排点了没反应的按钮(wfInfo 照样有值,光看 items 判断不出来)。 */
|
|
210
211
|
if (shouldMountHeaderActions({
|
|
@@ -221,6 +222,7 @@ f = async function (option) {
|
|
|
221
222
|
});
|
|
222
223
|
instances.push(dropdownVm);
|
|
223
224
|
appendPrevChildDom(wfBtnRegion, dropdownVm.$mount().$el);
|
|
225
|
+
contentInstance.headerActionsMounted = true;
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
registerDispose();
|
|
@@ -168,7 +168,9 @@ export default {
|
|
|
168
168
|
width: "1200px",
|
|
169
169
|
fullscreen: this.option.fullscreen,
|
|
170
170
|
};
|
|
171
|
-
config = Object.assign({}, config, this.option.dialogConfig, {
|
|
171
|
+
config = Object.assign({}, config, this.option.dialogConfig, {
|
|
172
|
+
customClass,
|
|
173
|
+
});
|
|
172
174
|
config.title = this.$t1(config.title);
|
|
173
175
|
return config;
|
|
174
176
|
},
|
|
@@ -190,7 +192,9 @@ export default {
|
|
|
190
192
|
if (this.option.showFooter == false) {
|
|
191
193
|
classStr = classStr + " nfootBtn";
|
|
192
194
|
}
|
|
193
|
-
config = Object.assign({}, config, this.option.bodyConfig, {
|
|
195
|
+
config = Object.assign({}, config, this.option.bodyConfig, {
|
|
196
|
+
class: classStr,
|
|
197
|
+
});
|
|
194
198
|
return config;
|
|
195
199
|
},
|
|
196
200
|
},
|
|
@@ -198,7 +202,6 @@ export default {
|
|
|
198
202
|
/*let dataId = this.option?.formConfig?.dataId??null
|
|
199
203
|
delete this.option?.formConfig?.dataId
|
|
200
204
|
this.dataId = dataId;*/
|
|
201
|
-
|
|
202
205
|
this.formConfig = this.option?.formConfig || {};
|
|
203
206
|
|
|
204
207
|
let currentFormConfig = this.getFormConfig();
|
|
@@ -305,9 +308,9 @@ export default {
|
|
|
305
308
|
this.close();
|
|
306
309
|
}
|
|
307
310
|
},
|
|
308
|
-
setConfirmButtonHidden(val){
|
|
311
|
+
setConfirmButtonHidden(val) {
|
|
309
312
|
this.confirmButtonVisible = val || false;
|
|
310
|
-
}
|
|
313
|
+
},
|
|
311
314
|
},
|
|
312
315
|
};
|
|
313
316
|
</script>
|
package/src/components/xform/form-designer/form-widget/field-widget/baseAttachment-widget.vue
CHANGED
|
@@ -86,6 +86,13 @@ export default {
|
|
|
86
86
|
if (this.isSameFieldModelValue(modelValue)) return;
|
|
87
87
|
this.handleChangeEvent(modelValue);
|
|
88
88
|
},
|
|
89
|
+
/* fieldMixin.setDisabled 只改 field.options.disabled 和 this.disabled,而本组件的
|
|
90
|
+
可编辑态是 initOption 里快照进 option.edit 的,不同步就永远停在 created 时的值。
|
|
91
|
+
这里只改 option 的字段、不换 option 引用:换引用会让 base-attachment 取值用的
|
|
92
|
+
$watch 重新求值,白跑一次 initRows(cloneDeep + nextTick 五个 init),表格里按行数放大。 */
|
|
93
|
+
'field.options.disabled'(val) {
|
|
94
|
+
this.option.edit = !val;
|
|
95
|
+
},
|
|
89
96
|
'option':function(){
|
|
90
97
|
|
|
91
98
|
}
|
|
@@ -156,6 +163,13 @@ export default {
|
|
|
156
163
|
},
|
|
157
164
|
methods: {
|
|
158
165
|
initOption(){
|
|
166
|
+
/* setDisabled 是裸赋值 this.field.options.disabled = xxx;存量表单 JSON 的 options
|
|
167
|
+
里缺这个键时会变成非响应式新增属性,上面的 watch 收不到通知(fieldMixin 补默认
|
|
168
|
+
options 的 initConfig 已注释掉,indexMixin/data-table-mixin 里都要 hasOwnProperty
|
|
169
|
+
兜底,说明确有缺键的 schema)。先补键,让后续赋值走响应式通道。 */
|
|
170
|
+
if (!this.field.options.hasOwnProperty('disabled')) {
|
|
171
|
+
this.$set(this.field.options, 'disabled', false);
|
|
172
|
+
}
|
|
159
173
|
this.option = {
|
|
160
174
|
title: '附件',
|
|
161
175
|
edit: !this.field.options.disabled,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const constantRoutes = [
|
|
2
|
-
{
|
|
2
|
+
/*{
|
|
3
3
|
// 呼叫中心(CC)专用入口:只带 appid/appkey/account/callNo,
|
|
4
4
|
// 先换取 tp 再 replace 到 /form_outLinkView
|
|
5
5
|
path: "/cc_form_outLinkView",
|
|
@@ -22,7 +22,7 @@ export const constantRoutes = [
|
|
|
22
22
|
icon: "dashboard",
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
|
-
|
|
25
|
+
{
|
|
26
26
|
// bizTab 示例 A:手写接口版(前端 mock,无流程,不吃后台前置条件)
|
|
27
27
|
path: "/demo/bizTab/simple",
|
|
28
28
|
component: () => import("@base/views/demo/bizTab/simple/list"),
|
|
@@ -70,7 +70,7 @@ export const constantRoutes = [
|
|
|
70
70
|
title: "formDialog 示例",
|
|
71
71
|
icon: "dashboard",
|
|
72
72
|
},
|
|
73
|
-
},
|
|
73
|
+
},
|
|
74
74
|
],
|
|
75
|
-
}
|
|
75
|
+
},*/
|
|
76
76
|
];
|
package/src/utils/pdfUtil.js
CHANGED
|
@@ -1,71 +1,301 @@
|
|
|
1
|
-
import html2canvas from "html2canvas";
|
|
2
|
-
import jspdf from "jspdf";
|
|
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
|
-
|
|
1
|
+
import html2canvas from "html2canvas";
|
|
2
|
+
import jspdf from "jspdf";
|
|
3
|
+
import { Message } from "element-ui";
|
|
4
|
+
|
|
5
|
+
//A4 纵向 210mm x 297mm,四边各留 10mm 边距,正文区 190 x 277
|
|
6
|
+
const PAGE_WIDTH = 190;
|
|
7
|
+
const PAGE_HEIGHT = 277;
|
|
8
|
+
const PAGE_MARGIN = 10;
|
|
9
|
+
|
|
10
|
+
//浏览器对 canvas 有硬限制(Chrome:单边 16384px、总面积 2^28 像素),
|
|
11
|
+
//超过之后 toDataURL 直接返回空白图——长详情页很容易撞上,这里按内容尺寸回退 scale。
|
|
12
|
+
const MAX_CANVAS_SIDE = 16384;
|
|
13
|
+
const MAX_CANVAS_AREA = 268435456;
|
|
14
|
+
|
|
15
|
+
const JPEG_QUALITY = 0.92;
|
|
16
|
+
|
|
17
|
+
const OVERFLOW_CLIP = /auto|scroll|hidden|overlay/;
|
|
18
|
+
|
|
19
|
+
//遮罩挂在导出根元素的父级上,正常不会进截图范围;这条是兜底,
|
|
20
|
+
//防止根元素本身就没有父级(游离节点)时遮罩落进截图里
|
|
21
|
+
const LOADING_MASK = ".el-loading-mask";
|
|
22
|
+
|
|
23
|
+
function nextFrame() {
|
|
24
|
+
//改完内联样式要等浏览器重排完再截图,一帧不够(vxe-table 的 resize 监听在下一帧才补高度)
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//纵向被裁掉内容的容器:详情页的 .d-cont、vxe-table 的 body-wrapper/fixed-wrapper 都属于这一类。
|
|
31
|
+
//只放开纵向:横向滚动的宽表放开也没用——截图区域按根元素的宽度取,
|
|
32
|
+
//溢出到根元素外面的列照样画不进去,反而会把版面撑乱。
|
|
33
|
+
function isVerticallyClipped(el, style) {
|
|
34
|
+
return (
|
|
35
|
+
OVERFLOW_CLIP.test(style.overflowY) && el.scrollHeight > el.clientHeight + 1
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//把 dom 自身、它的祖先、以及内部所有带纵向滚动/裁剪的容器统一放开高度,
|
|
40
|
+
//返回一个还原函数。原实现只处理了 .d-cont 一层,且在 html2canvas 真正克隆 DOM 之前
|
|
41
|
+
//就把高度还原了(没 await),所以放开高度这一步等于没做,导出的永远只有可视区那一屏。
|
|
42
|
+
export function expandForCapture(dom) {
|
|
43
|
+
//读写严格分成两趟:先只读地挑出要放开的元素,再统一写内联样式。
|
|
44
|
+
//边读 getComputedStyle/scrollHeight 边写样式,会让浏览器每个元素强制重排一次
|
|
45
|
+
//(layout thrashing)——详情页几千个节点就是几千次整页重排,点完按钮会明显卡住。
|
|
46
|
+
const targets = [];
|
|
47
|
+
|
|
48
|
+
//祖先:#containt / .app-main 这类布局层用 100vh、calc(100vh - n) 定死了高度,
|
|
49
|
+
//不放开的话 dom 自己 height:auto 也会被父级的滚动容器裁掉。
|
|
50
|
+
let parent = dom.parentElement;
|
|
51
|
+
while (parent && parent !== document.body && parent !== document.documentElement) {
|
|
52
|
+
const style = window.getComputedStyle(parent);
|
|
53
|
+
if (isVerticallyClipped(parent, style) || style.height !== "auto") {
|
|
54
|
+
targets.push(parent);
|
|
55
|
+
}
|
|
56
|
+
parent = parent.parentElement;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
targets.push(dom);
|
|
60
|
+
|
|
61
|
+
const children = dom.querySelectorAll("*");
|
|
62
|
+
for (let i = 0; i < children.length; i++) {
|
|
63
|
+
const el = children[i];
|
|
64
|
+
const style = window.getComputedStyle(el);
|
|
65
|
+
if (isVerticallyClipped(el, style)) targets.push(el);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//读:scrollTop 也是会触发重排的读操作,必须留在这一趟里
|
|
69
|
+
const records = targets.map((el) => ({
|
|
70
|
+
el: el,
|
|
71
|
+
style: el.getAttribute("style"),
|
|
72
|
+
scrollTop: el.scrollTop,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
//写
|
|
76
|
+
for (let i = 0; i < records.length; i++) {
|
|
77
|
+
const el = records[i].el;
|
|
78
|
+
el.style.height = "auto";
|
|
79
|
+
el.style.maxHeight = "none";
|
|
80
|
+
el.style.overflow = "visible";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return function restore() {
|
|
84
|
+
//倒序还原,先还原内层再还原外层,避免中间态触发多余的重排
|
|
85
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
86
|
+
const record = records[i];
|
|
87
|
+
if (record.style === null) {
|
|
88
|
+
record.el.removeAttribute("style");
|
|
89
|
+
} else {
|
|
90
|
+
record.el.setAttribute("style", record.style);
|
|
91
|
+
}
|
|
92
|
+
record.el.scrollTop = record.scrollTop;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function pickScale(width, height, expected) {
|
|
98
|
+
let scale = expected || Math.min(window.devicePixelRatio || 1, 2);
|
|
99
|
+
if (!width || !height) return scale;
|
|
100
|
+
scale = Math.min(
|
|
101
|
+
scale,
|
|
102
|
+
MAX_CANVAS_SIDE / width,
|
|
103
|
+
MAX_CANVAS_SIDE / height,
|
|
104
|
+
Math.sqrt(MAX_CANVAS_AREA / (width * height))
|
|
105
|
+
);
|
|
106
|
+
return Math.max(scale, 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildIgnore(selectors) {
|
|
110
|
+
let list = [LOADING_MASK];
|
|
111
|
+
if (selectors) list = list.concat(selectors);
|
|
112
|
+
const selector = list.join(",");
|
|
113
|
+
return function (el) {
|
|
114
|
+
return typeof el.matches === "function" && el.matches(selector);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//手写一层 element 样式的遮罩,而不是用 Loading.service,两个原因都躲不掉:
|
|
119
|
+
//1. `Loading.service({ fullscreen: true })` 是全局单例,会被 request.js 里的请求收尾顺手关掉,
|
|
120
|
+
// 而导出这几秒里页面上别的请求随时可能回来;
|
|
121
|
+
//2. `Loading.service({ target: dom })` 会给 dom 加 el-loading-parent--relative(position:relative !important),
|
|
122
|
+
// 页面里 .slide-nav 这类绝对定位元素的包含块跟着变,位置会挪——而且是连截进 PDF 里的。
|
|
123
|
+
//自己建的 DOM 节点两头都不沾,也不用担心被别处关掉。
|
|
124
|
+
//挂载点取根元素的父级,不挂 body:微前端下 body 是主应用的,往上加节点算越界;
|
|
125
|
+
//挂父级也让这个节点跟着页面走,页面被销毁时不会剩一层孤儿遮罩。
|
|
126
|
+
//只遮导出的那块区域,不铺满视口:左侧菜单、页签这些跟导出无关的地方不该被压住。
|
|
127
|
+
//做法是 fixed + 按导出元素的 getBoundingClientRect 圈定范围(取展开之前的可视位置),
|
|
128
|
+
//这样既不用给挂载点加 position:relative(会挪走 .slide-nav 这类绝对定位元素,
|
|
129
|
+
//也正是不能用 Loading.service({ target }) 的原因),也不会盖到别人头上。
|
|
130
|
+
//为什么不挂根元素自己:加子节点会改变根元素下 :last-child 的匹配,
|
|
131
|
+
//而那种样式差异是会被截进 PDF 的;挂在父级上则完全在截图范围之外。
|
|
132
|
+
function openLoading(dom, option) {
|
|
133
|
+
if (option.loading === false) return null;
|
|
134
|
+
const host = dom.parentElement || dom;
|
|
135
|
+
//必须在 expandForCapture 之前量:展开之后元素会被撑到完整内容高度,量出来是没用的
|
|
136
|
+
const rect = dom.getBoundingClientRect();
|
|
137
|
+
const top = Math.max(rect.top, 0);
|
|
138
|
+
const left = Math.max(rect.left, 0);
|
|
139
|
+
const height = Math.min(rect.bottom, window.innerHeight) - top;
|
|
140
|
+
const width = Math.min(rect.right, window.innerWidth) - left;
|
|
141
|
+
const mask = document.createElement("div");
|
|
142
|
+
mask.className = "el-loading-mask";
|
|
143
|
+
//.el-loading-mask 自带 top/right/bottom/left:0,right/bottom 要显式清掉,
|
|
144
|
+
//否则宽高会被拉回视口边缘,又变成全屏
|
|
145
|
+
mask.style.position = "fixed";
|
|
146
|
+
mask.style.top = top + "px";
|
|
147
|
+
mask.style.left = left + "px";
|
|
148
|
+
mask.style.right = "auto";
|
|
149
|
+
mask.style.bottom = "auto";
|
|
150
|
+
mask.style.width = width + "px";
|
|
151
|
+
mask.style.height = height + "px";
|
|
152
|
+
mask.innerHTML = [
|
|
153
|
+
'<div class="el-loading-spinner">',
|
|
154
|
+
'<svg class="circular" viewBox="25 25 50 50">',
|
|
155
|
+
'<circle class="path" cx="50" cy="50" r="20" fill="none"></circle>',
|
|
156
|
+
"</svg>",
|
|
157
|
+
'<p class="el-loading-text"></p>',
|
|
158
|
+
"</div>",
|
|
159
|
+
].join("");
|
|
160
|
+
const label = mask.querySelector(".el-loading-text");
|
|
161
|
+
label.textContent = option.loadingText || "正在生成PDF,请稍候…";
|
|
162
|
+
host.appendChild(mask);
|
|
163
|
+
return {
|
|
164
|
+
//分阶段换文案:截图那段主线程是整块占住的,转圈会定住,
|
|
165
|
+
//有个往前走的文案才看得出是在干活而不是卡死。调用方自带文案时不覆盖。
|
|
166
|
+
setText(next) {
|
|
167
|
+
if (!option.loadingText) label.textContent = next;
|
|
168
|
+
},
|
|
169
|
+
close() {
|
|
170
|
+
if (mask.parentNode) mask.parentNode.removeChild(mask);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//toDataURL 是同步编码,一整页 JPEG 能顶住主线程几百毫秒;
|
|
176
|
+
//toBlob 是异步的,浏览器可以把编码挪到主线程之外,转圈动画就不会整段定住。
|
|
177
|
+
//jsdom 和老浏览器没有 toBlob,退回同步版本。
|
|
178
|
+
function encodeJpeg(canvas) {
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const sync = () => resolve(canvas.toDataURL("image/jpeg", JPEG_QUALITY));
|
|
181
|
+
if (typeof canvas.toBlob !== "function") return sync();
|
|
182
|
+
canvas.toBlob(
|
|
183
|
+
(blob) => {
|
|
184
|
+
if (!blob) return sync();
|
|
185
|
+
const reader = new FileReader();
|
|
186
|
+
reader.onload = () => resolve(reader.result);
|
|
187
|
+
reader.onerror = sync;
|
|
188
|
+
reader.readAsDataURL(blob);
|
|
189
|
+
},
|
|
190
|
+
"image/jpeg",
|
|
191
|
+
JPEG_QUALITY
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function canvasToPdf(canvas, fileName, loading) {
|
|
197
|
+
const pdf = new jspdf("p", "mm", "a4");
|
|
198
|
+
//按 A4 正文区的宽高比换算一页对应的像素高度
|
|
199
|
+
const pageImgHeight = Math.floor((PAGE_HEIGHT * canvas.width) / PAGE_WIDTH);
|
|
200
|
+
const total = Math.ceil(canvas.height / pageImgHeight);
|
|
201
|
+
let page$index = 0;
|
|
202
|
+
let rendered = 0;
|
|
203
|
+
while (rendered < canvas.height) {
|
|
204
|
+
page$index++;
|
|
205
|
+
if (loading) loading.setText(`正在生成第 ${page$index}/${total} 页…`);
|
|
206
|
+
//每页之前让出一帧:切图+编码是大块同步活,不让出的话文案和转圈都刷不出来
|
|
207
|
+
await nextFrame();
|
|
208
|
+
const sliceHeight = Math.min(pageImgHeight, canvas.height - rendered);
|
|
209
|
+
const page = document.createElement("canvas");
|
|
210
|
+
page.width = canvas.width;
|
|
211
|
+
page.height = sliceHeight;
|
|
212
|
+
const ctx = page.getContext("2d");
|
|
213
|
+
//JPEG 没有透明通道,不先铺白底的话,页面上透明的地方会变成黑块
|
|
214
|
+
ctx.fillStyle = "#ffffff";
|
|
215
|
+
ctx.fillRect(0, 0, page.width, page.height);
|
|
216
|
+
//用 drawImage 而不是 getImageData/putImageData:后者逐像素拷贝,长页面上很慢
|
|
217
|
+
ctx.drawImage(
|
|
218
|
+
canvas,
|
|
219
|
+
0,
|
|
220
|
+
rendered,
|
|
221
|
+
canvas.width,
|
|
222
|
+
sliceHeight,
|
|
223
|
+
0,
|
|
224
|
+
0,
|
|
225
|
+
canvas.width,
|
|
226
|
+
sliceHeight
|
|
227
|
+
);
|
|
228
|
+
const dataUrl = await encodeJpeg(page);
|
|
229
|
+
pdf.addImage(
|
|
230
|
+
dataUrl,
|
|
231
|
+
"JPEG",
|
|
232
|
+
PAGE_MARGIN,
|
|
233
|
+
PAGE_MARGIN,
|
|
234
|
+
PAGE_WIDTH,
|
|
235
|
+
Math.min(PAGE_HEIGHT, (PAGE_WIDTH * sliceHeight) / page.width)
|
|
236
|
+
);
|
|
237
|
+
rendered += sliceHeight;
|
|
238
|
+
if (rendered < canvas.height) pdf.addPage();
|
|
239
|
+
}
|
|
240
|
+
pdf.save(`${fileName}.pdf`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 把 dom 整体(含被滚动容器裁掉的部分)截图并按 A4 分页导出 PDF
|
|
245
|
+
* @param {Object} option
|
|
246
|
+
* @param {HTMLElement} option.dom 要导出的根元素
|
|
247
|
+
* @param {String} option.fileName 文件名(不带扩展名)
|
|
248
|
+
* @param {Number} [option.scale] 截图倍率,默认取 devicePixelRatio(上限 2),会按 canvas 上限自动回退
|
|
249
|
+
* @param {String|String[]} [option.ignoreSelectors] 不参与截图的元素选择器,如操作按钮区
|
|
250
|
+
* @param {Boolean} [option.loading] 是否在导出期间盖加载遮罩,默认 true
|
|
251
|
+
* @param {String} [option.loadingText] 遮罩文案
|
|
252
|
+
* @returns {Promise<Boolean>} 是否导出成功。内部已经提示过失败,调用方可以直接不管返回值
|
|
253
|
+
*/
|
|
254
|
+
export async function exportPDF(option) {
|
|
255
|
+
const dom = option && option.dom;
|
|
256
|
+
if (!dom) return false;
|
|
257
|
+
const fileName = option.fileName || "export";
|
|
258
|
+
const loading = openLoading(dom, option);
|
|
259
|
+
//展开放进 try 里:万一它自己抛了,遮罩也得摘掉,不能把页面永远糊住
|
|
260
|
+
let restore = null;
|
|
261
|
+
try {
|
|
262
|
+
//先让浏览器把遮罩画出来,再做展开和截图——这两步都是同步卡主线程的,
|
|
263
|
+
//不先出让一帧的话遮罩要等截图结束才显示,等于没有。
|
|
264
|
+
await nextFrame();
|
|
265
|
+
restore = expandForCapture(dom);
|
|
266
|
+
if (loading) loading.setText("正在展开页面内容…");
|
|
267
|
+
await nextFrame();
|
|
268
|
+
const fullWidth = Math.max(dom.scrollWidth, dom.offsetWidth);
|
|
269
|
+
const fullHeight = Math.max(dom.scrollHeight, dom.offsetHeight);
|
|
270
|
+
const scale = pickScale(fullWidth, fullHeight, option.scale);
|
|
271
|
+
//html2canvas 这一段是整块同步活:它对每个节点都要 getComputedStyle 三次(自身/:before/:after)
|
|
272
|
+
//克隆一遍,再整棵树画进 canvas,中途没有让出主线程的机会,页面必然定住几秒。
|
|
273
|
+
//只能靠文案说明进度,真要更快就得降 scale 或少截点内容。
|
|
274
|
+
if (loading) loading.setText("正在渲染页面,请稍候…");
|
|
275
|
+
await nextFrame();
|
|
276
|
+
const canvas = await html2canvas(dom, {
|
|
277
|
+
backgroundColor: "#ffffff",
|
|
278
|
+
useCORS: true,
|
|
279
|
+
logging: false,
|
|
280
|
+
scale: scale,
|
|
281
|
+
//克隆出来的 iframe 默认只有一屏高,页面里的 100vh / height:100% 会在克隆里重新把内容压回一屏,
|
|
282
|
+
//所以要把它撑到完整内容高度;scrollX/scrollY 归零避免按当前滚动位置偏移取景。
|
|
283
|
+
scrollX: 0,
|
|
284
|
+
scrollY: 0,
|
|
285
|
+
windowWidth: Math.max(document.documentElement.clientWidth, fullWidth),
|
|
286
|
+
windowHeight: fullHeight + 200,
|
|
287
|
+
ignoreElements: buildIgnore(option.ignoreSelectors),
|
|
288
|
+
});
|
|
289
|
+
await canvasToPdf(canvas, fileName, loading);
|
|
290
|
+
return true;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
//不往外抛:调用方多是 @click 直接调用,抛出去只会变成一条 unhandledrejection
|
|
293
|
+
console.error("[exportPDF] 导出失败", err);
|
|
294
|
+
Message.error("导出PDF失败");
|
|
295
|
+
return false;
|
|
296
|
+
} finally {
|
|
297
|
+
//必须等 html2canvas 真正 resolve 之后再还原:它的 DOM 克隆是异步做的
|
|
298
|
+
if (restore) restore();
|
|
299
|
+
if (loading) loading.close();
|
|
300
|
+
}
|
|
71
301
|
}
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
class="allDel icon-quanbushanchu" @click="clearChecked()"></a></el-tooltip>
|
|
52
52
|
<div class="list">
|
|
53
53
|
<div class="item" v-for="(checkRow, index) in checkRows" :key="index">
|
|
54
|
-
<p>{{ getCheckLabel(checkRow, '
|
|
54
|
+
<p>{{ getCheckLabel(checkRow, 'bdCompanyCode') }}</p>
|
|
55
55
|
<a class="el-icon-close" @click="clearTable1Select(index)"></a>
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|