提交 3ad1a4f5 作者: vben

feat(tinymce): add image upload #170

上级 18ad1bcc
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
- 新增`PageWrapper`组件。并应用于示例页面 - 新增`PageWrapper`组件。并应用于示例页面
- 新增标签页折叠功能 - 新增标签页折叠功能
- 兼容旧版浏览器 - 兼容旧版浏览器
- tinymce 新增图片上传·
### 🐛 Bug Fixes ### 🐛 Bug Fixes
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
- 修复表格内存溢出问题 - 修复表格内存溢出问题
- 修复`layout` 收缩展开功能在分割模式下失效 - 修复`layout` 收缩展开功能在分割模式下失效
- 修复 modal 高度计算错误 - 修复 modal 高度计算错误
- 修复文件上传错误
### 🎫 Chores ### 🎫 Chores
......
<template> <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> <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea>
</div> </div>
</template> </template>
...@@ -24,6 +30,8 @@ ...@@ -24,6 +30,8 @@
import { bindHandlers } from './helper'; import { bindHandlers } from './helper';
import lineHeight from './lineHeight'; import lineHeight from './lineHeight';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'; 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'; const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';
...@@ -33,12 +41,15 @@ ...@@ -33,12 +41,15 @@
name: 'Tinymce', name: 'Tinymce',
inheritAttrs: false, inheritAttrs: false,
props: basicProps, props: basicProps,
components: { ImgUpload },
emits: ['change', 'update:modelValue'], emits: ['change', 'update:modelValue'],
setup(props, { emit, attrs }) { setup(props, { emit, attrs }) {
const editorRef = ref<any>(null); const editorRef = ref<any>(null);
const tinymceId = ref<string>(snowUuid('tiny-vue')); const tinymceId = ref<string>(snowUuid('tiny-vue'));
const elRef = ref<Nullable<HTMLElement>>(null); const elRef = ref<Nullable<HTMLElement>>(null);
const { prefixCls } = useDesign('tinymce-container');
const tinymceContent = computed(() => { const tinymceContent = computed(() => {
return props.modelValue; return props.modelValue;
}); });
...@@ -140,7 +151,7 @@ ...@@ -140,7 +151,7 @@
bindHandlers(e, attrs, unref(editorRef)); bindHandlers(e, attrs, unref(editorRef));
} }
function setValue(editor: any, val: string, prevVal: string) { function setValue(editor: Recordable, val: string, prevVal?: string) {
if ( if (
editor && editor &&
typeof val === 'string' && typeof val === 'string' &&
...@@ -179,45 +190,54 @@ ...@@ -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 { return {
prefixCls,
containerWidth, containerWidth,
initOptions, initOptions,
tinymceContent, tinymceContent,
tinymceScriptSrc, tinymceScriptSrc,
elRef, elRef,
tinymceId, tinymceId,
handleImageUploading,
handleDone,
editorRef,
}; };
}, },
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped></style>
.tinymce-container {
position: relative;
line-height: normal;
.mce-fullscreen {
z-index: 10000;
}
}
.editor-custom-btn-container {
position: absolute;
top: 6px;
right: 6px;
&.fullscreen { <style lang="less">
position: fixed; @prefix-cls: ~'@{namespace}-tinymce-container';
z-index: 10000;
}
}
.editor-upload-btn { .@{prefix-cls} {
display: inline-block; position: relative;
} line-height: normal;
textarea { textarea {
z-index: -1; z-index: -1;
visibility: hidden; visibility: hidden;
} }
}
</style> </style>
<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>
import { PropType } from 'vue'; import { PropType } from 'vue';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = { export const basicProps = {
options: { options: {
type: Object as PropType<any>, type: Object as PropType<any>,
default: {}, default: {},
}, },
value: { value: propTypes.string,
type: String as PropType<string>, modelValue: propTypes.string,
// default: ''
},
modelValue: {
type: String as PropType<string>,
// default: ''
},
// 高度 // 高度
height: { height: {
type: [Number, String] as PropType<string | number>, type: [Number, String] as PropType<string | number>,
...@@ -26,4 +21,5 @@ export const basicProps = { ...@@ -26,4 +21,5 @@ export const basicProps = {
required: false, required: false,
default: 'auto', default: 'auto',
}, },
showImageUpload: propTypes.bool.def(true),
}; };
...@@ -9,8 +9,13 @@ ...@@ -9,8 +9,13 @@
<template #overlay> <template #overlay>
<Menu @click="handleMenuClick"> <Menu @click="handleMenuClick">
<MenuItem key="doc" :text="t('layout.header.dropdownItemDoc')" icon="gg:loadbar-doc" /> <MenuItem
<MenuDivider v-if="getShowDoc" /> key="doc"
:text="t('layout.header.dropdownItemDoc')"
icon="gg:loadbar-doc"
v-if="getShowDoc"
/>
<MenuDivider />
<MenuItem <MenuItem
key="loginOut" key="loginOut"
:text="t('layout.header.dropdownItemLoginOut')" :text="t('layout.header.dropdownItemLoginOut')"
......
...@@ -84,7 +84,6 @@ ...@@ -84,7 +84,6 @@
} from './components'; } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject'; import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default defineComponent({ export default defineComponent({
name: 'LayoutHeader', name: 'LayoutHeader',
......
export default { export default {
save: 'Save', save: 'Save',
upload: 'Upload', upload: 'Upload',
imgUpload: 'ImageUpload',
uploaded: 'Uploaded', uploaded: 'Uploaded',
operating: 'Operating', operating: 'Operating',
......
export default { export default {
save: '保存', save: '保存',
upload: '上传', upload: '上传',
imgUpload: '图片上传',
uploaded: '已上传', uploaded: '已上传',
operating: '操作', operating: '操作',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论