| | |
| | | <template> |
| | | <view class="editor-container"> |
| | | <div class="editor"> |
| | | <QuillEditor v-model:content="content" |
| | | contentType="html" |
| | | @textChange="(e) => emit('update:modelValue', content)" |
| | | :options="options" |
| | | :style="styles" /> |
| | | </div> |
| | | <view class="container"> |
| | | <view class="editor-wrapper" |
| | | :style="{ height: height + 'px' }"> |
| | | <view class='toolbar' |
| | | @tap="format" |
| | | style="height: 120px;overflow-y: auto;"> |
| | | <view :class="formats.bold ? 'ql-active' : ''" |
| | | class="iconfont icon-zitijiacu" |
| | | data-name="bold"> |
| | | </view> |
| | | <view :class="formats.italic ? 'ql-active' : ''" |
| | | class="iconfont icon-zitixieti" |
| | | data-name="italic"> |
| | | </view> |
| | | <view :class="formats.underline ? 'ql-active' : ''" |
| | | class="iconfont icon-zitixiahuaxian" |
| | | data-name="underline"></view> |
| | | <view :class="formats.strike ? 'ql-active' : ''" |
| | | class="iconfont icon-zitishanchuxian" |
| | | data-name="strike"></view> |
| | | <!-- #ifndef MP-BAIDU --> |
| | | <view :class="formats.align === 'left' ? 'ql-active' : ''" |
| | | class="iconfont icon-zuoduiqi" |
| | | data-name="align" |
| | | data-value="left"></view> |
| | | <!-- #endif --> |
| | | <view :class="formats.align === 'center' ? 'ql-active' : ''" |
| | | class="iconfont icon-juzhongduiqi" |
| | | data-name="align" |
| | | data-value="center"></view> |
| | | <view :class="formats.align === 'right' ? 'ql-active' : ''" |
| | | class="iconfont icon-youduiqi" |
| | | data-name="align" |
| | | data-value="right"></view> |
| | | <view :class="formats.align === 'justify' ? 'ql-active' : ''" |
| | | class="iconfont icon-zuoyouduiqi" |
| | | data-name="align" |
| | | data-value="justify"></view> |
| | | <!-- #ifndef MP-BAIDU --> |
| | | <view :class="formats.lineHeight ? 'ql-active' : ''" |
| | | class="iconfont icon-line-height" |
| | | data-name="lineHeight" |
| | | data-value="2"></view> |
| | | <view :class="formats.letterSpacing ? 'ql-active' : ''" |
| | | class="iconfont icon-Character-Spacing" |
| | | data-name="letterSpacing" |
| | | data-value="2em"></view> |
| | | <view :class="formats.marginTop ? 'ql-active' : ''" |
| | | class="iconfont icon-722bianjiqi_duanqianju" |
| | | data-name="marginTop" |
| | | data-value="20px"></view> |
| | | <view :class="formats.marginBottom ? 'ql-active' : ''" |
| | | class="iconfont icon-723bianjiqi_duanhouju" |
| | | data-name="marginBottom" |
| | | data-value="20px"></view> |
| | | <!-- #endif --> |
| | | <view class="iconfont icon-clearedformat" |
| | | @tap="removeFormat"></view> |
| | | <!-- #ifndef MP-BAIDU --> |
| | | <view :class="formats.fontFamily ? 'ql-active' : ''" |
| | | class="iconfont icon-font" |
| | | data-name="fontFamily" |
| | | data-value="Pacifico"></view> |
| | | <view :class="formats.fontSize === '24px' ? 'ql-active' : ''" |
| | | class="iconfont icon-fontsize" |
| | | data-name="fontSize" |
| | | data-value="24px"></view> |
| | | <!-- #endif --> |
| | | <view :class="formats.color === '#0000ff' ? 'ql-active' : ''" |
| | | class="iconfont icon-text_color" |
| | | data-name="color" |
| | | data-value="#0000ff"></view> |
| | | <view :class="formats.backgroundColor === '#00ff00' ? 'ql-active' : ''" |
| | | class="iconfont icon-fontbgcolor" |
| | | data-name="backgroundColor" |
| | | data-value="#00ff00"></view> |
| | | <view class="iconfont icon-date" |
| | | @tap="insertDate"></view> |
| | | <view class="iconfont icon--checklist" |
| | | data-name="list" |
| | | data-value="check"></view> |
| | | <view :class="formats.list === 'ordered' ? 'ql-active' : ''" |
| | | class="iconfont icon-youxupailie" |
| | | data-name="list" |
| | | data-value="ordered"></view> |
| | | <view :class="formats.list === 'bullet' ? 'ql-active' : ''" |
| | | class="iconfont icon-wuxupailie" |
| | | data-name="list" |
| | | data-value="bullet"></view> |
| | | <view class="iconfont icon-undo" |
| | | @tap="undo"></view> |
| | | <view class="iconfont icon-redo" |
| | | @tap="redo"></view> |
| | | <view class="iconfont icon-outdent" |
| | | data-name="indent" |
| | | data-value="-1"></view> |
| | | <view class="iconfont icon-indent" |
| | | data-name="indent" |
| | | data-value="+1"></view> |
| | | <view class="iconfont icon-fengexian" |
| | | @tap="insertDivider"></view> |
| | | <view class="iconfont icon-charutupian" |
| | | @tap="insertImage"></view> |
| | | <view :class="formats.header === 1 ? 'ql-active' : ''" |
| | | class="iconfont icon-format-header-1" |
| | | data-name="header" |
| | | :data-value="1"></view> |
| | | <view :class="formats.script === 'sub' ? 'ql-active' : ''" |
| | | class="iconfont icon-zitixiabiao" |
| | | data-name="script" |
| | | data-value="sub"></view> |
| | | <view :class="formats.script === 'super' ? 'ql-active' : ''" |
| | | class="iconfont icon-zitishangbiao" |
| | | data-name="script" |
| | | data-value="super"></view> |
| | | <view class="iconfont icon-shanchu" |
| | | @tap="clear"></view> |
| | | <view :class="formats.direction === 'rtl' ? 'ql-active' : ''" |
| | | class="iconfont icon-direction-rtl" |
| | | data-name="direction" |
| | | data-value="rtl"></view> |
| | | </view> |
| | | <view class="content-wrapper" |
| | | :style="{ height: (height - 140) + 'px' }"> |
| | | <editor id="editor" |
| | | class="ql-container" |
| | | placeholder="开始输入..." |
| | | show-img-size |
| | | show-img-toolbar |
| | | show-img-resize |
| | | @statuschange="onStatusChange" |
| | | @input="onInput" |
| | | :read-only="readOnly" |
| | | @ready="onEditorReady"> |
| | | </editor> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch } from "vue"; |
| | | import { QuillEditor } from "@vueup/vue-quill"; |
| | | import "@vueup/vue-quill/dist/vue-quill.snow.css"; |
| | | import { getToken } from "@/utils/auth"; |
| | | <script> |
| | | export default { |
| | | props: { |
| | | modelValue: { |
| | | type: String, |
| | | default: "", |
| | | }, |
| | | height: { |
| | | type: Number, |
| | | default: 300, |
| | | }, |
| | | readOnly: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | }, |
| | | data() { |
| | | return { |
| | | formats: {}, |
| | | editorCtx: null, |
| | | }; |
| | | }, |
| | | watch: { |
| | | modelValue: { |
| | | handler(newValue) { |
| | | if (this.editorCtx && newValue) { |
| | | this.editorCtx.setContents({ html: newValue }); |
| | | } |
| | | }, |
| | | immediate: true, |
| | | }, |
| | | }, |
| | | onLoad() { |
| | | // #ifndef MP-BAIDU |
| | | uni.loadFontFace({ |
| | | family: "Pacifico", |
| | | source: 'url("https://sungd.github.io/Pacifico.ttf")', |
| | | }); |
| | | // #endif |
| | | }, |
| | | methods: { |
| | | onEditorReady() { |
| | | // #ifdef MP-BAIDU |
| | | this.editorCtx = |
| | | requireDynamicLib("editorLib").createEditorContext("editor"); |
| | | // #endif |
| | | |
| | | const props = defineProps({ |
| | | /* 编辑器的内容 */ |
| | | modelValue: { |
| | | type: String, |
| | | // #ifdef APP-PLUS || MP-WEIXIN || H5 |
| | | uni |
| | | .createSelectorQuery() |
| | | .select("#editor") |
| | | .context(res => { |
| | | this.editorCtx = res.context; |
| | | // 初始化内容 |
| | | if (this.modelValue) { |
| | | this.editorCtx.setContents({ html: this.modelValue }); |
| | | } |
| | | }) |
| | | .exec(); |
| | | // #endif |
| | | }, |
| | | onInput() { |
| | | if (this.editorCtx) { |
| | | this.editorCtx.getContents({ |
| | | success: res => { |
| | | this.$emit("update:modelValue", res.html); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | undo() { |
| | | if (this.editorCtx) { |
| | | this.editorCtx.undo(); |
| | | } |
| | | }, |
| | | redo() { |
| | | if (this.editorCtx) { |
| | | this.editorCtx.redo(); |
| | | } |
| | | }, |
| | | format(e) { |
| | | if (!this.editorCtx) return; |
| | | let { name, value } = e.target.dataset; |
| | | if (!name) return; |
| | | this.editorCtx.format(name, value); |
| | | }, |
| | | onStatusChange(e) { |
| | | const formats = e.detail; |
| | | this.formats = formats; |
| | | }, |
| | | insertDivider() { |
| | | if (this.editorCtx) { |
| | | this.editorCtx.insertDivider({ |
| | | success: function () { |
| | | console.log("insert divider success"); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | clear() { |
| | | uni.showModal({ |
| | | title: "清空编辑器", |
| | | content: "确定清空编辑器全部内容?", |
| | | success: res => { |
| | | if (res.confirm && this.editorCtx) { |
| | | this.editorCtx.clear({ |
| | | success: function (res) { |
| | | console.log("clear success"); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | removeFormat() { |
| | | if (this.editorCtx) { |
| | | this.editorCtx.removeFormat(); |
| | | } |
| | | }, |
| | | insertDate() { |
| | | if (!this.editorCtx) return; |
| | | const date = new Date(); |
| | | const formatDate = `${date.getFullYear()}/${ |
| | | date.getMonth() + 1 |
| | | }/${date.getDate()}`; |
| | | this.editorCtx.insertText({ |
| | | text: formatDate, |
| | | }); |
| | | }, |
| | | insertImage() { |
| | | if (!this.editorCtx) return; |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | success: res => { |
| | | this.editorCtx.insertImage({ |
| | | src: res.tempFilePaths[0], |
| | | alt: "图像", |
| | | success: function () { |
| | | console.log("insert image success"); |
| | | }, |
| | | }); |
| | | }, |
| | | }); |
| | | }, |
| | | }, |
| | | /* 高度 */ |
| | | height: { |
| | | type: Number, |
| | | default: null, |
| | | }, |
| | | /* 最小高度 */ |
| | | minHeight: { |
| | | type: Number, |
| | | default: null, |
| | | }, |
| | | /* 只读 */ |
| | | readOnly: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | /* 上传文件大小限制(MB) */ |
| | | fileSize: { |
| | | type: Number, |
| | | default: 5, |
| | | }, |
| | | /* 类型(base64格式、url格式) */ |
| | | type: { |
| | | type: String, |
| | | default: "url", |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | const styles = computed(() => { |
| | | let style = {}; |
| | | if (props.minHeight) { |
| | | style.minHeight = `${props.minHeight}px`; |
| | | } |
| | | if (props.height) { |
| | | style.height = `${props.height}px`; |
| | | } |
| | | return style; |
| | | }); |
| | | |
| | | const content = ref(""); |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | v => { |
| | | if (v !== content.value) { |
| | | content.value = v == undefined ? "<p></p>" : v; |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | const options = { |
| | | theme: "snow", |
| | | bounds: document.body, |
| | | debug: "warn", |
| | | modules: { |
| | | // 工具栏配置 |
| | | toolbar: [ |
| | | [{ align: [] }], // 对齐方式 |
| | | ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线 |
| | | ["blockquote", "code-block"], // 引用 代码块 |
| | | [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表 |
| | | [{ indent: "-1" }, { indent: "+1" }], // 缩进 |
| | | [{ size: ["small", false, "large", "huge"] }], // 字体大小 |
| | | [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题 |
| | | [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色 |
| | | ["clean"], // 清除文本格式 |
| | | ["link", "image", "video"], // 链接、图片、视频 |
| | | ], |
| | | }, |
| | | placeholder: "请输入内容", |
| | | readOnly: props.readOnly, |
| | | }; |
| | | </script> |
| | | |
| | | <style> |
| | | .editor-container { |
| | | width: 100%; |
| | | @import "./editor-icon.css"; |
| | | |
| | | .editor-wrapper { |
| | | background: #fff; |
| | | } |
| | | |
| | | .editor-img-uploader { |
| | | display: none; |
| | | } |
| | | |
| | | .editor { |
| | | width: 100%; |
| | | } |
| | | |
| | | .quill-editor { |
| | | border: 1px solid #e8e8e8; |
| | | border-radius: 8px; |
| | | .content-wrapper { |
| | | background: #fff; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* Quill编辑器样式 */ |
| | | :deep(.ql-toolbar.ql-snow) { |
| | | border-bottom: 1px solid #e8e8e8; |
| | | border-radius: 8px 8px 0 0; |
| | | padding: 8px 12px; |
| | | .iconfont { |
| | | display: inline-block; |
| | | padding: 8px 8px; |
| | | width: 24px; |
| | | height: 24px; |
| | | cursor: pointer; |
| | | font-size: 20px; |
| | | } |
| | | |
| | | :deep(.ql-container.ql-snow) { |
| | | min-height: 300px; |
| | | border-radius: 0 0 8px 8px; |
| | | .toolbar { |
| | | box-sizing: border-box; |
| | | border-bottom: 0; |
| | | font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif; |
| | | } |
| | | |
| | | :deep(.ql-editor) { |
| | | min-height: 300px; |
| | | font-size: 14px; |
| | | .ql-container { |
| | | box-sizing: border-box; |
| | | padding: 12px 15px; |
| | | width: 100%; |
| | | height: 100%; |
| | | font-size: 16px; |
| | | line-height: 1.5; |
| | | padding: 12px; |
| | | } |
| | | |
| | | /* 移动端适配 */ |
| | | @media (max-width: 768px) { |
| | | :deep(.ql-toolbar.ql-snow) { |
| | | padding: 6px 8px; |
| | | } |
| | | |
| | | :deep(.ql-editor) { |
| | | font-size: 13px; |
| | | padding: 10px; |
| | | } |
| | | .ql-active { |
| | | color: #06c; |
| | | } |
| | | |
| | | /* 图片样式 */ |
| | | :deep(.ql-editor img) { |
| | | max-width: 100%; |
| | | height: auto; |
| | | border-radius: 4px; |
| | | margin: 8px 0; |
| | | } |
| | | |
| | | /* 工具栏按钮样式 */ |
| | | :deep(.ql-toolbar.ql-snow .ql-button) { |
| | | height: 28px; |
| | | width: 28px; |
| | | padding: 4px; |
| | | } |
| | | |
| | | :deep(.ql-toolbar.ql-snow .ql-picker-label) { |
| | | height: 28px; |
| | | padding: 4px 8px; |
| | | } |
| | | |
| | | /* 提示框样式 */ |
| | | :deep(.ql-snow .ql-tooltip[data-mode="link"])::before { |
| | | content: "请输入链接地址:"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-tooltip.ql-editing a.ql-action)::after { |
| | | border-right: 0px; |
| | | content: "保存"; |
| | | padding-right: 0px; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-tooltip[data-mode="video"])::before { |
| | | content: "请输入视频地址:"; |
| | | } |
| | | |
| | | /* 字体大小选项 */ |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-label)::before, |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-item)::before { |
| | | content: "14px"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"])::before { |
| | | content: "10px"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"])::before { |
| | | content: "18px"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"])::before { |
| | | content: "32px"; |
| | | } |
| | | |
| | | /* 标题选项 */ |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label)::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item)::before { |
| | | content: "文本"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"])::before { |
| | | content: "标题1"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"])::before { |
| | | content: "标题2"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"])::before { |
| | | content: "标题3"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"])::before { |
| | | content: "标题4"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"])::before { |
| | | content: "标题5"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"])::before { |
| | | content: "标题6"; |
| | | } |
| | | |
| | | /* 字体选项 */ |
| | | :deep(.ql-snow .ql-picker.ql-font .ql-picker-label)::before, |
| | | :deep(.ql-snow .ql-picker.ql-font .ql-picker-item)::before { |
| | | content: "标准字体"; |
| | | } |
| | | |
| | | :deep(.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"])::before, |
| | | :deep(.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"])::before { |
| | | content: "衬线字体"; |
| | | } |
| | | |
| | | :deep( |
| | | .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"] |
| | | )::before, |
| | | :deep( |
| | | .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"] |
| | | )::before { |
| | | content: "等宽字体"; |
| | | } |
| | | </style> |
| | | </style> |