oxy-uni-ui 1.1.0 → 1.2.3

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.
Files changed (123) hide show
  1. package/attributes.json +1 -1
  2. package/components/common/abstracts/variable.scss +59 -1
  3. package/components/common/path.ts +9 -0
  4. package/components/common/util.ts +42 -0
  5. package/components/composables/index.ts +1 -0
  6. package/components/composables/useGlobalLoading.ts +42 -0
  7. package/components/composables/useGlobalMessage.ts +48 -0
  8. package/components/composables/useGlobalToast.ts +84 -0
  9. package/components/composables/useVirtualScroll.ts +173 -0
  10. package/components/oxy-cell/oxy-cell.vue +15 -2
  11. package/components/oxy-cell/types.ts +4 -0
  12. package/components/oxy-checkbox/index.scss +1 -1
  13. package/components/oxy-checkbox/oxy-checkbox.vue +2 -2
  14. package/components/oxy-col-picker/oxy-col-picker.vue +3 -0
  15. package/components/oxy-col-picker/types.ts +5 -1
  16. package/components/oxy-corner/index.scss +121 -1
  17. package/components/oxy-corner/oxy-corner.vue +18 -5
  18. package/components/oxy-corner/types.ts +24 -3
  19. package/components/oxy-date-strip/index.scss +10 -0
  20. package/components/oxy-date-strip/oxy-date-strip.vue +198 -0
  21. package/components/oxy-date-strip/types.ts +98 -0
  22. package/components/oxy-date-strip/utils.ts +67 -0
  23. package/components/oxy-date-strip-item/index.scss +94 -0
  24. package/components/oxy-date-strip-item/oxy-date-strip-item.vue +102 -0
  25. package/components/oxy-date-strip-item/types.ts +53 -0
  26. package/components/oxy-datetime-picker/oxy-datetime-picker.vue +3 -1
  27. package/components/oxy-datetime-picker/types.ts +5 -1
  28. package/components/oxy-echarts/index.scss +17 -0
  29. package/components/oxy-echarts/index.ts +1 -0
  30. package/components/oxy-echarts/oxy-echarts.vue +32 -0
  31. package/components/oxy-echarts/types.ts +12 -0
  32. package/components/oxy-file-list/index.scss +26 -0
  33. package/components/oxy-file-list/oxy-file-list.vue +208 -34
  34. package/components/oxy-file-list/types.ts +58 -2
  35. package/components/oxy-global-loading/oxy-global-loading.vue +53 -0
  36. package/components/oxy-global-message/oxy-global-message.vue +64 -0
  37. package/components/oxy-global-toast/oxy-global-toast.vue +53 -0
  38. package/components/oxy-img-lazy/index.scss +17 -0
  39. package/components/oxy-img-lazy/oxy-img-lazy.vue +332 -0
  40. package/components/oxy-img-lazy/types.ts +69 -0
  41. package/components/oxy-link/index.scss +57 -0
  42. package/components/oxy-link/oxy-link.vue +130 -0
  43. package/components/oxy-link/types.ts +81 -0
  44. package/components/oxy-list/index.scss +8 -1
  45. package/components/oxy-list/oxy-list.vue +121 -40
  46. package/components/oxy-list/types.ts +3 -15
  47. package/components/oxy-picker/oxy-picker.vue +3 -0
  48. package/components/oxy-picker/types.ts +5 -1
  49. package/components/oxy-radio/index.scss +3 -3
  50. package/components/oxy-radio/oxy-radio.vue +1 -1
  51. package/components/oxy-rich-text/icon/emjio.svg +1 -0
  52. package/components/oxy-rich-text/icon/quote.svg +1 -0
  53. package/components/oxy-rich-text/icon/text.svg +1 -0
  54. package/components/oxy-rich-text/icon/title.svg +1 -0
  55. package/components/oxy-rich-text/index.scss +159 -0
  56. package/components/oxy-rich-text/mp-html/card/card.vue +122 -0
  57. package/components/oxy-rich-text/mp-html/card/index.js +7 -0
  58. package/components/oxy-rich-text/mp-html/editable/config.js +15 -0
  59. package/components/oxy-rich-text/mp-html/editable/index.js +553 -0
  60. package/components/oxy-rich-text/mp-html/emoji/index.js +203 -0
  61. package/components/oxy-rich-text/mp-html/highlight/config.js +5 -0
  62. package/components/oxy-rich-text/mp-html/highlight/index.js +96 -0
  63. package/components/oxy-rich-text/mp-html/highlight/prism.css +1 -0
  64. package/components/oxy-rich-text/mp-html/highlight/prism.min.js +7 -0
  65. package/components/oxy-rich-text/mp-html/img-cache/index.js +138 -0
  66. package/components/oxy-rich-text/mp-html/latex/index.js +80 -0
  67. package/components/oxy-rich-text/mp-html/latex/katex.css +1 -0
  68. package/components/oxy-rich-text/mp-html/latex/katex.min.js +1 -0
  69. package/components/oxy-rich-text/mp-html/markdown/index.js +50 -0
  70. package/components/oxy-rich-text/mp-html/markdown/marked.min.js +71 -0
  71. package/components/oxy-rich-text/mp-html/mp-html.d.ts +184 -0
  72. package/components/oxy-rich-text/mp-html/mp-html.vue +675 -0
  73. package/components/oxy-rich-text/mp-html/node/node.vue +1161 -0
  74. package/components/oxy-rich-text/mp-html/parser.js +1428 -0
  75. package/components/oxy-rich-text/mp-html/search/index.js +132 -0
  76. package/components/oxy-rich-text/mp-html/style/index.js +129 -0
  77. package/components/oxy-rich-text/mp-html/style/parser.js +175 -0
  78. package/components/oxy-rich-text/mp-html/template/index.js +67 -0
  79. package/components/oxy-rich-text/mp-html/txv-video/index.js +46 -0
  80. package/components/oxy-rich-text/oxy-rich-text.vue +642 -0
  81. package/components/oxy-rich-text/types.ts +71 -0
  82. package/components/oxy-select/index.scss +255 -0
  83. package/components/oxy-select/oxy-select.vue +421 -0
  84. package/components/oxy-select/types.ts +71 -0
  85. package/components/oxy-select-picker/oxy-select-picker.vue +3 -0
  86. package/components/oxy-select-picker/types.ts +5 -1
  87. package/components/oxy-stream-render/index.scss +6 -0
  88. package/components/oxy-stream-render/oxy-stream-render.vue +204 -0
  89. package/components/oxy-stream-render/types.ts +5 -0
  90. package/components/oxy-tree/index.scss +43 -5
  91. package/components/oxy-tree/oxy-tree.vue +233 -35
  92. package/components/oxy-tree/types.ts +54 -7
  93. package/components/oxy-tree/utils.ts +51 -0
  94. package/components/oxy-virtual-scroll/index.scss +1 -1
  95. package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +69 -110
  96. package/components/oxy-virtual-scroll/types.ts +95 -5
  97. package/components/oxy-waterfall/index.scss +18 -0
  98. package/components/oxy-waterfall/oxy-waterfall.vue +218 -0
  99. package/components/oxy-waterfall/types.ts +90 -0
  100. package/components/oxy-waterfall-item/index.scss +8 -0
  101. package/components/oxy-waterfall-item/oxy-waterfall-item.vue +89 -0
  102. package/components/oxy-waterfall-item/types.ts +16 -0
  103. package/global.d.ts +7 -0
  104. package/index.ts +3 -0
  105. package/locale/lang/en-US.ts +35 -9
  106. package/locale/lang/zh-CN.ts +31 -5
  107. package/oxy-uni-ui.zip +0 -0
  108. package/package.json +1 -1
  109. package/tags.json +1 -1
  110. package/uni-echarts/changelog.md +2 -0
  111. package/uni-echarts/components/index.js +1 -0
  112. package/uni-echarts/components/uni-echarts/events.js +95 -0
  113. package/uni-echarts/components/uni-echarts/types.d.ts +183 -0
  114. package/uni-echarts/components/uni-echarts/types.js +1 -0
  115. package/uni-echarts/components/uni-echarts/uni-echarts.vue +530 -0
  116. package/uni-echarts/components/uni-echarts/uni-echarts.vue.d.ts +19 -0
  117. package/uni-echarts/global.d.ts +7 -0
  118. package/uni-echarts/index.d.ts +440 -0
  119. package/uni-echarts/index.js +2 -0
  120. package/uni-echarts/package.json +105 -0
  121. package/uni-echarts/shared-core.d.ts +269 -0
  122. package/uni-echarts/shared-core.js +900 -0
  123. package/web-types.json +1 -1
