@longhongguo/form-create-ant-design-vue 3.3.35 → 3.3.36
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/dist/form-create.esm.js +2 -2
- package/dist/form-create.esm.js.map +1 -1
- package/dist/form-create.js +2 -2
- package/dist/form-create.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FcEditorWrapper.vue +1257 -1257
- package/src/core/manager.js +674 -674
- package/src/parsers/flex.js +255 -264
- package/src/parsers/space.js +128 -137
|
@@ -1,1257 +1,1257 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="fc-editor-wrapper">
|
|
3
|
-
<!-- 只读/预览模式:直接用 v-html 渲染,并添加图片点击监听 -->
|
|
4
|
-
<div
|
|
5
|
-
v-if="readOnly"
|
|
6
|
-
ref="readOnlyContainer"
|
|
7
|
-
class="fc-editor-readonly"
|
|
8
|
-
v-html="modelValue"
|
|
9
|
-
></div>
|
|
10
|
-
<!-- 编辑模式:使用富文本编辑器 -->
|
|
11
|
-
|
|
12
|
-
v-else
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</div>
|
|
20
|
-
</template>
|
|
21
|
-
|
|
22
|
-
<script>
|
|
23
|
-
import { defineComponent } from 'vue'
|
|
24
|
-
import FcEditor from '@form-create/component-wangeditor/src'
|
|
25
|
-
import { parseFn } from '@form-create/utils/lib/json'
|
|
26
|
-
|
|
27
|
-
export default defineComponent({
|
|
28
|
-
name: 'FcEditorWrapper',
|
|
29
|
-
components: {
|
|
30
|
-
FcEditor
|
|
31
|
-
},
|
|
32
|
-
props: {
|
|
33
|
-
modelValue: String,
|
|
34
|
-
disabled: Boolean,
|
|
35
|
-
readOnly: Boolean,
|
|
36
|
-
// 图片上传相关配置
|
|
37
|
-
uploadImgServer: String,
|
|
38
|
-
uploadImgFieldName: {
|
|
39
|
-
type: String,
|
|
40
|
-
default: 'file'
|
|
41
|
-
},
|
|
42
|
-
uploadImgMaxSize: Number,
|
|
43
|
-
accept: String,
|
|
44
|
-
uploadImgHeaders: Object,
|
|
45
|
-
uploadImgParams: Object,
|
|
46
|
-
uploadImgCustomInsert: [String, Function],
|
|
47
|
-
withCredentials: Boolean,
|
|
48
|
-
// form-create 注入的 API
|
|
49
|
-
formCreateInject: Object,
|
|
50
|
-
placeholder: {
|
|
51
|
-
type: String,
|
|
52
|
-
default: '请输入正文'
|
|
53
|
-
},
|
|
54
|
-
height: {
|
|
55
|
-
type: [String, Number],
|
|
56
|
-
default: 300
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
emits: ['update:modelValue'],
|
|
60
|
-
watch: {
|
|
61
|
-
readOnly(newVal) {
|
|
62
|
-
if (!newVal && this._editor) {
|
|
63
|
-
// 切换到编辑模式
|
|
64
|
-
this.setReadOnlyMode(false)
|
|
65
|
-
} else if (newVal) {
|
|
66
|
-
// 切换到只读模式,设置图片点击监听
|
|
67
|
-
this.$nextTick(() => {
|
|
68
|
-
this.setupImageClickHandlers()
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
modelValue() {
|
|
73
|
-
// 内容变化时,重新设置图片点击监听(仅只读模式)
|
|
74
|
-
if (this.readOnly) {
|
|
75
|
-
this.$nextTick(() => {
|
|
76
|
-
this.setupImageClickHandlers()
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
mounted() {
|
|
82
|
-
// 如果是只读模式,设置图片点击监听
|
|
83
|
-
if (this.readOnly) {
|
|
84
|
-
this.$nextTick(() => {
|
|
85
|
-
this.setupImageClickHandlers()
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
computed: {
|
|
90
|
-
editorConfig() {
|
|
91
|
-
const config = {}
|
|
92
|
-
|
|
93
|
-
// 设置 placeholder
|
|
94
|
-
if (this.placeholder) {
|
|
95
|
-
config.placeholder = this.placeholder
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 设置高度
|
|
99
|
-
if (this.height) {
|
|
100
|
-
// 如果是数字,转换为像素字符串;如果是字符串,直接使用
|
|
101
|
-
config.height = this.height
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 排除不需要的工具栏菜单项
|
|
105
|
-
// 移除:全屏、代码、表情、删除线、缩进、待办事项、引用、分割线、斜体
|
|
106
|
-
config.excludeMenus = [
|
|
107
|
-
'fullScreen', // 全屏
|
|
108
|
-
'code', // 代码
|
|
109
|
-
'emoticon', // 表情
|
|
110
|
-
'strikeThrough', // 删除线
|
|
111
|
-
'indent', // 缩进
|
|
112
|
-
'todo', // 待办事项
|
|
113
|
-
'quote', // 引用
|
|
114
|
-
'splitLine', // 分割线
|
|
115
|
-
'italic' // 斜体
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
// 如果设置了 readOnly,配置只读模式
|
|
119
|
-
if (this.readOnly) {
|
|
120
|
-
// readOnly: true // 启用只读模式
|
|
121
|
-
// 注意:实际只读效果主要通过 setReadOnlyMode 方法实现
|
|
122
|
-
// (设置 contenteditable="false" 和阻止编辑事件)
|
|
123
|
-
// 只读模式允许复制文本和点击链接,但禁止编辑
|
|
124
|
-
config.readOnly = true
|
|
125
|
-
// 禁用自动聚焦,避免在只读模式下获得焦点
|
|
126
|
-
config.focus = false
|
|
127
|
-
config.autoFocus = false
|
|
128
|
-
// 只读模式下隐藏 placeholder
|
|
129
|
-
delete config.placeholder
|
|
130
|
-
// 只读模式下移除固定高度,使用自适应
|
|
131
|
-
delete config.height
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 如果设置了 uploadImgServer,配置图片上传
|
|
135
|
-
if (this.uploadImgServer) {
|
|
136
|
-
// wangeditor 4.x 使用这些配置项
|
|
137
|
-
config.uploadImgServer = this.uploadImgServer
|
|
138
|
-
config.uploadFileName = this.uploadImgFieldName || 'file'
|
|
139
|
-
config.uploadImgMaxSize = this.uploadImgMaxSize || 5 * 1024 * 1024 // 默认5MB
|
|
140
|
-
|
|
141
|
-
// 处理 accept 格式
|
|
142
|
-
if (this.accept) {
|
|
143
|
-
if (this.accept === 'image/*') {
|
|
144
|
-
config.uploadImgAccept = [
|
|
145
|
-
'jpg',
|
|
146
|
-
'jpeg',
|
|
147
|
-
'png',
|
|
148
|
-
'gif',
|
|
149
|
-
'bmp',
|
|
150
|
-
'webp'
|
|
151
|
-
]
|
|
152
|
-
} else {
|
|
153
|
-
// 处理类似 "image/png,image/jpeg" 的格式
|
|
154
|
-
const types = this.accept
|
|
155
|
-
.split(',')
|
|
156
|
-
.map((t) => {
|
|
157
|
-
const match = t.trim().match(/image\/(\w+)/)
|
|
158
|
-
return match ? match[1] : null
|
|
159
|
-
})
|
|
160
|
-
.filter(Boolean)
|
|
161
|
-
if (types.length > 0) {
|
|
162
|
-
config.uploadImgAccept = types
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 上传参数
|
|
168
|
-
if (this.uploadImgParams) {
|
|
169
|
-
config.uploadImgParams = this.uploadImgParams
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 请求头(wangeditor 4.x 可能不支持,但可以通过 customUploadImg 实现)
|
|
173
|
-
// withCredentials
|
|
174
|
-
if (this.withCredentials !== undefined) {
|
|
175
|
-
config.uploadImgParamsWithUrl = false // 参数放在 body 中
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 自定义上传函数 - 使用封装好的 api.request 方法
|
|
179
|
-
config.customUploadImg = (files, insertImgFn) => {
|
|
180
|
-
// files 是文件列表,insertImgFn 是插入函数
|
|
181
|
-
const file = files[0]
|
|
182
|
-
|
|
183
|
-
// 获取 form-create 注入的 API
|
|
184
|
-
const api = this.formCreateInject?.api
|
|
185
|
-
if (!api || !api.request) {
|
|
186
|
-
console.error('未找到 form-create API,无法使用封装的上传接口')
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 准备上传数据对象
|
|
191
|
-
const uploadData = {
|
|
192
|
-
// 文件字段
|
|
193
|
-
[this.uploadImgFieldName || 'file']: file
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 添加额外参数
|
|
197
|
-
if (this.uploadImgParams) {
|
|
198
|
-
Object.keys(this.uploadImgParams).forEach((key) => {
|
|
199
|
-
uploadData[key] = this.uploadImgParams[key]
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 使用封装好的 api.request 方法上传
|
|
204
|
-
// fetch.js 会自动将 data 转换为 FormData(当 dataType 不是 'json' 时)
|
|
205
|
-
api
|
|
206
|
-
.request({
|
|
207
|
-
action: this.uploadImgServer,
|
|
208
|
-
method: 'post',
|
|
209
|
-
data: uploadData,
|
|
210
|
-
dataType: 'form', // 使用表单格式,会自动创建 FormData
|
|
211
|
-
headers: this.uploadImgHeaders || {},
|
|
212
|
-
withCredentials: this.withCredentials || false
|
|
213
|
-
})
|
|
214
|
-
.then((res) => {
|
|
215
|
-
// 如果有自定义插入函数,使用它
|
|
216
|
-
if (this.uploadImgCustomInsert) {
|
|
217
|
-
const customInsert =
|
|
218
|
-
typeof this.uploadImgCustomInsert === 'function'
|
|
219
|
-
? this.uploadImgCustomInsert
|
|
220
|
-
: parseFn(this.uploadImgCustomInsert)
|
|
221
|
-
|
|
222
|
-
if (customInsert) {
|
|
223
|
-
// 调用自定义插入函数处理响应
|
|
224
|
-
// customInsert 接收 (res, insertFn) 参数
|
|
225
|
-
// insertFn 接收 (url, alt, href) 参数
|
|
226
|
-
customInsert(res, (url, alt, href) => {
|
|
227
|
-
insertImgFn(url, alt || '', href || '')
|
|
228
|
-
})
|
|
229
|
-
} else {
|
|
230
|
-
// 如果解析失败,使用默认格式
|
|
231
|
-
// this.handleDefaultResponse(res, insertImgFn)
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
// 使用默认响应格式处理
|
|
235
|
-
// this.handleDefaultResponse(res, insertImgFn)
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
.catch((error) => {
|
|
239
|
-
console.error('上传失败', error)
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return config
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
mounted() {
|
|
248
|
-
// 如果是只读模式,设置图片点击监听
|
|
249
|
-
if (this.readOnly) {
|
|
250
|
-
this.$nextTick(() => {
|
|
251
|
-
this.setupImageClickHandlers()
|
|
252
|
-
})
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
beforeUnmount() {
|
|
256
|
-
// 清理图片点击监听器
|
|
257
|
-
if (this._imageClickCleanup) {
|
|
258
|
-
this._imageClickCleanup()
|
|
259
|
-
this._imageClickCleanup = null
|
|
260
|
-
}
|
|
261
|
-
// 清理内容变化事件监听器
|
|
262
|
-
if (this._editor && this._editor._fcChangeHandlers) {
|
|
263
|
-
this._editor._fcChangeHandlers.forEach((item) => {
|
|
264
|
-
if (item.type === 'eventHook' && item.handler) {
|
|
265
|
-
// 从事件钩子中移除
|
|
266
|
-
if (
|
|
267
|
-
this._editor.txt &&
|
|
268
|
-
this._editor.txt.eventHooks &&
|
|
269
|
-
this._editor.txt.eventHooks.changeEvents
|
|
270
|
-
) {
|
|
271
|
-
const index = this._editor.txt.eventHooks.changeEvents.indexOf(
|
|
272
|
-
item.handler
|
|
273
|
-
)
|
|
274
|
-
if (index > -1) {
|
|
275
|
-
this._editor.txt.eventHooks.changeEvents.splice(index, 1)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
} else if (item.element && item.handler) {
|
|
279
|
-
// 移除 DOM 事件监听器
|
|
280
|
-
item.element.removeEventListener(item.event, item.handler, true)
|
|
281
|
-
}
|
|
282
|
-
})
|
|
283
|
-
this._editor._fcChangeHandlers = null
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 清理粘贴事件监听器
|
|
287
|
-
if (this._pasteHandler && this._pasteHandlerContainer) {
|
|
288
|
-
this._pasteHandlerContainer.removeEventListener(
|
|
289
|
-
'paste',
|
|
290
|
-
this._pasteHandler,
|
|
291
|
-
true
|
|
292
|
-
)
|
|
293
|
-
this._pasteHandler = null
|
|
294
|
-
this._pasteHandlerContainer = null
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// 清理只读模式相关的监听器和定时器
|
|
298
|
-
if (this._editor) {
|
|
299
|
-
this.$nextTick(() => {
|
|
300
|
-
let textContainer = null
|
|
301
|
-
if (this._editor.$textElem && this._editor.$textElem[0]) {
|
|
302
|
-
textContainer = this._editor.$textElem[0]
|
|
303
|
-
} else {
|
|
304
|
-
const editorId = this._editor.id
|
|
305
|
-
if (editorId) {
|
|
306
|
-
const editorEl = document.getElementById(editorId)
|
|
307
|
-
if (editorEl) {
|
|
308
|
-
textContainer = editorEl.querySelector('.w-e-text')
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (textContainer) {
|
|
314
|
-
// 清除定时器
|
|
315
|
-
if (textContainer._readOnlyInterval) {
|
|
316
|
-
clearInterval(textContainer._readOnlyInterval)
|
|
317
|
-
delete textContainer._readOnlyInterval
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 停止观察器
|
|
321
|
-
if (textContainer._readOnlyAttributeObserver) {
|
|
322
|
-
textContainer._readOnlyAttributeObserver.disconnect()
|
|
323
|
-
delete textContainer._readOnlyAttributeObserver
|
|
324
|
-
}
|
|
325
|
-
if (textContainer._readOnlyLinkObserver) {
|
|
326
|
-
textContainer._readOnlyLinkObserver.disconnect()
|
|
327
|
-
delete textContainer._readOnlyLinkObserver
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// 移除事件监听器
|
|
331
|
-
if (textContainer._readOnlyHandlers) {
|
|
332
|
-
const handlers = textContainer._readOnlyHandlers
|
|
333
|
-
if (handlers.preventEdit) {
|
|
334
|
-
textContainer.removeEventListener(
|
|
335
|
-
'keydown',
|
|
336
|
-
handlers.preventEdit,
|
|
337
|
-
true
|
|
338
|
-
)
|
|
339
|
-
}
|
|
340
|
-
if (handlers.preventAllEdit) {
|
|
341
|
-
textContainer.removeEventListener(
|
|
342
|
-
'keypress',
|
|
343
|
-
handlers.preventAllEdit,
|
|
344
|
-
true
|
|
345
|
-
)
|
|
346
|
-
textContainer.removeEventListener(
|
|
347
|
-
'paste',
|
|
348
|
-
handlers.preventAllEdit,
|
|
349
|
-
true
|
|
350
|
-
)
|
|
351
|
-
textContainer.removeEventListener(
|
|
352
|
-
'drop',
|
|
353
|
-
handlers.preventAllEdit,
|
|
354
|
-
true
|
|
355
|
-
)
|
|
356
|
-
textContainer.removeEventListener(
|
|
357
|
-
'input',
|
|
358
|
-
handlers.preventAllEdit,
|
|
359
|
-
true
|
|
360
|
-
)
|
|
361
|
-
textContainer.removeEventListener(
|
|
362
|
-
'beforeinput',
|
|
363
|
-
handlers.preventAllEdit,
|
|
364
|
-
true
|
|
365
|
-
)
|
|
366
|
-
textContainer.removeEventListener(
|
|
367
|
-
'compositionstart',
|
|
368
|
-
handlers.preventAllEdit,
|
|
369
|
-
true
|
|
370
|
-
)
|
|
371
|
-
textContainer.removeEventListener(
|
|
372
|
-
'compositionupdate',
|
|
373
|
-
handlers.preventAllEdit,
|
|
374
|
-
true
|
|
375
|
-
)
|
|
376
|
-
textContainer.removeEventListener(
|
|
377
|
-
'compositionend',
|
|
378
|
-
handlers.preventAllEdit,
|
|
379
|
-
true
|
|
380
|
-
)
|
|
381
|
-
}
|
|
382
|
-
delete textContainer._readOnlyHandlers
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
methods: {
|
|
389
|
-
// 设置只读模式下图片的点击监听器
|
|
390
|
-
setupImageClickHandlers() {
|
|
391
|
-
if (!this.readOnly || !this.$refs.readOnlyContainer) {
|
|
392
|
-
console.log(
|
|
393
|
-
'[FcEditorWrapper] setupImageClickHandlers: Not in readOnly mode or container not found.'
|
|
394
|
-
)
|
|
395
|
-
return
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const container = this.$refs.readOnlyContainer
|
|
399
|
-
|
|
400
|
-
// 清理旧的监听器
|
|
401
|
-
if (this._imageClickCleanup) {
|
|
402
|
-
console.log('[FcEditorWrapper] Cleaning up old image click handlers.')
|
|
403
|
-
this._imageClickCleanup()
|
|
404
|
-
this._imageClickCleanup = null
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// 使用事件委托,在容器上监听点击事件
|
|
408
|
-
const handleContainerClick = (e) => {
|
|
409
|
-
// 查找被点击的图片元素
|
|
410
|
-
let target = e.target
|
|
411
|
-
while (target && target !== container) {
|
|
412
|
-
if (target.tagName === 'IMG') {
|
|
413
|
-
// 找到了图片
|
|
414
|
-
const img = target
|
|
415
|
-
console.log('[FcEditorWrapper] Image clicked:', img.src)
|
|
416
|
-
|
|
417
|
-
const imgSrc = img.getAttribute('src') || img.src
|
|
418
|
-
const imgAlt = img.getAttribute('alt') || ''
|
|
419
|
-
const imgTitle = img.getAttribute('title') || imgAlt
|
|
420
|
-
|
|
421
|
-
// 检查图片是否在链接内
|
|
422
|
-
const parentLink = img.closest('a')
|
|
423
|
-
let imgUrl = imgSrc
|
|
424
|
-
if (parentLink) {
|
|
425
|
-
imgUrl = parentLink.getAttribute('href') || imgSrc
|
|
426
|
-
console.log('[FcEditorWrapper] Image is inside a link:', imgUrl)
|
|
427
|
-
// 不阻止默认行为,让链接正常跳转
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// 生成 uid(基于图片 URL)
|
|
431
|
-
const uid = imgSrc.split('').reduce((acc, char) => {
|
|
432
|
-
return ((acc << 5) - acc + char.charCodeAt(0)) | 0
|
|
433
|
-
}, 0)
|
|
434
|
-
|
|
435
|
-
// 发送预览消息到父窗口(类似 Upload 组件)
|
|
436
|
-
if (window.parent && window.parent !== window) {
|
|
437
|
-
const message = {
|
|
438
|
-
type: 'upload-preview',
|
|
439
|
-
file: {
|
|
440
|
-
url: imgUrl,
|
|
441
|
-
name: imgTitle || imgAlt || '图片',
|
|
442
|
-
uid: uid,
|
|
443
|
-
size: 0,
|
|
444
|
-
type: 'image'
|
|
445
|
-
},
|
|
446
|
-
timestamp: Date.now()
|
|
447
|
-
}
|
|
448
|
-
console.log('[FcEditorWrapper] Sending postMessage:', message)
|
|
449
|
-
window.parent.postMessage(message, '*')
|
|
450
|
-
} else {
|
|
451
|
-
console.warn('[FcEditorWrapper] No parent window to send message')
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// 不阻止默认行为和事件冒泡,让链接和图片都能正常工作
|
|
455
|
-
// 如果图片在链接内,链接会正常跳转
|
|
456
|
-
// 如果图片不在链接内,图片也没有默认行为需要阻止
|
|
457
|
-
break
|
|
458
|
-
}
|
|
459
|
-
target = target.parentElement
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// 在容器上添加点击事件监听器
|
|
464
|
-
container.addEventListener('click', handleContainerClick, true) // 使用 capture 阶段
|
|
465
|
-
console.log('[FcEditorWrapper] Added container click listener for images')
|
|
466
|
-
|
|
467
|
-
// 为所有图片设置样式
|
|
468
|
-
const images = container.querySelectorAll('img')
|
|
469
|
-
console.log('[FcEditorWrapper] Found images:', images.length)
|
|
470
|
-
images.forEach((img) => {
|
|
471
|
-
img.style.cursor = 'pointer'
|
|
472
|
-
img.style.pointerEvents = 'auto'
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
// 保存清理函数
|
|
476
|
-
this._imageClickCleanup = () => {
|
|
477
|
-
console.log('[FcEditorWrapper] Executing image click cleanup.')
|
|
478
|
-
container.removeEventListener('click', handleContainerClick, true)
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
initEditor(editor) {
|
|
482
|
-
if (!editor) return
|
|
483
|
-
|
|
484
|
-
// 保存编辑器引用
|
|
485
|
-
this._editor = editor
|
|
486
|
-
console.log('[FcEditorWrapper] initEditor called', {
|
|
487
|
-
editor,
|
|
488
|
-
hasConfig: !!editor.config
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
// 监听编辑器内容变化,实时触发更新以便校验能及时响应
|
|
492
|
-
// 方法1: 在 config 中设置 onchange(必须在 editor.create() 之前)
|
|
493
|
-
if (editor.config) {
|
|
494
|
-
const originalOnchange = editor.config.onchange
|
|
495
|
-
console.log('[FcEditorWrapper] Setting config.onchange', {
|
|
496
|
-
hasOriginalOnchange: !!originalOnchange
|
|
497
|
-
})
|
|
498
|
-
editor.config.onchange = (html) => {
|
|
499
|
-
console.log('[FcEditorWrapper] config.onchange triggered', {
|
|
500
|
-
html: html ? html.substring(0, 50) + '...' : html
|
|
501
|
-
})
|
|
502
|
-
// 调用原有的 onchange(如果有)
|
|
503
|
-
if (originalOnchange && typeof originalOnchange === 'function') {
|
|
504
|
-
originalOnchange(html)
|
|
505
|
-
}
|
|
506
|
-
// 触发 modelValue 更新,这样 form-create 可以实时触发校验
|
|
507
|
-
console.log('[FcEditorWrapper] Emitting update:modelValue', {
|
|
508
|
-
html: html ? html.substring(0, 50) + '...' : html
|
|
509
|
-
})
|
|
510
|
-
this.$emit('update:modelValue', html)
|
|
511
|
-
|
|
512
|
-
// 尝试触发校验重新执行
|
|
513
|
-
this.$nextTick(() => {
|
|
514
|
-
if (
|
|
515
|
-
this.formCreateInject &&
|
|
516
|
-
this.formCreateInject.api &&
|
|
517
|
-
this.formCreateInject.field
|
|
518
|
-
) {
|
|
519
|
-
const api = this.formCreateInject.api
|
|
520
|
-
const field = this.formCreateInject.field
|
|
521
|
-
console.log(
|
|
522
|
-
'[FcEditorWrapper] Attempting to trigger validation',
|
|
523
|
-
{
|
|
524
|
-
field,
|
|
525
|
-
hasValidateField: typeof api.validateField === 'function'
|
|
526
|
-
}
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
// 如果值不为空,尝试重新校验该字段
|
|
530
|
-
const isEmpty =
|
|
531
|
-
!html ||
|
|
532
|
-
!html.trim() ||
|
|
533
|
-
html === '<p><br></p>' ||
|
|
534
|
-
html === '<p></p>'
|
|
535
|
-
if (!isEmpty && typeof api.validateField === 'function') {
|
|
536
|
-
// 重新校验字段,这会根据当前值重新执行校验规则
|
|
537
|
-
api.validateField(field).catch(() => {
|
|
538
|
-
// 校验失败是正常的,不需要处理
|
|
539
|
-
})
|
|
540
|
-
console.log(
|
|
541
|
-
'[FcEditorWrapper] validateField called for field:',
|
|
542
|
-
field
|
|
543
|
-
)
|
|
544
|
-
} else if (
|
|
545
|
-
isEmpty &&
|
|
546
|
-
typeof api.clearValidateState === 'function'
|
|
547
|
-
) {
|
|
548
|
-
// 如果值为空,清除校验状态(但这可能不是我们想要的,因为可能还是需要显示错误)
|
|
549
|
-
// api.clearValidateState([field])
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
console.log('[FcEditorWrapper] Cannot trigger validation', {
|
|
553
|
-
hasInject: !!this.formCreateInject,
|
|
554
|
-
hasApi: !!(this.formCreateInject && this.formCreateInject.api),
|
|
555
|
-
hasField: !!(
|
|
556
|
-
this.formCreateInject && this.formCreateInject.field
|
|
557
|
-
),
|
|
558
|
-
injectKeys: this.formCreateInject
|
|
559
|
-
? Object.keys(this.formCreateInject)
|
|
560
|
-
: []
|
|
561
|
-
})
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// 方法2: 在编辑器创建后,也监听 DOM 事件作为备用方案
|
|
568
|
-
// 这样可以确保即使 config.onchange 不生效,也能触发更新
|
|
569
|
-
this.$nextTick(() => {
|
|
570
|
-
// 等待编辑器完全创建后再设置
|
|
571
|
-
setTimeout(() => {
|
|
572
|
-
console.log('[FcEditorWrapper] Setting up DOM event listeners', {
|
|
573
|
-
hasEditor: !!editor,
|
|
574
|
-
hasTxt: !!(editor && editor.txt),
|
|
575
|
-
editorId: editor && editor.id
|
|
576
|
-
})
|
|
577
|
-
if (editor && editor.txt) {
|
|
578
|
-
// 获取文本容器
|
|
579
|
-
let textContainer = null
|
|
580
|
-
if (editor.$textElem && editor.$textElem[0]) {
|
|
581
|
-
textContainer = editor.$textElem[0]
|
|
582
|
-
console.log('[FcEditorWrapper] Found textContainer via $textElem')
|
|
583
|
-
} else if (editor.id) {
|
|
584
|
-
const editorEl = document.getElementById(editor.id)
|
|
585
|
-
if (editorEl) {
|
|
586
|
-
textContainer = editorEl.querySelector('.w-e-text')
|
|
587
|
-
console.log(
|
|
588
|
-
'[FcEditorWrapper] Found textContainer via querySelector',
|
|
589
|
-
{ editorId: editor.id, found: !!textContainer }
|
|
590
|
-
)
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (textContainer) {
|
|
595
|
-
// 监听 input 事件
|
|
596
|
-
const handleInput = () => {
|
|
597
|
-
const html = editor.txt.html()
|
|
598
|
-
console.log(
|
|
599
|
-
'[FcEditorWrapper] DOM event triggered (input/keyup/paste)',
|
|
600
|
-
{ html: html ? html.substring(0, 50) + '...' : html }
|
|
601
|
-
)
|
|
602
|
-
this.$emit('update:modelValue', html)
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// 监听多种事件以确保捕获所有内容变化
|
|
606
|
-
textContainer.addEventListener('input', handleInput)
|
|
607
|
-
textContainer.addEventListener('keyup', handleInput)
|
|
608
|
-
textContainer.addEventListener('paste', handleInput, true)
|
|
609
|
-
console.log('[FcEditorWrapper] Added DOM event listeners', {
|
|
610
|
-
textContainer
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
// 保存事件处理器引用,以便清理
|
|
614
|
-
if (!editor._fcChangeHandlers) {
|
|
615
|
-
editor._fcChangeHandlers = []
|
|
616
|
-
}
|
|
617
|
-
editor._fcChangeHandlers.push({
|
|
618
|
-
element: textContainer,
|
|
619
|
-
event: 'input',
|
|
620
|
-
handler: handleInput
|
|
621
|
-
})
|
|
622
|
-
editor._fcChangeHandlers.push({
|
|
623
|
-
element: textContainer,
|
|
624
|
-
event: 'keyup',
|
|
625
|
-
handler: handleInput
|
|
626
|
-
})
|
|
627
|
-
editor._fcChangeHandlers.push({
|
|
628
|
-
element: textContainer,
|
|
629
|
-
event: 'paste',
|
|
630
|
-
handler: handleInput
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
// 如果编辑器支持事件钩子,也使用它
|
|
634
|
-
if (editor.txt.eventHooks && editor.txt.eventHooks.changeEvents) {
|
|
635
|
-
console.log('[FcEditorWrapper] Found eventHooks.changeEvents', {
|
|
636
|
-
count: editor.txt.eventHooks.changeEvents.length
|
|
637
|
-
})
|
|
638
|
-
const changeHandler = () => {
|
|
639
|
-
const html = editor.txt.html()
|
|
640
|
-
console.log(
|
|
641
|
-
'[FcEditorWrapper] eventHook changeEvents triggered',
|
|
642
|
-
{ html: html ? html.substring(0, 50) + '...' : html }
|
|
643
|
-
)
|
|
644
|
-
this.$emit('update:modelValue', html)
|
|
645
|
-
}
|
|
646
|
-
editor.txt.eventHooks.changeEvents.push(changeHandler)
|
|
647
|
-
console.log('[FcEditorWrapper] Added eventHook handler', {
|
|
648
|
-
newCount: editor.txt.eventHooks.changeEvents.length
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
if (!editor._fcChangeHandlers) {
|
|
652
|
-
editor._fcChangeHandlers = []
|
|
653
|
-
}
|
|
654
|
-
editor._fcChangeHandlers.push({
|
|
655
|
-
type: 'eventHook',
|
|
656
|
-
handler: changeHandler
|
|
657
|
-
})
|
|
658
|
-
} else {
|
|
659
|
-
console.log(
|
|
660
|
-
'[FcEditorWrapper] No eventHooks.changeEvents found'
|
|
661
|
-
)
|
|
662
|
-
}
|
|
663
|
-
} else {
|
|
664
|
-
console.warn('[FcEditorWrapper] Could not find textContainer')
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}, 100)
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
// 如果设置了只读模式,配置编辑器为只读
|
|
671
|
-
// 使用多重延迟和监听,确保编辑器完全创建后再设置
|
|
672
|
-
if (this.readOnly) {
|
|
673
|
-
// 立即设置一次
|
|
674
|
-
this.$nextTick(() => {
|
|
675
|
-
this.setReadOnlyMode(true)
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
// 第一次延迟:等待编辑器 DOM 创建
|
|
679
|
-
setTimeout(() => {
|
|
680
|
-
this.setReadOnlyMode(true)
|
|
681
|
-
// 第二次延迟:确保设置生效
|
|
682
|
-
setTimeout(() => {
|
|
683
|
-
this.setReadOnlyMode(true)
|
|
684
|
-
// 第三次延迟:确保完全生效
|
|
685
|
-
setTimeout(() => {
|
|
686
|
-
this.setReadOnlyMode(true)
|
|
687
|
-
}, 300)
|
|
688
|
-
}, 200)
|
|
689
|
-
}, 100)
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// 等待编辑器完全创建后,监听粘贴事件
|
|
693
|
-
this.$nextTick(() => {
|
|
694
|
-
// 尝试多种方式查找编辑器容器
|
|
695
|
-
let editorContainer = null
|
|
696
|
-
|
|
697
|
-
// 方式1: 通过编辑器实例属性查找
|
|
698
|
-
if (editor.$textContainerElem && editor.$textContainerElem[0]) {
|
|
699
|
-
editorContainer = editor.$textContainerElem[0]
|
|
700
|
-
} else if (editor.$textElem && editor.$textElem[0]) {
|
|
701
|
-
editorContainer = editor.$textElem[0]
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// 方式2: 通过编辑器 ID 查找
|
|
705
|
-
if (!editorContainer && editor.id) {
|
|
706
|
-
const editorEl = document.getElementById(editor.id)
|
|
707
|
-
if (editorEl) {
|
|
708
|
-
editorContainer =
|
|
709
|
-
editorEl.querySelector('.w-e-text-container') ||
|
|
710
|
-
editorEl.querySelector('.w-e-text')
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// 方式3: 通过 class 查找(通用方法)
|
|
715
|
-
if (!editorContainer) {
|
|
716
|
-
const textContainer = document.querySelector('.w-e-text-container')
|
|
717
|
-
if (textContainer) {
|
|
718
|
-
editorContainer = textContainer
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (editorContainer) {
|
|
723
|
-
this.setupPasteHandler(editor, editorContainer)
|
|
724
|
-
} else {
|
|
725
|
-
// 延迟重试,等待编辑器完全渲染
|
|
726
|
-
setTimeout(() => {
|
|
727
|
-
const textContainer =
|
|
728
|
-
document.querySelector('.w-e-text-container') ||
|
|
729
|
-
document.querySelector('.w-e-text')
|
|
730
|
-
if (textContainer && editor) {
|
|
731
|
-
this.setupPasteHandler(editor, textContainer)
|
|
732
|
-
} else {
|
|
733
|
-
console.warn(
|
|
734
|
-
'无法找到 wangEditor 文本容器,链接自动转换功能可能无法正常工作'
|
|
735
|
-
)
|
|
736
|
-
}
|
|
737
|
-
}, 500)
|
|
738
|
-
}
|
|
739
|
-
})
|
|
740
|
-
},
|
|
741
|
-
// 设置粘贴事件处理器
|
|
742
|
-
setupPasteHandler(editor, container) {
|
|
743
|
-
const handlePaste = (event) => {
|
|
744
|
-
// 获取粘贴的文本内容
|
|
745
|
-
const pasteText = (
|
|
746
|
-
event.clipboardData || window.clipboardData
|
|
747
|
-
)?.getData('text')
|
|
748
|
-
|
|
749
|
-
// 检查是否为纯链接(URL格式)
|
|
750
|
-
if (pasteText && this.isValidUrl(pasteText.trim())) {
|
|
751
|
-
// 阻止默认粘贴行为
|
|
752
|
-
event.preventDefault()
|
|
753
|
-
event.stopPropagation()
|
|
754
|
-
|
|
755
|
-
try {
|
|
756
|
-
const url = pasteText.trim()
|
|
757
|
-
// 确保 URL 有协议
|
|
758
|
-
const fullUrl =
|
|
759
|
-
url.startsWith('http://') || url.startsWith('https://')
|
|
760
|
-
? url
|
|
761
|
-
: `http://${url}`
|
|
762
|
-
|
|
763
|
-
// 尝试使用 wangEditor 的命令插入链接
|
|
764
|
-
// wangEditor 4.x 支持通过 cmd.do 执行命令
|
|
765
|
-
if (editor.cmd && editor.cmd.do) {
|
|
766
|
-
try {
|
|
767
|
-
// 先插入文本,然后选中并转换为链接
|
|
768
|
-
editor.cmd.do(
|
|
769
|
-
'insertHTML',
|
|
770
|
-
`<a href="${fullUrl}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
|
771
|
-
)
|
|
772
|
-
return false
|
|
773
|
-
} catch (e) {
|
|
774
|
-
// 如果 cmd.do 失败,继续使用 DOM 方式
|
|
775
|
-
console.debug('使用 cmd.do 插入链接失败,尝试 DOM 方式:', e)
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// 使用 DOM 方式插入链接
|
|
780
|
-
const selection = window.getSelection()
|
|
781
|
-
if (selection && selection.rangeCount > 0) {
|
|
782
|
-
const range = selection.getRangeAt(0)
|
|
783
|
-
|
|
784
|
-
// 删除选中的内容
|
|
785
|
-
range.deleteContents()
|
|
786
|
-
|
|
787
|
-
// 创建链接元素
|
|
788
|
-
const linkElement = document.createElement('a')
|
|
789
|
-
linkElement.href = fullUrl
|
|
790
|
-
linkElement.textContent = url
|
|
791
|
-
linkElement.target = '_blank'
|
|
792
|
-
linkElement.rel = 'noopener noreferrer'
|
|
793
|
-
|
|
794
|
-
// 插入链接
|
|
795
|
-
range.insertNode(linkElement)
|
|
796
|
-
|
|
797
|
-
// 移动光标到链接后面,并添加一个空格
|
|
798
|
-
const textNode = document.createTextNode(' ')
|
|
799
|
-
range.setStartAfter(linkElement)
|
|
800
|
-
range.insertNode(textNode)
|
|
801
|
-
range.setStartAfter(textNode)
|
|
802
|
-
range.collapse(true)
|
|
803
|
-
selection.removeAllRanges()
|
|
804
|
-
selection.addRange(range)
|
|
805
|
-
|
|
806
|
-
// 触发编辑器内容变化
|
|
807
|
-
// 尝试多种方式触发更新
|
|
808
|
-
if (editor.txt) {
|
|
809
|
-
// 方式1: 触发 change 事件
|
|
810
|
-
if (
|
|
811
|
-
editor.txt.eventHooks &&
|
|
812
|
-
editor.txt.eventHooks.changeEvents
|
|
813
|
-
) {
|
|
814
|
-
editor.txt.eventHooks.changeEvents.forEach((fn) => {
|
|
815
|
-
if (typeof fn === 'function') {
|
|
816
|
-
try {
|
|
817
|
-
fn()
|
|
818
|
-
} catch (e) {
|
|
819
|
-
console.debug('触发编辑器变化事件失败:', e)
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
})
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// 方式2: 手动触发 input 事件
|
|
826
|
-
const inputEvent = new Event('input', {
|
|
827
|
-
bubbles: true,
|
|
828
|
-
cancelable: true
|
|
829
|
-
})
|
|
830
|
-
container.dispatchEvent(inputEvent)
|
|
831
|
-
|
|
832
|
-
// 方式3: 触发 change 事件
|
|
833
|
-
const changeEvent = new Event('change', {
|
|
834
|
-
bubbles: true,
|
|
835
|
-
cancelable: true
|
|
836
|
-
})
|
|
837
|
-
container.dispatchEvent(changeEvent)
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
} catch (error) {
|
|
841
|
-
console.error('插入链接失败:', error)
|
|
842
|
-
// 如果出错,回退到普通粘贴
|
|
843
|
-
setTimeout(() => {
|
|
844
|
-
const textNode = document.createTextNode(pasteText)
|
|
845
|
-
const selection = window.getSelection()
|
|
846
|
-
if (selection && selection.rangeCount > 0) {
|
|
847
|
-
const range = selection.getRangeAt(0)
|
|
848
|
-
range.deleteContents()
|
|
849
|
-
range.insertNode(textNode)
|
|
850
|
-
range.setStartAfter(textNode)
|
|
851
|
-
range.collapse(true)
|
|
852
|
-
selection.removeAllRanges()
|
|
853
|
-
selection.addRange(range)
|
|
854
|
-
}
|
|
855
|
-
}, 0)
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
return false
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// 在捕获阶段监听粘贴事件
|
|
863
|
-
container.addEventListener('paste', handlePaste, true)
|
|
864
|
-
|
|
865
|
-
// 保存处理器引用,以便在组件销毁时移除
|
|
866
|
-
this._pasteHandler = handlePaste
|
|
867
|
-
this._pasteHandlerContainer = container
|
|
868
|
-
},
|
|
869
|
-
// 设置只读模式:允许选择和点击链接,但禁止编辑
|
|
870
|
-
setReadOnlyMode(readOnly) {
|
|
871
|
-
if (!this._editor) return
|
|
872
|
-
|
|
873
|
-
// 使用 wangEditor 的 disable/enable 方法
|
|
874
|
-
if (readOnly) {
|
|
875
|
-
// 禁用编辑器
|
|
876
|
-
if (
|
|
877
|
-
this._editor.disable &&
|
|
878
|
-
typeof this._editor.disable === 'function'
|
|
879
|
-
) {
|
|
880
|
-
this._editor.disable()
|
|
881
|
-
}
|
|
882
|
-
} else {
|
|
883
|
-
// 启用编辑器
|
|
884
|
-
if (this._editor.enable && typeof this._editor.enable === 'function') {
|
|
885
|
-
this._editor.enable()
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
this.$nextTick(() => {
|
|
890
|
-
// 查找编辑器的根元素
|
|
891
|
-
let editorRootElement = null
|
|
892
|
-
if (this._editor.id) {
|
|
893
|
-
editorRootElement = document.getElementById(this._editor.id)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// 查找编辑器的文本容器
|
|
897
|
-
let textContainer = null
|
|
898
|
-
let containerElement = null
|
|
899
|
-
|
|
900
|
-
if (this._editor.$textElem && this._editor.$textElem[0]) {
|
|
901
|
-
textContainer = this._editor.$textElem[0]
|
|
902
|
-
} else {
|
|
903
|
-
const editorId = this._editor.id
|
|
904
|
-
if (editorId) {
|
|
905
|
-
const editorEl = document.getElementById(editorId)
|
|
906
|
-
if (editorEl) {
|
|
907
|
-
textContainer = editorEl.querySelector('.w-e-text')
|
|
908
|
-
containerElement = editorEl.querySelector('.w-e-text-container')
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// 也尝试查找容器
|
|
914
|
-
if (!textContainer) {
|
|
915
|
-
if (
|
|
916
|
-
this._editor.$textContainerElem &&
|
|
917
|
-
this._editor.$textContainerElem[0]
|
|
918
|
-
) {
|
|
919
|
-
containerElement = this._editor.$textContainerElem[0]
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// 查找容器元素(如果还没找到)
|
|
924
|
-
if (
|
|
925
|
-
!containerElement &&
|
|
926
|
-
this._editor.$textContainerElem &&
|
|
927
|
-
this._editor.$textContainerElem[0]
|
|
928
|
-
) {
|
|
929
|
-
containerElement = this._editor.$textContainerElem[0]
|
|
930
|
-
} else if (!containerElement && this._editor.id) {
|
|
931
|
-
const editorEl = document.getElementById(this._editor.id)
|
|
932
|
-
if (editorEl) {
|
|
933
|
-
containerElement = editorEl.querySelector('.w-e-text-container')
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
if (textContainer) {
|
|
938
|
-
if (readOnly) {
|
|
939
|
-
// 设置根元素为自适应高度(移除内联样式中的固定高度)
|
|
940
|
-
if (editorRootElement) {
|
|
941
|
-
editorRootElement.style.height = 'auto'
|
|
942
|
-
editorRootElement.style.minHeight = 'auto'
|
|
943
|
-
editorRootElement.style.maxHeight = 'none'
|
|
944
|
-
}
|
|
945
|
-
// 设置容器和文本区域为自适应高度
|
|
946
|
-
if (containerElement) {
|
|
947
|
-
containerElement.style.height = 'auto'
|
|
948
|
-
containerElement.style.minHeight = 'auto'
|
|
949
|
-
containerElement.style.maxHeight = 'none'
|
|
950
|
-
}
|
|
951
|
-
if (textContainer) {
|
|
952
|
-
textContainer.style.height = 'auto'
|
|
953
|
-
textContainer.style.minHeight = 'auto'
|
|
954
|
-
textContainer.style.maxHeight = 'none'
|
|
955
|
-
}
|
|
956
|
-
// 强制设置为只读:禁用编辑,但允许选择和点击链接
|
|
957
|
-
const forceReadOnly = () => {
|
|
958
|
-
// 强制设置 contenteditable
|
|
959
|
-
textContainer.setAttribute('contenteditable', 'false')
|
|
960
|
-
// 确保文本可以选择
|
|
961
|
-
textContainer.style.userSelect = 'text'
|
|
962
|
-
textContainer.style.webkitUserSelect = 'text'
|
|
963
|
-
textContainer.style.mozUserSelect = 'text'
|
|
964
|
-
textContainer.style.msUserSelect = 'text'
|
|
965
|
-
textContainer.style.cursor = 'text'
|
|
966
|
-
// 保持根元素自适应高度
|
|
967
|
-
if (editorRootElement) {
|
|
968
|
-
editorRootElement.style.height = 'auto'
|
|
969
|
-
editorRootElement.style.minHeight = 'auto'
|
|
970
|
-
editorRootElement.style.maxHeight = 'none'
|
|
971
|
-
}
|
|
972
|
-
// 保持容器和文本区域自适应高度
|
|
973
|
-
if (containerElement) {
|
|
974
|
-
containerElement.style.height = 'auto'
|
|
975
|
-
containerElement.style.minHeight = 'auto'
|
|
976
|
-
containerElement.style.maxHeight = 'none'
|
|
977
|
-
}
|
|
978
|
-
textContainer.style.height = 'auto'
|
|
979
|
-
textContainer.style.minHeight = 'auto'
|
|
980
|
-
textContainer.style.maxHeight = 'none'
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
forceReadOnly()
|
|
984
|
-
|
|
985
|
-
// 监听 contenteditable 属性的变化,强制保持只读
|
|
986
|
-
const attributeObserver = new MutationObserver(() => {
|
|
987
|
-
if (textContainer.getAttribute('contenteditable') !== 'false') {
|
|
988
|
-
forceReadOnly()
|
|
989
|
-
}
|
|
990
|
-
})
|
|
991
|
-
attributeObserver.observe(textContainer, {
|
|
992
|
-
attributes: true,
|
|
993
|
-
attributeFilter: ['contenteditable']
|
|
994
|
-
})
|
|
995
|
-
|
|
996
|
-
// 阻止所有编辑操作的事件处理器
|
|
997
|
-
const preventAllEdit = (e) => {
|
|
998
|
-
// 阻止所有输入操作
|
|
999
|
-
e.preventDefault()
|
|
1000
|
-
e.stopPropagation()
|
|
1001
|
-
e.stopImmediatePropagation()
|
|
1002
|
-
return false
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// 阻止键盘输入(但允许选择快捷键如 Ctrl+A, Ctrl+C)
|
|
1006
|
-
const preventEdit = (e) => {
|
|
1007
|
-
// 允许复制、全选等快捷键
|
|
1008
|
-
if (
|
|
1009
|
-
(e.ctrlKey || e.metaKey) &&
|
|
1010
|
-
(e.key === 'a' || e.key === 'c' || e.key === 'x')
|
|
1011
|
-
) {
|
|
1012
|
-
return true
|
|
1013
|
-
}
|
|
1014
|
-
// 阻止所有其他键盘输入
|
|
1015
|
-
if (e.key && e.key.length === 1) {
|
|
1016
|
-
e.preventDefault()
|
|
1017
|
-
e.stopPropagation()
|
|
1018
|
-
return false
|
|
1019
|
-
}
|
|
1020
|
-
// 阻止删除键(当没有选中内容时)
|
|
1021
|
-
if (
|
|
1022
|
-
(e.key === 'Delete' || e.key === 'Backspace') &&
|
|
1023
|
-
!e.ctrlKey &&
|
|
1024
|
-
!e.metaKey
|
|
1025
|
-
) {
|
|
1026
|
-
const selection = window.getSelection()
|
|
1027
|
-
if (
|
|
1028
|
-
!selection ||
|
|
1029
|
-
selection.rangeCount === 0 ||
|
|
1030
|
-
selection.isCollapsed
|
|
1031
|
-
) {
|
|
1032
|
-
e.preventDefault()
|
|
1033
|
-
e.stopPropagation()
|
|
1034
|
-
return false
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// 添加事件监听器(使用捕获阶段,优先级最高)
|
|
1040
|
-
textContainer.addEventListener('keydown', preventEdit, true)
|
|
1041
|
-
textContainer.addEventListener('keypress', preventAllEdit, true)
|
|
1042
|
-
textContainer.addEventListener('paste', preventAllEdit, true)
|
|
1043
|
-
textContainer.addEventListener('drop', preventAllEdit, true)
|
|
1044
|
-
textContainer.addEventListener('input', preventAllEdit, true)
|
|
1045
|
-
textContainer.addEventListener('beforeinput', preventAllEdit, true)
|
|
1046
|
-
textContainer.addEventListener(
|
|
1047
|
-
'compositionstart',
|
|
1048
|
-
preventAllEdit,
|
|
1049
|
-
true
|
|
1050
|
-
)
|
|
1051
|
-
textContainer.addEventListener(
|
|
1052
|
-
'compositionupdate',
|
|
1053
|
-
preventAllEdit,
|
|
1054
|
-
true
|
|
1055
|
-
)
|
|
1056
|
-
textContainer.addEventListener(
|
|
1057
|
-
'compositionend',
|
|
1058
|
-
preventAllEdit,
|
|
1059
|
-
true
|
|
1060
|
-
)
|
|
1061
|
-
|
|
1062
|
-
// 保存事件处理器引用,以便清理
|
|
1063
|
-
if (!textContainer._readOnlyHandlers) {
|
|
1064
|
-
textContainer._readOnlyHandlers = {}
|
|
1065
|
-
}
|
|
1066
|
-
textContainer._readOnlyHandlers.preventEdit = preventEdit
|
|
1067
|
-
textContainer._readOnlyHandlers.preventAllEdit = preventAllEdit
|
|
1068
|
-
textContainer._readOnlyAttributeObserver = attributeObserver
|
|
1069
|
-
|
|
1070
|
-
// 允许链接点击
|
|
1071
|
-
const enableLinks = () => {
|
|
1072
|
-
const links = textContainer.querySelectorAll('a')
|
|
1073
|
-
links.forEach((link) => {
|
|
1074
|
-
link.style.pointerEvents = 'auto'
|
|
1075
|
-
link.style.cursor = 'pointer'
|
|
1076
|
-
})
|
|
1077
|
-
}
|
|
1078
|
-
enableLinks()
|
|
1079
|
-
|
|
1080
|
-
// 监听新添加的链接
|
|
1081
|
-
const linkObserver = new MutationObserver(() => {
|
|
1082
|
-
enableLinks()
|
|
1083
|
-
// 确保 contenteditable 保持为 false
|
|
1084
|
-
forceReadOnly()
|
|
1085
|
-
})
|
|
1086
|
-
linkObserver.observe(textContainer, {
|
|
1087
|
-
childList: true,
|
|
1088
|
-
subtree: true,
|
|
1089
|
-
attributes: true,
|
|
1090
|
-
attributeFilter: ['contenteditable']
|
|
1091
|
-
})
|
|
1092
|
-
textContainer._readOnlyLinkObserver = linkObserver
|
|
1093
|
-
|
|
1094
|
-
// 定期检查并强制设置(防止被覆盖)
|
|
1095
|
-
if (!textContainer._readOnlyInterval) {
|
|
1096
|
-
textContainer._readOnlyInterval = setInterval(() => {
|
|
1097
|
-
if (textContainer.getAttribute('contenteditable') !== 'false') {
|
|
1098
|
-
forceReadOnly()
|
|
1099
|
-
}
|
|
1100
|
-
}, 100)
|
|
1101
|
-
}
|
|
1102
|
-
} else {
|
|
1103
|
-
// 取消只读:恢复编辑功能
|
|
1104
|
-
textContainer.setAttribute('contenteditable', 'true')
|
|
1105
|
-
|
|
1106
|
-
// 清除定时器
|
|
1107
|
-
if (textContainer._readOnlyInterval) {
|
|
1108
|
-
clearInterval(textContainer._readOnlyInterval)
|
|
1109
|
-
delete textContainer._readOnlyInterval
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// 移除事件监听器
|
|
1113
|
-
if (textContainer._readOnlyHandlers) {
|
|
1114
|
-
const handlers = textContainer._readOnlyHandlers
|
|
1115
|
-
textContainer.removeEventListener(
|
|
1116
|
-
'keydown',
|
|
1117
|
-
handlers.preventEdit,
|
|
1118
|
-
true
|
|
1119
|
-
)
|
|
1120
|
-
textContainer.removeEventListener(
|
|
1121
|
-
'keypress',
|
|
1122
|
-
handlers.preventAllEdit,
|
|
1123
|
-
true
|
|
1124
|
-
)
|
|
1125
|
-
textContainer.removeEventListener(
|
|
1126
|
-
'paste',
|
|
1127
|
-
handlers.preventAllEdit,
|
|
1128
|
-
true
|
|
1129
|
-
)
|
|
1130
|
-
textContainer.removeEventListener(
|
|
1131
|
-
'drop',
|
|
1132
|
-
handlers.preventAllEdit,
|
|
1133
|
-
true
|
|
1134
|
-
)
|
|
1135
|
-
textContainer.removeEventListener(
|
|
1136
|
-
'input',
|
|
1137
|
-
handlers.preventAllEdit,
|
|
1138
|
-
true
|
|
1139
|
-
)
|
|
1140
|
-
textContainer.removeEventListener(
|
|
1141
|
-
'beforeinput',
|
|
1142
|
-
handlers.preventAllEdit,
|
|
1143
|
-
true
|
|
1144
|
-
)
|
|
1145
|
-
textContainer.removeEventListener(
|
|
1146
|
-
'compositionstart',
|
|
1147
|
-
handlers.preventAllEdit,
|
|
1148
|
-
true
|
|
1149
|
-
)
|
|
1150
|
-
textContainer.removeEventListener(
|
|
1151
|
-
'compositionupdate',
|
|
1152
|
-
handlers.preventAllEdit,
|
|
1153
|
-
true
|
|
1154
|
-
)
|
|
1155
|
-
textContainer.removeEventListener(
|
|
1156
|
-
'compositionend',
|
|
1157
|
-
handlers.preventAllEdit,
|
|
1158
|
-
true
|
|
1159
|
-
)
|
|
1160
|
-
delete textContainer._readOnlyHandlers
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// 停止观察
|
|
1164
|
-
if (textContainer._readOnlyAttributeObserver) {
|
|
1165
|
-
textContainer._readOnlyAttributeObserver.disconnect()
|
|
1166
|
-
delete textContainer._readOnlyAttributeObserver
|
|
1167
|
-
}
|
|
1168
|
-
if (textContainer._readOnlyLinkObserver) {
|
|
1169
|
-
textContainer._readOnlyLinkObserver.disconnect()
|
|
1170
|
-
delete textContainer._readOnlyLinkObserver
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
})
|
|
1175
|
-
},
|
|
1176
|
-
// 验证是否为有效的URL
|
|
1177
|
-
isValidUrl(str) {
|
|
1178
|
-
if (!str || typeof str !== 'string') return false
|
|
1179
|
-
|
|
1180
|
-
const trimmed = str.trim()
|
|
1181
|
-
if (!trimmed) return false
|
|
1182
|
-
|
|
1183
|
-
try {
|
|
1184
|
-
// 检查是否包含常见协议
|
|
1185
|
-
const urlPattern = /^(https?|ftp|file):\/\//i
|
|
1186
|
-
if (urlPattern.test(trimmed)) {
|
|
1187
|
-
new URL(trimmed)
|
|
1188
|
-
return true
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// 检查是否看起来像URL(包含域名)
|
|
1192
|
-
// 匹配形如 www.example.com 或 example.com 的格式
|
|
1193
|
-
const domainPattern =
|
|
1194
|
-
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/i
|
|
1195
|
-
// 或者简单的 http/https URL 格式
|
|
1196
|
-
const simpleUrlPattern = /^[a-zA-Z0-9][a-zA-Z0-9\-]*\.[a-zA-Z]{2,}/
|
|
1197
|
-
|
|
1198
|
-
if (domainPattern.test(trimmed) || simpleUrlPattern.test(trimmed)) {
|
|
1199
|
-
// 尝试添加 http:// 前缀来验证
|
|
1200
|
-
try {
|
|
1201
|
-
new URL(`http://${trimmed}`)
|
|
1202
|
-
return true
|
|
1203
|
-
} catch (e) {
|
|
1204
|
-
return false
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
return false
|
|
1209
|
-
} catch (e) {
|
|
1210
|
-
return false
|
|
1211
|
-
}
|
|
1212
|
-
},
|
|
1213
|
-
handleDefaultResponse(res, insertImgFn) {
|
|
1214
|
-
// 处理标准响应格式: {errno: 0, data: {url: "...", alt: "...", href: "..."}}
|
|
1215
|
-
if (res.code === 200 && res.data) {
|
|
1216
|
-
insertImgFn(
|
|
1217
|
-
res.data.realURL,
|
|
1218
|
-
res.data.alt || '图片',
|
|
1219
|
-
res.data.realURL || ''
|
|
1220
|
-
)
|
|
1221
|
-
} else if (res.url) {
|
|
1222
|
-
// 兼容简单格式: {url: "..."}
|
|
1223
|
-
insertImgFn(res.url, res.alt || '', res.href || '')
|
|
1224
|
-
} else {
|
|
1225
|
-
console.error('上传失败,响应格式不正确', res)
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
})
|
|
1230
|
-
</script>
|
|
1231
|
-
|
|
1232
|
-
<style scoped>
|
|
1233
|
-
.fc-editor-wrapper {
|
|
1234
|
-
width: 100%;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
.fc-editor-readonly {
|
|
1238
|
-
padding: 0 10px;
|
|
1239
|
-
border-radius: 4px;
|
|
1240
|
-
/* 覆盖预览模式的全局 pointer-events: none 规则 */
|
|
1241
|
-
pointer-events: auto !important;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
.fc-editor-readonly img {
|
|
1245
|
-
cursor: pointer;
|
|
1246
|
-
max-width: 100%;
|
|
1247
|
-
height: auto;
|
|
1248
|
-
/* 确保图片可以点击,覆盖预览模式的全局规则 */
|
|
1249
|
-
pointer-events: auto !important;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
.fc-editor-readonly a {
|
|
1253
|
-
/* 确保链接可以点击,覆盖预览模式的全局规则 */
|
|
1254
|
-
pointer-events: auto !important;
|
|
1255
|
-
cursor: pointer;
|
|
1256
|
-
}
|
|
1257
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div class="fc-editor-wrapper">
|
|
3
|
+
<!-- 只读/预览模式:直接用 v-html 渲染,并添加图片点击监听 -->
|
|
4
|
+
<div
|
|
5
|
+
v-if="readOnly"
|
|
6
|
+
ref="readOnlyContainer"
|
|
7
|
+
class="fc-editor-readonly"
|
|
8
|
+
v-html="modelValue"
|
|
9
|
+
></div>
|
|
10
|
+
<!-- 编辑模式:使用富文本编辑器 -->
|
|
11
|
+
<FcEditor
|
|
12
|
+
v-else
|
|
13
|
+
:model-value="modelValue"
|
|
14
|
+
:disabled="disabled"
|
|
15
|
+
:config="editorConfig"
|
|
16
|
+
:init="initEditor"
|
|
17
|
+
@update:model-value="$emit('update:modelValue', $event)"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import { defineComponent } from 'vue'
|
|
24
|
+
import FcEditor from '@form-create/component-wangeditor/src'
|
|
25
|
+
import { parseFn } from '@form-create/utils/lib/json'
|
|
26
|
+
|
|
27
|
+
export default defineComponent({
|
|
28
|
+
name: 'FcEditorWrapper',
|
|
29
|
+
components: {
|
|
30
|
+
FcEditor
|
|
31
|
+
},
|
|
32
|
+
props: {
|
|
33
|
+
modelValue: String,
|
|
34
|
+
disabled: Boolean,
|
|
35
|
+
readOnly: Boolean,
|
|
36
|
+
// 图片上传相关配置
|
|
37
|
+
uploadImgServer: String,
|
|
38
|
+
uploadImgFieldName: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: 'file'
|
|
41
|
+
},
|
|
42
|
+
uploadImgMaxSize: Number,
|
|
43
|
+
accept: String,
|
|
44
|
+
uploadImgHeaders: Object,
|
|
45
|
+
uploadImgParams: Object,
|
|
46
|
+
uploadImgCustomInsert: [String, Function],
|
|
47
|
+
withCredentials: Boolean,
|
|
48
|
+
// form-create 注入的 API
|
|
49
|
+
formCreateInject: Object,
|
|
50
|
+
placeholder: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: '请输入正文'
|
|
53
|
+
},
|
|
54
|
+
height: {
|
|
55
|
+
type: [String, Number],
|
|
56
|
+
default: 300
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
emits: ['update:modelValue'],
|
|
60
|
+
watch: {
|
|
61
|
+
readOnly(newVal) {
|
|
62
|
+
if (!newVal && this._editor) {
|
|
63
|
+
// 切换到编辑模式
|
|
64
|
+
this.setReadOnlyMode(false)
|
|
65
|
+
} else if (newVal) {
|
|
66
|
+
// 切换到只读模式,设置图片点击监听
|
|
67
|
+
this.$nextTick(() => {
|
|
68
|
+
this.setupImageClickHandlers()
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
modelValue() {
|
|
73
|
+
// 内容变化时,重新设置图片点击监听(仅只读模式)
|
|
74
|
+
if (this.readOnly) {
|
|
75
|
+
this.$nextTick(() => {
|
|
76
|
+
this.setupImageClickHandlers()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
mounted() {
|
|
82
|
+
// 如果是只读模式,设置图片点击监听
|
|
83
|
+
if (this.readOnly) {
|
|
84
|
+
this.$nextTick(() => {
|
|
85
|
+
this.setupImageClickHandlers()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
computed: {
|
|
90
|
+
editorConfig() {
|
|
91
|
+
const config = {}
|
|
92
|
+
|
|
93
|
+
// 设置 placeholder
|
|
94
|
+
if (this.placeholder) {
|
|
95
|
+
config.placeholder = this.placeholder
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 设置高度
|
|
99
|
+
if (this.height) {
|
|
100
|
+
// 如果是数字,转换为像素字符串;如果是字符串,直接使用
|
|
101
|
+
config.height = this.height
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 排除不需要的工具栏菜单项
|
|
105
|
+
// 移除:全屏、代码、表情、删除线、缩进、待办事项、引用、分割线、斜体
|
|
106
|
+
config.excludeMenus = [
|
|
107
|
+
'fullScreen', // 全屏
|
|
108
|
+
'code', // 代码
|
|
109
|
+
'emoticon', // 表情
|
|
110
|
+
'strikeThrough', // 删除线
|
|
111
|
+
'indent', // 缩进
|
|
112
|
+
'todo', // 待办事项
|
|
113
|
+
'quote', // 引用
|
|
114
|
+
'splitLine', // 分割线
|
|
115
|
+
'italic' // 斜体
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
// 如果设置了 readOnly,配置只读模式
|
|
119
|
+
if (this.readOnly) {
|
|
120
|
+
// readOnly: true // 启用只读模式
|
|
121
|
+
// 注意:实际只读效果主要通过 setReadOnlyMode 方法实现
|
|
122
|
+
// (设置 contenteditable="false" 和阻止编辑事件)
|
|
123
|
+
// 只读模式允许复制文本和点击链接,但禁止编辑
|
|
124
|
+
config.readOnly = true
|
|
125
|
+
// 禁用自动聚焦,避免在只读模式下获得焦点
|
|
126
|
+
config.focus = false
|
|
127
|
+
config.autoFocus = false
|
|
128
|
+
// 只读模式下隐藏 placeholder
|
|
129
|
+
delete config.placeholder
|
|
130
|
+
// 只读模式下移除固定高度,使用自适应
|
|
131
|
+
delete config.height
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 如果设置了 uploadImgServer,配置图片上传
|
|
135
|
+
if (this.uploadImgServer) {
|
|
136
|
+
// wangeditor 4.x 使用这些配置项
|
|
137
|
+
config.uploadImgServer = this.uploadImgServer
|
|
138
|
+
config.uploadFileName = this.uploadImgFieldName || 'file'
|
|
139
|
+
config.uploadImgMaxSize = this.uploadImgMaxSize || 5 * 1024 * 1024 // 默认5MB
|
|
140
|
+
|
|
141
|
+
// 处理 accept 格式
|
|
142
|
+
if (this.accept) {
|
|
143
|
+
if (this.accept === 'image/*') {
|
|
144
|
+
config.uploadImgAccept = [
|
|
145
|
+
'jpg',
|
|
146
|
+
'jpeg',
|
|
147
|
+
'png',
|
|
148
|
+
'gif',
|
|
149
|
+
'bmp',
|
|
150
|
+
'webp'
|
|
151
|
+
]
|
|
152
|
+
} else {
|
|
153
|
+
// 处理类似 "image/png,image/jpeg" 的格式
|
|
154
|
+
const types = this.accept
|
|
155
|
+
.split(',')
|
|
156
|
+
.map((t) => {
|
|
157
|
+
const match = t.trim().match(/image\/(\w+)/)
|
|
158
|
+
return match ? match[1] : null
|
|
159
|
+
})
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
if (types.length > 0) {
|
|
162
|
+
config.uploadImgAccept = types
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 上传参数
|
|
168
|
+
if (this.uploadImgParams) {
|
|
169
|
+
config.uploadImgParams = this.uploadImgParams
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 请求头(wangeditor 4.x 可能不支持,但可以通过 customUploadImg 实现)
|
|
173
|
+
// withCredentials
|
|
174
|
+
if (this.withCredentials !== undefined) {
|
|
175
|
+
config.uploadImgParamsWithUrl = false // 参数放在 body 中
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 自定义上传函数 - 使用封装好的 api.request 方法
|
|
179
|
+
config.customUploadImg = (files, insertImgFn) => {
|
|
180
|
+
// files 是文件列表,insertImgFn 是插入函数
|
|
181
|
+
const file = files[0]
|
|
182
|
+
|
|
183
|
+
// 获取 form-create 注入的 API
|
|
184
|
+
const api = this.formCreateInject?.api
|
|
185
|
+
if (!api || !api.request) {
|
|
186
|
+
console.error('未找到 form-create API,无法使用封装的上传接口')
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 准备上传数据对象
|
|
191
|
+
const uploadData = {
|
|
192
|
+
// 文件字段
|
|
193
|
+
[this.uploadImgFieldName || 'file']: file
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 添加额外参数
|
|
197
|
+
if (this.uploadImgParams) {
|
|
198
|
+
Object.keys(this.uploadImgParams).forEach((key) => {
|
|
199
|
+
uploadData[key] = this.uploadImgParams[key]
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 使用封装好的 api.request 方法上传
|
|
204
|
+
// fetch.js 会自动将 data 转换为 FormData(当 dataType 不是 'json' 时)
|
|
205
|
+
api
|
|
206
|
+
.request({
|
|
207
|
+
action: this.uploadImgServer,
|
|
208
|
+
method: 'post',
|
|
209
|
+
data: uploadData,
|
|
210
|
+
dataType: 'form', // 使用表单格式,会自动创建 FormData
|
|
211
|
+
headers: this.uploadImgHeaders || {},
|
|
212
|
+
withCredentials: this.withCredentials || false
|
|
213
|
+
})
|
|
214
|
+
.then((res) => {
|
|
215
|
+
// 如果有自定义插入函数,使用它
|
|
216
|
+
if (this.uploadImgCustomInsert) {
|
|
217
|
+
const customInsert =
|
|
218
|
+
typeof this.uploadImgCustomInsert === 'function'
|
|
219
|
+
? this.uploadImgCustomInsert
|
|
220
|
+
: parseFn(this.uploadImgCustomInsert)
|
|
221
|
+
|
|
222
|
+
if (customInsert) {
|
|
223
|
+
// 调用自定义插入函数处理响应
|
|
224
|
+
// customInsert 接收 (res, insertFn) 参数
|
|
225
|
+
// insertFn 接收 (url, alt, href) 参数
|
|
226
|
+
customInsert(res, (url, alt, href) => {
|
|
227
|
+
insertImgFn(url, alt || '', href || '')
|
|
228
|
+
})
|
|
229
|
+
} else {
|
|
230
|
+
// 如果解析失败,使用默认格式
|
|
231
|
+
// this.handleDefaultResponse(res, insertImgFn)
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// 使用默认响应格式处理
|
|
235
|
+
// this.handleDefaultResponse(res, insertImgFn)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
.catch((error) => {
|
|
239
|
+
console.error('上传失败', error)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return config
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
mounted() {
|
|
248
|
+
// 如果是只读模式,设置图片点击监听
|
|
249
|
+
if (this.readOnly) {
|
|
250
|
+
this.$nextTick(() => {
|
|
251
|
+
this.setupImageClickHandlers()
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
beforeUnmount() {
|
|
256
|
+
// 清理图片点击监听器
|
|
257
|
+
if (this._imageClickCleanup) {
|
|
258
|
+
this._imageClickCleanup()
|
|
259
|
+
this._imageClickCleanup = null
|
|
260
|
+
}
|
|
261
|
+
// 清理内容变化事件监听器
|
|
262
|
+
if (this._editor && this._editor._fcChangeHandlers) {
|
|
263
|
+
this._editor._fcChangeHandlers.forEach((item) => {
|
|
264
|
+
if (item.type === 'eventHook' && item.handler) {
|
|
265
|
+
// 从事件钩子中移除
|
|
266
|
+
if (
|
|
267
|
+
this._editor.txt &&
|
|
268
|
+
this._editor.txt.eventHooks &&
|
|
269
|
+
this._editor.txt.eventHooks.changeEvents
|
|
270
|
+
) {
|
|
271
|
+
const index = this._editor.txt.eventHooks.changeEvents.indexOf(
|
|
272
|
+
item.handler
|
|
273
|
+
)
|
|
274
|
+
if (index > -1) {
|
|
275
|
+
this._editor.txt.eventHooks.changeEvents.splice(index, 1)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else if (item.element && item.handler) {
|
|
279
|
+
// 移除 DOM 事件监听器
|
|
280
|
+
item.element.removeEventListener(item.event, item.handler, true)
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
this._editor._fcChangeHandlers = null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 清理粘贴事件监听器
|
|
287
|
+
if (this._pasteHandler && this._pasteHandlerContainer) {
|
|
288
|
+
this._pasteHandlerContainer.removeEventListener(
|
|
289
|
+
'paste',
|
|
290
|
+
this._pasteHandler,
|
|
291
|
+
true
|
|
292
|
+
)
|
|
293
|
+
this._pasteHandler = null
|
|
294
|
+
this._pasteHandlerContainer = null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 清理只读模式相关的监听器和定时器
|
|
298
|
+
if (this._editor) {
|
|
299
|
+
this.$nextTick(() => {
|
|
300
|
+
let textContainer = null
|
|
301
|
+
if (this._editor.$textElem && this._editor.$textElem[0]) {
|
|
302
|
+
textContainer = this._editor.$textElem[0]
|
|
303
|
+
} else {
|
|
304
|
+
const editorId = this._editor.id
|
|
305
|
+
if (editorId) {
|
|
306
|
+
const editorEl = document.getElementById(editorId)
|
|
307
|
+
if (editorEl) {
|
|
308
|
+
textContainer = editorEl.querySelector('.w-e-text')
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (textContainer) {
|
|
314
|
+
// 清除定时器
|
|
315
|
+
if (textContainer._readOnlyInterval) {
|
|
316
|
+
clearInterval(textContainer._readOnlyInterval)
|
|
317
|
+
delete textContainer._readOnlyInterval
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 停止观察器
|
|
321
|
+
if (textContainer._readOnlyAttributeObserver) {
|
|
322
|
+
textContainer._readOnlyAttributeObserver.disconnect()
|
|
323
|
+
delete textContainer._readOnlyAttributeObserver
|
|
324
|
+
}
|
|
325
|
+
if (textContainer._readOnlyLinkObserver) {
|
|
326
|
+
textContainer._readOnlyLinkObserver.disconnect()
|
|
327
|
+
delete textContainer._readOnlyLinkObserver
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 移除事件监听器
|
|
331
|
+
if (textContainer._readOnlyHandlers) {
|
|
332
|
+
const handlers = textContainer._readOnlyHandlers
|
|
333
|
+
if (handlers.preventEdit) {
|
|
334
|
+
textContainer.removeEventListener(
|
|
335
|
+
'keydown',
|
|
336
|
+
handlers.preventEdit,
|
|
337
|
+
true
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
if (handlers.preventAllEdit) {
|
|
341
|
+
textContainer.removeEventListener(
|
|
342
|
+
'keypress',
|
|
343
|
+
handlers.preventAllEdit,
|
|
344
|
+
true
|
|
345
|
+
)
|
|
346
|
+
textContainer.removeEventListener(
|
|
347
|
+
'paste',
|
|
348
|
+
handlers.preventAllEdit,
|
|
349
|
+
true
|
|
350
|
+
)
|
|
351
|
+
textContainer.removeEventListener(
|
|
352
|
+
'drop',
|
|
353
|
+
handlers.preventAllEdit,
|
|
354
|
+
true
|
|
355
|
+
)
|
|
356
|
+
textContainer.removeEventListener(
|
|
357
|
+
'input',
|
|
358
|
+
handlers.preventAllEdit,
|
|
359
|
+
true
|
|
360
|
+
)
|
|
361
|
+
textContainer.removeEventListener(
|
|
362
|
+
'beforeinput',
|
|
363
|
+
handlers.preventAllEdit,
|
|
364
|
+
true
|
|
365
|
+
)
|
|
366
|
+
textContainer.removeEventListener(
|
|
367
|
+
'compositionstart',
|
|
368
|
+
handlers.preventAllEdit,
|
|
369
|
+
true
|
|
370
|
+
)
|
|
371
|
+
textContainer.removeEventListener(
|
|
372
|
+
'compositionupdate',
|
|
373
|
+
handlers.preventAllEdit,
|
|
374
|
+
true
|
|
375
|
+
)
|
|
376
|
+
textContainer.removeEventListener(
|
|
377
|
+
'compositionend',
|
|
378
|
+
handlers.preventAllEdit,
|
|
379
|
+
true
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
delete textContainer._readOnlyHandlers
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
methods: {
|
|
389
|
+
// 设置只读模式下图片的点击监听器
|
|
390
|
+
setupImageClickHandlers() {
|
|
391
|
+
if (!this.readOnly || !this.$refs.readOnlyContainer) {
|
|
392
|
+
console.log(
|
|
393
|
+
'[FcEditorWrapper] setupImageClickHandlers: Not in readOnly mode or container not found.'
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const container = this.$refs.readOnlyContainer
|
|
399
|
+
|
|
400
|
+
// 清理旧的监听器
|
|
401
|
+
if (this._imageClickCleanup) {
|
|
402
|
+
console.log('[FcEditorWrapper] Cleaning up old image click handlers.')
|
|
403
|
+
this._imageClickCleanup()
|
|
404
|
+
this._imageClickCleanup = null
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 使用事件委托,在容器上监听点击事件
|
|
408
|
+
const handleContainerClick = (e) => {
|
|
409
|
+
// 查找被点击的图片元素
|
|
410
|
+
let target = e.target
|
|
411
|
+
while (target && target !== container) {
|
|
412
|
+
if (target.tagName === 'IMG') {
|
|
413
|
+
// 找到了图片
|
|
414
|
+
const img = target
|
|
415
|
+
console.log('[FcEditorWrapper] Image clicked:', img.src)
|
|
416
|
+
|
|
417
|
+
const imgSrc = img.getAttribute('src') || img.src
|
|
418
|
+
const imgAlt = img.getAttribute('alt') || ''
|
|
419
|
+
const imgTitle = img.getAttribute('title') || imgAlt
|
|
420
|
+
|
|
421
|
+
// 检查图片是否在链接内
|
|
422
|
+
const parentLink = img.closest('a')
|
|
423
|
+
let imgUrl = imgSrc
|
|
424
|
+
if (parentLink) {
|
|
425
|
+
imgUrl = parentLink.getAttribute('href') || imgSrc
|
|
426
|
+
console.log('[FcEditorWrapper] Image is inside a link:', imgUrl)
|
|
427
|
+
// 不阻止默认行为,让链接正常跳转
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 生成 uid(基于图片 URL)
|
|
431
|
+
const uid = imgSrc.split('').reduce((acc, char) => {
|
|
432
|
+
return ((acc << 5) - acc + char.charCodeAt(0)) | 0
|
|
433
|
+
}, 0)
|
|
434
|
+
|
|
435
|
+
// 发送预览消息到父窗口(类似 Upload 组件)
|
|
436
|
+
if (window.parent && window.parent !== window) {
|
|
437
|
+
const message = {
|
|
438
|
+
type: 'upload-preview',
|
|
439
|
+
file: {
|
|
440
|
+
url: imgUrl,
|
|
441
|
+
name: imgTitle || imgAlt || '图片',
|
|
442
|
+
uid: uid,
|
|
443
|
+
size: 0,
|
|
444
|
+
type: 'image'
|
|
445
|
+
},
|
|
446
|
+
timestamp: Date.now()
|
|
447
|
+
}
|
|
448
|
+
console.log('[FcEditorWrapper] Sending postMessage:', message)
|
|
449
|
+
window.parent.postMessage(message, '*')
|
|
450
|
+
} else {
|
|
451
|
+
console.warn('[FcEditorWrapper] No parent window to send message')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 不阻止默认行为和事件冒泡,让链接和图片都能正常工作
|
|
455
|
+
// 如果图片在链接内,链接会正常跳转
|
|
456
|
+
// 如果图片不在链接内,图片也没有默认行为需要阻止
|
|
457
|
+
break
|
|
458
|
+
}
|
|
459
|
+
target = target.parentElement
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 在容器上添加点击事件监听器
|
|
464
|
+
container.addEventListener('click', handleContainerClick, true) // 使用 capture 阶段
|
|
465
|
+
console.log('[FcEditorWrapper] Added container click listener for images')
|
|
466
|
+
|
|
467
|
+
// 为所有图片设置样式
|
|
468
|
+
const images = container.querySelectorAll('img')
|
|
469
|
+
console.log('[FcEditorWrapper] Found images:', images.length)
|
|
470
|
+
images.forEach((img) => {
|
|
471
|
+
img.style.cursor = 'pointer'
|
|
472
|
+
img.style.pointerEvents = 'auto'
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// 保存清理函数
|
|
476
|
+
this._imageClickCleanup = () => {
|
|
477
|
+
console.log('[FcEditorWrapper] Executing image click cleanup.')
|
|
478
|
+
container.removeEventListener('click', handleContainerClick, true)
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
initEditor(editor) {
|
|
482
|
+
if (!editor) return
|
|
483
|
+
|
|
484
|
+
// 保存编辑器引用
|
|
485
|
+
this._editor = editor
|
|
486
|
+
console.log('[FcEditorWrapper] initEditor called', {
|
|
487
|
+
editor,
|
|
488
|
+
hasConfig: !!editor.config
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// 监听编辑器内容变化,实时触发更新以便校验能及时响应
|
|
492
|
+
// 方法1: 在 config 中设置 onchange(必须在 editor.create() 之前)
|
|
493
|
+
if (editor.config) {
|
|
494
|
+
const originalOnchange = editor.config.onchange
|
|
495
|
+
console.log('[FcEditorWrapper] Setting config.onchange', {
|
|
496
|
+
hasOriginalOnchange: !!originalOnchange
|
|
497
|
+
})
|
|
498
|
+
editor.config.onchange = (html) => {
|
|
499
|
+
console.log('[FcEditorWrapper] config.onchange triggered', {
|
|
500
|
+
html: html ? html.substring(0, 50) + '...' : html
|
|
501
|
+
})
|
|
502
|
+
// 调用原有的 onchange(如果有)
|
|
503
|
+
if (originalOnchange && typeof originalOnchange === 'function') {
|
|
504
|
+
originalOnchange(html)
|
|
505
|
+
}
|
|
506
|
+
// 触发 modelValue 更新,这样 form-create 可以实时触发校验
|
|
507
|
+
console.log('[FcEditorWrapper] Emitting update:modelValue', {
|
|
508
|
+
html: html ? html.substring(0, 50) + '...' : html
|
|
509
|
+
})
|
|
510
|
+
this.$emit('update:modelValue', html)
|
|
511
|
+
|
|
512
|
+
// 尝试触发校验重新执行
|
|
513
|
+
this.$nextTick(() => {
|
|
514
|
+
if (
|
|
515
|
+
this.formCreateInject &&
|
|
516
|
+
this.formCreateInject.api &&
|
|
517
|
+
this.formCreateInject.field
|
|
518
|
+
) {
|
|
519
|
+
const api = this.formCreateInject.api
|
|
520
|
+
const field = this.formCreateInject.field
|
|
521
|
+
console.log(
|
|
522
|
+
'[FcEditorWrapper] Attempting to trigger validation',
|
|
523
|
+
{
|
|
524
|
+
field,
|
|
525
|
+
hasValidateField: typeof api.validateField === 'function'
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
// 如果值不为空,尝试重新校验该字段
|
|
530
|
+
const isEmpty =
|
|
531
|
+
!html ||
|
|
532
|
+
!html.trim() ||
|
|
533
|
+
html === '<p><br></p>' ||
|
|
534
|
+
html === '<p></p>'
|
|
535
|
+
if (!isEmpty && typeof api.validateField === 'function') {
|
|
536
|
+
// 重新校验字段,这会根据当前值重新执行校验规则
|
|
537
|
+
api.validateField(field).catch(() => {
|
|
538
|
+
// 校验失败是正常的,不需要处理
|
|
539
|
+
})
|
|
540
|
+
console.log(
|
|
541
|
+
'[FcEditorWrapper] validateField called for field:',
|
|
542
|
+
field
|
|
543
|
+
)
|
|
544
|
+
} else if (
|
|
545
|
+
isEmpty &&
|
|
546
|
+
typeof api.clearValidateState === 'function'
|
|
547
|
+
) {
|
|
548
|
+
// 如果值为空,清除校验状态(但这可能不是我们想要的,因为可能还是需要显示错误)
|
|
549
|
+
// api.clearValidateState([field])
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
console.log('[FcEditorWrapper] Cannot trigger validation', {
|
|
553
|
+
hasInject: !!this.formCreateInject,
|
|
554
|
+
hasApi: !!(this.formCreateInject && this.formCreateInject.api),
|
|
555
|
+
hasField: !!(
|
|
556
|
+
this.formCreateInject && this.formCreateInject.field
|
|
557
|
+
),
|
|
558
|
+
injectKeys: this.formCreateInject
|
|
559
|
+
? Object.keys(this.formCreateInject)
|
|
560
|
+
: []
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 方法2: 在编辑器创建后,也监听 DOM 事件作为备用方案
|
|
568
|
+
// 这样可以确保即使 config.onchange 不生效,也能触发更新
|
|
569
|
+
this.$nextTick(() => {
|
|
570
|
+
// 等待编辑器完全创建后再设置
|
|
571
|
+
setTimeout(() => {
|
|
572
|
+
console.log('[FcEditorWrapper] Setting up DOM event listeners', {
|
|
573
|
+
hasEditor: !!editor,
|
|
574
|
+
hasTxt: !!(editor && editor.txt),
|
|
575
|
+
editorId: editor && editor.id
|
|
576
|
+
})
|
|
577
|
+
if (editor && editor.txt) {
|
|
578
|
+
// 获取文本容器
|
|
579
|
+
let textContainer = null
|
|
580
|
+
if (editor.$textElem && editor.$textElem[0]) {
|
|
581
|
+
textContainer = editor.$textElem[0]
|
|
582
|
+
console.log('[FcEditorWrapper] Found textContainer via $textElem')
|
|
583
|
+
} else if (editor.id) {
|
|
584
|
+
const editorEl = document.getElementById(editor.id)
|
|
585
|
+
if (editorEl) {
|
|
586
|
+
textContainer = editorEl.querySelector('.w-e-text')
|
|
587
|
+
console.log(
|
|
588
|
+
'[FcEditorWrapper] Found textContainer via querySelector',
|
|
589
|
+
{ editorId: editor.id, found: !!textContainer }
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (textContainer) {
|
|
595
|
+
// 监听 input 事件
|
|
596
|
+
const handleInput = () => {
|
|
597
|
+
const html = editor.txt.html()
|
|
598
|
+
console.log(
|
|
599
|
+
'[FcEditorWrapper] DOM event triggered (input/keyup/paste)',
|
|
600
|
+
{ html: html ? html.substring(0, 50) + '...' : html }
|
|
601
|
+
)
|
|
602
|
+
this.$emit('update:modelValue', html)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 监听多种事件以确保捕获所有内容变化
|
|
606
|
+
textContainer.addEventListener('input', handleInput)
|
|
607
|
+
textContainer.addEventListener('keyup', handleInput)
|
|
608
|
+
textContainer.addEventListener('paste', handleInput, true)
|
|
609
|
+
console.log('[FcEditorWrapper] Added DOM event listeners', {
|
|
610
|
+
textContainer
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// 保存事件处理器引用,以便清理
|
|
614
|
+
if (!editor._fcChangeHandlers) {
|
|
615
|
+
editor._fcChangeHandlers = []
|
|
616
|
+
}
|
|
617
|
+
editor._fcChangeHandlers.push({
|
|
618
|
+
element: textContainer,
|
|
619
|
+
event: 'input',
|
|
620
|
+
handler: handleInput
|
|
621
|
+
})
|
|
622
|
+
editor._fcChangeHandlers.push({
|
|
623
|
+
element: textContainer,
|
|
624
|
+
event: 'keyup',
|
|
625
|
+
handler: handleInput
|
|
626
|
+
})
|
|
627
|
+
editor._fcChangeHandlers.push({
|
|
628
|
+
element: textContainer,
|
|
629
|
+
event: 'paste',
|
|
630
|
+
handler: handleInput
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
// 如果编辑器支持事件钩子,也使用它
|
|
634
|
+
if (editor.txt.eventHooks && editor.txt.eventHooks.changeEvents) {
|
|
635
|
+
console.log('[FcEditorWrapper] Found eventHooks.changeEvents', {
|
|
636
|
+
count: editor.txt.eventHooks.changeEvents.length
|
|
637
|
+
})
|
|
638
|
+
const changeHandler = () => {
|
|
639
|
+
const html = editor.txt.html()
|
|
640
|
+
console.log(
|
|
641
|
+
'[FcEditorWrapper] eventHook changeEvents triggered',
|
|
642
|
+
{ html: html ? html.substring(0, 50) + '...' : html }
|
|
643
|
+
)
|
|
644
|
+
this.$emit('update:modelValue', html)
|
|
645
|
+
}
|
|
646
|
+
editor.txt.eventHooks.changeEvents.push(changeHandler)
|
|
647
|
+
console.log('[FcEditorWrapper] Added eventHook handler', {
|
|
648
|
+
newCount: editor.txt.eventHooks.changeEvents.length
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
if (!editor._fcChangeHandlers) {
|
|
652
|
+
editor._fcChangeHandlers = []
|
|
653
|
+
}
|
|
654
|
+
editor._fcChangeHandlers.push({
|
|
655
|
+
type: 'eventHook',
|
|
656
|
+
handler: changeHandler
|
|
657
|
+
})
|
|
658
|
+
} else {
|
|
659
|
+
console.log(
|
|
660
|
+
'[FcEditorWrapper] No eventHooks.changeEvents found'
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
console.warn('[FcEditorWrapper] Could not find textContainer')
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}, 100)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
// 如果设置了只读模式,配置编辑器为只读
|
|
671
|
+
// 使用多重延迟和监听,确保编辑器完全创建后再设置
|
|
672
|
+
if (this.readOnly) {
|
|
673
|
+
// 立即设置一次
|
|
674
|
+
this.$nextTick(() => {
|
|
675
|
+
this.setReadOnlyMode(true)
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
// 第一次延迟:等待编辑器 DOM 创建
|
|
679
|
+
setTimeout(() => {
|
|
680
|
+
this.setReadOnlyMode(true)
|
|
681
|
+
// 第二次延迟:确保设置生效
|
|
682
|
+
setTimeout(() => {
|
|
683
|
+
this.setReadOnlyMode(true)
|
|
684
|
+
// 第三次延迟:确保完全生效
|
|
685
|
+
setTimeout(() => {
|
|
686
|
+
this.setReadOnlyMode(true)
|
|
687
|
+
}, 300)
|
|
688
|
+
}, 200)
|
|
689
|
+
}, 100)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 等待编辑器完全创建后,监听粘贴事件
|
|
693
|
+
this.$nextTick(() => {
|
|
694
|
+
// 尝试多种方式查找编辑器容器
|
|
695
|
+
let editorContainer = null
|
|
696
|
+
|
|
697
|
+
// 方式1: 通过编辑器实例属性查找
|
|
698
|
+
if (editor.$textContainerElem && editor.$textContainerElem[0]) {
|
|
699
|
+
editorContainer = editor.$textContainerElem[0]
|
|
700
|
+
} else if (editor.$textElem && editor.$textElem[0]) {
|
|
701
|
+
editorContainer = editor.$textElem[0]
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 方式2: 通过编辑器 ID 查找
|
|
705
|
+
if (!editorContainer && editor.id) {
|
|
706
|
+
const editorEl = document.getElementById(editor.id)
|
|
707
|
+
if (editorEl) {
|
|
708
|
+
editorContainer =
|
|
709
|
+
editorEl.querySelector('.w-e-text-container') ||
|
|
710
|
+
editorEl.querySelector('.w-e-text')
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 方式3: 通过 class 查找(通用方法)
|
|
715
|
+
if (!editorContainer) {
|
|
716
|
+
const textContainer = document.querySelector('.w-e-text-container')
|
|
717
|
+
if (textContainer) {
|
|
718
|
+
editorContainer = textContainer
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (editorContainer) {
|
|
723
|
+
this.setupPasteHandler(editor, editorContainer)
|
|
724
|
+
} else {
|
|
725
|
+
// 延迟重试,等待编辑器完全渲染
|
|
726
|
+
setTimeout(() => {
|
|
727
|
+
const textContainer =
|
|
728
|
+
document.querySelector('.w-e-text-container') ||
|
|
729
|
+
document.querySelector('.w-e-text')
|
|
730
|
+
if (textContainer && editor) {
|
|
731
|
+
this.setupPasteHandler(editor, textContainer)
|
|
732
|
+
} else {
|
|
733
|
+
console.warn(
|
|
734
|
+
'无法找到 wangEditor 文本容器,链接自动转换功能可能无法正常工作'
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
}, 500)
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
},
|
|
741
|
+
// 设置粘贴事件处理器
|
|
742
|
+
setupPasteHandler(editor, container) {
|
|
743
|
+
const handlePaste = (event) => {
|
|
744
|
+
// 获取粘贴的文本内容
|
|
745
|
+
const pasteText = (
|
|
746
|
+
event.clipboardData || window.clipboardData
|
|
747
|
+
)?.getData('text')
|
|
748
|
+
|
|
749
|
+
// 检查是否为纯链接(URL格式)
|
|
750
|
+
if (pasteText && this.isValidUrl(pasteText.trim())) {
|
|
751
|
+
// 阻止默认粘贴行为
|
|
752
|
+
event.preventDefault()
|
|
753
|
+
event.stopPropagation()
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const url = pasteText.trim()
|
|
757
|
+
// 确保 URL 有协议
|
|
758
|
+
const fullUrl =
|
|
759
|
+
url.startsWith('http://') || url.startsWith('https://')
|
|
760
|
+
? url
|
|
761
|
+
: `http://${url}`
|
|
762
|
+
|
|
763
|
+
// 尝试使用 wangEditor 的命令插入链接
|
|
764
|
+
// wangEditor 4.x 支持通过 cmd.do 执行命令
|
|
765
|
+
if (editor.cmd && editor.cmd.do) {
|
|
766
|
+
try {
|
|
767
|
+
// 先插入文本,然后选中并转换为链接
|
|
768
|
+
editor.cmd.do(
|
|
769
|
+
'insertHTML',
|
|
770
|
+
`<a href="${fullUrl}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
|
771
|
+
)
|
|
772
|
+
return false
|
|
773
|
+
} catch (e) {
|
|
774
|
+
// 如果 cmd.do 失败,继续使用 DOM 方式
|
|
775
|
+
console.debug('使用 cmd.do 插入链接失败,尝试 DOM 方式:', e)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// 使用 DOM 方式插入链接
|
|
780
|
+
const selection = window.getSelection()
|
|
781
|
+
if (selection && selection.rangeCount > 0) {
|
|
782
|
+
const range = selection.getRangeAt(0)
|
|
783
|
+
|
|
784
|
+
// 删除选中的内容
|
|
785
|
+
range.deleteContents()
|
|
786
|
+
|
|
787
|
+
// 创建链接元素
|
|
788
|
+
const linkElement = document.createElement('a')
|
|
789
|
+
linkElement.href = fullUrl
|
|
790
|
+
linkElement.textContent = url
|
|
791
|
+
linkElement.target = '_blank'
|
|
792
|
+
linkElement.rel = 'noopener noreferrer'
|
|
793
|
+
|
|
794
|
+
// 插入链接
|
|
795
|
+
range.insertNode(linkElement)
|
|
796
|
+
|
|
797
|
+
// 移动光标到链接后面,并添加一个空格
|
|
798
|
+
const textNode = document.createTextNode(' ')
|
|
799
|
+
range.setStartAfter(linkElement)
|
|
800
|
+
range.insertNode(textNode)
|
|
801
|
+
range.setStartAfter(textNode)
|
|
802
|
+
range.collapse(true)
|
|
803
|
+
selection.removeAllRanges()
|
|
804
|
+
selection.addRange(range)
|
|
805
|
+
|
|
806
|
+
// 触发编辑器内容变化
|
|
807
|
+
// 尝试多种方式触发更新
|
|
808
|
+
if (editor.txt) {
|
|
809
|
+
// 方式1: 触发 change 事件
|
|
810
|
+
if (
|
|
811
|
+
editor.txt.eventHooks &&
|
|
812
|
+
editor.txt.eventHooks.changeEvents
|
|
813
|
+
) {
|
|
814
|
+
editor.txt.eventHooks.changeEvents.forEach((fn) => {
|
|
815
|
+
if (typeof fn === 'function') {
|
|
816
|
+
try {
|
|
817
|
+
fn()
|
|
818
|
+
} catch (e) {
|
|
819
|
+
console.debug('触发编辑器变化事件失败:', e)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 方式2: 手动触发 input 事件
|
|
826
|
+
const inputEvent = new Event('input', {
|
|
827
|
+
bubbles: true,
|
|
828
|
+
cancelable: true
|
|
829
|
+
})
|
|
830
|
+
container.dispatchEvent(inputEvent)
|
|
831
|
+
|
|
832
|
+
// 方式3: 触发 change 事件
|
|
833
|
+
const changeEvent = new Event('change', {
|
|
834
|
+
bubbles: true,
|
|
835
|
+
cancelable: true
|
|
836
|
+
})
|
|
837
|
+
container.dispatchEvent(changeEvent)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
console.error('插入链接失败:', error)
|
|
842
|
+
// 如果出错,回退到普通粘贴
|
|
843
|
+
setTimeout(() => {
|
|
844
|
+
const textNode = document.createTextNode(pasteText)
|
|
845
|
+
const selection = window.getSelection()
|
|
846
|
+
if (selection && selection.rangeCount > 0) {
|
|
847
|
+
const range = selection.getRangeAt(0)
|
|
848
|
+
range.deleteContents()
|
|
849
|
+
range.insertNode(textNode)
|
|
850
|
+
range.setStartAfter(textNode)
|
|
851
|
+
range.collapse(true)
|
|
852
|
+
selection.removeAllRanges()
|
|
853
|
+
selection.addRange(range)
|
|
854
|
+
}
|
|
855
|
+
}, 0)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return false
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// 在捕获阶段监听粘贴事件
|
|
863
|
+
container.addEventListener('paste', handlePaste, true)
|
|
864
|
+
|
|
865
|
+
// 保存处理器引用,以便在组件销毁时移除
|
|
866
|
+
this._pasteHandler = handlePaste
|
|
867
|
+
this._pasteHandlerContainer = container
|
|
868
|
+
},
|
|
869
|
+
// 设置只读模式:允许选择和点击链接,但禁止编辑
|
|
870
|
+
setReadOnlyMode(readOnly) {
|
|
871
|
+
if (!this._editor) return
|
|
872
|
+
|
|
873
|
+
// 使用 wangEditor 的 disable/enable 方法
|
|
874
|
+
if (readOnly) {
|
|
875
|
+
// 禁用编辑器
|
|
876
|
+
if (
|
|
877
|
+
this._editor.disable &&
|
|
878
|
+
typeof this._editor.disable === 'function'
|
|
879
|
+
) {
|
|
880
|
+
this._editor.disable()
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
// 启用编辑器
|
|
884
|
+
if (this._editor.enable && typeof this._editor.enable === 'function') {
|
|
885
|
+
this._editor.enable()
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
this.$nextTick(() => {
|
|
890
|
+
// 查找编辑器的根元素
|
|
891
|
+
let editorRootElement = null
|
|
892
|
+
if (this._editor.id) {
|
|
893
|
+
editorRootElement = document.getElementById(this._editor.id)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 查找编辑器的文本容器
|
|
897
|
+
let textContainer = null
|
|
898
|
+
let containerElement = null
|
|
899
|
+
|
|
900
|
+
if (this._editor.$textElem && this._editor.$textElem[0]) {
|
|
901
|
+
textContainer = this._editor.$textElem[0]
|
|
902
|
+
} else {
|
|
903
|
+
const editorId = this._editor.id
|
|
904
|
+
if (editorId) {
|
|
905
|
+
const editorEl = document.getElementById(editorId)
|
|
906
|
+
if (editorEl) {
|
|
907
|
+
textContainer = editorEl.querySelector('.w-e-text')
|
|
908
|
+
containerElement = editorEl.querySelector('.w-e-text-container')
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// 也尝试查找容器
|
|
914
|
+
if (!textContainer) {
|
|
915
|
+
if (
|
|
916
|
+
this._editor.$textContainerElem &&
|
|
917
|
+
this._editor.$textContainerElem[0]
|
|
918
|
+
) {
|
|
919
|
+
containerElement = this._editor.$textContainerElem[0]
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// 查找容器元素(如果还没找到)
|
|
924
|
+
if (
|
|
925
|
+
!containerElement &&
|
|
926
|
+
this._editor.$textContainerElem &&
|
|
927
|
+
this._editor.$textContainerElem[0]
|
|
928
|
+
) {
|
|
929
|
+
containerElement = this._editor.$textContainerElem[0]
|
|
930
|
+
} else if (!containerElement && this._editor.id) {
|
|
931
|
+
const editorEl = document.getElementById(this._editor.id)
|
|
932
|
+
if (editorEl) {
|
|
933
|
+
containerElement = editorEl.querySelector('.w-e-text-container')
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (textContainer) {
|
|
938
|
+
if (readOnly) {
|
|
939
|
+
// 设置根元素为自适应高度(移除内联样式中的固定高度)
|
|
940
|
+
if (editorRootElement) {
|
|
941
|
+
editorRootElement.style.height = 'auto'
|
|
942
|
+
editorRootElement.style.minHeight = 'auto'
|
|
943
|
+
editorRootElement.style.maxHeight = 'none'
|
|
944
|
+
}
|
|
945
|
+
// 设置容器和文本区域为自适应高度
|
|
946
|
+
if (containerElement) {
|
|
947
|
+
containerElement.style.height = 'auto'
|
|
948
|
+
containerElement.style.minHeight = 'auto'
|
|
949
|
+
containerElement.style.maxHeight = 'none'
|
|
950
|
+
}
|
|
951
|
+
if (textContainer) {
|
|
952
|
+
textContainer.style.height = 'auto'
|
|
953
|
+
textContainer.style.minHeight = 'auto'
|
|
954
|
+
textContainer.style.maxHeight = 'none'
|
|
955
|
+
}
|
|
956
|
+
// 强制设置为只读:禁用编辑,但允许选择和点击链接
|
|
957
|
+
const forceReadOnly = () => {
|
|
958
|
+
// 强制设置 contenteditable
|
|
959
|
+
textContainer.setAttribute('contenteditable', 'false')
|
|
960
|
+
// 确保文本可以选择
|
|
961
|
+
textContainer.style.userSelect = 'text'
|
|
962
|
+
textContainer.style.webkitUserSelect = 'text'
|
|
963
|
+
textContainer.style.mozUserSelect = 'text'
|
|
964
|
+
textContainer.style.msUserSelect = 'text'
|
|
965
|
+
textContainer.style.cursor = 'text'
|
|
966
|
+
// 保持根元素自适应高度
|
|
967
|
+
if (editorRootElement) {
|
|
968
|
+
editorRootElement.style.height = 'auto'
|
|
969
|
+
editorRootElement.style.minHeight = 'auto'
|
|
970
|
+
editorRootElement.style.maxHeight = 'none'
|
|
971
|
+
}
|
|
972
|
+
// 保持容器和文本区域自适应高度
|
|
973
|
+
if (containerElement) {
|
|
974
|
+
containerElement.style.height = 'auto'
|
|
975
|
+
containerElement.style.minHeight = 'auto'
|
|
976
|
+
containerElement.style.maxHeight = 'none'
|
|
977
|
+
}
|
|
978
|
+
textContainer.style.height = 'auto'
|
|
979
|
+
textContainer.style.minHeight = 'auto'
|
|
980
|
+
textContainer.style.maxHeight = 'none'
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
forceReadOnly()
|
|
984
|
+
|
|
985
|
+
// 监听 contenteditable 属性的变化,强制保持只读
|
|
986
|
+
const attributeObserver = new MutationObserver(() => {
|
|
987
|
+
if (textContainer.getAttribute('contenteditable') !== 'false') {
|
|
988
|
+
forceReadOnly()
|
|
989
|
+
}
|
|
990
|
+
})
|
|
991
|
+
attributeObserver.observe(textContainer, {
|
|
992
|
+
attributes: true,
|
|
993
|
+
attributeFilter: ['contenteditable']
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
// 阻止所有编辑操作的事件处理器
|
|
997
|
+
const preventAllEdit = (e) => {
|
|
998
|
+
// 阻止所有输入操作
|
|
999
|
+
e.preventDefault()
|
|
1000
|
+
e.stopPropagation()
|
|
1001
|
+
e.stopImmediatePropagation()
|
|
1002
|
+
return false
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 阻止键盘输入(但允许选择快捷键如 Ctrl+A, Ctrl+C)
|
|
1006
|
+
const preventEdit = (e) => {
|
|
1007
|
+
// 允许复制、全选等快捷键
|
|
1008
|
+
if (
|
|
1009
|
+
(e.ctrlKey || e.metaKey) &&
|
|
1010
|
+
(e.key === 'a' || e.key === 'c' || e.key === 'x')
|
|
1011
|
+
) {
|
|
1012
|
+
return true
|
|
1013
|
+
}
|
|
1014
|
+
// 阻止所有其他键盘输入
|
|
1015
|
+
if (e.key && e.key.length === 1) {
|
|
1016
|
+
e.preventDefault()
|
|
1017
|
+
e.stopPropagation()
|
|
1018
|
+
return false
|
|
1019
|
+
}
|
|
1020
|
+
// 阻止删除键(当没有选中内容时)
|
|
1021
|
+
if (
|
|
1022
|
+
(e.key === 'Delete' || e.key === 'Backspace') &&
|
|
1023
|
+
!e.ctrlKey &&
|
|
1024
|
+
!e.metaKey
|
|
1025
|
+
) {
|
|
1026
|
+
const selection = window.getSelection()
|
|
1027
|
+
if (
|
|
1028
|
+
!selection ||
|
|
1029
|
+
selection.rangeCount === 0 ||
|
|
1030
|
+
selection.isCollapsed
|
|
1031
|
+
) {
|
|
1032
|
+
e.preventDefault()
|
|
1033
|
+
e.stopPropagation()
|
|
1034
|
+
return false
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 添加事件监听器(使用捕获阶段,优先级最高)
|
|
1040
|
+
textContainer.addEventListener('keydown', preventEdit, true)
|
|
1041
|
+
textContainer.addEventListener('keypress', preventAllEdit, true)
|
|
1042
|
+
textContainer.addEventListener('paste', preventAllEdit, true)
|
|
1043
|
+
textContainer.addEventListener('drop', preventAllEdit, true)
|
|
1044
|
+
textContainer.addEventListener('input', preventAllEdit, true)
|
|
1045
|
+
textContainer.addEventListener('beforeinput', preventAllEdit, true)
|
|
1046
|
+
textContainer.addEventListener(
|
|
1047
|
+
'compositionstart',
|
|
1048
|
+
preventAllEdit,
|
|
1049
|
+
true
|
|
1050
|
+
)
|
|
1051
|
+
textContainer.addEventListener(
|
|
1052
|
+
'compositionupdate',
|
|
1053
|
+
preventAllEdit,
|
|
1054
|
+
true
|
|
1055
|
+
)
|
|
1056
|
+
textContainer.addEventListener(
|
|
1057
|
+
'compositionend',
|
|
1058
|
+
preventAllEdit,
|
|
1059
|
+
true
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
// 保存事件处理器引用,以便清理
|
|
1063
|
+
if (!textContainer._readOnlyHandlers) {
|
|
1064
|
+
textContainer._readOnlyHandlers = {}
|
|
1065
|
+
}
|
|
1066
|
+
textContainer._readOnlyHandlers.preventEdit = preventEdit
|
|
1067
|
+
textContainer._readOnlyHandlers.preventAllEdit = preventAllEdit
|
|
1068
|
+
textContainer._readOnlyAttributeObserver = attributeObserver
|
|
1069
|
+
|
|
1070
|
+
// 允许链接点击
|
|
1071
|
+
const enableLinks = () => {
|
|
1072
|
+
const links = textContainer.querySelectorAll('a')
|
|
1073
|
+
links.forEach((link) => {
|
|
1074
|
+
link.style.pointerEvents = 'auto'
|
|
1075
|
+
link.style.cursor = 'pointer'
|
|
1076
|
+
})
|
|
1077
|
+
}
|
|
1078
|
+
enableLinks()
|
|
1079
|
+
|
|
1080
|
+
// 监听新添加的链接
|
|
1081
|
+
const linkObserver = new MutationObserver(() => {
|
|
1082
|
+
enableLinks()
|
|
1083
|
+
// 确保 contenteditable 保持为 false
|
|
1084
|
+
forceReadOnly()
|
|
1085
|
+
})
|
|
1086
|
+
linkObserver.observe(textContainer, {
|
|
1087
|
+
childList: true,
|
|
1088
|
+
subtree: true,
|
|
1089
|
+
attributes: true,
|
|
1090
|
+
attributeFilter: ['contenteditable']
|
|
1091
|
+
})
|
|
1092
|
+
textContainer._readOnlyLinkObserver = linkObserver
|
|
1093
|
+
|
|
1094
|
+
// 定期检查并强制设置(防止被覆盖)
|
|
1095
|
+
if (!textContainer._readOnlyInterval) {
|
|
1096
|
+
textContainer._readOnlyInterval = setInterval(() => {
|
|
1097
|
+
if (textContainer.getAttribute('contenteditable') !== 'false') {
|
|
1098
|
+
forceReadOnly()
|
|
1099
|
+
}
|
|
1100
|
+
}, 100)
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
// 取消只读:恢复编辑功能
|
|
1104
|
+
textContainer.setAttribute('contenteditable', 'true')
|
|
1105
|
+
|
|
1106
|
+
// 清除定时器
|
|
1107
|
+
if (textContainer._readOnlyInterval) {
|
|
1108
|
+
clearInterval(textContainer._readOnlyInterval)
|
|
1109
|
+
delete textContainer._readOnlyInterval
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// 移除事件监听器
|
|
1113
|
+
if (textContainer._readOnlyHandlers) {
|
|
1114
|
+
const handlers = textContainer._readOnlyHandlers
|
|
1115
|
+
textContainer.removeEventListener(
|
|
1116
|
+
'keydown',
|
|
1117
|
+
handlers.preventEdit,
|
|
1118
|
+
true
|
|
1119
|
+
)
|
|
1120
|
+
textContainer.removeEventListener(
|
|
1121
|
+
'keypress',
|
|
1122
|
+
handlers.preventAllEdit,
|
|
1123
|
+
true
|
|
1124
|
+
)
|
|
1125
|
+
textContainer.removeEventListener(
|
|
1126
|
+
'paste',
|
|
1127
|
+
handlers.preventAllEdit,
|
|
1128
|
+
true
|
|
1129
|
+
)
|
|
1130
|
+
textContainer.removeEventListener(
|
|
1131
|
+
'drop',
|
|
1132
|
+
handlers.preventAllEdit,
|
|
1133
|
+
true
|
|
1134
|
+
)
|
|
1135
|
+
textContainer.removeEventListener(
|
|
1136
|
+
'input',
|
|
1137
|
+
handlers.preventAllEdit,
|
|
1138
|
+
true
|
|
1139
|
+
)
|
|
1140
|
+
textContainer.removeEventListener(
|
|
1141
|
+
'beforeinput',
|
|
1142
|
+
handlers.preventAllEdit,
|
|
1143
|
+
true
|
|
1144
|
+
)
|
|
1145
|
+
textContainer.removeEventListener(
|
|
1146
|
+
'compositionstart',
|
|
1147
|
+
handlers.preventAllEdit,
|
|
1148
|
+
true
|
|
1149
|
+
)
|
|
1150
|
+
textContainer.removeEventListener(
|
|
1151
|
+
'compositionupdate',
|
|
1152
|
+
handlers.preventAllEdit,
|
|
1153
|
+
true
|
|
1154
|
+
)
|
|
1155
|
+
textContainer.removeEventListener(
|
|
1156
|
+
'compositionend',
|
|
1157
|
+
handlers.preventAllEdit,
|
|
1158
|
+
true
|
|
1159
|
+
)
|
|
1160
|
+
delete textContainer._readOnlyHandlers
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// 停止观察
|
|
1164
|
+
if (textContainer._readOnlyAttributeObserver) {
|
|
1165
|
+
textContainer._readOnlyAttributeObserver.disconnect()
|
|
1166
|
+
delete textContainer._readOnlyAttributeObserver
|
|
1167
|
+
}
|
|
1168
|
+
if (textContainer._readOnlyLinkObserver) {
|
|
1169
|
+
textContainer._readOnlyLinkObserver.disconnect()
|
|
1170
|
+
delete textContainer._readOnlyLinkObserver
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
})
|
|
1175
|
+
},
|
|
1176
|
+
// 验证是否为有效的URL
|
|
1177
|
+
isValidUrl(str) {
|
|
1178
|
+
if (!str || typeof str !== 'string') return false
|
|
1179
|
+
|
|
1180
|
+
const trimmed = str.trim()
|
|
1181
|
+
if (!trimmed) return false
|
|
1182
|
+
|
|
1183
|
+
try {
|
|
1184
|
+
// 检查是否包含常见协议
|
|
1185
|
+
const urlPattern = /^(https?|ftp|file):\/\//i
|
|
1186
|
+
if (urlPattern.test(trimmed)) {
|
|
1187
|
+
new URL(trimmed)
|
|
1188
|
+
return true
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// 检查是否看起来像URL(包含域名)
|
|
1192
|
+
// 匹配形如 www.example.com 或 example.com 的格式
|
|
1193
|
+
const domainPattern =
|
|
1194
|
+
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/i
|
|
1195
|
+
// 或者简单的 http/https URL 格式
|
|
1196
|
+
const simpleUrlPattern = /^[a-zA-Z0-9][a-zA-Z0-9\-]*\.[a-zA-Z]{2,}/
|
|
1197
|
+
|
|
1198
|
+
if (domainPattern.test(trimmed) || simpleUrlPattern.test(trimmed)) {
|
|
1199
|
+
// 尝试添加 http:// 前缀来验证
|
|
1200
|
+
try {
|
|
1201
|
+
new URL(`http://${trimmed}`)
|
|
1202
|
+
return true
|
|
1203
|
+
} catch (e) {
|
|
1204
|
+
return false
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return false
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
return false
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
handleDefaultResponse(res, insertImgFn) {
|
|
1214
|
+
// 处理标准响应格式: {errno: 0, data: {url: "...", alt: "...", href: "..."}}
|
|
1215
|
+
if (res.code === 200 && res.data) {
|
|
1216
|
+
insertImgFn(
|
|
1217
|
+
res.data.realURL,
|
|
1218
|
+
res.data.alt || '图片',
|
|
1219
|
+
res.data.realURL || ''
|
|
1220
|
+
)
|
|
1221
|
+
} else if (res.url) {
|
|
1222
|
+
// 兼容简单格式: {url: "..."}
|
|
1223
|
+
insertImgFn(res.url, res.alt || '', res.href || '')
|
|
1224
|
+
} else {
|
|
1225
|
+
console.error('上传失败,响应格式不正确', res)
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
})
|
|
1230
|
+
</script>
|
|
1231
|
+
|
|
1232
|
+
<style scoped>
|
|
1233
|
+
.fc-editor-wrapper {
|
|
1234
|
+
width: 100%;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.fc-editor-readonly {
|
|
1238
|
+
padding: 0 10px;
|
|
1239
|
+
border-radius: 4px;
|
|
1240
|
+
/* 覆盖预览模式的全局 pointer-events: none 规则 */
|
|
1241
|
+
pointer-events: auto !important;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.fc-editor-readonly img {
|
|
1245
|
+
cursor: pointer;
|
|
1246
|
+
max-width: 100%;
|
|
1247
|
+
height: auto;
|
|
1248
|
+
/* 确保图片可以点击,覆盖预览模式的全局规则 */
|
|
1249
|
+
pointer-events: auto !important;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.fc-editor-readonly a {
|
|
1253
|
+
/* 确保链接可以点击,覆盖预览模式的全局规则 */
|
|
1254
|
+
pointer-events: auto !important;
|
|
1255
|
+
cursor: pointer;
|
|
1256
|
+
}
|
|
1257
|
+
</style>
|