mirror of
https://github.com/vbenjs/gf-vben-admin.git
synced 2025-01-23 11:50:20 +08:00
feat(tinymce): add image upload #170
This commit is contained in:
parent
18ad1bcc6e
commit
3ad1a4f5a6
@ -16,6 +16,7 @@
|
||||
- 新增`PageWrapper`组件。并应用于示例页面
|
||||
- 新增标签页折叠功能
|
||||
- 兼容旧版浏览器
|
||||
- tinymce 新增图片上传·
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@ -24,6 +25,7 @@
|
||||
- 修复表格内存溢出问题
|
||||
- 修复`layout` 收缩展开功能在分割模式下失效
|
||||
- 修复 modal 高度计算错误
|
||||
- 修复文件上传错误
|
||||
|
||||
### 🎫 Chores
|
||||
|
||||
|
@ -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 {
|
||||
<style lang="less" scoped></style>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-tinymce-container';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
|
||||
.mce-fullscreen {
|
||||
z-index: 10000;
|
||||
textarea {
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-custom-btn-container {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-upload-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
textarea {
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
72
src/components/Tinymce/src/ImgUpload.vue
Normal file
72
src/components/Tinymce/src/ImgUpload.vue
Normal file
@ -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>
|
@ -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),
|
||||
};
|
||||
|
@ -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')"
|
||||
|
@ -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,6 +1,7 @@
|
||||
export default {
|
||||
save: 'Save',
|
||||
upload: 'Upload',
|
||||
imgUpload: 'ImageUpload',
|
||||
uploaded: 'Uploaded',
|
||||
|
||||
operating: 'Operating',
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default {
|
||||
save: '保存',
|
||||
upload: '上传',
|
||||
imgUpload: '图片上传',
|
||||
uploaded: '已上传',
|
||||
|
||||
operating: '操作',
|
||||
|
Loading…
Reference in New Issue
Block a user