1006 lines
34 KiB
Vue
1006 lines
34 KiB
Vue
<template>
|
||
<view class="editor-container">
|
||
<view class="editor" :style="{height: editorHeight <= 0 ? 'auto': `${editorHeight}px`}">
|
||
<!-- #ifdef H5 -->
|
||
<web-editor
|
||
id="editor"
|
||
@ready="onEditorReady"
|
||
@statuschange="onStatusChange"
|
||
@textchange="(e) => $emit('textchange', e)"
|
||
showImgSize
|
||
showImgToolbar
|
||
showImgResize
|
||
:upload-handler="uploadHandlerForH5"
|
||
@focus="onEditorFocus"
|
||
@blur="onEditorBlur"
|
||
></web-editor>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifndef H5 -->
|
||
<editor
|
||
id="editor"
|
||
class="ql-container"
|
||
placeholder="输入内容"
|
||
@ready="onEditorReady"
|
||
@statuschange="onStatusChange"
|
||
showImgSize
|
||
showImgToolbar
|
||
showImgResize
|
||
@focus="onEditorFocus"
|
||
@blur="onEditorBlur"
|
||
></editor>
|
||
<!-- #endif -->
|
||
</view>
|
||
<view class="editor-toolbar pc">
|
||
<view class="editor-toolbar-box">
|
||
<tool-undo @change="({type, value}) => format(type, value)" :disabled="!showFooterToolBar"></tool-undo>
|
||
<tool-redo @change="({type, value}) => format(type, value)" :disabled="!showFooterToolBar"></tool-redo>
|
||
<tool-format-clear @change="({type, value}) => format(type, value)"
|
||
:disabled="!showFooterToolBar"></tool-format-clear>
|
||
<view class="editor-toolbar-divider"></view>
|
||
<tool-header @change="({type, value}) => format(type, value)" :active="formats.header"
|
||
:disabled="!showFooterToolBar"></tool-header>
|
||
<tool-font-size @change="({type, value}) => format(type, value)" :active="formats.fontSize"
|
||
:disabled="!showFooterToolBar"></tool-font-size>
|
||
<tool-bold @change="({type, value}) => format(type, value)" :active="formats.bold"
|
||
:disabled="!showFooterToolBar"></tool-bold>
|
||
<tool-italic @change="({type, value}) => format(type, value)" :active="formats.italic"
|
||
:disabled="!showFooterToolBar"></tool-italic>
|
||
<tool-strike @change="({type, value}) => format(type, value)" :active="formats.strike"
|
||
:disabled="!showFooterToolBar"></tool-strike>
|
||
<tool-underline @change="({type, value}) => format(type, value)" :active="formats.underline"
|
||
:disabled="!showFooterToolBar"></tool-underline>
|
||
<tool-link @change="({type, value}) => format(type, value)" :active="formats.link"
|
||
:disabled="!showFooterToolBar"></tool-link>
|
||
<tool-align @change="({type, value}) => format(type, value)" :active="formats.align"
|
||
:disabled="!showFooterToolBar"></tool-align>
|
||
<tool-hr @change="({type, value}) => format(type, value)" :disabled="!showFooterToolBar"></tool-hr>
|
||
<tool-list @change="({type, value}) => format(type, value)" :active="formats.list"
|
||
:disabled="!showFooterToolBar"></tool-list>
|
||
<tool-line-indent @change="({type, value}) => format(type, value)" :active="formats.textIndent"
|
||
:disabled="!showFooterToolBar"></tool-line-indent>
|
||
<tool-space-both @change="({type, value}) => format(type, value)"
|
||
:active="formats.marginLeft && formats.marginRight"
|
||
:disabled="!showFooterToolBar"></tool-space-both>
|
||
<tool-line-height @change="({type, value}) => format(type, value)"
|
||
:active="formats.lineHeight" :disabled="!showFooterToolBar"></tool-line-height>
|
||
<tool-letter-space @change="({type, value}) => format(type, value)"
|
||
:active="formats.letterSpacing" :disabled="!showFooterToolBar"></tool-letter-space>
|
||
<view class="editor-toolbar-divider"></view>
|
||
<tool-background @change="({type, value}) => format(type, value)"
|
||
:active="formats.background" :disabled="!showFooterToolBar"></tool-background>
|
||
<tool-color @change="({type, value}) => format(type, value)" :active="formats.color"
|
||
:disabled="!showFooterToolBar"></tool-color>
|
||
<view class="editor-toolbar-divider"></view>
|
||
<tool-image @change="({type, value}) => format(type, value)" :disabled="!showFooterToolBar"></tool-image>
|
||
<tool-video @change="({type, value}) => format(type, value)" :disabled="!showFooterToolBar"></tool-video>
|
||
<!-- <tool-unlock-content @change="({type, value}) => format(type, value)"
|
||
:disabled="!showFooterToolBar"></tool-unlock-content> -->
|
||
<tool-ai @change="({type, value}) => format(type, value)" :active="showImChat"
|
||
:disabled="!showFooterToolBar"></tool-ai>
|
||
</view>
|
||
</view>
|
||
<view class="editor-toolbar m" :hidden="!showFooterToolBar"
|
||
:style="{transform: `translateY(-${keyboardHeight}px)`}" @touchend.stop="">
|
||
<view class="editor-toolbar-tools">
|
||
<view class="tool" @click="showInsertBlockView">
|
||
<uni-icons type="plus" size="60rpx"/>
|
||
</view>
|
||
<view class="editor-toolbar-divider"></view>
|
||
<view :class="{tool: true, active: showToolPopup === 'm-tool-header'}"
|
||
@click="showToolPopupView('m-tool-header')" id="m-tool-header">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-text-group" size="54rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: showToolPopup === 'm-tool-text'}" @click="showToolPopupView('m-tool-text')"
|
||
id="m-tool-text">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-attr-group" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: showToolPopup === 'm-tool-align'}" @click="showToolPopupView('m-tool-align')"
|
||
id="m-tool-align">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-align-left" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.link}" @click="showLinkPopup(formats.link)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-link" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view class="tool" @click="() => $refs.insertImageDrawer.open()">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-image" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true}" @click="format('ai')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-ai" size="60rpx" color="#b454ff"></uni-icons>
|
||
</view>
|
||
<view class="editor-toolbar-divider"></view>
|
||
<view class="tool hide-keyboard" @click="hideKeyboard()">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-jianpan" size="70rpx"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view
|
||
class="editor-toolbar-popup"
|
||
id="m-tool-header-popup"
|
||
:class="{show: showToolPopup === 'm-tool-header'}"
|
||
:style="{left: `${toolPopupRect.left}px`, right: `${toolPopupRect.right}px`}"
|
||
>
|
||
<view :class="{tool: true, active: formats.header === 1}" @click="format('header', 1)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-header1" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.header === 2}" @click="format('header', 2)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-header2" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.header === 3}" @click="format('header', 3)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-header3" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.header === 4}" @click="format('header', 4)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-header4" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.header === 5}" @click="format('header', 5)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-header5" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.header === 6}" @click="format('header', 6)">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-header6" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.list === 'ordered'}" @click="format('list', 'ordered')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-ul" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.list === 'bullet'}" @click="format('list', 'bullet')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-ol" size="60rpx"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view
|
||
class="editor-toolbar-popup"
|
||
id="m-tool-text-popup"
|
||
:class="{show: showToolPopup === 'm-tool-text'}"
|
||
:style="{left: `${toolPopupRect.left}px`, right: `${toolPopupRect.right}px`}"
|
||
>
|
||
<view :class="{tool: true, active: formats.bold}" @click="format('bold')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-bold" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.italic}" @click="format('italic')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-italic" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.underline}" @click="format('underline')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-underline" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.strike}" @click="format('strike')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-strike" size="60rpx"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view
|
||
class="editor-toolbar-popup"
|
||
id="m-tool-align-popup"
|
||
:class="{show: showToolPopup === 'm-tool-align'}"
|
||
:style="{left: `${toolPopupRect.left}px`, right: `${toolPopupRect.right}px`}"
|
||
>
|
||
<view :class="{tool: true, active: formats.indent}" @click="format('indent', formats.indent ? '-1' : '+1')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-line-indent" size="48rpx"></uni-icons>
|
||
</view>
|
||
<view class="editor-toolbar-popup-divider"></view>
|
||
<view :class="{tool: true, active: formats.align === 'left'}" @click="format('align', 'left')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-align-left" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.align === 'center'}" @click="format('align', 'center')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-align-center" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.align === 'right'}" @click="format('align', 'right')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-align-right" size="60rpx"></uni-icons>
|
||
</view>
|
||
<view :class="{tool: true, active: formats.align === 'justify'}" @click="format('align', 'justify')">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-align-justify" size="60rpx"></uni-icons>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<uni-transition :mode-class="['slide-bottom', 'fade']" :show="showInsertBlockToolSetting" class="tool-setting">
|
||
<view>
|
||
<view class="tool-setting-header">
|
||
<view class="close" @click="closeToolSetting">
|
||
<uni-icons type="closeempty" size="40rpx" class="icon"/>
|
||
</view>
|
||
<text>添加</text>
|
||
</view>
|
||
<view class="body insert-block">
|
||
<view class="item" @click="() => $refs.insertImageDrawer.open()">
|
||
<view class="icon">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-image" size="70rpx"></uni-icons>
|
||
</view>
|
||
<text class="text">图片</text>
|
||
</view>
|
||
<view class="item" @click="format('hr')">
|
||
<view class="icon">
|
||
<uni-icons custom-prefix="editor-icon" type="icon-hr" size="70rpx"></uni-icons>
|
||
</view>
|
||
<text class="text">分割线</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</uni-transition>
|
||
<uni-transition :show="showToolSettingMask">
|
||
<view class="tool-setting-mask"></view>
|
||
</uni-transition>
|
||
|
||
<uni-popup ref="popup" type="center">
|
||
<view class="popup-body" @touchend.stop="">
|
||
<!-- #ifndef H5 -->
|
||
<view class="tip">
|
||
<text>由于平台限制,插入超链接将转换为Markdown格式,不影响内容展示</text>
|
||
</view>
|
||
<!-- #endif -->
|
||
<uni-forms label-width="90px">
|
||
<uni-forms-item label="链接名称" name="link">
|
||
<uni-easyinput v-model="currentLink.title"/>
|
||
</uni-forms-item>
|
||
<uni-forms-item label="链接地址" name="link">
|
||
<uni-easyinput v-model="currentLink.url"/>
|
||
</uni-forms-item>
|
||
</uni-forms>
|
||
<view class="popup-body-btn-group">
|
||
<button class="btn" size="mini" @click="$refs.popup.close()">取消</button>
|
||
<button class="btn" size="mini" type="primary" @click="linkChange">确定</button>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<view class="uni-im-chat-components" :hidden="!showImChat">
|
||
<view class="close-bar">
|
||
<view class="head">
|
||
<text class="title">ai</text>
|
||
<text class="desc">输入prompt提示词后AI根据你的输入生成文章</text>
|
||
</view>
|
||
<view class="close" @click="showImChat=false">
|
||
<uni-icons type="closeempty" size="40rpx" class="icon"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<uni-im-chat ref="uniImChat"></uni-im-chat>
|
||
</view>
|
||
|
||
<uni-drawer
|
||
class="insert-image-drawer"
|
||
v-if="drawerWidth"
|
||
ref="insertImageDrawer"
|
||
mode="right"
|
||
:width="drawerWidth"
|
||
>
|
||
<uni-media-library
|
||
mode="picker"
|
||
:selected-count="1"
|
||
:media-tabs="['image']"
|
||
@onInsert="onInsertImage"
|
||
></uni-media-library>
|
||
</uni-drawer>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 导入编辑器相关组件
|
||
// 导入工具栏中的空格、行高、字间距、背景、图片、清除格式、解锁内容、AI等组件。同时导入WebEditor组件。
|
||
import ToolColor from "./tools/color.vue";
|
||
import ToolBold from "./tools/bold.vue";
|
||
import ToolItalic from "./tools/italic.vue";
|
||
import ToolStrike from "./tools/strike.vue";
|
||
import ToolList from "./tools/list.vue";
|
||
import ToolHr from "./tools/hr.vue";
|
||
import ToolUndo from "./tools/undo.vue";
|
||
import ToolRedo from "./tools/redo.vue";
|
||
import ToolHeader from "./tools/header.vue";
|
||
import ToolAlign from "./tools/align.vue";
|
||
import ToolLink from "./tools/link.vue";
|
||
import ToolUnderline from "./tools/underline.vue";
|
||
import ToolLineIndent from "./tools/line-indent.vue";
|
||
import ToolSpaceBoth from "./tools/space-both.vue";
|
||
import ToolLineHeight from "./tools/line-height.vue";
|
||
import ToolLetterSpace from "./tools/letter-space.vue";
|
||
import ToolBackground from "./tools/background.vue";
|
||
import ToolImage from "./tools/image.vue";
|
||
import ToolFormatClear from "./tools/format-clear.vue";
|
||
import ToolUnlockContent from "./tools/unlock-content.vue";
|
||
import ToolAi from "./tools/ai.vue";
|
||
import ToolVideo from "./tools/video.vue";
|
||
import uniImChat from '../ai/chat.vue';
|
||
import ToolFontSize from './tools/font-size.vue'
|
||
|
||
// #ifdef H5
|
||
import WebEditor from "./web/editor.vue";
|
||
// #endif
|
||
|
||
export default {
|
||
name: "editor",
|
||
emits: ['change', 'textchange'],
|
||
components: {
|
||
ToolBackground,
|
||
ToolLetterSpace,
|
||
ToolLineHeight,
|
||
ToolLineIndent,
|
||
ToolUnderline,
|
||
ToolAlign,
|
||
ToolUndo,
|
||
ToolItalic,
|
||
ToolBold,
|
||
ToolStrike,
|
||
ToolList,
|
||
ToolHr,
|
||
ToolRedo,
|
||
ToolHeader,
|
||
ToolLink,
|
||
ToolSpaceBoth,
|
||
ToolImage,
|
||
ToolColor,
|
||
ToolFormatClear,
|
||
ToolUnlockContent,
|
||
ToolAi,
|
||
ToolVideo,
|
||
uniImChat,
|
||
ToolFontSize,
|
||
// #ifdef H5
|
||
WebEditor,
|
||
// #endif
|
||
},
|
||
data() {
|
||
return {
|
||
user_id:"",
|
||
formats: {}, // 编辑器格式状态
|
||
keyboardHeight: 0, // 键盘高度
|
||
showFooterToolBar: false, // 是否显示底部工具栏
|
||
showToolSettingMask: false, // 是否显示工具栏设置遮罩层
|
||
showInsertBlockToolSetting: false, // 是否显示插入块工具栏
|
||
showToolPopup: '', // 当前工具栏弹出的工具名称,为空则表示没有弹出的工具
|
||
toolPopupRect: {}, // 弹出工具栏的位置和大小
|
||
showImChat: false, // 是否显示 AI 聊天窗口
|
||
currentLink: { // 当前链接的信息
|
||
title: '', // 链接标题
|
||
url: '' // 链接URL
|
||
},
|
||
drawerWidth: 0, // 插入图片抽屉宽度
|
||
}
|
||
},
|
||
computed: {
|
||
// `editorHeight`计算属性,返回编辑器高度。当键盘弹出时,编辑器高度为键盘高度减去工具栏高度,否则为窗口高度减去工具栏高度
|
||
editorHeight() {
|
||
// toolbar 默认高度
|
||
const toolbarHeight = 150
|
||
let toolHeight = 0
|
||
|
||
// 如果有弹出框,设置弹出框高度为50
|
||
if (this.showToolPopup !== '') {
|
||
toolHeight = 50
|
||
}
|
||
// 如果键盘高度大于0,返回窗口高度减去键盘高度减去工具栏高度减去工具栏弹出框高度,否则返回0。
|
||
return this.keyboardHeight > 0 ? (this.systemInfo.windowHeight - this.keyboardHeight - toolbarHeight - toolHeight) : 0
|
||
}
|
||
},
|
||
watch: {
|
||
// 监听`showFooterToolBar`属性,当值为false时,清空弹出工具栏名称和弹出工具栏位置信息
|
||
showFooterToolBar(newValue) {
|
||
if (!newValue) {
|
||
this.showToolPopup = ''
|
||
this.toolPopupRect = {}
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
// 调用`uni.getSystemInfoSync()`获取系统信息,保存到`systemInfo`属性中。
|
||
this.systemInfo = uni.getSystemInfoSync()
|
||
// 设置插入图片抽屉宽度为窗口宽度的80%
|
||
this.drawerWidth = this.systemInfo.windowWidth * .8
|
||
},
|
||
async onReady() {
|
||
let res = await db.collection('uni-id-users')
|
||
.where('_id==$cloudEnv_uid')
|
||
.field('username,nickname,mobile,email,role,permission')
|
||
.get();
|
||
this.user_id=res.result.data[0]._id;
|
||
// if (res.result.data[0].role.includes("admin") === false) {
|
||
// this.where = {
|
||
// user_id: res.result.data[0]._id
|
||
// };
|
||
// } else {
|
||
|
||
// }
|
||
|
||
|
||
},
|
||
methods: {
|
||
// 编辑器加载完成时触发
|
||
onEditorReady(editorCtx) {
|
||
// 通过 #ifdef 条件编译根据不同的平台执行不同的操作
|
||
|
||
// #ifdef H5
|
||
// // 在 H5 平台上,将编辑器的 `editorCtx` 对象保存到组件的 `editorCtx` 属性中,并触发 `ready` 事件,将 `editorCtx` 对象作为参数传递给父组件。
|
||
if (editorCtx) {
|
||
this.editorCtx = editorCtx
|
||
this.$emit('ready', editorCtx)
|
||
}
|
||
// // 在 H5 平台上,为了实现键盘弹出时自动调整编辑器高度,监听 `window.visualViewport` 对象的 `resize` 事件,并绑定 `resizeHandler` 方法。
|
||
window.visualViewport.addEventListener('resize', resizeHandler.bind(this));
|
||
|
||
function resizeHandler() {
|
||
// 获取窗口高度和 `visualViewport` 高度
|
||
const innerHeight = window.innerHeight
|
||
const visualViewportHeight = window.visualViewport.height
|
||
|
||
// 如果键盘高度大于0,调用 `this.onKeyboardHeightChange()` 方法,传入一个对象作为参数,对象包含 `height` 和 `duration` 两个属性,分别表示键盘高度和键盘弹出动画的持续时间,否则不做任何操作。
|
||
this.onKeyboardHeightChange({
|
||
height: parseInt(innerHeight - visualViewportHeight),
|
||
duration: 0
|
||
})
|
||
}
|
||
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// 在非 H5 平台上,调用 `this.createSelectorQuery()` 方法获取编辑器的 `editorCtx` 对象,并将其保存到组件的 `editorCtx` 属性中。
|
||
this.createSelectorQuery().select('#editor').context((res) => {
|
||
this.editorCtx = res.context
|
||
// 将 `editorCtx` 对象作为参数传递给父组件。
|
||
this.$emit('ready', res.context)
|
||
}).exec()
|
||
// 监听键盘高度变化事件
|
||
uni.onKeyboardHeightChange(this.onKeyboardHeightChange.bind(this))
|
||
// #endif
|
||
},
|
||
// 监听键盘高度变化事件
|
||
onKeyboardHeightChange(res) {
|
||
// 如果键盘高度等于 `this.keyboardHeight`,则直接返回。
|
||
if (res.height < 20 || res.height === this.keyboardHeight) return
|
||
// 根据 `res.height` 的值设置 `duration` 变量的值,如果 `res.height` 大于 0,则将 `duration` 的值设置为 `res.duration * 1000`,否则将其设置为 0。
|
||
const duration = res.height > 0 ? res.duration * 1000 : 0
|
||
// 调用 `setTimeout` 函数,将 `uni.pageScrollTo` 方法的调用放入异步任务队列中,以确保在页面滚动到顶部后再更新编辑器的位置。
|
||
setTimeout(() => {
|
||
uni.pageScrollTo({
|
||
scrollTop: 0,
|
||
success: () => {
|
||
// 在页面滚动到顶部后,调用 `this.updatePosition()` 方法,传入 `res.height` 作为参数,更新编辑器的位置。
|
||
this.updatePosition(res.height)
|
||
// 调用 `this.editorCtx.scrollIntoView()` 方法,将编辑器滚动到可视区域内。
|
||
setTimeout(() => this.editorCtx.scrollIntoView(), 0)
|
||
}
|
||
})
|
||
}, duration)
|
||
},
|
||
// 更新键盘高度
|
||
updatePosition(keyboardHeight) {
|
||
this.keyboardHeight = keyboardHeight
|
||
},
|
||
// 显示工具栏弹出层
|
||
showToolPopupView(id) {
|
||
uni.createSelectorQuery().in(this).select('#' + id).boundingClientRect(btnData => {
|
||
uni.createSelectorQuery().in(this).select('#' + id + '-popup').boundingClientRect(popupData => {
|
||
const center = popupData.width / 2
|
||
|
||
// 根据位置信息计算出工具栏弹出层的位置,始终将弹出层显示在最中间,将位置信息保存到组件的 `toolPopupRect` 属性中
|
||
if (btnData.left > center) {
|
||
this.toolPopupRect.left = (btnData.left - center) + btnData.width / 2
|
||
} else {
|
||
this.toolPopupRect.left = 10
|
||
}
|
||
if (popupData.width > this.systemInfo.windowWidth) {
|
||
this.toolPopupRect.right = 10
|
||
}
|
||
|
||
// 根据传递的id参数,设置组件的 `showToolPopup` 属性的值,如果 `showToolPopup` 属性的值等于传递的id参数,则将其设置为空字符串(隐藏工具栏弹出层),否则将其设置为传递的id参数(显示工具栏弹出层)。
|
||
this.showToolPopup = this.showToolPopup !== id ? id : ''
|
||
// 如果 `showToolPopup` 属性的值为空字符串,则将组件的 `toolPopupRect` 属性设置为空对象,清空工具栏弹出层位置信息。
|
||
if (this.showToolPopup === '') {
|
||
this.toolPopupRect = {}
|
||
}
|
||
}).exec();
|
||
}).exec();
|
||
},
|
||
// 显示插入块元素工具栏
|
||
showInsertBlockView() {
|
||
this.editorCtx.blur()
|
||
uni.hideKeyboard()
|
||
this.showToolSettingMask = true
|
||
this.showInsertBlockToolSetting = true
|
||
this.showToolPopup = false
|
||
this.toolPopupRect = {}
|
||
|
||
},
|
||
// 关闭插入块元素工具栏
|
||
closeToolSetting() {
|
||
this.showToolSettingMask = false
|
||
this.showInsertBlockToolSetting = false
|
||
},
|
||
// 编辑器辑器状态改变时触发
|
||
onStatusChange(e) {
|
||
this.formats = e.detail
|
||
},
|
||
// 编辑器获取焦点时触发
|
||
onEditorFocus() {
|
||
// 显示工具栏
|
||
this.showFooterToolBar = true
|
||
},
|
||
// 编辑器失去焦点时触发
|
||
onEditorBlur() {
|
||
// 将键盘高度设置为 0
|
||
this.updatePosition(0)
|
||
// 隐藏工具栏
|
||
this.showFooterToolBar = false
|
||
},
|
||
// 手动隐藏键盘
|
||
hideKeyboard() {
|
||
this.editorCtx.blur()
|
||
},
|
||
// 设置编辑器格式
|
||
format(name, value) {
|
||
// name 不存在时,不设置编辑器格式
|
||
if (!name || !this.editorCtx) return
|
||
|
||
// 根据传递的 name 和 value 参数,设置编辑器的格式。
|
||
switch (name) {
|
||
case 'hr':
|
||
// 插入分割线
|
||
this.editorCtx.insertDivider()
|
||
break
|
||
case 'undo':
|
||
// 撤销操作
|
||
this.editorCtx.undo()
|
||
break
|
||
case 'redo':
|
||
// 重做操作
|
||
this.editorCtx.redo()
|
||
break
|
||
case 'link':
|
||
// 插入链接,由于小程序端不支持插入链接,所以在小程序端通过 insertText 方法插入 markdown 格式的链接。在 H5 端,通过 format 方法插入链接。
|
||
// #ifdef H5
|
||
this.editorCtx.format('link', value)
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
this.editorCtx.insertText({
|
||
text: `[${value.text}](${value.link})`
|
||
})
|
||
// #endif
|
||
break
|
||
case 'space-both':
|
||
// 设置两端缩进左右间距
|
||
this.editorCtx.format('marginLeft', value)
|
||
this.editorCtx.format('marginRight', value)
|
||
break
|
||
case 'format-clear':
|
||
// 清除格式
|
||
this.editorCtx.removeFormat()
|
||
break
|
||
case 'image':
|
||
// 插入图片
|
||
this.editorCtx.insertImage(value)
|
||
// this.updateCmsImage()
|
||
break
|
||
case 'unlockContent':
|
||
// 插入付费解锁内容块
|
||
const isExist = document.querySelector('.ql-unlock-content')
|
||
|
||
// 如果已经存在付费解锁内容块,则弹出提示框,否则插入付费解锁内容块。
|
||
if (isExist) {
|
||
uni.showModal({
|
||
content: '付费解锁不允许重新插入',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
this.editorCtx.insertUnlockContent(value)
|
||
break
|
||
case 'ai':
|
||
// 打开AI智能助手
|
||
this.showImChat = !this.showImChat
|
||
break
|
||
case 'mediaVideo':
|
||
// 仅H5支持
|
||
// #ifdef H5
|
||
this.editorCtx.insertMediaVideo(value)
|
||
// #endif
|
||
break
|
||
default:
|
||
this.editorCtx.format(name, value)
|
||
break
|
||
}
|
||
// 插入内容后,将编辑器滚动到底部
|
||
setTimeout(() => this.editorCtx.scrollIntoView(), 100)
|
||
},
|
||
// 获取编辑器所选内容的格式
|
||
getFormat(range) {
|
||
this.formats = range ? this.editorCtx.getFormat(range) : {}
|
||
return this.formats
|
||
},
|
||
|
||
// 复制图片进入编辑器上传
|
||
uploadHandlerForH5({blob, ext, size}, el) {
|
||
console.log(blob, exe, size);
|
||
|
||
console.log(el)
|
||
// 返回一个 Promise 对象,以便在上传成功后,将图片的 URL 插入到编辑器中。
|
||
return new Promise((resolve, reject) => {
|
||
this.uploadEditorImage({
|
||
filePath: blob,
|
||
fileExt: ext,
|
||
size
|
||
}).then(fileID => {
|
||
resolve(fileID)
|
||
}).catch(error => {
|
||
// 防止万一,只要检测失败就删除图片
|
||
Quill && Quill.find(el).deleteAt(0)
|
||
uni.showModal({
|
||
content: error.message,
|
||
showCancel: false
|
||
})
|
||
reject()
|
||
}).finally(() => {
|
||
uni.hideLoading()
|
||
})
|
||
})
|
||
},
|
||
onInsertImage (selectMediaItems) {
|
||
const image = selectMediaItems[0]
|
||
|
||
this.$refs.insertImageDrawer.close()
|
||
this.format('image', {
|
||
src: image.src,
|
||
data: {
|
||
source: image.src
|
||
}
|
||
})
|
||
setTimeout(() => this.editorCtx.scrollIntoView(), 0)
|
||
},
|
||
uploadEditorImage ({filePath, fileExt, size}) {
|
||
|
||
uni.showLoading({
|
||
title: '上传中...'
|
||
})
|
||
|
||
return new Promise((resolve, reject) => {
|
||
new Promise((res, rej) => {
|
||
uniCloud.uploadFile({
|
||
filePath,
|
||
cloudPath: `cms/media/images/${Date.now()}_${Math.round(Math.random() * 10000)}.${fileExt || 'jpg'}`,
|
||
fileType: 'image',
|
||
success: _res => {
|
||
res(_res)
|
||
},
|
||
fail: error => {
|
||
rej(error)
|
||
},
|
||
complete: () => {
|
||
uni.hideLoading()
|
||
}
|
||
})
|
||
}).then(res => {
|
||
uni.showLoading({
|
||
title: '图片检测中...'
|
||
})
|
||
|
||
const uniCmsCo = uniCloud.importObject('uni-cms-co', {
|
||
customUI: true
|
||
})
|
||
|
||
return uniCmsCo.checkImageSec(res.fileID).then(() => {
|
||
return res.fileID
|
||
})
|
||
}).then(fileID => {
|
||
const isCloudFile = fileID.startsWith('cloud://')
|
||
|
||
if (!isCloudFile) return fileID
|
||
|
||
return uniCloud.getTempFileURL({
|
||
fileList: [fileID]
|
||
}).then(res => {
|
||
if (res.fileList && res.fileList.length) {
|
||
console.log(res.fileList[0].tempFileURL);
|
||
return res.fileList[0].tempFileURL
|
||
}
|
||
|
||
return fileID
|
||
})
|
||
}).then(fileID => {
|
||
uni.showLoading({
|
||
title: '上传媒体库...'
|
||
})
|
||
|
||
const uniMediaLibrary = uniCloud.importObject('uni-media-library-co', {
|
||
customUI: true
|
||
})
|
||
|
||
return new Promise((_resolve) => {
|
||
const image = new Image()
|
||
image.onload = () => {
|
||
_resolve({
|
||
fileID,
|
||
width: image.width,
|
||
height: image.height
|
||
})
|
||
}
|
||
|
||
image.src = fileID
|
||
}).then(({fileID, width, height}) => {
|
||
return uniMediaLibrary.report({
|
||
src: fileID,
|
||
cover: fileID,
|
||
type: 'image',
|
||
user_id: this.user_id,
|
||
originalName: filePath.split('/').pop(),
|
||
fileType: fileExt || 'jpg',
|
||
size,
|
||
|
||
resolution: {
|
||
width,
|
||
height
|
||
},
|
||
uploadUser: uniCloud.getCurrentUserInfo().uid
|
||
})
|
||
}).then(() => {
|
||
return fileID
|
||
}).catch(e => {
|
||
console.log(e, 'e')
|
||
})
|
||
}).then((fileID) => {
|
||
resolve(fileID)
|
||
}).catch(error => {
|
||
reject(error)
|
||
}).finally(() => {
|
||
uni.hideLoading()
|
||
})
|
||
})
|
||
},
|
||
// 在移动端插入超链接
|
||
showLinkPopup(link) {
|
||
this.currentLink.url = link
|
||
this.hideKeyboard()
|
||
this.$refs.popup.open()
|
||
},
|
||
// 插入超链接
|
||
linkChange() {
|
||
if (!this.currentLink.url) return
|
||
this.format('link', {
|
||
text: this.currentLink.title || this.currentLink.url,
|
||
link: this.currentLink.url
|
||
})
|
||
this.$refs.popup.close()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
// #ifdef H5
|
||
@import '@/uni_modules/uni-cms/common/style/editor-icon.css';
|
||
// #endif
|
||
|
||
/* #ifdef H5 */
|
||
@import "quill.scss";
|
||
/* #endif */
|
||
|
||
@media screen and (max-width: 768px) {
|
||
@import "app.scss";
|
||
}
|
||
|
||
@media screen and (min-width: 768px) {
|
||
@import "h5.scss";
|
||
}
|
||
|
||
|
||
.popup-body {
|
||
background: #fff;
|
||
padding: 30px;
|
||
border-radius: 4px;
|
||
max-width: 80%;
|
||
margin: 0 auto;
|
||
|
||
.tip {
|
||
font-size: 14px;
|
||
text-align: center;
|
||
color: #666;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.popup-body-btn-group {
|
||
text-align: center;
|
||
|
||
.btn {
|
||
margin: 0 5px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 修改编辑器默认样式
|
||
::v-deep .ql-container {
|
||
.ql-editor {
|
||
&.ql-blank:before {
|
||
font-style: normal;
|
||
line-height: 1.75;
|
||
font-size: 17px;
|
||
}
|
||
|
||
@mixin font {
|
||
font-size: 17px;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
@mixin mb {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.ql-syntax {
|
||
@include mb;
|
||
}
|
||
|
||
p {
|
||
@include font;
|
||
@include mb;
|
||
}
|
||
|
||
h1, h2, h3, h4, h5, h6 {
|
||
@include mb;
|
||
}
|
||
|
||
ol, ul {
|
||
@include font;
|
||
@include mb;
|
||
|
||
li {
|
||
margin-bottom: 10px !important;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.divider {
|
||
@include mb;
|
||
}
|
||
|
||
code {
|
||
word-break: break-word;
|
||
border-radius: 2px;
|
||
overflow-x: auto;
|
||
background-color: #fff5f5;
|
||
color: #ff502c;
|
||
padding: 0.065em 0.4em;
|
||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||
}
|
||
}
|
||
}
|
||
|
||
.uni-im-chat-components {
|
||
width: 450px;
|
||
position: fixed;
|
||
right: 50px;
|
||
bottom: 70px;
|
||
z-index: 999;
|
||
border: #fefefe solid 2px;
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, .1);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
|
||
.close-bar {
|
||
height: 50px;
|
||
background: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
justify-content: flex-end;
|
||
position: relative;
|
||
|
||
.head {
|
||
display: flex;
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
text-align: center;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
|
||
.title {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
padding-bottom: 0;
|
||
border: none;
|
||
}
|
||
|
||
.desc {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.close {
|
||
width: 30px;
|
||
height: 30px;
|
||
background: #f1f1f1;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
z-index: 2;
|
||
cursor: pointer;
|
||
|
||
.icon {
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
|
||
::v-deep {
|
||
//* {
|
||
// max-width: 100%;
|
||
//}
|
||
|
||
.page {
|
||
height: 720px;
|
||
}
|
||
|
||
.container {
|
||
width: 100%;
|
||
height: 100%;
|
||
margin: 0;
|
||
box-shadow: none;
|
||
flex: 1;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.menu {
|
||
padding: 0;
|
||
}
|
||
.msg-item {
|
||
.content {
|
||
max-width: 88%;
|
||
}
|
||
}
|
||
|
||
.trash {
|
||
width: auto;
|
||
}
|
||
|
||
.foot-box {
|
||
gap: 10px;
|
||
padding: 10px;
|
||
.send {
|
||
width: 50px;
|
||
}
|
||
.textarea-box {
|
||
flex: 1;
|
||
width: auto;
|
||
.textarea {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 768px) {
|
||
.uni-im-chat-components {
|
||
width: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
margin: 0 auto;
|
||
border: none;
|
||
|
||
&:before {
|
||
content: "";
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, .5);
|
||
z-index: -1;
|
||
}
|
||
//
|
||
//::v-deep {
|
||
// .page {
|
||
// height: 75vh;
|
||
// }
|
||
//
|
||
// .foot-box {
|
||
// width: 100%;
|
||
// }
|
||
// .foot-box-content {
|
||
// width: 100%;
|
||
// display: flex;
|
||
// align-items: center;
|
||
// gap: 10px;
|
||
// }
|
||
//}
|
||
}
|
||
}
|
||
</style>
|