Jelajahi Sumber

feat(tinymce): add image upload #170

vben 4 tahun lalu
induk
melakukan
3ad1a4f5a6

+ 2 - 0
CHANGELOG.zh_CN.md

@@ -16,6 +16,7 @@
 - 新增`PageWrapper`组件。并应用于示例页面
 - 新增标签页折叠功能
 - 兼容旧版浏览器
+- tinymce 新增图片上传·
 
 ### 🐛 Bug Fixes
 
@@ -24,6 +25,7 @@
 - 修复表格内存溢出问题
 - 修复`layout` 收缩展开功能在分割模式下失效
 - 修复 modal 高度计算错误
+- 修复文件上传错误
 
 ### 🎫 Chores
 

+ 46 - 26
src/components/Tinymce/src/Editor.vue

@@ -1,5 +1,11 @@
 <template>
-  <div class="tinymce-container" :style="{ width: containerWidth }">
+  <div :class="prefixCls" :style="{ width: containerWidth }">
+    <ImgUpload
+      @uploading="handleImageUploading"
+      @done="handleDone"
+      v-if="showImageUpload"
+      v-show="editorRef"
+    />
     <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea>
   </div>
 </template>
@@ -24,6 +30,8 @@
   import { bindHandlers } from './helper';
   import lineHeight from './lineHeight';
   import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
+  import ImgUpload from './ImgUpload.vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
 
   const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';
 
@@ -33,12 +41,15 @@
     name: 'Tinymce',
     inheritAttrs: false,
     props: basicProps,
+    components: { ImgUpload },
     emits: ['change', 'update:modelValue'],
     setup(props, { emit, attrs }) {
       const editorRef = ref<any>(null);
       const tinymceId = ref<string>(snowUuid('tiny-vue'));
       const elRef = ref<Nullable<HTMLElement>>(null);
 
+      const { prefixCls } = useDesign('tinymce-container');
+
       const tinymceContent = computed(() => {
         return props.modelValue;
       });
@@ -140,7 +151,7 @@
         bindHandlers(e, attrs, unref(editorRef));
       }
 
-      function setValue(editor: any, val: string, prevVal: string) {
+      function setValue(editor: Recordable, val: string, prevVal?: string) {
         if (
           editor &&
           typeof val === 'string' &&
@@ -179,45 +190,54 @@
         });
       }
 
+      function handleImageUploading(name: string) {
+        const editor = unref(editorRef);
+        if (!editor) return;
+        const content = editor?.getContent() ?? '';
+        setValue(editor, `${content}\n${getImgName(name)}`);
+      }
+
+      function handleDone(name: string, url: string) {
+        const editor = unref(editorRef);
+        if (!editor) return;
+
+        const content = editor?.getContent() ?? '';
+        const val = content?.replace(getImgName(name), `<img src="${url}"/>`) ?? '';
+        setValue(editor, val);
+      }
+
+      function getImgName(name: string) {
+        return `[uploading:${name}]`;
+      }
+
       return {
+        prefixCls,
         containerWidth,
         initOptions,
         tinymceContent,
         tinymceScriptSrc,
         elRef,
         tinymceId,
+        handleImageUploading,
+        handleDone,
+        editorRef,
       };
     },
   });
 </script>
 
-<style lang="less" scoped>
-  .tinymce-container {
-    position: relative;
-    line-height: normal;
+<style lang="less" scoped></style>
 
-    .mce-fullscreen {
-      z-index: 10000;
-    }
-  }
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-tinymce-container';
 
