uni-image-editor 1.0.0
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/README.md +98 -0
- package/components/edit-graffiti-config.vue +151 -0
- package/components/edit-text-config.vue +112 -0
- package/components/image-clipper/image-clipper.vue +1009 -0
- package/components/image-clipper/img/photo.svg +19 -0
- package/components/image-clipper/img/rotate.svg +15 -0
- package/components/image-clipper/index.scss +184 -0
- package/components/image-clipper/utils.js +280 -0
- package/components/input-text-modal.vue +431 -0
- package/image-editor.vue +971 -0
- package/js/const.js +242 -0
- package/js/editor.js +1179 -0
- package/js/utils.js +316 -0
- package/package.json +29 -0
package/image-editor.vue
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="image-editor-container">
|
|
3
|
+
<view
|
|
4
|
+
class="image-editor-container-main"
|
|
5
|
+
v-show="!isCutting"
|
|
6
|
+
@tap="submitDrawText"
|
|
7
|
+
>
|
|
8
|
+
<view class="image-editor-main-content">
|
|
9
|
+
<view class="content-canvas-area">
|
|
10
|
+
<view class="content-canvas-container">
|
|
11
|
+
<div class="image-editor-canvas-loading-mask" v-show="renderCanvasLoading"></div>
|
|
12
|
+
<canvas
|
|
13
|
+
:id="canvasId"
|
|
14
|
+
:canvas-id="canvasId"
|
|
15
|
+
class="image-editor-canvas"
|
|
16
|
+
:style="canvasStyle"
|
|
17
|
+
:disable-scroll="true"
|
|
18
|
+
@touchstart="handleGraffitiStart"
|
|
19
|
+
@touchmove="handleGraffitiMove"
|
|
20
|
+
@touchend="handleGraffitiEnd"
|
|
21
|
+
@tap.stop="showInputModal"
|
|
22
|
+
@error="handleCanvasError"
|
|
23
|
+
>
|
|
24
|
+
</canvas>
|
|
25
|
+
<!-- 输入文字弹窗组件 -->
|
|
26
|
+
<input-text-modal
|
|
27
|
+
v-model="text"
|
|
28
|
+
ref="inputTextModal"
|
|
29
|
+
:color.sync="curTextConfig.color"
|
|
30
|
+
:size.sync="curTextConfig.size"
|
|
31
|
+
@submit="drawtTextFinish"
|
|
32
|
+
@tap.stop="() => {}"
|
|
33
|
+
@cancel="drawTextCancel"
|
|
34
|
+
:containerPosition="canvasPostion"
|
|
35
|
+
/>
|
|
36
|
+
</view>
|
|
37
|
+
</view>
|
|
38
|
+
<!-- 图片编辑器工具配置区域-->
|
|
39
|
+
<view class="content-tool-area">
|
|
40
|
+
<!-- 点击配置区域不提交文本输入 -->
|
|
41
|
+
<view class="content-tool-config" @tap.stop="() => {}">
|
|
42
|
+
<edit-graffiti-config
|
|
43
|
+
v-if="isEditGraffitiConfig"
|
|
44
|
+
:configs="graffitiConfigsData"
|
|
45
|
+
:color.sync="curGraffitiConfig.color"
|
|
46
|
+
:shape.sync="curGraffitiConfig.shape"
|
|
47
|
+
:rough.sync="curGraffitiConfig.rough"
|
|
48
|
+
></edit-graffiti-config>
|
|
49
|
+
|
|
50
|
+
<edit-text-config
|
|
51
|
+
v-else
|
|
52
|
+
:configs="textConfigsData"
|
|
53
|
+
:color.sync="curTextConfig.color"
|
|
54
|
+
:size.sync="curTextConfig.size"
|
|
55
|
+
></edit-text-config>
|
|
56
|
+
</view>
|
|
57
|
+
<!-- 图片编辑器工具栏根据列表-->
|
|
58
|
+
<view class="content-tool-list">
|
|
59
|
+
<view
|
|
60
|
+
v-for="(config, index) in filterToolConfigs"
|
|
61
|
+
:key="index"
|
|
62
|
+
class="content-tool-item"
|
|
63
|
+
:class="{ 'content-tool-item-disabled': config.disabled }"
|
|
64
|
+
:style="{
|
|
65
|
+
color:
|
|
66
|
+
curEditConfigTool === config.name
|
|
67
|
+
? config.activeColor
|
|
68
|
+
: config.color,
|
|
69
|
+
}"
|
|
70
|
+
@tap="doToolAction(config)"
|
|
71
|
+
>
|
|
72
|
+
<i
|
|
73
|
+
:class="`iconfont ${config.icon}`"
|
|
74
|
+
:style="{
|
|
75
|
+
transform:
|
|
76
|
+
config.name === 'redo' ? 'scaleX(-1)' : 'scaleX(1)',
|
|
77
|
+
fontSize: config.iconSize + 'px',
|
|
78
|
+
}"
|
|
79
|
+
></i>
|
|
80
|
+
<text
|
|
81
|
+
:style="{
|
|
82
|
+
fontSize: config.textSize + 'px',
|
|
83
|
+
}"
|
|
84
|
+
>{{ config.text }}</text
|
|
85
|
+
>
|
|
86
|
+
</view>
|
|
87
|
+
</view>
|
|
88
|
+
</view>
|
|
89
|
+
</view>
|
|
90
|
+
<!-- 图片编辑器底部区域 -->
|
|
91
|
+
<view class="image-editor-bottom">
|
|
92
|
+
<slot name="cancel">
|
|
93
|
+
<i class="iconfont icon-cuowuguanbiquxiao" @tap="cancel"></i>
|
|
94
|
+
</slot>
|
|
95
|
+
<text class="image-editor-bottom-title">{{ bottomTitle }}</text>
|
|
96
|
+
<slot name="confirm">
|
|
97
|
+
<i class="iconfont icon-dui" @tap="confirm"></i>
|
|
98
|
+
</slot>
|
|
99
|
+
</view>
|
|
100
|
+
</view>
|
|
101
|
+
<!-- 图片裁剪组件 -->
|
|
102
|
+
<image-clipper
|
|
103
|
+
v-if="isGetCanvasSize"
|
|
104
|
+
ref="image-clipper"
|
|
105
|
+
v-show="isCutting"
|
|
106
|
+
:image-url="clipImgUrl"
|
|
107
|
+
@success="cutSuccess"
|
|
108
|
+
:is-show-photo-btn="false"
|
|
109
|
+
@cancel="cutFail"
|
|
110
|
+
@fail="cutFail"
|
|
111
|
+
:beforeConfirm="beforeCutConfirm"
|
|
112
|
+
:width="canvasSizeRpx.width"
|
|
113
|
+
:height="canvasSizeRpx.height"
|
|
114
|
+
></image-clipper>
|
|
115
|
+
</view>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
import {
|
|
120
|
+
CONVERT_IMG_NET_TO_LOCAL_TYPE,
|
|
121
|
+
DEFAULT_TOOLS_CONFIGS,
|
|
122
|
+
IMG_EDITOR_RENDER_TYPE,
|
|
123
|
+
IMG_EDITOR_TOOL_TYPE,
|
|
124
|
+
IMG_EDITOR_TOOL_TYPE_TEXT,
|
|
125
|
+
} from "./js/const";
|
|
126
|
+
import ImageClipper from "./components/image-clipper/image-clipper.vue";
|
|
127
|
+
import EditGraffitiConfig from "./components/edit-graffiti-config.vue";
|
|
128
|
+
import EditTextConfig from "./components/edit-text-config.vue";
|
|
129
|
+
import Editor from "./js/editor";
|
|
130
|
+
import { getAutoFitSize, renderAwait, base64ToUrl, convertNetImageUrlToBase64OfH5, convertNetImgUrlToLocal, isBase64Image, isNetImageUrl } from "./js/utils";
|
|
131
|
+
|
|
132
|
+
import InputTextModal from "./components/input-text-modal.vue";
|
|
133
|
+
|
|
134
|
+
let editorCtx;
|
|
135
|
+
|
|
136
|
+
export default {
|
|
137
|
+
props: {
|
|
138
|
+
toolConfigs: {
|
|
139
|
+
type: Array,
|
|
140
|
+
default(){
|
|
141
|
+
return DEFAULT_TOOLS_CONFIGS
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
canvasId: {
|
|
145
|
+
type: String,
|
|
146
|
+
default: "image-editor-canvas",
|
|
147
|
+
},
|
|
148
|
+
imgUrl: {
|
|
149
|
+
type: String,
|
|
150
|
+
default: "",
|
|
151
|
+
},
|
|
152
|
+
convertImgType: {
|
|
153
|
+
type: String,
|
|
154
|
+
default: CONVERT_IMG_NET_TO_LOCAL_TYPE.canvas,
|
|
155
|
+
},
|
|
156
|
+
maxTransparencyRate:{
|
|
157
|
+
type: Number,
|
|
158
|
+
default: 5
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
components: {
|
|
162
|
+
ImageClipper,
|
|
163
|
+
EditGraffitiConfig,
|
|
164
|
+
EditTextConfig,
|
|
165
|
+
InputTextModal,
|
|
166
|
+
},
|
|
167
|
+
data() {
|
|
168
|
+
return {
|
|
169
|
+
// 当前涂鸦的配置对象
|
|
170
|
+
curGraffitiConfig: {
|
|
171
|
+
// 涂鸦的颜色
|
|
172
|
+
color: "",
|
|
173
|
+
// 涂鸦的形状
|
|
174
|
+
shape: "",
|
|
175
|
+
// 涂鸦的粗糙度(可能是笔触的粗细或不规则程度)
|
|
176
|
+
rough: "",
|
|
177
|
+
},
|
|
178
|
+
// 当前文本的配置对象
|
|
179
|
+
curTextConfig: {
|
|
180
|
+
// 文本的颜色
|
|
181
|
+
color: "",
|
|
182
|
+
// 文本的大小
|
|
183
|
+
size: "",
|
|
184
|
+
},
|
|
185
|
+
// 画布的大小配置
|
|
186
|
+
canvasSize: {
|
|
187
|
+
// 画布的宽度
|
|
188
|
+
width: "",
|
|
189
|
+
// 画布的高度
|
|
190
|
+
height: "",
|
|
191
|
+
},
|
|
192
|
+
// 当前选中的工具(可能是画笔、橡皮擦等)
|
|
193
|
+
curTool: "",
|
|
194
|
+
// 当前编辑配置的工具(用于调整涂鸦或文本属性的工具)
|
|
195
|
+
curEditConfigTool: "",
|
|
196
|
+
// 当前画布上的图片
|
|
197
|
+
curImgUrl: "",
|
|
198
|
+
// 当前步骤
|
|
199
|
+
curStep: -1,
|
|
200
|
+
// 裁剪的图片URL
|
|
201
|
+
clipImgUrl: "",
|
|
202
|
+
// 输入模态框是否可见
|
|
203
|
+
inputModalVisible: false,
|
|
204
|
+
// 输入模态框的文本内容(
|
|
205
|
+
text: "",
|
|
206
|
+
// 画布上元素的位置配置
|
|
207
|
+
canvasPostion: {
|
|
208
|
+
// x坐标
|
|
209
|
+
x: 0,
|
|
210
|
+
// y坐标
|
|
211
|
+
y: 0,
|
|
212
|
+
},
|
|
213
|
+
// 编辑记录数据
|
|
214
|
+
editRecordData: [],
|
|
215
|
+
renderCanvasLoading: false, // 渲染画布加载状态
|
|
216
|
+
startGraffitiTime: null, // 开始涂鸦时间
|
|
217
|
+
isGraffiting: false, // 是否正在涂鸦
|
|
218
|
+
confirmLoading: false,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
computed: {
|
|
222
|
+
// 过滤后的工具栏配置
|
|
223
|
+
filterToolConfigs() {
|
|
224
|
+
return this.toolConfigs.map((item) => {
|
|
225
|
+
if (item.name === IMG_EDITOR_TOOL_TYPE.redo) {
|
|
226
|
+
item.disabled =
|
|
227
|
+
!this.editRecordData.length || // 如果没有编辑记录
|
|
228
|
+
this.curStep === this.editRecordData.length - 1; // 或者当前步骤是最后一步
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (item.name === IMG_EDITOR_TOOL_TYPE.repeal) {
|
|
232
|
+
item.disabled = this.curStep < 0 || !this.editRecordData.length; // 如果当前步骤小于0或没有编辑记录
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
...item,
|
|
236
|
+
iconSize: uni.upx2px(item.iconSize),
|
|
237
|
+
textSize: uni.upx2px(item.textSize),
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
curEditRecordData() {
|
|
242
|
+
return this.editRecordData[this.curStep];
|
|
243
|
+
},
|
|
244
|
+
// 获取涂鸦配置数据
|
|
245
|
+
graffitiConfigsData() {
|
|
246
|
+
return (
|
|
247
|
+
this.toolConfigs.find(
|
|
248
|
+
(config) => config.name === IMG_EDITOR_TOOL_TYPE.graffiti
|
|
249
|
+
)?.graffitiConfigs || {} // 如果找到涂鸦工具配置,则返回涂鸦配置,否则返回空对象
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
// 返回当前底部的标题(根据是否在编辑涂鸦配置)
|
|
254
|
+
bottomTitle() {
|
|
255
|
+
return IMG_EDITOR_TOOL_TYPE_TEXT[this.curEditConfigTool];
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
// 获取文本配置数据
|
|
261
|
+
textConfigsData() {
|
|
262
|
+
return (
|
|
263
|
+
this.toolConfigs.find(
|
|
264
|
+
(config) => config.name === IMG_EDITOR_TOOL_TYPE.text
|
|
265
|
+
)?.textConfigs || {} // 如果找到文本工具配置,则返回文本配置,否则返回空对象
|
|
266
|
+
);
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
// 判断当前是否在编辑涂鸦配置
|
|
270
|
+
isEditGraffitiConfig() {
|
|
271
|
+
return this.curEditConfigTool === IMG_EDITOR_TOOL_TYPE.graffiti;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// 判断当前是否在使用裁剪工具
|
|
275
|
+
isCutting() {
|
|
276
|
+
return this.curTool === IMG_EDITOR_TOOL_TYPE.cut;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// 返回canvas的样式,包括宽度、高度和背景图片(当前canvas的图片)
|
|
280
|
+
canvasStyle() {
|
|
281
|
+
return `width:${this.canvasSize.width}px;height:${this.canvasSize.height}px;background-image:url(${this.curImgUrl});background-size:${this.canvasSize.width}px ${this.canvasSize.height}px;`;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// 判断是否已经获取了canvas的大小
|
|
285
|
+
isGetCanvasSize() {
|
|
286
|
+
return this.canvasSize.width && this.canvasSize.height;
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// 返回canvas的高度与宽度的比例
|
|
290
|
+
canvasSizeRatio() {
|
|
291
|
+
return this.canvasSize.height / this.canvasSize.width;
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
// 不能传小数否则图片拖动会失效
|
|
295
|
+
// 传给裁剪组件要传rpx
|
|
296
|
+
canvasSizeRpx() {
|
|
297
|
+
return {
|
|
298
|
+
width: 750,
|
|
299
|
+
height: parseInt(750 * this.canvasSizeRatio || 1),
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
mounted() {
|
|
305
|
+
this.init();
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
methods: {
|
|
309
|
+
// 处理canvas渲染错误
|
|
310
|
+
handleCanvasError(err){
|
|
311
|
+
console.log(`canvas渲染错误`,err);
|
|
312
|
+
}, // 取消
|
|
313
|
+
cancel() {
|
|
314
|
+
this.$emit("cancel");
|
|
315
|
+
},
|
|
316
|
+
// 确认
|
|
317
|
+
async confirm() {
|
|
318
|
+
if (this.confirmLoading) {
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.confirmLoading = true
|
|
323
|
+
|
|
324
|
+
uni.showLoading({
|
|
325
|
+
title: "生成图片中",
|
|
326
|
+
mask: true
|
|
327
|
+
});
|
|
328
|
+
await this.submitDrawText(true);
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
// 确保编辑器渲染完成
|
|
333
|
+
await renderAwait(100)
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
console.log('校验图片中...');
|
|
337
|
+
await this.validImage();
|
|
338
|
+
} catch (error) {
|
|
339
|
+
this.confirmLoading = false
|
|
340
|
+
uni.hideLoading()
|
|
341
|
+
// uni.showToast({
|
|
342
|
+
// title: "图片有空白部分,已重置为初始编辑状态,请重新编辑",
|
|
343
|
+
// icon: "none",
|
|
344
|
+
// });
|
|
345
|
+
// 执行兜底策略,重置图片到初始状态
|
|
346
|
+
this.clear()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
console.log(`图片校验失败${error}`);
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
console.log('校验图片成功');
|
|
353
|
+
let curData
|
|
354
|
+
|
|
355
|
+
// 当前步骤为初始状态,且原生图片是网络图片,app端需要转化网络图片为临时路径,h5端需要转化为base64
|
|
356
|
+
if (this.curStep === -1 && isNetImageUrl(editorCtx.originImgData.url)) {
|
|
357
|
+
console.log('初始步骤但原生图片是网络图片');
|
|
358
|
+
let imgData = {
|
|
359
|
+
url: '',
|
|
360
|
+
tempFilePath: ''
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
|
|
364
|
+
// 使用uni.downlaod转化图片中
|
|
365
|
+
if (this.convertImgType === CONVERT_IMG_NET_TO_LOCAL_TYPE.download) {
|
|
366
|
+
|
|
367
|
+
console.log('使用uni.downlaod转化图片中...');
|
|
368
|
+
// 转化原始网络路径url为临时图片url
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
const blobUrl = await convertNetImgUrlToLocal(
|
|
372
|
+
editorCtx.originImgData.url
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
imgData.url = blobUrl;
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
// #ifdef H5
|
|
382
|
+
const tempFilePath = await convertNetImageUrlToBase64OfH5(
|
|
383
|
+
editorCtx.originImgData.url
|
|
384
|
+
);
|
|
385
|
+
imgData.tempFilePath = tempFilePath;
|
|
386
|
+
|
|
387
|
+
// #endif
|
|
388
|
+
} else {
|
|
389
|
+
// 使用canvas转化图片中
|
|
390
|
+
console.log('使用canvas转化图片中...');
|
|
391
|
+
const res = await editorCtx.canvasToImgUrl();
|
|
392
|
+
|
|
393
|
+
await this.validImage();
|
|
394
|
+
imgData.url = res.url;
|
|
395
|
+
imgData.tempFilePath = res.tempFilePath;
|
|
396
|
+
console.log(`使用canvas转化图片成功`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.confirmLoading = false
|
|
405
|
+
uni.hideLoading()
|
|
406
|
+
// 执行兜底策略,重置图片到初始状态
|
|
407
|
+
this.clear()
|
|
408
|
+
console.log(`转化网络图片失败${error}`);
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
curData = {
|
|
413
|
+
imgData
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 当前步骤为初始状态,且原生图片不是网络图片,app端不需要转化,h5端需要转化为base64
|
|
417
|
+
} else if (this.curStep === -1 && !isNetImageUrl(editorCtx.originImgData.url)) {
|
|
418
|
+
console.log('初始步骤但不原生图片不是网络图片');
|
|
419
|
+
curData = { imgData: editorCtx.originImgData }
|
|
420
|
+
|
|
421
|
+
// #ifdef H5
|
|
422
|
+
console.log('h5不是网络图片转化初始图片');
|
|
423
|
+
|
|
424
|
+
const tempFilePath = await convertNetImageUrlToBase64OfH5(
|
|
425
|
+
editorCtx.originImgData.url
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
curData = {
|
|
429
|
+
imgData: {
|
|
430
|
+
url: editorCtx.originImgData.url,
|
|
431
|
+
tempFilePath
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// #endif
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
else {
|
|
443
|
+
console.log('不是初始步骤');
|
|
444
|
+
curData = this.curEditRecordData || { imgData: editorCtx.originImgData }
|
|
445
|
+
}
|
|
446
|
+
this.confirmLoading = false
|
|
447
|
+
uni.hideLoading()
|
|
448
|
+
|
|
449
|
+
this.$emit(
|
|
450
|
+
"success",
|
|
451
|
+
curData,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
},
|
|
455
|
+
// 校验图片
|
|
456
|
+
validImage() {
|
|
457
|
+
return editorCtx.validImage();
|
|
458
|
+
},
|
|
459
|
+
// 获取编辑器实例
|
|
460
|
+
getEditorInsatcne() {
|
|
461
|
+
return editorCtx;
|
|
462
|
+
},
|
|
463
|
+
// 执行工具动作
|
|
464
|
+
doToolAction(config) {
|
|
465
|
+
// 是否提交渲染文字
|
|
466
|
+
const isSumbitEmptyText = this.inputModalVisible && !this.text;
|
|
467
|
+
this.submitDrawText();
|
|
468
|
+
|
|
469
|
+
// 设置当前工具
|
|
470
|
+
this.curTool = config.name;
|
|
471
|
+
|
|
472
|
+
// 如果工具是涂鸦或文本类型,则设置当前编辑配置工具
|
|
473
|
+
if (
|
|
474
|
+
[IMG_EDITOR_TOOL_TYPE.graffiti, IMG_EDITOR_TOOL_TYPE.text].includes(
|
|
475
|
+
config.name
|
|
476
|
+
)
|
|
477
|
+
) {
|
|
478
|
+
this.curEditConfigTool = config.name;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 根据当前工具执行相应的操作
|
|
482
|
+
switch (this.curTool) {
|
|
483
|
+
case IMG_EDITOR_TOOL_TYPE.redo: // 重做
|
|
484
|
+
// 输入空文字时点击撤销或重置提交时,不执行重做和撤销
|
|
485
|
+
!isSumbitEmptyText && this.redo();
|
|
486
|
+
break;
|
|
487
|
+
case IMG_EDITOR_TOOL_TYPE.repeal: // 撤销
|
|
488
|
+
// 输入空文字时点击撤销或重置提交时,不执行重做和撤销
|
|
489
|
+
!isSumbitEmptyText && this.repeal();
|
|
490
|
+
break;
|
|
491
|
+
case IMG_EDITOR_TOOL_TYPE.clear: // 清除
|
|
492
|
+
this.clear();
|
|
493
|
+
break;
|
|
494
|
+
case IMG_EDITOR_TOOL_TYPE.cut: // 裁剪
|
|
495
|
+
this.cutStart();
|
|
496
|
+
break;
|
|
497
|
+
default:
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
// 撤销操作
|
|
503
|
+
repeal() {
|
|
504
|
+
editorCtx.repeal(); // 调用editorCtx的repeal方法
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
// 重做操作
|
|
508
|
+
redo() {
|
|
509
|
+
editorCtx.redo(); // 调用editorCtx的redo方法
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
// 清除操作
|
|
513
|
+
clear() {
|
|
514
|
+
editorCtx.clear(); // 调用editorCtx的clear方法
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// 更新canvas的位置
|
|
518
|
+
updateCanvasPosition() {
|
|
519
|
+
// 创建一个选择器查询,在当前Vue组件的上下文中查找
|
|
520
|
+
const query = uni.createSelectorQuery().in(this);
|
|
521
|
+
query
|
|
522
|
+
.select(`#${this.canvasId}`) // 选择ID为canvasId的元素
|
|
523
|
+
.boundingClientRect((res) => {
|
|
524
|
+
// 获取该元素的边界信息
|
|
525
|
+
// 更新canvas的位置信息
|
|
526
|
+
this.canvasPostion.x = res.left;
|
|
527
|
+
this.canvasPostion.y = res.top;
|
|
528
|
+
})
|
|
529
|
+
.exec(); // 执行查询
|
|
530
|
+
},
|
|
531
|
+
async beforeCutConfirm(valid){
|
|
532
|
+
if (!valid) {
|
|
533
|
+
uni.showToast({
|
|
534
|
+
title: "裁剪失败,图片的尺寸至少与裁剪框的尺寸相等,或者更大",
|
|
535
|
+
icon: "none",
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
return valid
|
|
542
|
+
},
|
|
543
|
+
// 初始化
|
|
544
|
+
init() {
|
|
545
|
+
// 初始化当前工具为涂鸦工具
|
|
546
|
+
this.curTool = IMG_EDITOR_TOOL_TYPE.graffiti;
|
|
547
|
+
// 初始化当前编辑配置工具为涂鸦工具
|
|
548
|
+
this.curEditConfigTool = IMG_EDITOR_TOOL_TYPE.graffiti;
|
|
549
|
+
|
|
550
|
+
// 初始化涂鸦工具的配置
|
|
551
|
+
this.curGraffitiConfig = {
|
|
552
|
+
// 使用涂鸦配置数据中第一个颜色
|
|
553
|
+
color: this.graffitiConfigsData.colors[0],
|
|
554
|
+
// 使用涂鸦配置数据中第一个形状的名称
|
|
555
|
+
shape: this.graffitiConfigsData.shapes[0].name,
|
|
556
|
+
// 使用涂鸦配置数据中第一个粗糙度
|
|
557
|
+
rough: this.graffitiConfigsData.roughs[0],
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// 初始化文本工具的配置
|
|
561
|
+
this.curTextConfig = {
|
|
562
|
+
// 使用文本配置数据中第一个颜色
|
|
563
|
+
color: this.textConfigsData.colors[0],
|
|
564
|
+
// 使用文本配置数据中第一个大小
|
|
565
|
+
size: this.textConfigsData.sizes[0],
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// 将当前Vue实例的this保存到_this变量中,以便在闭包中使用
|
|
569
|
+
const _this = this;
|
|
570
|
+
|
|
571
|
+
// 获取图片信息
|
|
572
|
+
uni.getImageInfo({
|
|
573
|
+
src: this.imgUrl, // 图片的URL
|
|
574
|
+
success: (imgInfo) => {
|
|
575
|
+
// 调用函数来获取自动适应的画布大小
|
|
576
|
+
const size = getAutoFitSize(imgInfo.width, imgInfo.height);
|
|
577
|
+
|
|
578
|
+
// 设置画布大小
|
|
579
|
+
_this.canvasSize = size;
|
|
580
|
+
|
|
581
|
+
// 创建一个配置对象opts,用于初始化Editor实例
|
|
582
|
+
const opts = {
|
|
583
|
+
initConvert: false, //初始化转化初图片
|
|
584
|
+
editFinishValid: true, // 编辑完成后校验
|
|
585
|
+
canvasId: _this.canvasId, // 画布ID
|
|
586
|
+
originImgUrl: _this.imgUrl, // 原始图片URL
|
|
587
|
+
vueInstance: _this, // Vue实例
|
|
588
|
+
// 展开_this.canvasSize对象到opts对象中
|
|
589
|
+
..._this.canvasSize,
|
|
590
|
+
// 当渲染步骤更新时调用此回调函数,并更新当前步骤
|
|
591
|
+
updateStepCb: (step) => (_this.curStep = step),
|
|
592
|
+
// 当图片URL更新时调用此回调函数,并更新当前画布图片
|
|
593
|
+
updateImgUrlCb: (url) => (_this.curImgUrl = url),
|
|
594
|
+
// 当图片渲染完成后调用此回调函数,并获取新的画布宽度和高度
|
|
595
|
+
renderImgCb: (width, height) => {
|
|
596
|
+
_this.canvasSize = { width, height }; // 更新画布大小
|
|
597
|
+
|
|
598
|
+
setTimeout(() => {
|
|
599
|
+
_this.updateCanvasPosition();
|
|
600
|
+
}, 100);
|
|
601
|
+
},
|
|
602
|
+
// 当编辑记录数据更新时调用此回调函数,并更新编辑记录数据
|
|
603
|
+
updateEditRecordDataCb: (data) => (_this.editRecordData = data),
|
|
604
|
+
renderCanvasLoadingCb: (val) => _this.renderCanvasLoading = val,
|
|
605
|
+
maxTransparencyRate: this.maxTransparencyRate
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// 使用opts配置对象初始化Editor实例
|
|
609
|
+
editorCtx = new Editor(opts);
|
|
610
|
+
},
|
|
611
|
+
fail: (err) => {
|
|
612
|
+
// 图片信息获取失败时打印错误信息
|
|
613
|
+
console.log(err);
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
// 开始图片裁剪
|
|
619
|
+
cutStart(data) {
|
|
620
|
+
// 如果 editorCtx 不存在,则直接返回
|
|
621
|
+
if (!editorCtx || this.curTool !== IMG_EDITOR_TOOL_TYPE.cut) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 初始化 configs 对象,用于存储编辑配置
|
|
626
|
+
let configs;
|
|
627
|
+
|
|
628
|
+
// 将当前画布上的图片 URL 保存到 this.clipImgUrl
|
|
629
|
+
// 剪切开始时才把当前图片赋值给剪切组件,防止剪切组件过早执行某些操作
|
|
630
|
+
this.clipImgUrl = this.curImgUrl;
|
|
631
|
+
|
|
632
|
+
// 设置 configs 对象,包括编辑类型和渲染类型
|
|
633
|
+
configs = {
|
|
634
|
+
type: this.curTool, // 当前使用的编辑工具
|
|
635
|
+
renderType: IMG_EDITOR_RENDER_TYPE.img, // 渲染类型为图片
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// 调用 editorCtx 的 handleEditStart 方法,开始编辑操作
|
|
639
|
+
editorCtx.handleEditStart(configs);
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
// 图片裁剪失败
|
|
643
|
+
cutFail(err) {
|
|
644
|
+
// 如果 editorCtx 不存在,则直接返回
|
|
645
|
+
if (!editorCtx || this.curTool !== IMG_EDITOR_TOOL_TYPE.cut) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 将当前工具重置为默认的或之前配置的编辑工具
|
|
650
|
+
this.curTool = this.curEditConfigTool;
|
|
651
|
+
|
|
652
|
+
// 调用 editorCtx 的 handleEditFail 方法,传入错误信息
|
|
653
|
+
editorCtx.handleEditFail(err);
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
// 图片裁剪成功
|
|
657
|
+
cutSuccess(res) {
|
|
658
|
+
// 如果 editorCtx 不存在,则直接返回
|
|
659
|
+
if (!editorCtx || this.curTool !== IMG_EDITOR_TOOL_TYPE.cut) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 根据编辑后的图片尺寸计算适应的画布大小
|
|
664
|
+
this.canvasSize = getAutoFitSize(res.width, res.height);
|
|
665
|
+
|
|
666
|
+
// 初始化 configs 对象,包括图片高度、宽度、图片URL和编辑类型
|
|
667
|
+
|
|
668
|
+
const configs = {
|
|
669
|
+
imgData: {
|
|
670
|
+
...res,
|
|
671
|
+
tempFilePath: res.url,
|
|
672
|
+
height: this.canvasSize.height, // 编辑后的图片高度
|
|
673
|
+
width: this.canvasSize.width, // 编辑后的图片宽度
|
|
674
|
+
url: isBase64Image(res.url) ? base64ToUrl(res.url) : res.url, // 编辑后的图片URL(如果是Base64则转换为URL)
|
|
675
|
+
},
|
|
676
|
+
type: this.curTool, // 当前使用的编辑工具
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// 将当前工具重置为默认的或之前配置的编辑工具
|
|
680
|
+
this.curTool = this.curEditConfigTool;
|
|
681
|
+
|
|
682
|
+
// 调用 editorCtx 的 handleEditFinish 方法,传入编辑成功的配置
|
|
683
|
+
editorCtx.handleEditFinish(configs);
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
// 开始涂鸦编辑
|
|
687
|
+
handleGraffitiStart(e) {
|
|
688
|
+
// 如果 editorCtx 不存在、当前编辑配置工具不是涂鸦工具、或当前工具是裁剪工具,则直接返回
|
|
689
|
+
if (
|
|
690
|
+
!editorCtx ||
|
|
691
|
+
this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.graffiti ||
|
|
692
|
+
this.curTool === IMG_EDITOR_TOOL_TYPE.cut
|
|
693
|
+
) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
this.startGraffitiTime = new Date().getTime();
|
|
698
|
+
|
|
699
|
+
// 初始化 configs 对象
|
|
700
|
+
let configs;
|
|
701
|
+
|
|
702
|
+
// 获取触摸事件的第一个触摸点的 x 和 y 坐标
|
|
703
|
+
const x = e.touches[0].x;
|
|
704
|
+
const y = e.touches[0].y;
|
|
705
|
+
|
|
706
|
+
// 设置 configs 对象,包括涂鸦类型、坐标、颜色、形状、粗糙度、渲染类型和填充颜色
|
|
707
|
+
configs = {
|
|
708
|
+
type: this.curEditConfigTool, // 涂鸦类型
|
|
709
|
+
x, // 涂鸦开始的 x 坐标
|
|
710
|
+
y, // 涂鸦开始的 y 坐标
|
|
711
|
+
color: this.curGraffitiConfig.color, // 涂鸦颜色
|
|
712
|
+
shape: this.curGraffitiConfig.shape, // 涂鸦形状(可能是一个固定的值或多种可选形状)
|
|
713
|
+
rough: this.curGraffitiConfig.rough, // 涂鸦的粗糙度
|
|
714
|
+
renderType: IMG_EDITOR_RENDER_TYPE.img, // 渲染类型为图片
|
|
715
|
+
fillColor: this.curGraffitiConfig.color, // 填充颜色(对于涂鸦来说,可能与颜色相同)
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// 调用 editorCtx 的 handleEditStart 方法,传入涂鸦配置
|
|
719
|
+
editorCtx.handleEditStart(configs);
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
// 结束涂鸦编辑
|
|
723
|
+
handleGraffitiEnd(e) {
|
|
724
|
+
// 如果 editorCtx 不存在、当前编辑配置工具不是涂鸦工具、或当前工具是裁剪工具,则直接返回
|
|
725
|
+
if (
|
|
726
|
+
!editorCtx ||
|
|
727
|
+
this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.graffiti ||
|
|
728
|
+
this.curTool === IMG_EDITOR_TOOL_TYPE.cut
|
|
729
|
+
) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
const now = new Date().getTime()
|
|
736
|
+
|
|
737
|
+
// 判断当前时间与上一次涂鸦时间的时间差是否小于200ms进行涂鸦防抖或是点击或者长按,执行编辑失败
|
|
738
|
+
if (now - this.startGraffitiTime < 200 || !this.isGraffiting) {
|
|
739
|
+
editorCtx.handleEditFail();
|
|
740
|
+
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
this.isGraffiting = false
|
|
745
|
+
|
|
746
|
+
// 初始化 configs 对象
|
|
747
|
+
let configs;
|
|
748
|
+
|
|
749
|
+
// 获取触摸事件变化中第一个触摸点的 x 和 y 坐标(结束点)
|
|
750
|
+
const x = e.changedTouches[0].x;
|
|
751
|
+
const y = e.changedTouches[0].y;
|
|
752
|
+
|
|
753
|
+
// 设置 configs 对象,包括涂鸦类型和坐标(结束点)
|
|
754
|
+
configs = {
|
|
755
|
+
type: this.curEditConfigTool, // 涂鸦类型
|
|
756
|
+
x, // 涂鸦结束的 x 坐标
|
|
757
|
+
y, // 涂鸦结束的 y 坐标
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
editorCtx.handleEditFinish(configs);
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
// 处理涂鸦移动
|
|
764
|
+
handleGraffitiMove(e) {
|
|
765
|
+
// 如果 editorCtx 不存在、当前编辑配置工具不是涂鸦工具、或当前工具是裁剪工具,则直接返回
|
|
766
|
+
if (
|
|
767
|
+
!editorCtx ||
|
|
768
|
+
this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.graffiti ||
|
|
769
|
+
this.curTool === IMG_EDITOR_TOOL_TYPE.cut
|
|
770
|
+
) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 点击或者长按不会触发move事件,根据这个区分是触摸还是点击长按,过滤这些操作
|
|
775
|
+
this.isGraffiting = true
|
|
776
|
+
|
|
777
|
+
// 获取触摸事件的第一个触摸点的 x 和 y 坐标(移动中的点)
|
|
778
|
+
const x = e.touches[0].x;
|
|
779
|
+
const y = e.touches[0].y;
|
|
780
|
+
|
|
781
|
+
// 调用 editorCtx 的 handleEditting 方法,传入涂鸦的 x、y 坐标和涂鸦类型
|
|
782
|
+
// 涂鸦过程中实时更新涂鸦的绘制
|
|
783
|
+
editorCtx.handleEditting({ x, y, type: IMG_EDITOR_TOOL_TYPE.graffiti });
|
|
784
|
+
},
|
|
785
|
+
// 当在canvas上点击时触发的方法
|
|
786
|
+
showInputModal(e) {
|
|
787
|
+
// 如果editorCtx不存在或者当前编辑配置工具不是文本工具,则直接返回
|
|
788
|
+
if (!editorCtx || this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.text) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 判断输入模态框是否可见
|
|
793
|
+
if (!this.inputModalVisible) {
|
|
794
|
+
// 如果不可见,则设置为可见,并调用开始绘制文本的方法
|
|
795
|
+
this.inputModalVisible = true;
|
|
796
|
+
this.drawtTextStart(e);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
this.submitDrawText();
|
|
801
|
+
},
|
|
802
|
+
async submitDrawText(noSubmit = false) {
|
|
803
|
+
if (!editorCtx) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (this.inputModalVisible) {
|
|
808
|
+
this.inputModalVisible = false;
|
|
809
|
+
const data = this.$refs.inputTextModal.submit(noSubmit);
|
|
810
|
+
noSubmit && await this.drawtTextFinish(data)
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
|
|
814
|
+
// 开始绘制文本的方法
|
|
815
|
+
drawtTextStart(e) {
|
|
816
|
+
// 如果editorCtx不存在、输入模态框不可见或者当前编辑配置工具不是文本工具,则直接返回
|
|
817
|
+
if (
|
|
818
|
+
!editorCtx ||
|
|
819
|
+
!this.inputModalVisible ||
|
|
820
|
+
this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.text
|
|
821
|
+
) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 初始化配置对象
|
|
826
|
+
let configs;
|
|
827
|
+
configs = {
|
|
828
|
+
type: this.curEditConfigTool, // 文本类型
|
|
829
|
+
color: this.curTextConfig.color, // 文本颜色
|
|
830
|
+
size: this.curTextConfig.size, // 文本大小
|
|
831
|
+
renderType: IMG_EDITOR_RENDER_TYPE.img, // 渲染类型为图片
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// 调用editorCtx的handleEditStart方法,开始编辑文本
|
|
835
|
+
editorCtx.handleEditStart(configs);
|
|
836
|
+
|
|
837
|
+
// 获取触摸点的客户端坐标(即屏幕上的坐标)
|
|
838
|
+
const x = e.touches[0].clientX;
|
|
839
|
+
const y = e.touches[0].clientY;
|
|
840
|
+
|
|
841
|
+
// 在输入模态框中显示输入框,并定位到触摸点的位置
|
|
842
|
+
this.$refs.inputTextModal.showAt(x, y);
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
drawTextCancel() {
|
|
846
|
+
if (!editorCtx || this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.text) {
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this.inputModalVisible && (this.inputModalVisible = false);
|
|
850
|
+
|
|
851
|
+
// 调用 editorCtx 的 handleEditFail 方法,传入错误信息
|
|
852
|
+
editorCtx.handleEditFail();
|
|
853
|
+
},
|
|
854
|
+
|
|
855
|
+
// 完成绘制文本的方法
|
|
856
|
+
async drawtTextFinish(state = {}) {
|
|
857
|
+
// 如果editorCtx不存在、当前编辑配置工具不是文本工具或者输入的内容为空,则直接返回
|
|
858
|
+
if (!editorCtx || this.curEditConfigTool !== IMG_EDITOR_TOOL_TYPE.text) {
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!state.text.trim()) {
|
|
862
|
+
this.drawTextCancel();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// 初始化配置对象
|
|
867
|
+
let configs = {
|
|
868
|
+
text: state.text, // 文本内容
|
|
869
|
+
x: state.left, // 文本开始的x坐标
|
|
870
|
+
y: state.top, // 文本开始的y坐标
|
|
871
|
+
maxWidth: state.width, // 文本的最大宽度
|
|
872
|
+
color: state.color, // 文本颜色
|
|
873
|
+
size: state.size, // 文本大小
|
|
874
|
+
lineHeight: state.lineHeight, // 文本行高
|
|
875
|
+
type: this.curEditConfigTool,
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// 清空当前文本内容
|
|
879
|
+
this.text = "";
|
|
880
|
+
|
|
881
|
+
// 调用editorCtx的handleEditSuccess方法,完成文本编辑
|
|
882
|
+
await editorCtx.handleEditFinish(configs);
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
</script>
|
|
887
|
+
|
|
888
|
+
<style lang="scss" scoped>
|
|
889
|
+
.image-editor-container {
|
|
890
|
+
position: relative;
|
|
891
|
+
height: 100%;
|
|
892
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
893
|
+
.image-editor-container-main {
|
|
894
|
+
height: 100%;
|
|
895
|
+
display: flex;
|
|
896
|
+
flex-direction: column;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.image-editor-bottom {
|
|
900
|
+
display: flex;
|
|
901
|
+
justify-content: space-between;
|
|
902
|
+
align-items: center;
|
|
903
|
+
height: 140rpx;
|
|
904
|
+
padding: 0 40rpx;
|
|
905
|
+
background-color: #fff;
|
|
906
|
+
|
|
907
|
+
.image-editor-bottom-title {
|
|
908
|
+
font-weight: bold;
|
|
909
|
+
font-size: 32rpx;
|
|
910
|
+
color: #000000;
|
|
911
|
+
}
|
|
912
|
+
.iconfont {
|
|
913
|
+
font-size: 55rpx;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.icon-dui {
|
|
917
|
+
font-size: 50rpx;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
.image-editor-main-content {
|
|
921
|
+
flex: 1;
|
|
922
|
+
display: flex;
|
|
923
|
+
flex-direction: column;
|
|
924
|
+
|
|
925
|
+
.content-canvas-area {
|
|
926
|
+
flex: 1;
|
|
927
|
+
display: flex;
|
|
928
|
+
align-items: center;
|
|
929
|
+
overflow-y: auto;
|
|
930
|
+
.content-canvas-container {
|
|
931
|
+
position: relative;
|
|
932
|
+
.image-editor-canvas-loading-mask{
|
|
933
|
+
background-color: transparent;
|
|
934
|
+
z-index: 10070;
|
|
935
|
+
position: absolute;
|
|
936
|
+
top: 0;
|
|
937
|
+
left: 0;
|
|
938
|
+
right: 0;
|
|
939
|
+
bottom: 0;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
.content-tool-area {
|
|
944
|
+
background-color: #fff;
|
|
945
|
+
|
|
946
|
+
.content-tool-config {
|
|
947
|
+
padding: 20rpx 0 20rpx 120rpx;
|
|
948
|
+
font-size: 24rpx;
|
|
949
|
+
}
|
|
950
|
+
.active-edit-config-tool {
|
|
951
|
+
color: #2979ff;
|
|
952
|
+
}
|
|
953
|
+
.content-tool-list {
|
|
954
|
+
display: flex;
|
|
955
|
+
padding: 0 30rpx;
|
|
956
|
+
.content-tool-item-disabled {
|
|
957
|
+
color: #aaaaaa !important;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.content-tool-item {
|
|
961
|
+
flex: 1;
|
|
962
|
+
display: flex;
|
|
963
|
+
flex-direction: column;
|
|
964
|
+
align-items: center;
|
|
965
|
+
justify-content: center;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
</style>
|