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,6 +16,7 @@ | ||
16 | - 新增`PageWrapper`组件。并应用于示例页面 | 16 | - 新增`PageWrapper`组件。并应用于示例页面 |
17 | - 新增标签页折叠功能 | 17 | - 新增标签页折叠功能 |
18 | - 兼容旧版浏览器 | 18 | - 兼容旧版浏览器 |
19 | +- tinymce 新增图片上传· | ||
19 | 20 | ||
20 | ### 🐛 Bug Fixes | 21 | ### 🐛 Bug Fixes |
21 | 22 | ||
@@ -24,6 +25,7 @@ | @@ -24,6 +25,7 @@ | ||
24 | - 修复表格内存溢出问题 | 25 | - 修复表格内存溢出问题 |
25 | - 修复`layout` 收缩展开功能在分割模式下失效 | 26 | - 修复`layout` 收缩展开功能在分割模式下失效 |
26 | - 修复 modal 高度计算错误 | 27 | - 修复 modal 高度计算错误 |
28 | +- 修复文件上传错误 | ||
27 | 29 | ||
28 | ### 🎫 Chores | 30 | ### 🎫 Chores |
29 | 31 |
src/components/Tinymce/src/Editor.vue
1 | <template> | 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 | <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea> | 9 | <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea> |
4 | </div> | 10 | </div> |
5 | </template> | 11 | </template> |
@@ -24,6 +30,8 @@ | @@ -24,6 +30,8 @@ | ||
24 | import { bindHandlers } from './helper'; | 30 | import { bindHandlers } from './helper'; |
25 | import lineHeight from './lineHeight'; | 31 | import lineHeight from './lineHeight'; |
26 | import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'; | 32 | import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'; |
33 | + import ImgUpload from './ImgUpload.vue'; | ||
34 | + import { useDesign } from '/@/hooks/web/useDesign'; | ||
27 | 35 | ||
28 | const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1'; | 36 | const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1'; |
29 | 37 | ||
@@ -33,12 +41,15 @@ | @@ -33,12 +41,15 @@ | ||
33 | name: 'Tinymce', | 41 | name: 'Tinymce', |
34 | inheritAttrs: false, | 42 | inheritAttrs: false, |
35 | props: basicProps, | 43 | props: basicProps, |
44 | + components: { ImgUpload }, | ||
36 | emits: ['change', 'update:modelValue'], | 45 | emits: ['change', 'update:modelValue'], |
37 | setup(props, { emit, attrs }) { | 46 | setup(props, { emit, attrs }) { |
38 | const editorRef = ref<any>(null); | 47 | const editorRef = ref<any>(null); |
39 | const tinymceId = ref<string>(snowUuid('tiny-vue')); | 48 | const tinymceId = ref<string>(snowUuid('tiny-vue')); |
40 | const elRef = ref<Nullable<HTMLElement>>(null); | 49 | const elRef = ref<Nullable<HTMLElement>>(null); |
41 | 50 | ||
51 | + const { prefixCls } = useDesign('tinymce-container'); | ||
52 | + | ||
42 | const tinymceContent = computed(() => { | 53 | const tinymceContent = computed(() => { |
43 | return props.modelValue; | 54 | return props.modelValue; |
44 | }); | 55 | }); |
@@ -140,7 +151,7 @@ | @@ -140,7 +151,7 @@ | ||
140 | bindHandlers(e, attrs, unref(editorRef)); | 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 | if ( | 155 | if ( |
145 | editor && | 156 | editor && |
146 | typeof val === 'string' && | 157 | typeof val === 'string' && |
@@ -179,45 +190,54 @@ | @@ -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 | return { | 213 | return { |
214 | + prefixCls, | ||
183 | containerWidth, | 215 | containerWidth, |
184 | initOptions, | 216 | initOptions, |
185 | tinymceContent, | 217 | tinymceContent, |
186 | tinymceScriptSrc, | 218 | tinymceScriptSrc, |
187 | elRef, | 219 | elRef, |
188 | tinymceId, | 220 | tinymceId, |
221 | + handleImageUploading, | ||
222 | + handleDone, | ||
223 | + editorRef, | ||
189 | }; | 224 | }; |
190 | }, | 225 | }, |
191 | }); | 226 | }); |
192 | </script> | 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 | </style> | 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 | import { PropType } from 'vue'; | 1 | import { PropType } from 'vue'; |
2 | +import { propTypes } from '/@/utils/propTypes'; | ||
2 | 3 | ||
3 | export const basicProps = { | 4 | export const basicProps = { |
4 | options: { | 5 | options: { |
5 | type: Object as PropType<any>, | 6 | type: Object as PropType<any>, |
6 | default: {}, | 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 | height: { | 12 | height: { |
18 | type: [Number, String] as PropType<string | number>, | 13 | type: [Number, String] as PropType<string | number>, |
@@ -26,4 +21,5 @@ export const basicProps = { | @@ -26,4 +21,5 @@ export const basicProps = { | ||
26 | required: false, | 21 | required: false, |
27 | default: 'auto', | 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,8 +9,13 @@ | ||
9 | 9 | ||
10 | <template #overlay> | 10 | <template #overlay> |
11 | <Menu @click="handleMenuClick"> | 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 | <MenuItem | 19 | <MenuItem |
15 | key="loginOut" | 20 | key="loginOut" |
16 | :text="t('layout.header.dropdownItemLoginOut')" | 21 | :text="t('layout.header.dropdownItemLoginOut')" |
src/layouts/default/header/index.vue
@@ -84,7 +84,6 @@ | @@ -84,7 +84,6 @@ | ||
84 | } from './components'; | 84 | } from './components'; |
85 | import { useAppInject } from '/@/hooks/web/useAppInject'; | 85 | import { useAppInject } from '/@/hooks/web/useAppInject'; |
86 | import { useDesign } from '/@/hooks/web/useDesign'; | 86 | import { useDesign } from '/@/hooks/web/useDesign'; |
87 | - import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; | ||
88 | 87 | ||
89 | export default defineComponent({ | 88 | export default defineComponent({ |
90 | name: 'LayoutHeader', | 89 | name: 'LayoutHeader', |
src/locales/lang/en/component/upload.ts