@@ -0,0 +1,642 @@
1
+ <template>
2
+ <view class="oxy-rich-text" :style="customStyle" :class="{ editable }">
3
+ <view v-if="editable" class="editor_toolbox">
4
+ <template v-for="(button, index) in toolButtons" :key="index">
5
+ <image width="16px" height="16px" v-if="button.icon" :src="button.icon" alt="" @click="button.tap()" />
6
+ <oxy-icon v-else size="18" :name="button.name" :class="button.class" @click="button.tap()" />
7
+ </template>
8
+ </view>
9
+ <view class="rich-text-main">
10
+ <mp-html
11
+ ref="articleRef"
12
+ v-bind="$attrs"
13
+ class="oxy-rich-text__html"
14
+ :key="mpkey"
15
+ :editable="editable"
16
+ :tag-style="tagStyle"
17
+ :markdown="true"
18
+ :content="contentAi"
19
+ @remove="remove"
20
+ ></mp-html>
21
+ </view>
22
+
23
+ <oxy-popup v-model="dialog" position="bottom" closable @close="dialog = false">
24
+ <view class="oxy-rich-text__popup">
25
+ <view class="dialog-title">{{ translate('insert' + (dialogType === 'emoji' ? 'Emoji' : 'Template')) }}</view>
26
+ <template v-if="dialogType === 'emoji'">
27
+ <view v-for="(line, index) in emojis" :key="index" class="oxy-rich-text__emoji-content">
28
+ <view class="oxy-rich-text__emoji-block" v-for="(item, index2) in line" :key="index2" @click="insertEmoji(item)">
29
+ {{ item }}
30
+ </view>
31
+ </view>
32
+ </template>
33
+ <template v-if="dialogType === 'template'">
34
+ <view v-for="(item, index) in templates" @click="closeDialog" :key="index">
35
+ <view @click="insertHtml(item)">
36
+ <rich-text :nodes="item" />
37
+ <view class="oxy-rich-text__popup-divider" />
38
+ </view>
39
+ </view>
40
+ </template>
41
+ </view>
42
+ </oxy-popup>
43
+ <oxy-popup v-model="linkDialog" position="bottom" closable @close="linkDialog = false">
44
+ <view class="oxy-rich-text__popup">
45
+ <view class="dialog-title">{{ translate('insertLink') }}</view>
46
+ <view class="oxy-rich-text__link-input">
47
+ <view class="oxy-rich-text__link-label">{{ translate('linkText') }}</view>
48
+ <oxy-input v-model="linkText" :placeholder="translate('linkTextPlaceholder')" />
49
+ </view>
50
+ <view class="oxy-rich-text__link-input">
51
+ <view class="oxy-rich-text__link-label">{{ translate('linkAddress') }}</view>
52
+ <oxy-input v-model="linkHref" :placeholder="translate('linkAddressPlaceholder')" />
53
+ </view>
54
+ <view class="oxy-rich-text__link-buttons">
55
+ <oxy-button type="default" @click="linkDialog = false">{{ translate('cancel') }}</oxy-button>
56
+ <oxy-button type="primary" @click="confirmInsertLink">{{ translate('confirm') }}</oxy-button>
57
+ </view>
58
+ </view>
59
+ </oxy-popup>
60
+ <oxy-action-sheet
61
+ v-model="actionSheetShow"
62
+ :actions="actions"
63
+ @close="mediaActionClose"
64
+ @select="actionSelected"
65
+ :cancel-text="translate('cancel')"
66
+ />
67
+ <oxy-message-box />
68
+ <oxy-message-box selector="oxy-message-box-table">
69
+ <oxy-input-number v-model="rows" />
70
+ {{ translate('row') }}
71
+ <oxy-input-number v-model="cols" />
72
+ {{ translate('column') }}
73
+ </oxy-message-box>
74
+ <oxy-toast />
75
+ </view>
76
+ </template>
77
+ <script lang="ts">
78
+ export default {
79
+ name: 'oxy-rich-mardown',
80
+ options: {
81
+ virtualHost: true,
82
+ addGlobalClass: true,
83
+ styleIsolation: 'shared'
84
+ }
85
+ }
86
+ </script>
87
+ <script lang="ts" setup>
88
+ import { ref, computed, watch, nextTick, onBeforeMount, onMounted } from 'vue'
89
+ import { uuid } from '../common/util'
90
+ import mpHtml from './mp-html/mp-html.vue'
91
+ import type { MpHtmlComponent } from './mp-html/mp-html.d.js'
92
+ import OxyIcon from '../oxy-icon/oxy-icon.vue'
93
+ import OxyInputNumber from '../oxy-input-number/oxy-input-number.vue'
94
+ import OxyInput from '../oxy-input/oxy-input.vue'
95
+ import OxyButton from '../oxy-button/oxy-button.vue'
96
+ import { useMessage } from '../../index'
97
+ import { useTranslate } from '../composables/useTranslate'
98
+ import { type ActionSheetInfo, type RemoveEvent, richTextProps, type ToolButton } from './types'
99
+ import emjio from './icon/emjio.svg'
100
+ import quote from './icon/quote.svg'
101
+ import text from './icon/text.svg'
102
+ import title from './icon/title.svg'
103
+
104
+ const props = defineProps(richTextProps)
105
+
106
+ const emit = defineEmits<{
107
+ save: [value: string]
108
+ clear: [void]
109
+ remove: [event: RemoveEvent]
110
+ }>()
111
+ const { translate } = useTranslate('richText')
112
+ const templates = [
113
+ '<section style="text-align: center; margin: 0px auto;"><section style="border-radius: 4px; border: 1px solid #757576; display: inline-block; padding: 5px 20px;"><span style="font-size: 18px; color: #595959;">标题</span></section></section>',
114
+ '<div style="width: 100%; box-sizing: border-box; border-radius: 5px; background-color: #f6f6f6; padding: 10px; margin: 10px 0"><div>卡片</div><div style="font-size: 12px; color: gray">正文</div></div>',
115
+ '<div style="border: 1px solid gray; box-shadow: 3px 3px 0px #cfcfce; padding: 10px; margin: 10px 0">段落</div>'
116
+ ]
117
+ const emojis = [
118
+ ['😄', '😷', '😂', '😝', '😳', '😱', '😔', '😒', '😉'],
119
+ ['😎', '😭', '😍', '😘', '🤔', '😕', '🙃', '🤑', '😲'],
120
+ ['🙄', '😤', '😴', '🤓', '😡', '😑', '😮', '🤒', '🤮']
121
+ ]
122
+ const message = useMessage()
123
+ const messageTable = useMessage('oxy-message-box-table')
124
+ const content = ref<string>('')
125
+ const mpkey = ref<string>(uuid())
126
+ const articleRef = ref<MpHtmlComponent | null>(null)
127
+ const dialog = ref<boolean>(false)
128
+ const dialogType = ref<string>('')
129
+ const linkDialog = ref<boolean>(false)
130
+ const linkText = ref<string>('')
131
+ const linkHref = ref<string>('')
132
+ const rows = ref<number>(1)
133
+ const cols = ref<number>(1)
134
+ const actionSheetShow = ref<boolean>(false)
135
+ const actions = ref([
136
+ {
137
+ name: translate('localSelect')
138
+ },
139
+ {
140
+ name: translate('remoteLink')
141
+ }
142
+ ])
143
+
144
+ const customTagStyle = {
145
+ p: `
146
+ font-size: 16px;
147
+ color: var(--oxy-rich-text-primary-color, #262626);
148
+ `,
149
+ h1: `
150
+ margin:18px 0 10px 0;
151
+ font-size: 24px;
152
+ font-weight: 700;
153
+ color: var(--oxy-rich-text-title-color, #000);
154
+ `,
155
+ h2: `
156
+ margin:14px 0 10px 0;
157
+ font-size: 20px;
158
+ font-weight: 600;
159
+ color: var(--oxy-rich-text-title-color, #000);
160
+ `,
161
+ h3: `
162
+ margin:12px 0 8px 0;
163
+ font-size: 18px;
164
+ font-weight: 600;
165
+ color: var(--oxy-rich-text-title-color, #000);
166
+ `,
167
+ h4: `
168
+ margin:12px 0 8px 0;
169
+ font-size: 16px;
170
+ font-weight: 600;
171
+ color: var(--oxy-rich-text-title-color, #000);
172
+ `,
173
+ h5: `
174
+ margin:10px 0 8px 0;
175
+ font-size: 15px;
176
+ font-weight: 600;
177
+ color: var(--oxy-rich-text-title-color, #000);
178
+ `,
179
+ h6: `
180
+ margin:8px 0 8px 0;
181
+ font-size: 14px;
182
+ color: var(--oxy-rich-text-title-color, #000);
183
+ `,
184
+ blockquote: `
185
+ margin:14px 0;
186
+ padding: 10px 12px;
187
+ color: var(--oxy-rich-text-blockquote-color, #555);
188
+ background: var(--oxy-rich-text-blockquote-bg, #f7f8fa);
189
+ border-left: 4px solid var(--oxy-rich-text-blockquote-border-color, #007AFF);
190
+ `,
191
+ 'blockquote>p': `
192
+ margin-block-start: 0;
193
+ margin-block-end: 0;
194
+ `,
195
+ ul: `
196
+ margin: 0 0 10px 0;
197
+ color: var(--oxy-rich-text-list-color, #444);
198
+ `,
199
+ li: `
200
+ margin: 6px 0;
201
+ line-height: 1.65;
202
+ color: var(--oxy-rich-text-list-color, #444);
203
+ `,
204
+ a: `
205
+ color: var(--oxy-rich-text-a-color, #007AFF);
206
+ text-decoration: underline;
207
+ `,
208
+ strong: `
209
+ font-weight: 700;
210
+ color: var(--oxy-rich-text-strong-color, #222);
211
+ `,
212
+ em: `
213
+ font-style: italic;
214
+ color: var(--oxy-rich-text-em-color, #222);
215
+ `,
216
+ hr: `
217
+ height: 1px;
218
+ padding: 0;
219
+ border: none;
220
+ background: var(--oxy-rich-text-hr-color, #e5e6eb);
221
+ margin: 18px 0;
222
+ `,
223
+ table: `
224
+ border-spacing: 0;
225
+ overflow: auto;
226
+ min-width: 100%;
227
+ margin: 12px 0;
228
+ border-collapse: collapse;
229
+ `,
230
+ th: `
231
+ border: 1px solid var(--oxy-rich-text-table-border-color, #e5e6eb);
232
+ background: var(--oxy-rich-text-table-head-bg, #f7f8fa);
233
+ color: var(--oxy-rich-text-table-head-color, #333);
234
+ padding: 8px 12px;
235
+ font-weight: 600;
236
+ `,
237
+ td: `
238
+ border: 1px solid var(--oxy-rich-text-table-border-color, #e5e6eb);
239
+ color: var(--oxy-rich-text-table-text-color, #444);
240
+ padding: 8px 12px;
241
+ `,
242
+ pre: `
243
+ border-radius: 6px;
244
+ white-space: pre;
245
+ background: var(--oxy-rich-text-code-bg, #2d2d2d);
246
+ color: var(--oxy-rich-text-code-color, #f8f8f2);
247
+ font-size: 13px;
248
+ line-height: 1.65;
249
+ padding: 12px;
250
+ overflow: auto;
251
+ position: relative;
252
+ `
253
+ }
254
+
255
+ const tagStyle = computed(() => {
256
+ // Merge customTagStyle with props.tagStyle, with props.tagStyle taking precedence
257
+ return {
258
+ ...customTagStyle,
259
+ ...props.tagStyle
260
+ }
261
+ })
262
+
263
+ const contentAi = computed<string>(() => {
264
+ if (!content.value) {
265
+ return ''
266
+ }
267
+ let htmlString = ''
268
+ const codeBlocks = content.value.match(/```[\s\S]*?```|```[\s\S]*?$/g) || []
269
+ const lastBlock = codeBlocks[codeBlocks.length - 1]
270
+ if (lastBlock && !lastBlock.endsWith('```')) {
271
+ htmlString = content.value + '\n'
272
+ } else {
273
+ htmlString = content.value
274
+ }
275
+ return htmlString
276
+ })
277
+
278
+ watch(
279
+ () => props.markdown,
280
+ (val) => {
281
+ nextTick(() => {
282
+ content.value = val
283
+ })
284
+ },
285
+ { immediate: true }
286
+ )
287
+ onBeforeMount(() => {
288
+ content.value = props.markdown
289
+ })
290
+ onMounted(() => {
291
+ if (articleRef.value) {
292
+ articleRef.value.getSrc = (type: string, value: any) => {
293
+ return new Promise((resolve, reject) => {
294
+ const titleMap: { img: string; video: string; audio: string; link: string; [key: string]: any } = {
295
+ img: translate('imageLink'),
296
+ video: translate('videoLink'),
297
+ audio: translate('audioLink'),
298
+ link: translate('linkAddress')
299
+ }
300
+ let title = titleMap[type] || translate('linkAddress')
301
+
302
+ const tapIndex = actionSheetInfo.value.index
303
+ if (type === 'img' || type === 'video') {
304
+ if (tapIndex === 0) {
305
+ // 本地选取
306
+ if (type === 'img') {
307
+ uni.chooseImage({
308
+ count: value === undefined ? 9 : 1, // 2.2.0 版本起插入图片时支持多张(修改图片链接时仅限一张)
309
+ success: (res) => {
310
+ // #ifdef MP-WEIXIN
311
+ if (res.tempFilePaths.length == 1 && wx.editImage) {
312
+ // 单张图片时进行编辑
313
+ wx.editImage({
314
+ src: res.tempFilePaths[0],
315
+ complete: (res2: any) => {
316
+ uni.showLoading({
317
+ title: translate('uploading')
318
+ })
319
+ upload(res2.tempFilePath || res.tempFilePaths[0], type).then((res) => {
320
+ uni.hideLoading()
321
+ resolve(res)
322
+ })
323
+ }
324
+ })
325
+ } else {
326
+ // #endif
327
+ uni.showLoading({
328
+ title: translate('uploading')
329
+ })
330
+ ;(async () => {
331
+ const arr = []
332
+ for (let item of res.tempFilePaths) {
333
+ // 依次上传
334
+ const src = await upload(item, type)
335
+ arr.push(src)
336
+ }
337
+ return arr
338
+ })().then((res) => {
339
+ uni.hideLoading()
340
+ resolve(res)
341
+ })
342
+ // #ifdef MP-WEIXIN
343
+ }
344
+ // #endif
345
+ },
346
+ fail: reject
347
+ })
348
+ } else {
349
+ uni.chooseVideo({
350
+ success: (res) => {
351
+ uni.showLoading({
352
+ title: translate('uploading')
353
+ })
354
+ upload(res.tempFilePath, type).then((res) => {
355
+ uni.hideLoading()
356
+ resolve(res)
357
+ })
358
+ },
359
+ fail: reject
360
+ })
361
+ }
362
+ } else {
363
+ message
364
+ .prompt({
365
+ title: title,
366
+ inputValue: value
367
+ })
368
+ .then((resp) => {
369
+ resolve(resp.value || '')
370
+ })
371
+ }
372
+ } else {
373
+ message
374
+ .prompt({
375
+ title: title,
376
+ inputValue: value
377
+ })
378
+ .then((resp) => {
379
+ resolve(resp.value || '')
380
+ })
381
+ }
382
+ })
383
+ }
384
+ }
385
+ })
386
+
387
+ // 上传图片方法
388
+ function upload(src: string, type: string): Promise<string> {
389
+ if (props.uploadMethod) {
390
+ return props.uploadMethod(src, type)
391
+ }
392
+ return Promise.resolve(src)
393
+ }
394
+
395
+ function remove(e: RemoveEvent): void {
396
+ emit('remove', e)
397
+ }
398
+
399
+ // 处理底部弹窗
400
+ function openDialog(btn: ToolButton): void {
401
+ dialog.value = true
402
+ dialogType.value = btn.method || ''
403
+ }
404
+
405
+ function closeDialog(): void {
406
+ dialog.value = false
407
+ }
408
+
409
+ const actionSheetInfo = ref<ActionSheetInfo>({ type: '' })
410
+
411
+ const openActionSheet = (type: string): void => {
412
+ actionSheetShow.value = true
413
+ actionSheetInfo.value = {
414
+ type: type
415
+ }
416
+ }
417
+
418
+ const actionSelected = ({ index }: { index: number }): void => {
419
+ const type = actionSheetInfo.value.type
420
+ actionSheetInfo.value.index = index
421
+
422
+ if (type === 'insertHtml') {
423
+ const tagName = ['h1', 'h3', 'h5'][index]
424
+ if (tagName && articleRef.value) {
425
+ articleRef.value.insertHtml(`<${tagName}>标题</${tagName}>`)
426
+ }
427
+ } else if (type === 'code') {
428
+ const lan = ['css', 'javascript', 'json'][index]
429
+ if (lan && articleRef.value) {
430
+ articleRef.value.insertHtml(`<pre><code class="language-${lan}">${lan} code</code></pre>`)
431
+ }
432
+ } else {
433
+ articleRef.value?.[type]()
434
+ }
435
+ }
436
+
437
+ const mediaActionClose = (): void => {
438
+ actionSheetInfo.value = { type: '' }
439
+ }
440
+
441
+ // 清空编辑器内容
442
+ const clear = (): void => {
443
+ message
444
+ .confirm({
445
+ msg: translate('clearConfirm'),
446
+ title: translate('confirm')
447
+ })
448
+ .then(() => {
449
+ articleRef.value?.clear()
450
+ content.value = ''
451
+ emit('clear')
452
+ })
453
+ }
454
+
455
+ // 保存编辑器内容
456
+ const save = (): void => {
457
+ setTimeout(() => {
458
+ if (props.editable && articleRef.value) {
459
+ const contentHtml = articleRef.value.getContent()
460
+ emit('save', contentHtml)
461
+ }
462
+ }, 50)
463
+ }
464
+
465
+ // 撤销操作
466
+ const undo = (): void => {
467
+ articleRef.value?.undo()
468
+ }
469
+
470
+ // 重做操作
471
+ const redo = (): void => {
472
+ articleRef.value?.redo()
473
+ }
474
+
475
+ // 插入图片
476
+ const insertImg = (): void => {
477
+ actions.value = [
478
+ {
479
+ name: translate('localSelect')
480
+ },
481
+ {
482
+ name: translate('remoteLink')
483
+ }
484
+ ]
485
+ openActionSheet('insertImg')
486
+ }
487
+
488
+ // 插入视频
489
+ const insertVideo = (): void => {
490
+ actions.value = [
491
+ {
492
+ name: translate('localSelect')
493
+ },
494
+ {
495
+ name: translate('remoteLink')
496
+ }
497
+ ]
498
+ openActionSheet('insertVideo')
499
+ }
500
+
501
+ // 插入链接
502
+ const insertLink = (): void => {
503
+ linkText.value = ''
504
+ linkHref.value = ''
505
+ linkDialog.value = true
506
+ }
507
+
508
+ // 确认插入链接
509
+ const confirmInsertLink = (): void => {
510
+ if (articleRef.value) {
511
+ const text = linkText.value || linkHref.value
512
+ const href = linkHref.value
513
+ if (href) {
514
+ articleRef.value.insertHtml(`<a href="${href}">${text}</a>`)
515
+ }
516
+ }
517
+ linkDialog.value = false
518
+ }
519
+
520
+ // 插入文本
521
+ const insertText = (): void => {
522
+ articleRef.value?.insertText()
523
+ }
524
+
525
+ // 插入HTML
526
+ const insertHtml = (html: string): void => {
527
+ articleRef.value?.insertHtml(html)
528
+ }
529
+
530
+ // 插入表格
531
+ const insertTable = (row?: number, col?: number): void => {
532
+ messageTable
533
+ .confirm({
534
+ title: translate('insertTable')
535
+ })
536
+ .then(() => {
537
+ if (articleRef.value) {
538
+ const rowValue = row ?? rows.value
539
+ const colValue = col ?? cols.value
540
+ articleRef.value.insertTable(rowValue, colValue)
541
+ }
542
+ })
543
+ }
544
+
545
+ // 插入代码
546
+ const insertCode = (language?: 'css' | 'javascript' | 'json'): void => {
547
+ const lan = language || 'javascript'
548
+ actions.value = [
549
+ {
550
+ name: 'css'
551
+ },
552
+ {
553
+ name: 'javascript'
554
+ },
555
+ {
556
+ name: 'json'
557
+ }
558
+ ]
559
+ openActionSheet('code')
560
+ }
561
+
562
+ // 插入表情
563
+ const insertEmoji = (emoji: string): void => {
564
+ if (articleRef.value) {
565
+ articleRef.value.insertHtml(emoji)
566
+ }
567
+ closeDialog()
568
+ }
569
+
570
+ // 插入模板
571
+ const insertTemplate = (): void => {
572
+ openDialog({ method: 'template', name: '', tap: () => {} })
573
+ }
574
+
575
+ // 插入标题
576
+ const insertHead = (level?: 'h1' | 'h3' | 'h5'): void => {
577
+ actions.value = [
578
+ {
579
+ name: translate('largeTitle')
580
+ },
581
+ {
582
+ name: translate('mediumTitle')
583
+ },
584
+ {
585
+ name: translate('smallTitle')
586
+ }
587
+ ]
588
+ openActionSheet('insertHtml')
589
+ }
590
+ // 工具按钮数组,按组分类
591
+ const toolButtons = ref<ToolButton[]>([
592
+ { name: 'rollback', method: 'undo', tap: () => undo() },
593
+ { name: 'rollback', method: 'redo', tap: () => redo(), class: 'reverse' },
594
+ { name: 'image', method: 'insertImg', tap: () => insertImg() },
595
+ { name: 'video1', method: 'insertVideo', tap: () => insertVideo() },
596
+ { name: 'link', method: 'insertLink', tap: () => insertLink() },
597
+ { name: 'note', method: 'insertText', tap: () => insertText(), icon: text },
598
+ { name: 'format-vertical-align-center', tap: () => insertHead(), icon: title },
599
+ {
600
+ name: 'discount',
601
+ method: 'insertHtml',
602
+ tap: () => insertHtml('<blockquote >引用</blockquote>'),
603
+ icon: quote
604
+ },
605
+ { name: 'windows', tap: () => insertTable() },
606
+ { name: 'code', tap: () => insertCode() },
607
+ { name: 'emjio', method: 'emoji', tap: () => openDialog({ method: 'emoji', name: '', tap: () => {} }), icon: emjio },
608
+ { name: 'file-copy', method: 'template', tap: () => insertTemplate() },
609
+ { name: 'clear', tap: clear },
610
+ { name: 'save', tap: save }
611
+ ])
612
+
613
+ // 暴露方法给外部调用
614
+ defineExpose({
615
+ undo,
616
+ redo,
617
+ insertImg,
618
+ insertVideo,
619
+ insertLink,
620
+ insertText,
621
+ insertHtml,
622
+ insertTable,
623
+ insertCode,
624
+ insertEmoji,
625
+ insertTemplate,
626
+ insertHead,
627
+ clear,
628
+ save,
629
+ getContent: () => articleRef.value?.getContent() || '',
630
+ in: (page: any, selector: string, scrollTop: string) => articleRef.value?.in(page, selector, scrollTop),
631
+ navigateTo: (id: string, offset?: number) => articleRef.value?.navigateTo(id, offset),
632
+ getText: (nodes?: any[]) => articleRef.value?.getText(nodes) || '',
633
+ getRect: () => articleRef.value?.getRect(),
634
+ pauseMedia: () => articleRef.value?.pauseMedia(),
635
+ setPlaybackRate: (rate: number) => articleRef.value?.setPlaybackRate(rate),
636
+ setContent: (content: string, append?: boolean) => articleRef.value?.setContent(content, append)
637
+ })
638
+ </script>
639
+
640
+ <style lang="scss" scoped>
641
+ @import './index.scss';
642
+ </style>
@@ -0,0 +1,71 @@
1
+ import type { ComponentPublicInstance, ExtractPropTypes } from 'vue'
2
+ import { baseProps } from '../common/props'
3
+
4
+ export const richTextProps = {
5
+ ...baseProps,
6
+ markdown: {
7
+ type: String,
8
+ default: ''
9
+ },
10
+ tagStyle: {
11
+ type: Object,
12
+ default: () => ({})
13
+ },
14
+ editable: {
15
+ type: Boolean,
16
+ default: false
17
+ },
18
+ uploadMethod: {
19
+ type: Function as any,
20
+ default: null
21
+ }
22
+ }
23
+
24
+ export type RichTextExpose = {
25
+ undo: () => void
26
+ redo: () => void
27
+ insertImg: () => void
28
+ insertVideo: () => void
29
+ insertLink: () => void
30
+ insertText: () => void
31
+ insertHtml: (html: string) => void
32
+ insertTable: (row?: number, col?: number) => void
33
+ insertCode: (language?: 'css' | 'javascript' | 'json') => void
34
+ insertEmoji: (emoji: string) => void
35
+ insertTemplate: () => void
36
+ insertHead: (level?: 'h1' | 'h3' | 'h5') => void
37
+ clear: () => void
38
+ save: () => void
39
+ getContent: () => string
40
+ in: (page: any, selector: string, scrollTop: string) => void
41
+ navigateTo: (id: string, offset?: number) => Promise<void>
42
+ getText: (nodes?: any[]) => string
43
+ getRect: () => Promise<{ width: number; height: number; top: number; left: number }>
44
+ pauseMedia: () => void
45
+ setPlaybackRate: (rate: number) => void
46
+ setContent: (content: string, append?: boolean) => void
47
+ }
48
+
49
+ export type RichTextProps = ExtractPropTypes<typeof richTextProps>
50
+
51
+ export type RichTextInstance = ComponentPublicInstance<RichTextExpose, RichTextProps>
52
+
53
+ export interface ToolButton {
54
+ name: string
55
+ method?: string
56
+ param?: string
57
+ tap: () => void
58
+ icon?: string
59
+ class?: string
60
+ }
61
+
62
+ export interface ActionSheetInfo {
63
+ type: string
64
+ index?: number
65
+ }
66
+
67
+ // 删除图片/视频/音频标签事件
68
+ export interface RemoveEvent {
69
+ src: string
70
+ type: string
71
+ }