@sy-common/wang-editor 1.0.0-beta.21 → 1.0.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sy-common/wang-editor",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "author": "lambo",
@@ -0,0 +1,231 @@
1
+ import { Transforms, Element } from 'slate'
2
+ import { h } from 'snabbdom'
3
+
4
+ class CustomHrMenu {
5
+ constructor() {
6
+ this.title = '彩色分割线'
7
+ this.tag = 'button'
8
+ this.showDropPanel = true // ✅ 关键:必须设置为 true 才能弹出面板
9
+ this.iconSvg = '<svg viewBox="0 0 1092 1024"><path d="M0 51.2m51.2 0l989.866667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-989.866667 0q-51.2 0-51.2-51.2l0 0q0 51.2 51.2-51.2Z" fill="#999"/><path d="M0 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0 51.2 51.2-51.2Z" fill="#ff4d4f"/><path d="M273 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0 51.2 51.2-51.2Z" fill="#faad14"/><path d="M546 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0 51.2 51.2-51.2Z" fill="#52c41a"/><path d="M819 460.8m51.2 0l170.666667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-170.666667 0q-51.2 0-51.2-51.2l0 0q0 51.2 51.2-51.2Z" fill="#1890ff"/><path d="M0 870.4m51.2 0l989.866667 0q51.2 0 51.2 51.2l0 0q0 51.2-51.2 51.2l-989.866667 0q-51.2 0-51.2-51.2l0 0q0 51.2 51.2-51.2Z" fill="#999"/></svg>' // 可选,设置菜单图标
10
+ }
11
+
12
+ // 判断菜单是否禁用
13
+ isDisabled(editor) {
14
+ return false
15
+ }
16
+
17
+ // 判断菜单是否激活
18
+ isActive(editor) {
19
+ return false
20
+ }
21
+
22
+ // 获取当前值
23
+ getValue(editor) {
24
+ return ''
25
+ }
26
+
27
+ // 点击菜单时执行(当没有下拉面板时才会调用此方法)
28
+ exec(editor, value) {
29
+ if (value) {
30
+ this.insertColoredHr(editor, value)
31
+ }
32
+ }
33
+
34
+ // ✅ 关键:创建下拉面板内容(颜色选择器)
35
+ getPanelContentElem(editor) {
36
+ const panel = document.createElement('div')
37
+ panel.style.cssText = `
38
+ display: grid;
39
+ grid-template-columns: repeat(5, 1fr);
40
+ gap: 8px;
41
+ padding: 12px;
42
+ background: white;
43
+ border-radius: 4px;
44
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
45
+ `
46
+
47
+ // 预设颜色列表
48
+ const colors = [
49
+ '#000000', '#333333', '#666666', '#999999', '#cccccc',
50
+ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff',
51
+ '#ff6600', '#00ccff', '#ff0066', '#9933ff', '#33ff99'
52
+ ]
53
+
54
+ colors.forEach(color => {
55
+ const btn = document.createElement('button')
56
+ btn.setAttribute('data-color', color)
57
+ btn.style.cssText = `
58
+ width: 32px;
59
+ height: 32px;
60
+ background-color: ${color};
61
+ border: 1px solid #ddd;
62
+ border-radius: 4px;
63
+ cursor: pointer;
64
+ transition: transform 0.2s;
65
+ `
66
+ btn.addEventListener('mouseenter', () => {
67
+ btn.style.transform = 'scale(1.1)'
68
+ })
69
+ btn.addEventListener('mouseleave', () => {
70
+ btn.style.transform = 'scale(1)'
71
+ })
72
+ btn.addEventListener('click', () => {
73
+ // 点击颜色时插入彩色分割线
74
+ this.insertColoredHr(editor, color)
75
+ // 关闭下拉面板
76
+ editor.hidePanelOrModal()
77
+ })
78
+ panel.appendChild(btn)
79
+ })
80
+
81
+ return panel
82
+ }
83
+
84
+ // 插入彩色分割线的公共方法
85
+ insertColoredHr(editor, color) {
86
+ // 插入自定义节点
87
+ editor.insertNode({
88
+ type: 'colored-hr',
89
+ color: color,
90
+ children: [{ text: '' }]
91
+ })
92
+
93
+ // 插入换行段落,方便继续编辑
94
+ editor.insertNode({
95
+ type: 'paragraph',
96
+ children: [{ text: '' }]
97
+ })
98
+ }
99
+ }
100
+
101
+ const renderColoredHr = (elem, children, editor) => {
102
+ const color = elem.color || '#cccccc'
103
+
104
+ // 使用 snabbdom 的 h() 函数创建 VNode
105
+ return h('hr', {
106
+ attrs: {
107
+ 'data-color': color,
108
+ 'data-w-e-type': 'colored-hr'
109
+ },
110
+ style: {
111
+ border: 'none',
112
+ borderTop: `2px solid ${color}`,
113
+ margin: '16px 0'
114
+ }
115
+ })
116
+ }
117
+
118
+ // 2. HTML 转换函数:保存时把自定义节点转成 HTML
119
+ const hrToHtml = (elem, childrenHtml) => {
120
+ // 调试:打印 elem 看看是什么
121
+
122
+ const color = elem.color || '#cccccc'
123
+ const html = `<hr style="border: none; border-top: 2px solid ${color}; margin: 16px 0;" data-color="${color}" data-w-e-type="colored-hr">`
124
+ return html
125
+ }
126
+
127
+ // 3. HTML 解析函数:加载时把 HTML 转回自定义节点
128
+ const parseHrHtml = (domElem, children, editor) => {
129
+ console.log('parseHrHtml received domElem:', domElem)
130
+ const color = domElem.getAttribute('data-color') || '#cccccc'
131
+ console.log('parseHrHtml color:', color)
132
+ return {
133
+ type: 'colored-hr',
134
+ color: color,
135
+ children: [{ text: '' }]
136
+ }
137
+ }
138
+
139
+ function withColoredHr(editor) {
140
+ const { isVoid, normalizeNode } = editor
141
+
142
+ // 标记 colored-hr 为 void 元素(不能输入文字)
143
+ editor.isVoid = (elem) => {
144
+ if (Element.isElement(elem) && elem.type === 'colored-hr') {
145
+ return true
146
+ }
147
+ return isVoid(elem)
148
+ }
149
+
150
+ // 可选:规范化节点,确保结构正确
151
+ editor.normalizeNode = ([node, path]) => {
152
+ if (Element.isElement(node) && node.type === 'colored-hr') {
153
+ // 确保 colored-hr 有 color 属性
154
+ if (node.color == null) {
155
+ Transforms.setNodes(editor, { color: '#cccccc' }, { at: path })
156
+ return
157
+ }
158
+ }
159
+ normalizeNode([node, path])
160
+ }
161
+
162
+ return editor
163
+ }
164
+
165
+ // 4. 组装模块并注册
166
+ const coloredHrModule = {
167
+ elemsToHtml: [{
168
+ type: 'colored-hr',
169
+ elemToHtml: hrToHtml
170
+ }],
171
+ renderElems: [{
172
+ type: 'colored-hr',
173
+ renderElem: renderColoredHr
174
+ }],
175
+ parseElemsHtml: [{
176
+ selector: 'hr[data-w-e-type="colored-hr"]',
177
+ parseElemHtml: parseHrHtml
178
+ }],
179
+
180
+ editorPlugin: withColoredHr,
181
+
182
+ menus: [{
183
+ key: 'customHrMenu',
184
+ factory() {
185
+ return new CustomHrMenu()
186
+ }
187
+ }]
188
+ }
189
+
190
+
191
+
192
+ export function registerMenu(Boot) {
193
+ try {
194
+ // 1. 注册菜单(你确认这个可以)
195
+ Boot.registerMenu({
196
+ key: 'customHrMenu',
197
+ factory: () => new CustomHrMenu()
198
+ })
199
+ console.log('✅ 菜单注册成功')
200
+
201
+ // 2. 注册渲染
202
+ Boot.registerRenderElem({
203
+ type: 'colored-hr',
204
+ renderElem: renderColoredHr
205
+ })
206
+ console.log('✅ 渲染注册成功')
207
+
208
+ // 3. 注册 HTML 导出
209
+ Boot.registerElemToHtml({
210
+ type: 'colored-hr',
211
+ elemToHtml: hrToHtml
212
+ })
213
+ console.log('✅ HTML导出注册成功')
214
+
215
+ // 4. 注册 HTML 解析
216
+ Boot.registerParseElemHtml({
217
+ selector: 'hr[data-color]',
218
+ parseElemHtml: parseHrHtml
219
+ })
220
+ console.log('✅ HTML解析注册成功')
221
+
222
+ // 5. 注册插件
223
+ Boot.registerPlugin(withColoredHr)
224
+ console.log('✅ 插件注册成功')
225
+ } catch (e) {
226
+ if (!e.message.includes('already') && !e.message.includes('duplicate')) {
227
+ console.warn('注册失败:', e)
228
+ throw e
229
+ }
230
+ }
231
+ }
package/src/index.vue CHANGED
@@ -1,466 +1,317 @@
1
- <template>
2
- <div style="z-index: 999">
3
- <!-- 工具栏 -->
4
- <Toolbar
5
- :editor="editor"
6
- :defaultConfig="toolbarConfig"
7
- />
8
- <!-- 编辑器 -->
9
- <Editor
10
- :style="editorStyle"
11
- :defaultConfig="editorConfig"
12
- v-model="html"
13
- @onChange="onChange"
14
- @onCreated="onCreated"
15
- />
16
- </div>
17
- </template>
18
-
19
- <script>
20
- import '@wangeditor-next/editor/dist/css/style.css'
21
- import {oneOf} from '@lambo-design/shared/utils/platform';
22
- import {Editor, Toolbar} from '@wangeditor-next/editor-for-vue2'
23
- import {Boot} from '@wangeditor-next/editor'
24
- import {registerHrColorMenu, MENU_KEY, extractHrColorsFromHtml} from './hr-color-menu'
25
-
26
- // 注册分割线颜色菜单(模块级别,只执行一次)
27
- registerHrColorMenu(Boot)
28
-
29
- export default {
30
- name: 'WangEditor',
31
- components: {Editor, Toolbar},
32
- props: {
33
- value: {
34
- type: String,
35
- default: ''
36
- },
37
- /**
38
- * 绑定的值的类型, enum: ['html', 'text']
39
- */
40
- valueType: {
41
- type: String,
42
- default: 'html',
43
- validator: (val) => {
44
- return oneOf(val, ['html', 'text'])
45
- }
46
- },
47
- /**
48
- * @description 设置change事件触发时间间隔
49
- */
50
- changeInterval: {
51
- type: Number,
52
- default: 200
53
- },
54
- /**
55
- * @description 是否开启本地存储
56
- */
57
- cache: {
58
- type: Boolean,
59
- default: true
60
- },
61
- /**
62
- * @description 是否使用 base64 保存图片
63
- */
64
- uploadImgShowBase64: {
65
- type: Boolean,
66
- default: false
67
- },
68
- /**
69
- * 上传图片上下文
70
- */
71
- ossServerContext: {
72
- type: String,
73
- default: ''
74
- },
75
- /**
76
- * 上传图片Url地址
77
- */
78
- ossFilePutUrl: {
79
- type: String,
80
- default: "/oss/file/put"
81
- },
82
- /**
83
- * 获取图片url地址
84
- */
85
- ossFileGetImgUrl: {
86
- type: String,
87
- default: "/oss/file/get/"
88
- },
89
- /**
90
- * @description 是否保留粘贴内容的样式
91
- */
92
- preservePasteStyle: {
93
- type: Boolean,
94
- default: false
95
- },
96
-
97
- readOnly: {
98
- type: Boolean,
99
- default: false
100
- },
101
- /**
102
- * @description: 隐藏菜单
103
- */
104
- hideMenu: {
105
- type: Array,
106
- default: () => []
107
- },
108
-
109
- height: {
110
- type: [Number, String],
111
- default: 400
112
- }
113
- },
114
- data() {
115
- return {
116
- editor: null,
117
- html: '',
118
- hrCount: 0, // 追踪分割线数量
119
- toolbarConfig: {
120
- insertKeys: {
121
- index: 24, // 在分割线菜单后面插入
122
- keys: [MENU_KEY]
123
- }
124
- },
125
- editorConfig: {
126
- placeholder: '请输入内容...',
127
- MENU_CONF: {
128
- fontFamily: {
129
- fontFamilyList: [
130
- "黑体",
131
- {
132
- name: "仿宋",
133
- value: "仿宋"
134
- },
135
- "仿宋_GB2312",
136
- "楷体",
137
- "楷体_GB2312",
138
- "标楷体",
139
- "华文仿宋",
140
- "华文楷体",
141
- {
142
- name: "宋体",
143
- value: "宋体"
144
- }, "隶书", "方正小标宋简体", "思源宋体‌", "微软雅黑", "Arial", "Tahoma", "Verdana", "Times New Roman", "Courier New"]
145
- },
146
- color: {
147
- colors: [
148
- "rgb(0, 0, 0)",
149
- "rgb(38, 38, 38)",
150
- "rgb(89, 89, 89)",
151
- "rgb(140, 140, 140)",
152
- "rgb(191, 191, 191)",
153
- "rgb(217, 217, 217)",
154
- "rgb(233, 233, 233)",
155
- "rgb(245, 245, 245)",
156
- "rgb(250, 250, 250)",
157
- "rgb(255, 255, 255)",
158
- "rgb(225, 60, 57)",
159
- "rgb(231, 95, 51)",
160
- "rgb(235, 144, 58)",
161
- "rgb(245, 219, 77)",
162
- "rgb(114, 192, 64)",
163
- "rgb(89, 191, 192)",
164
- "rgb(66, 144, 247)",
165
- "rgb(54, 88, 226)",
166
- "rgb(106, 57, 201)",
167
- "rgb(216, 68, 147)",
168
- "rgb(251, 233, 230)",
169
- "rgb(252, 237, 225)",
170
- "rgb(252, 239, 212)",
171
- "rgb(252, 251, 207)",
172
- "rgb(231, 246, 213)",
173
- "rgb(218, 244, 240)",
174
- "rgb(217, 237, 250)",
175
- "rgb(224, 232, 250)",
176
- "rgb(237, 225, 248)",
177
- "rgb(246, 226, 234)",
178
- "rgb(255, 163, 158)",
179
- "rgb(255, 187, 150)",
180
- "rgb(255, 213, 145)",
181
- "rgb(255, 251, 143)",
182
- "rgb(183, 235, 143)",
183
- "rgb(135, 232, 222)",
184
- "rgb(145, 213, 255)",
185
- "rgb(173, 198, 255)",
186
- "rgb(211, 173, 247)",
187
- "rgb(255, 173, 210)",
188
- "rgb(255, 77, 79)",
189
- "rgb(255, 122, 69)",
190
- "rgb(255, 169, 64)",
191
- "rgb(255, 236, 61)",
192
- "rgb(115, 209, 61)",
193
- "rgb(54, 207, 201)",
194
- "rgb(64, 169, 255)",
195
- "rgb(89, 126, 247)",
196
- "rgb(146, 84, 222)",
197
- "rgb(247, 89, 171)",
198
- "rgb(207, 19, 34)",
199
- "rgb(212, 56, 13)",
200
- "rgb(212, 107, 8)",
201
- "rgb(212, 177, 6)",
202
- "rgb(56, 158, 13)",
203
- "rgb(8, 151, 156)",
204
- "rgb(9, 109, 217)",
205
- "rgb(29, 57, 196)",
206
- "rgb(83, 29, 171)",
207
- "rgb(196, 29, 127)",
208
- "rgb(130, 0, 20)",
209
- "rgb(135, 20, 0)",
210
- "rgb(135, 56, 0)",
211
- "rgb(97, 71, 0)",
212
- "rgb(19, 82, 0)",
213
- "rgb(0, 71, 79)",
214
- "rgb(0, 58, 140)",
215
- "rgb(6, 17, 120)",
216
- "rgb(34, 7, 94)",
217
- "rgb(120, 6, 80)",
218
- "rgb(250,64,6)"
219
- ]
220
- },
221
- uploadImage: {
222
- //上传图片配置
223
- server: this.ossServerContext + this.ossFilePutUrl, //上传接口地址
224
- fieldName: 'file', //上传文件名
225
- methods: 'post',
226
- metaWithUrl: true, // 参数拼接到 url 上
227
- //自定义插入图片
228
- customInsert: (res, insertFn) => {
229
- let self = this;
230
- const imgInfo = res.data[0] || {}
231
- const {fileId, fileName, contentType} = imgInfo
232
- insertFn(self.ossServerContext + self.ossFileGetImgUrl + fileId, fileName, self.ossServerContext + self.ossFileGetImgUrl + fileId)
233
- },
234
-
235
- },
236
- }
237
- }
238
- }
239
- },
240
- watch: {
241
- value: {
242
- handler: function (newVal, oldVal) {
243
- // 初次赋值或从空内容变为有内容时处理
244
- const isEmpty = !newVal || newVal === '<p><br></p>'
245
- const wasEmpty = !oldVal || oldVal === '<p><br></p>'
246
-
247
- if (!isEmpty && wasEmpty) {
248
- if (this.valueType === 'html') {
249
- let html = newVal;
250
-
251
- // 先从 HTML 提取所有 hr 的颜色
252
- const hrColors = extractHrColorsFromHtml(html)
253
-
254
- // 正则匹配 <table>...</table> 区域
255
- const tableRegex = /(<table)([\s\S]*?)(<\/table>)/gi;
256
- // 替换表格区域中的 borderWidth: **px;
257
- html = html.replace(tableRegex, (match, startTag, content, endTag) => {
258
- // 只对表格内容进行替换
259
- const updatedContent = content.replace(/border-width:\s*(\d+)px;/g, 'border-width: $1;');
260
- return `${startTag}${updatedContent}${endTag}`;
261
- });
262
-
263
- // 标记已存表格
264
- const tempDiv = document.createElement('div'); // 创建临时容器解析 HTML
265
- tempDiv.innerHTML = html;
266
- const tables = tempDiv.querySelectorAll('table');
267
- tables.forEach(table => {
268
- table.setAttribute('data-processed', 'true');
269
- });
270
- // 更新处理后的 HTML
271
- this.editor.setHtml(tempDiv.innerHTML)
272
-
273
- // setHtml 后设置 hr 颜色到 DOM
274
- if (hrColors.length > 0) {
275
- // 用 setTimeout 等待 DOM 渲染完成
276
- setTimeout(() => {
277
- try {
278
- const container = this.editor.getEditableContainer()
279
- if (!container) return
280
- const hrs = container.querySelectorAll('hr')
281
- hrs.forEach((hr, index) => {
282
- const color = hrColors[index] || ''
283
- if (color) {
284
- hr.style.border = 'none'
285
- hr.style.borderTop = '2px solid ' + color
286
- hr.style.margin = '10px 0'
287
- hr.setAttribute('data-divider-color', color)
288
- }
289
- })
290
- } catch (e) {
291
- console.warn('设置 hr 颜色失败:', e)
292
- }
293
- }, 100)
294
- }
295
- } else {
296
- this.editor.insertText(newVal)
297
- }
298
- }
299
- }
300
- },
301
-
302
- readOnly: {
303
- handler(newVal){
304
- this.editorConfig.readOnly = newVal
305
- if(newVal){
306
- this.toolbarConfig.toolbarKeys = ['fullScreen']
307
- }
308
- },
309
- immediate: true,
310
- },
311
-
312
- hideMenu: {
313
- handler(newVal){
314
- this.toolbarConfig.excludeKeys = newVal
315
- },
316
- immediate: true,
317
- deep: true
318
- }
319
- },
320
- computed: {
321
- editorStyle() {
322
- let finalHeight = this.height;
323
- // 如果是数字,自动拼接 px 单位
324
- if (typeof finalHeight === 'number') {
325
- finalHeight = `${finalHeight}px`;
326
- }
327
-
328
- return {
329
- height: finalHeight,
330
- 'overflow-y': 'hidden'
331
- }
332
- }
333
- },
334
- methods: {
335
-
336
- onCreated(editor) {
337
- this.editor = Object.seal(editor) // 【注意】一定要用 Object.seal() 否则会报错
338
-
339
- let html = this.value
340
- if (html) {
341
- this.editor.setHtml(html)
342
- // 处理已有颜色的 hr 元素,从 style 中提取颜色
343
- this.$nextTick(() => {
344
- try {
345
- const container = this.editor.getEditableContainer()
346
- if (!container) return
347
- const hrs = container.querySelectorAll('hr')
348
- // 初始化 hrCount,记录已有分割线数量
349
- this.hrCount = hrs.length
350
- hrs.forEach(hr => {
351
- const style = hr.getAttribute('style') || ''
352
- // 匹配 border 中的颜色
353
- const colorMatch = style.match(/border\s*:[^;]*solid\s+(rgb\([^)]+\)|#[a-fA-F0-9]+)/i)
354
- if (colorMatch) {
355
- hr.setAttribute('data-divider-color', colorMatch[1])
356
- }
357
- })
358
- } catch (e) {
359
- console.warn('处理已存在 hr 颜色失败:', e)
360
- }
361
- })
362
- }
363
- },
364
- onChange(editor) {
365
- let self = this;
366
- let html = this.editor.getHtml()
367
-
368
- // 创建一个临时的 div 容器来解析 HTML
369
- const tempDiv = document.createElement('div');
370
- tempDiv.innerHTML = html;
371
-
372
- // 从编辑器 DOM 获取所有 hr 的颜色,写入输出 HTML
373
- const editorContainer = this.editor.getEditableContainer()
374
- const editorHrs = editorContainer ? editorContainer.querySelectorAll('hr') : []
375
- const outputHrs = tempDiv.querySelectorAll('hr')
376
-
377
- // 检测新增的分割线,设置默认颜色
378
- const DEFAULT_HR_COLOR = 'rgb(250, 64, 6)'
379
- if (editorHrs.length > this.hrCount) {
380
- // 有新增的分割线
381
- for (let i = this.hrCount; i < editorHrs.length; i++) {
382
- const hr = editorHrs[i]
383
- hr.style.border = 'none'
384
- hr.style.borderTop = '2px solid ' + DEFAULT_HR_COLOR
385
- hr.style.margin = '10px 0'
386
- hr.setAttribute('data-divider-color', DEFAULT_HR_COLOR)
387
- }
388
- this.hrCount = editorHrs.length
389
- }
390
-
391
- editorHrs.forEach((hr, index) => {
392
- const color = hr.getAttribute('data-divider-color') || ''
393
- if (color && outputHrs[index]) {
394
- outputHrs[index].setAttribute('style', 'border: none; border-top: 2px solid ' + color + '; margin: 10px 0;')
395
- }
396
- })
397
-
398
- // 处理 table 元素 -- 查找新增的表格并添加默认样式
399
- const tables = tempDiv.querySelectorAll('table:not([data-processed])');
400
- tables.forEach(table => {
401
- const currentStyle = table.getAttribute('style') || '';
402
- const newStyle = `border-collapse: collapse;border-style: solid; border-color: rgb(204, 204, 204);border-width: 1;${currentStyle}`.trim();
403
- table.setAttribute('class', 'wangeditor-styled-table');
404
- table.setAttribute('style', newStyle);
405
-
406
- // 处理表头
407
- const thCells = table.querySelectorAll('th');
408
- thCells.forEach(cell => {
409
- const cellCurrentStyle = cell.getAttribute('style') || '';
410
- const cellNewStyle = `background-color: rgb(245, 242, 240);border-style: solid; border-color: rgb(204, 204, 204);border-width: 1;padding: 3px 5px;${cellCurrentStyle}`.trim();
411
- cell.setAttribute('style', cellNewStyle);
412
- });
413
-
414
- // 处理单元格
415
- const tdCells = table.querySelectorAll('td');
416
- tdCells.forEach(cell => {
417
- const cellCurrentStyle = cell.getAttribute('style') || '';
418
- const cellNewStyle = `border-style: solid; border-color: rgb(204, 204, 204);border-width: 1;padding: 3px 5px;${cellCurrentStyle}`.trim();
419
- cell.setAttribute('style', cellNewStyle);
420
- });
421
-
422
- // 标记已处理
423
- table.setAttribute('data-processed', 'true');
424
- });
425
-
426
- // 更新处理后的 HTML
427
- html = tempDiv.innerHTML;
428
- this.$emit('input', self.valueType === 'html' ? html : editor.getText())
429
- this.$emit('on-change', html, editor.getText())
430
- },
431
- customPaste(editor, event, callback) {
432
- //console.log('ClipboardEvent 粘贴事件对象', event)
433
- const html = event.clipboardData.getData('text/html') // 获取粘贴的 html
434
- const text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
435
- const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
436
-
437
- //console.log("customPaste-html",rtf)
438
- // 自定义插入内容
439
- editor.setHtml(html)
440
-
441
- // 返回 false ,阻止默认粘贴行为
442
- event.preventDefault()
443
- callback(false) // 返回值(注意,vue 事件的返回值,不能用 return)
444
-
445
- // 返回 true ,继续默认的粘贴行为
446
- // callback(true)
447
- },
448
- },
449
- mounted() {
450
-
451
- },
452
- beforeDestroy() {
453
- const editor = this.editor
454
- if (editor == null) return
455
- editor.destroy() // 组件销毁时,及时销毁 editor ,重要!!!
456
- },
457
-
458
- }
459
- </script>
460
-
461
- <style lang="less" scoped>
462
- /deep/ .table-container {
463
- width: fit-content !important;
464
- margin: 10px auto !important;
465
- }
466
- </style>
1
+ <template>
2
+ <div style="z-index: 999">
3
+ <!-- 工具栏 -->
4
+ <Toolbar
5
+ :editor="editor"
6
+ :defaultConfig="toolbarConfig"
7
+ />
8
+ <!-- 编辑器 -->
9
+ <Editor
10
+ :style="editorStyle"
11
+ :defaultConfig="editorConfig"
12
+ v-model="html"
13
+ @onChange="onChange"
14
+ @onCreated="onCreated"
15
+ />
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ import '@wangeditor-next/editor/dist/css/style.css'
21
+ import {oneOf} from '@lambo-design/shared/utils/platform';
22
+ import {Editor, Toolbar} from '@wangeditor-next/editor-for-vue2'
23
+ import {Boot} from '@wangeditor-next/editor'
24
+ import {registerMenu} from './customHrMenu'
25
+ // 注册分割线颜色菜单(模块级别,只执行一次)
26
+ registerMenu(Boot)
27
+
28
+ export default {
29
+ name: 'WangEditor',
30
+ components: {Editor, Toolbar},
31
+ props: {
32
+ value: {
33
+ type: String,
34
+ default: ''
35
+ },
36
+ /**
37
+ * 绑定的值的类型, enum: ['html', 'text']
38
+ */
39
+ valueType: {
40
+ type: String,
41
+ default: 'html',
42
+ validator: (val) => {
43
+ return oneOf(val, ['html', 'text'])
44
+ }
45
+ },
46
+ /**
47
+ * @description 设置change事件触发时间间隔
48
+ */
49
+ changeInterval: {
50
+ type: Number,
51
+ default: 200
52
+ },
53
+ /**
54
+ * @description 是否开启本地存储
55
+ */
56
+ cache: {
57
+ type: Boolean,
58
+ default: true
59
+ },
60
+ /**
61
+ * @description 是否使用 base64 保存图片
62
+ */
63
+ uploadImgShowBase64: {
64
+ type: Boolean,
65
+ default: false
66
+ },
67
+ /**
68
+ * 上传图片上下文
69
+ */
70
+ ossServerContext: {
71
+ type: String,
72
+ default: ''
73
+ },
74
+ /**
75
+ * 上传图片Url地址
76
+ */
77
+ ossFilePutUrl: {
78
+ type: String,
79
+ default: "/oss/file/put"
80
+ },
81
+ /**
82
+ * 获取图片url地址
83
+ */
84
+ ossFileGetImgUrl: {
85
+ type: String,
86
+ default: "/oss/file/get/"
87
+ },
88
+ /**
89
+ * @description 是否保留粘贴内容的样式
90
+ */
91
+ preservePasteStyle: {
92
+ type: Boolean,
93
+ default: false
94
+ },
95
+
96
+ readOnly: {
97
+ type: Boolean,
98
+ default: false
99
+ },
100
+ /**
101
+ * @description: 隐藏菜单
102
+ */
103
+ hideMenu: {
104
+ type: Array,
105
+ default: () => []
106
+ },
107
+
108
+ height: {
109
+ type: [Number, String],
110
+ default: 400
111
+ }
112
+ },
113
+ data() {
114
+ return {
115
+ editor: null,
116
+ html: '',
117
+ hrCount: 0, // 追踪分割线数量
118
+ toolbarConfig: {
119
+ insertKeys: {
120
+ index: 24, // 在分割线菜单后面插入
121
+ keys: ['customHrMenu']
122
+ }
123
+ },
124
+ editorConfig: {
125
+ placeholder: '请输入内容...',
126
+ MENU_CONF: {
127
+ fontFamily: {
128
+ fontFamilyList: [
129
+ "黑体",
130
+ {
131
+ name: "仿宋",
132
+ value: "仿宋"
133
+ },
134
+ "仿宋_GB2312",
135
+ "楷体",
136
+ "楷体_GB2312",
137
+ "标楷体",
138
+ "华文仿宋",
139
+ "华文楷体",
140
+ {
141
+ name: "宋体",
142
+ value: "宋体"
143
+ }, "隶书", "方正小标宋简体", "思源宋体‌", "微软雅黑", "Arial", "Tahoma", "Verdana", "Times New Roman", "Courier New"]
144
+ },
145
+ color: {
146
+ colors: [
147
+ "rgb(0, 0, 0)",
148
+ "rgb(38, 38, 38)",
149
+ "rgb(89, 89, 89)",
150
+ "rgb(140, 140, 140)",
151
+ "rgb(191, 191, 191)",
152
+ "rgb(217, 217, 217)",
153
+ "rgb(233, 233, 233)",
154
+ "rgb(245, 245, 245)",
155
+ "rgb(250, 250, 250)",
156
+ "rgb(255, 255, 255)",
157
+ "rgb(225, 60, 57)",
158
+ "rgb(231, 95, 51)",
159
+ "rgb(235, 144, 58)",
160
+ "rgb(245, 219, 77)",
161
+ "rgb(114, 192, 64)",
162
+ "rgb(89, 191, 192)",
163
+ "rgb(66, 144, 247)",
164
+ "rgb(54, 88, 226)",
165
+ "rgb(106, 57, 201)",
166
+ "rgb(216, 68, 147)",
167
+ "rgb(251, 233, 230)",
168
+ "rgb(252, 237, 225)",
169
+ "rgb(252, 239, 212)",
170
+ "rgb(252, 251, 207)",
171
+ "rgb(231, 246, 213)",
172
+ "rgb(218, 244, 240)",
173
+ "rgb(217, 237, 250)",
174
+ "rgb(224, 232, 250)",
175
+ "rgb(237, 225, 248)",
176
+ "rgb(246, 226, 234)",
177
+ "rgb(255, 163, 158)",
178
+ "rgb(255, 187, 150)",
179
+ "rgb(255, 213, 145)",
180
+ "rgb(255, 251, 143)",
181
+ "rgb(183, 235, 143)",
182
+ "rgb(135, 232, 222)",
183
+ "rgb(145, 213, 255)",
184
+ "rgb(173, 198, 255)",
185
+ "rgb(211, 173, 247)",
186
+ "rgb(255, 173, 210)",
187
+ "rgb(255, 77, 79)",
188
+ "rgb(255, 122, 69)",
189
+ "rgb(255, 169, 64)",
190
+ "rgb(255, 236, 61)",
191
+ "rgb(115, 209, 61)",
192
+ "rgb(54, 207, 201)",
193
+ "rgb(64, 169, 255)",
194
+ "rgb(89, 126, 247)",
195
+ "rgb(146, 84, 222)",
196
+ "rgb(247, 89, 171)",
197
+ "rgb(207, 19, 34)",
198
+ "rgb(212, 56, 13)",
199
+ "rgb(212, 107, 8)",
200
+ "rgb(212, 177, 6)",
201
+ "rgb(56, 158, 13)",
202
+ "rgb(8, 151, 156)",
203
+ "rgb(9, 109, 217)",
204
+ "rgb(29, 57, 196)",
205
+ "rgb(83, 29, 171)",
206
+ "rgb(196, 29, 127)",
207
+ "rgb(130, 0, 20)",
208
+ "rgb(135, 20, 0)",
209
+ "rgb(135, 56, 0)",
210
+ "rgb(97, 71, 0)",
211
+ "rgb(19, 82, 0)",
212
+ "rgb(0, 71, 79)",
213
+ "rgb(0, 58, 140)",
214
+ "rgb(6, 17, 120)",
215
+ "rgb(34, 7, 94)",
216
+ "rgb(120, 6, 80)",
217
+ "rgb(250,64,6)"
218
+ ]
219
+ },
220
+ uploadImage: {
221
+ //上传图片配置
222
+ server: this.ossServerContext + this.ossFilePutUrl, //上传接口地址
223
+ fieldName: 'file', //上传文件名
224
+ methods: 'post',
225
+ metaWithUrl: true, // 参数拼接到 url 上
226
+ //自定义插入图片
227
+ customInsert: (res, insertFn) => {
228
+ let self = this;
229
+ const imgInfo = res.data[0] || {}
230
+ const {fileId, fileName, contentType} = imgInfo
231
+ insertFn(self.ossServerContext + self.ossFileGetImgUrl + fileId, fileName, self.ossServerContext + self.ossFileGetImgUrl + fileId)
232
+ },
233
+
234
+ },
235
+ }
236
+ }
237
+ }
238
+ },
239
+ watch: {
240
+ value: {
241
+ handler: function (newVal, oldVal) {
242
+ const isEmpty = !newVal || newVal === '<p><br></p>'
243
+ const wasEmpty = !oldVal || oldVal === '<p><br></p>'
244
+
245
+ if (!isEmpty && wasEmpty) {
246
+ if (this.valueType === 'html') {
247
+ let html = newVal
248
+ this.editor.setHtml(html)
249
+ } else {
250
+ this.editor.insertText(newVal)
251
+ }
252
+ }
253
+ }
254
+ },
255
+
256
+ readOnly: {
257
+ handler(newVal){
258
+ this.editorConfig.readOnly = newVal
259
+ if(newVal){
260
+ this.toolbarConfig.toolbarKeys = ['fullScreen']
261
+ }
262
+ },
263
+ immediate: true,
264
+ },
265
+
266
+ hideMenu: {
267
+ handler(newVal){
268
+ this.toolbarConfig.excludeKeys = newVal
269
+ },
270
+ immediate: true,
271
+ deep: true
272
+ }
273
+ },
274
+ computed: {
275
+ editorStyle() {
276
+ let finalHeight = this.height;
277
+ // 如果是数字,自动拼接 px 单位
278
+ if (typeof finalHeight === 'number') {
279
+ finalHeight = `${finalHeight}px`;
280
+ }
281
+
282
+ return {
283
+ height: finalHeight,
284
+ 'overflow-y': 'hidden'
285
+ }
286
+ }
287
+ },
288
+ methods: {
289
+
290
+ onCreated(editor) {
291
+ this.editor = Object.seal(editor)
292
+ if (this.value) {
293
+ this.editor.setHtml(this.value)
294
+ }
295
+ },
296
+ onChange(editor) {
297
+ let html = editor.getHtml()
298
+ this.$emit('input', this.valueType === 'html' ? html : editor.getText())
299
+ this.$emit('on-change', html, editor.getText())
300
+ },
301
+ },
302
+
303
+ beforeDestroy() {
304
+ const editor = this.editor
305
+ if (editor == null) return
306
+ editor.destroy() // 组件销毁时,及时销毁 editor ,重要!!!
307
+ },
308
+
309
+ }
310
+ </script>
311
+
312
+ <style lang="less" scoped>
313
+ /deep/ .table-container {
314
+ width: fit-content !important;
315
+ margin: 10px auto !important;
316
+ }
317
+ </style>