@longhongguo/form-create-ant-design-vue 3.3.34 → 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.
@@ -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
- <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>
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>