Commit 3ad1a4f5a69b4242d55e6bc17aceab7279241e14
1 parent
18ad1bcc
feat(tinymce): add image upload #170
Showing
8 changed files
with
133 additions
and
37 deletions
CHANGELOG.zh_CN.md
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | - 新增`PageWrapper`组件。并应用于示例页面 |
17 | 17 | - 新增标签页折叠功能 |
18 | 18 | - 兼容旧版浏览器 |
19 | +- tinymce 新增图片上传· | |
19 | 20 | |
20 | 21 | ### 🐛 Bug Fixes |
21 | 22 | |
... | ... | @@ -24,6 +25,7 @@ |
24 | 25 | - 修复表格内存溢出问题 |
25 | 26 | - 修复`layout` 收缩展开功能在分割模式下失效 |
26 | 27 | - 修复 modal 高度计算错误 |
28 | +- 修复文件上传错误 | |
27 | 29 | |
28 | 30 | ### 🎫 Chores |
29 | 31 | ... | ... |
src/components/Tinymce/src/Editor.vue
1 | 1 | <template> |
2 | - <div class="tinymce-container" :style="{ width: containerWidth }"> | |
2 | + <div :class="prefixCls" :style="{ width: containerWidth }"> | |
3 | + <ImgUpload | |
4 | + @uploading="handleImageUploading" | |
5 | + @done="handleDone" | |
6 | + v-if="showImageUpload" | |
7 | + v-show="editorRef" | |
8 | + /> | |
3 | 9 | <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea> |
4 | 10 | </div> |
5 | 11 | </template> |
... | ... | @@ -24,6 +30,8 @@ |
24 | 30 | import { bindHandlers } from './helper'; |
25 | 31 | import lineHeight from './lineHeight'; |
26 | 32 | import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'; |
33 | + import ImgUpload from './ImgUpload.vue'; | |
34 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
27 | 35 | |
28 | 36 | const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1'; |
29 | 37 | |
... | ... | @@ -33,12 +41,15 @@ |
33 | 41 | name: 'Tinymce', |
34 | 42 | inheritAttrs: false, |
35 | 43 | props: basicProps, |
44 | + components: { ImgUpload }, | |
36 | 45 | emits: ['change', 'update:modelValue'], |
37 | 46 | setup(props, { emit, attrs }) { |
38 | 47 | const editorRef = ref<any>(null); |
39 | 48 | const tinymceId = ref<string>(snowUuid('tiny-vue')); |
40 | 49 | const elRef = ref<Nullable<HTMLElement>>(null); |
41 | 50 | |
51 | + const { prefixCls } = useDesign('tinymce-container'); | |
52 | + | |
42 | 53 | const tinymceContent = computed(() => { |
43 | 54 | return props.modelValue; |
44 | 55 | }); |
... | ... | @@ -140,7 +151,7 @@ |
140 | 151 | bindHandlers(e, attrs, unref(editorRef)); |
141 | 152 | } |
142 | 153 | |
143 | - function setValue(editor: any, val: string, prevVal: string) { | |
154 | + function setValue(editor: Recordable, val: string, prevVal?: string) { | |
144 | 155 | if ( |
145 | 156 | editor && |
146 | 157 | typeof val === 'string' && |
... | ... | @@ -179,45 +190,54 @@ |
179 | 190 | }); |
180 | 191 | } |
181 | 192 | |
193 | + function handleImageUploading(name: string) { | |
194 | + const editor = unref(editorRef); | |
195 | + if (!editor) return; | |
196 | + const content = editor?.getContent() ?? ''; | |
197 | + setValue(editor, `${content}\n${getImgName(name)}`); | |
198 | + } | |
199 | + | |
200 | + function handleDone(name: string, url: string) { | |
201 | + const editor = unref(editorRef); | |
202 | + if (!editor) return; | |
203 | + | |
204 | + const content = editor?.getContent() ?? ''; | |
205 | + const val = content?.replace(getImgName(name), `<img src="${url}"/>`) ?? ''; | |
206 | + setValue(editor, val); | |
207 | + } | |
208 | + | |
209 | + function getImgName(name: string) { | |
210 | + return `[uploading:${name}]`; | |
211 | + } | |
212 | + | |
182 | 213 | return { |
214 | + prefixCls, | |
183 | 215 | containerWidth, |
184 | 216 | initOptions, |
185 | 217 | tinymceContent, |
186 | 218 | tinymceScriptSrc, |
187 | 219 | elRef, |
188 | 220 | tinymceId, |
221 | + handleImageUploading, | |
222 | + handleDone, | |
223 | + editorRef, | |
189 | 224 | }; |
190 | 225 | }, |
191 | 226 | }); |
192 | 227 | </script> |
193 | 228 | |
194 | -<style lang="less" scoped> | |
195 | - .tinymce-container { | |
196 | - position: relative; | |
197 | - line-height: normal; | |
229 | +<style lang="less" scoped></style> | |
198 | 230 | |
199 | - .mce-fullscreen { | |
200 | - z-index: 10000; | |
201 | - } | |
202 | - } | |
231 | +<style lang="less"> | |
232 | + @prefix-cls: ~'@{namespace}-tinymce-container'; | |
203 | 233 | |
204 | - .editor-custom-btn-container { | |
205 | - position: absolute; | |
206 | - top: 6px; | |
207 | - right: 6px; | |
234 | + .@{prefix-cls} { | |
235 | + position: relative; | |
236 | + line-height: normal; | |
208 | 237 | |
209 | - &.fullscreen { | |
210 | - position: fixed; | |
211 | - z-index: 10000; | |
238 | + textarea { | |
239 | + z-index: -1; | |
240 | + visibility: hidden; | |
212 | 241 | } |
213 | 242 | } |
214 | - | |
215 | - .editor-upload-btn { | |
216 | - display: inline-block; | |
217 | - } | |
218 | - | |
219 | - textarea { | |
220 | - z-index: -1; | |
221 | - visibility: hidden; | |
222 | - } | |
223 | 243 | </style> | ... | ... |
src/components/Tinymce/src/ImgUpload.vue
0 → 100644
1 | +<template> | |
2 | + <div :class="prefixCls"> | |
3 | + <Upload | |
4 | + name="file" | |
5 | + multiple | |
6 | + @change="handleChange" | |
7 | + :action="uploadUrl" | |
8 | + :showUploadList="false" | |
9 | + accept=".jpg,.jpeg,.gif,.png,.webp" | |
10 | + > | |
11 | + <a-button type="primary">{{ t('component.upload.imgUpload') }}</a-button> | |
12 | + </Upload> | |
13 | + </div> | |
14 | +</template> | |
15 | +<script lang="ts"> | |
16 | + import { defineComponent } from 'vue'; | |
17 | + | |
18 | + import { Upload } from 'ant-design-vue'; | |
19 | + import { InboxOutlined } from '@ant-design/icons-vue'; | |
20 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
21 | + import { useGlobSetting } from '/@/hooks/setting'; | |
22 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
23 | + | |
24 | + export default defineComponent({ | |
25 | + name: 'TinymceImageUpload', | |
26 | + components: { Upload, InboxOutlined }, | |
27 | + emits: ['uploading', 'done', 'error'], | |
28 | + setup(_, { emit }) { | |
29 | + let uploading = false; | |
30 | + | |
31 | + const { uploadUrl } = useGlobSetting(); | |
32 | + const { t } = useI18n(); | |
33 | + const { prefixCls } = useDesign('tinymce-img-upload'); | |
34 | + function handleChange(info: Recordable) { | |
35 | + const file = info.file; | |
36 | + const status = file?.status; | |
37 | + | |
38 | + const url = file?.response?.url; | |
39 | + const name = file?.name; | |
40 | + if (status === 'uploading') { | |
41 | + if (!uploading) { | |
42 | + emit('uploading', name); | |
43 | + uploading = true; | |
44 | + } | |
45 | + } else if (status === 'done') { | |
46 | + emit('done', name, url); | |
47 | + uploading = false; | |
48 | + } else if (status === 'error') { | |
49 | + emit('error'); | |
50 | + uploading = false; | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + return { | |
55 | + prefixCls, | |
56 | + handleChange, | |
57 | + uploadUrl, | |
58 | + t, | |
59 | + }; | |
60 | + }, | |
61 | + }); | |
62 | +</script> | |
63 | +<style lang="less" scoped> | |
64 | + @prefix-cls: ~'@{namespace}-tinymce-img-upload'; | |
65 | + | |
66 | + .@{prefix-cls} { | |
67 | + position: absolute; | |
68 | + top: 4px; | |
69 | + right: 10px; | |
70 | + z-index: 20; | |
71 | + } | |
72 | +</style> | ... | ... |
src/components/Tinymce/src/props.ts
1 | 1 | import { PropType } from 'vue'; |
2 | +import { propTypes } from '/@/utils/propTypes'; | |
2 | 3 | |
3 | 4 | export const basicProps = { |
4 | 5 | options: { |
5 | 6 | type: Object as PropType<any>, |
6 | 7 | default: {}, |
7 | 8 | }, |
8 | - value: { | |
9 | - type: String as PropType<string>, | |
10 | - // default: '' | |
11 | - }, | |
12 | - modelValue: { | |
13 | - type: String as PropType<string>, | |
14 | - // default: '' | |
15 | - }, | |
9 | + value: propTypes.string, | |
10 | + modelValue: propTypes.string, | |
16 | 11 | // 高度 |
17 | 12 | height: { |
18 | 13 | type: [Number, String] as PropType<string | number>, |
... | ... | @@ -26,4 +21,5 @@ export const basicProps = { |
26 | 21 | required: false, |
27 | 22 | default: 'auto', |
28 | 23 | }, |
24 | + showImageUpload: propTypes.bool.def(true), | |
29 | 25 | }; | ... | ... |
src/layouts/default/header/components/user-dropdown/index.vue
... | ... | @@ -9,8 +9,13 @@ |
9 | 9 | |
10 | 10 | <template #overlay> |
11 | 11 | <Menu @click="handleMenuClick"> |
12 | - <MenuItem key="doc" :text="t('layout.header.dropdownItemDoc')" icon="gg:loadbar-doc" /> | |
13 | - <MenuDivider v-if="getShowDoc" /> | |
12 | + <MenuItem | |
13 | + key="doc" | |
14 | + :text="t('layout.header.dropdownItemDoc')" | |
15 | + icon="gg:loadbar-doc" | |
16 | + v-if="getShowDoc" | |
17 | + /> | |
18 | + <MenuDivider /> | |
14 | 19 | <MenuItem |
15 | 20 | key="loginOut" |
16 | 21 | :text="t('layout.header.dropdownItemLoginOut')" | ... | ... |
src/layouts/default/header/index.vue
... | ... | @@ -84,7 +84,6 @@ |
84 | 84 | } from './components'; |
85 | 85 | import { useAppInject } from '/@/hooks/web/useAppInject'; |
86 | 86 | import { useDesign } from '/@/hooks/web/useDesign'; |
87 | - import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; | |
88 | 87 | |
89 | 88 | export default defineComponent({ |
90 | 89 | name: 'LayoutHeader', | ... | ... |
src/locales/lang/en/component/upload.ts