-  .editor-custom-btn-container {
-    position: absolute;
-    top: 6px;
-    right: 6px;
+  .@{prefix-cls} {
+    position: relative;
+    line-height: normal;
 
-    &.fullscreen {
-      position: fixed;
-      z-index: 10000;
+    textarea {
+      z-index: -1;
+      visibility: hidden;
     }
   }
-
-  .editor-upload-btn {
-    display: inline-block;
-  }
-
-  textarea {
-    z-index: -1;
-    visibility: hidden;
-  }
 </style>

+ 72 - 0
src/components/Tinymce/src/ImgUpload.vue

@@ -0,0 +1,72 @@
+<template>
+  <div :class="prefixCls">
+    <Upload
+      name="file"
+      multiple
+      @change="handleChange"
+      :action="uploadUrl"
+      :showUploadList="false"
+      accept=".jpg,.jpeg,.gif,.png,.webp"
+    >
+      <a-button type="primary">{{ t('component.upload.imgUpload') }}</a-button>
+    </Upload>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import { Upload } from 'ant-design-vue';
+  import { InboxOutlined } from '@ant-design/icons-vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { useI18n } from '/@/hooks/web/useI18n';
+
+  export default defineComponent({
+    name: 'TinymceImageUpload',
+    components: { Upload, InboxOutlined },
+    emits: ['uploading', 'done', 'error'],
+    setup(_, { emit }) {
+      let uploading = false;
+
+      const { uploadUrl } = useGlobSetting();
+      const { t } = useI18n();
+      const { prefixCls } = useDesign('tinymce-img-upload');
+      function handleChange(info: Recordable) {
+        const file = info.file;
+        const status = file?.status;
+
+        const url = file?.response?.url;
+        const name = file?.name;
+        if (status === 'uploading') {
+          if (!uploading) {
+            emit('uploading', name);
+            uploading = true;
+          }
+        } else if (status === 'done') {
+          emit('done', name, url);
+          uploading = false;
+        } else if (status === 'error') {
+          emit('error');
+          uploading = false;
+        }
+      }
+
+      return {
+        prefixCls,
+        handleChange,
+        uploadUrl,
+        t,
+      };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  @prefix-cls: ~'@{namespace}-tinymce-img-upload';
+
+  .@{prefix-cls} {
+    position: absolute;
+    top: 4px;
+    right: 10px;
+    z-index: 20;
+  }
+</style>

+ 4 - 8
src/components/Tinymce/src/props.ts

@@ -1,18 +1,13 @@
 import { PropType } from 'vue';
+import { propTypes } from '/@/utils/propTypes';
 
 export const basicProps = {
   options: {
     type: Object as PropType<any>,
     default: {},
   },
-  value: {
-    type: String as PropType<string>,
-    // default: ''
-  },
-  modelValue: {
-    type: String as PropType<string>,
-    // default: ''
-  },
+  value: propTypes.string,
+  modelValue: propTypes.string,
   // 高度
   height: {
     type: [Number, String] as PropType<string | number>,
@@ -26,4 +21,5 @@ export const basicProps = {
     required: false,
     default: 'auto',
   },
+  showImageUpload: propTypes.bool.def(true),
 };

+ 7 - 2
src/layouts/default/header/components/user-dropdown/index.vue

@@ -9,8 +9,13 @@
 
     <template #overlay>
       <Menu @click="handleMenuClick">
-        <MenuItem key="doc" :text="t('layout.header.dropdownItemDoc')" icon="gg:loadbar-doc" />
-        <MenuDivider v-if="getShowDoc" />
+        <MenuItem
+          key="doc"
+          :text="t('layout.header.dropdownItemDoc')"
+          icon="gg:loadbar-doc"
+          v-if="getShowDoc"
+        />
+        <MenuDivider />
         <MenuItem
           key="loginOut"
           :text="t('layout.header.dropdownItemLoginOut')"

+ 0 - 1
src/layouts/default/header/index.vue

@@ -84,7 +84,6 @@
   } from './components';
   import { useAppInject } from '/@/hooks/web/useAppInject';
   import { useDesign } from '/@/hooks/web/useDesign';
-  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 
   export default defineComponent({
     name: 'LayoutHeader',

+ 1 - 0
src/locales/lang/en/component/upload.ts

@@ -1,6 +1,7 @@
 export default {
   save: 'Save',
   upload: 'Upload',
+  imgUpload: 'ImageUpload',
   uploaded: 'Uploaded',
 
   operating: 'Operating',

+ 1 - 0
src/locales/lang/zh_CN/component/upload.ts

@@ -1,6 +1,7 @@
 export default {
   save: '保存',
   upload: '上传',
+  imgUpload: '图片上传',
   uploaded: '已上传',
 
   operating: '操作',