Commit 3ad1a4f5a69b4242d55e6bc17aceab7279241e14

Authored by vben
1 parent 18ad1bcc

feat(tinymce): add image upload #170

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
1 export default { 1 export default {
2 save: 'Save', 2 save: 'Save',
3 upload: 'Upload', 3 upload: 'Upload',
  4 + imgUpload: 'ImageUpload',
4 uploaded: 'Uploaded', 5 uploaded: 'Uploaded',
5 6
6 operating: 'Operating', 7 operating: 'Operating',
src/locales/lang/zh_CN/component/upload.ts
1 export default { 1 export default {
2 save: '保存', 2 save: '保存',
3 upload: '上传', 3 upload: '上传',
  4 + imgUpload: '图片上传',
4 uploaded: '已上传', 5 uploaded: '已上传',
5 6
6 operating: '操作', 7 operating: '操作',