Files
xk-doctor-wx/components/d-editor/d-editor.vue
2024-09-19 12:57:55 +08:00

1093 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="editor-container">
<view id="fixed-top" class="fixed-top flex-col" :class="{ 'isFixed': isFixed }">
<view class="flex-row flex-ali-center flex-jus-sp bg">
<d-text color="#2979FF" text="保存到草稿" @click="save(1)"></d-text>
<view class="flex-row flex-ali-center">
<view class="iconfont2 icon-release primary-c"></view>
<d-text text="发布" @click="save(0)" className="m-l-1 primary-c"></d-text>
</view>
</view>
<view style="margin-top: 34rpx;" class="p-row-32">
<u-input v-model="title" placeholder="请输入标题2~30个字" height="68" :custom-style="{
fontSize:'58rpx'
}" :maxlength="30" />
</view>
</view>
<view class="fixed-top__place"></view>
<scroll-view class="scroll-view" scroll-y :style="{ height: scrollViewHeight + 'px' }">
<editor id="editor" class="cu-editor" :placeholder="placeholder" :read-only="readOnly"
:show-img-size="showImgSize" :show-img-toolbar="showImgToolbar" :show-img-resize="showImgResize"
@statuschange="onStatusChange" @ready="onEditorReady" @input="onEditorInput" @focus="onEditorFocus"
@blur="onEditorBlur"></editor>
</scroll-view>
<view class="fixed-bottom" :hidden="!toolbarShow" :style="{ bottom: fixedBottom + 'px' }" @touchstart.stop="">
<view class="toolbar selector" :style="{height: toolBarHeight + 'rpx'}">
<view class="toolbar-item-header" @touchend.stop="changeKeyBoard"><i class="iconfont icon-keyboard"></i>
</view>
<view v-for="(icon, index) in ['icon-add', 'icon-textformat', 'icon-align-left']" class="toolbar-item"
@touchend.stop="changeSwiper(index)">
<i class="iconfont" :class="[icon, { active: toolBarContentShow && swiperCurrent == index }]"></i>
</view>
<view class="toolbar-item" @touchend.stop="formatformat('check')"><i
class="iconfont icon-list-check"></i></view>
<view class="toolbar-item" @touchend.stop="formatformat('undo')"><i class="iconfont icon-undo"></i>
</view>
<view class="toolbar-item" @touchend.stop="formatformat('redo')"><i class="iconfont icon-redo"></i>
</view>
<view class="toolbar-item-footer" @touchend.stop="hideToolbar"><i class="iconfont icon-check"></i>
</view>
</view>
<swiper :hidden="!toolBarContentShow" class="toolbar-content swiper-box"
:style="{height: toolBarContentHeight + 'rpx'}" :current="swiperCurrent" duration="300">
<!-- @touchstart.stop="" 禁用手动滑动 -->
<swiper-item v-for="(page, i) in formatArray" :key="i" class="swiper-item" @touchstart.stop="">
<template v-if="page.type === 'feature'">
<view class="feature-items flex">
<view v-for="(pitem, pindex) in page.array" :key="pindex" class="feature"
@touchend.stop="formatformat(pitem.name)">
<view class="icon"><text class="iconfont" :class="'icon-' + pitem.icon"></text></view>
</view>
</view>
</template>
<template v-else-if="page.type === 'tool'">
<view v-for="(pitem, pindex) in page.array" :key="pindex" class="tool-items flex">
<view v-for="(item, index) in pitem.items" :key="index" class="tool-item"
@touchend.stop="formatformat('format', item, pitem)"
:class="{ 'ql-active': isActive(item, pitem), noBgColor: pitem.name == 'color' }">
<view v-if="pitem.name == 'color'" class="color-circle"
:style="{ 'background-color': item.value }"></view>
<i v-else-if="pitem.label == 'icon'" class="iconfont" :class="'icon-' + item.icon"></i>
<text v-else class="txt"
:style="[{fontSize : pitem.name == 'fontSize' ? item.value : ''}, item.style]">{{ item.title || item.value }}</text>
</view>
</view>
</template>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
import {
handleHtmlImage
} from './util'
export default {
name: 'cuEditor',
props: {
//editor属性提示信息
placeholder: {
type: String,
default: '请输入内容'
},
//editor属性点击图片时显示图片大小控件
showImgSize: {
type: Boolean,
default: false
},
//editor属性点击图片时显示工具栏控件
showImgToolbar: {
type: Boolean,
default: false
},
//editor属性点击图片时显示修改尺寸控件
showImgResize: {
type: Boolean,
default: false
},
//编辑器内容,必填
content: {
type: String,
default: ''
},
//chooseImage参数最大文件大小上传文件前校验是否符合规则单位MB
maxSize: {
type: Number,
default: 5
},
//chooseImage参数最多可以选择的图片张数
count: {
type: Number,
default: 5
},
//chooseImage参数所选的图片的尺寸
sizeType: {
type: Array,
default () {
return ['original', 'compressed'] //['original', 'compressed']
}
},
//chooseImage参数选择图片的来源
sourceType: {
type: Array,
default () {
return ['album', 'camera']
}
},
//不允许上传的图片类型
noAllowType: {
type: Array,
default () {
return [] //['gif']
}
},
//uploadFile参数必填
url: {
type: String,
default: ''
},
//uploadFile参数
header: {
type: Object,
default () {
return {}
}
},
//uploadFile参数
formData: {
type: Object,
default () {
return {}
}
},
//uploadFile参数
name: {
type: String,
default: 'file'
},
//uploadFile参数
type: {
type: String,
default: 'article'
},
title:{
type: String,
default: ''
}
},
data() {
return {
title: '',
isFixed: true,
iphoneXBottomH: 0,
scrollHeightDefault: 0,
keyboardHeight: 0,
readOnly: true,
isDefaultFormat: true, // 首次聚集时设置默认格式
isIos: false,
inputFocus: false,
formats: {},
formatArray: [{
type: 'feature',
array: [{
name: 'chooseImage',
icon: 'image'
},
{
name: 'chooseImagebyCamera',
icon: 'photo'
},
{
name: 'insertDivider',
icon: 'line'
}
]
},
{
type: 'tool',
array: [{
name: 'text',
label: 'icon',
items: [{
name: 'bold',
icon: 'bold'
},
{
name: 'italic',
icon: 'italic'
},
{
name: 'underline',
icon: 'underline'
},
{
name: 'strike',
icon: 'strikethrough'
},
{
name: 'backgroundColor',
value: 'yellow',
icon: 'fontbgcolor'
}
]
},
{
name: 'defaultFormat',
items: [{
title: '标题',
format: {
fontSize: '18px',
bold: 'strong'
},
style: {
fontSize: '18px',
fontWeight: 'bold'
}
},
{
title: '小标题',
format: {
fontSize: '16px',
bold: 'strong'
},
style: {
fontSize: '16px',
fontWeight: 'bold'
}
},
{
title: '正文',
format: {
fontSize: '14px'
},
style: {
fontSize: '14px'
}
},
{
title: '注释',
format: {
fontSize: '12px',
color: '#888888'
},
style: {
fontSize: '12px',
color: '#888888',
}
}
]
},
{
name: 'fontSize',
items: [{
title: '18',
value: '18px'
},
{
title: '16',
value: '16px'
},
{
title: '14',
value: '14px'
},
{
title: '12',
value: '12px'
},
{
title: '11',
value: '11px'
},
{
title: '10',
value: '10px'
}
]
},
{
name: 'color',
items: [{
value: '#000000'
},
{
value: '#888888'
},
{
value: '#ffffff'
},
{
value: '#f6de41'
},
{
value: '#f68c41'
},
{
value: '#fd3136'
},
{
value: '#5ad8a6'
}
]
}
]
},
{
type: 'tool',
array: [{
name: 'align',
label: 'icon',
items: [{
value: 'left',
icon: 'align-left'
},
{
value: 'center',
icon: 'align-center'
},
{
value: 'right',
icon: 'align-right'
}
]
},
{
name: 'text',
label: 'icon',
items: [{
name: 'list',
value: 'ordered',
icon: 'orderedlist'
},
{
name: 'list',
value: 'bullet',
icon: 'unorderedlist'
},
{
name: 'indent',
icon: 'outdent',
value: '+1'
},
{
name: 'indent',
icon: 'indent',
value: '-1'
}
]
},
{
name: 'lineHeight',
items: [{
value: 1
},
{
value: 1.3
},
{
value: 1.5
},
{
value: 2
},
{
value: 3
}
]
}
]
}
],
curLength: 0,
swiperCurrent: 0,
toolbarShow: false,
toolBarContentShow: false,
fixedTopHeight: 0, // 顶部工具栏高度
toolBarHeight: 100, // 工具栏高度
toolBarContentHeight: 530, // 工具栏内容高度
progress: true //判断是否监听上传进度变化
};
},
computed: {
fullToolBarHeight() {
let height = 0
this.toolbarShow ? height += this.toolBarHeight : ''
this.toolBarContentShow ? height += this.toolBarContentHeight : ''
return uni.upx2px(height)
},
scrollHeight() {
return this.scrollHeightDefault - this.fixedTopHeight - this.fullToolBarHeight;
},
scrollViewHeight() {
let scrollViewHeight = this.scrollHeight - this.keyboardHeight;
return this.keyboardHeight > 0 ? scrollViewHeight + this.iphoneXBottomH : scrollViewHeight;
},
fixedBottom() {
return this.isIos || this.iphoneXBottomH > 0 ? (this.keyboardHeight > 0 ? this.keyboardHeight :
this.iphoneXBottomH) : 0
}
},
watch: {
title() {
this.title = this.title.replace(/^\s*|\s*$/g, "")
uni.setNavigationBarTitle({
title: this.title ? this.type == 'draft' ? '草稿' : this.title : '无标题文档'
})
},
keyboardHeight(newVal, oldVal) {
if (newVal > 0) {
this.toolBarContentShow = false
}
// this.updatePosition(newVal)
},
toolbarShow(val) {
if (!val) this.toolBarContentShow = val
}
},
created() {
this.index = 0
this.createdAt = Date.now()
this.getUid = () => `wux-upload--${this.createdAt}-${++this.index}`
this.uploadTask = {}
this.tempFilePaths = []
},
mounted() {
const query = wx.createSelectorQuery().in(this)
query.select('#fixed-top').boundingClientRect(res => {
this.fixedTopHeight = res.height
}).exec()
const system = uni.getSystemInfo({
success: e => {
this.isIos = e.platform == 'ios'
let isIphoneX = (e.platform == 'devtools' || this.isIos) && e.safeArea.top == 44
this.iphoneXBottomH = isIphoneX ? 34 : 0
this.scrollHeightDefault = e.windowHeight - 34
}
})
uni.onKeyboardHeightChange(res => {
let keyboardHeight = this.keyboardHeight
if (res.height === keyboardHeight) return
this.keyboardHeight = res.height;
const duration = res.height > 0 ? res.duration * 1000 : 0
keyboardHeight = res.height;
setTimeout(() => {
uni.pageScrollTo({
scrollTop: 0,
success: () => {
this.updatePosition(keyboardHeight)
this.editorCtx.scrollIntoView() //使得编辑器光标处滚动到窗口可视区域内
}
})
}, duration)
})
},
beforeDestroy() {
console.log("editor beforeDestroy")
},
methods: {
isActive(item, pitem) {
let {
name,
value,
format
} = item
!name ? name = pitem.name : ''
if (format) {
for (let name in format) {
if (this.formats[name] !== format[name]) {
return false
}
}
return true
} else {
return value ? this.formats[name] === value : this.formats[name]
}
},
hideKeyboard() {
// uni.hideKeyboard() //uni-app提供了隐藏软键盘的api但是没有生效
this.editorCtx.blur()
},
changeSwiper(current) {
this.toolBarContentShow = true
this.swiperCurrent = current
this.hideKeyboard()
},
updatePosition(keyboardHeight) {
this.keyboardHeight = keyboardHeight
},
onEditorReady() {
const that = this
uni.createSelectorQuery()
.in(this)
.select('#editor')
.context(function(res) {
that.editorCtx = res.context
that.setValue(that.content)
// //设置默认格式
// // that.editorCtx.format('header', '4')
// that.editorCtx.format('fontSize', '14px')
// that.editorCtx.format('align', 'left')
// that.editorCtx.format('lineHeight', '1.3')
//setContents设置内容后editor会自动聚焦解决先设置read_only为true,赋值后再把read_only属性设置为false
that.readOnly = false
})
.exec()
},
onEditorInput(e) {
let {
html,
text
} = e.detail
this.curLength = text.length - 1
},
onEditorFocus(e) {
this.toolbarShow = true
this.inputFocus = true
if (this.isDefaultFormat) {
//设置默认格式
this.editorCtx.format('fontSize', '14px')
this.editorCtx.format('align', 'left')
this.isDefaultFormat = false
}
},
onEditorBlur() {
this.editorCtx.blur()
this.updatePosition(0)
this.inputFocus = false
},
changeKeyBoard() {
this.toolBarContentShow = false
this.hideKeyboard()
},
hideToolbar() {
this.hideKeyboard()
this.toolbarShow = false
},
// 修改默认样式
formatDefault(format) {
for (let name in format) {
this.editorCtx.format(name, format[name])
}
if (format.bold) {
this.editorCtx.format('bold', true)
} else if (this.formats.bold) {
this.editorCtx.format('bold', '')
}
this.editorCtx.format('lineHeight', '') //选择默认样式时,取消当前行高的选择
},
formatformat(bind, item = {}, pitem = {}) {
item.name = item.name || pitem.name || ''
let {
name,
value
} = item
switch (bind) {
case 'format': //改变文本样式
if (!name) return
if (name == 'defaultFormat') { //选择标题样式时,取消当前字号的选择
this.formatDefault(item.format)
} else {
this.editorCtx.format(name, value)
}
break
case 'removeFormat': //删除字体样式
this.editorCtx.removeFormat()
break
case 'insertDate': //插入时间
var date = new Date()
var formatDate = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
this.editorCtx.insertText({
text: formatDate
})
break
case 'check': //设置当前行为待办列表格式
this.editorCtx.format('list', 'check')
break
case 'undo': //撤销操作
this.editorCtx.undo()
break
case 'redo': //恢复操作
this.editorCtx.redo()
break
case 'insertDivider': //添加分割线
this.editorCtx.insertDivider()
break
case 'clear': //清除内容
this.editorCtx.clear()
break
case 'chooseImage': //插入相册图片
this.chooseImage()
break;
case 'chooseImagebyCamera': //拍摄
this.chooseImage(true)
break
}
},
onStatusChange(e) {
this.formats = e.detail
console.log(this.formats)
},
chooseImage(onlyCamera) {
const success = res => {
this.tempFilePaths = res.tempFiles.map(item => ({
url: item.path,
size: item.size,
type: item.path.substring(item.path.lastIndexOf('.') + 1, item.path.length),
uid: this.getUid()
}))
// 当前插入图片src地址直接使用临时路径如果对接接口上传更改为使用【上传文件】代码片段
/* 直接插入临时图片地址 start */
// this.tempFilePaths.forEach(file => {
// this.insertImage(file.url, file)
// })
/* 直接插入临时图片地址 end */
/* 上传文件 start */
this.$emit('before', res)
this.verifyFile()
this.$nextTick(() => {
this.uploadFile(this.tempFilePaths.length)
})
/* 上传文件 end */
}
const {
count,
sizeType
} = this
setTimeout(() => {
uni.chooseImage({
count,
sizeType,
sourceType: onlyCamera ? ['camera'] : this.sourceType,
success
})
}, 100)
},
insertImage(src, file) {
var that = this
that.editorCtx.insertImage({
src,
data: {
id: file.uid
},
// extClass:'editor-img',
extClass: 'editor--editor-img', //添加到图片 img标签上的类名为editor-img设置前缀editor--才生效。部分机型点击图片右边的光标时不灵敏需将样式editor-img宽度调小 max-width:98%;从而在图片右侧中留出部分位置供用户点击聚集。
success(e) {
//真机会自动插入一行空格
}
})
},
/**
* 上传文件,支持多图递归上传
*/
uploadFile(uploadCount, curIndex) {
if (!this.tempFilePaths.length) return
const {
url,
name,
header,
formData,
progress
} = this
const file = this.tempFilePaths.shift()
curIndex ? (file.index = curIndex + 1) : (file.index = 1)
let {
uid,
url: filePath
} = file
if (!url || !filePath) return
this.uploadTask[uid] = uni.uploadFile({
url,
filePath,
name,
header,
formData,
success: res => this.onSuccess(file, res),
fail: res => this.onFail(file, res),
complete: res => {
delete this.uploadTask[uid]
this.$emit('complete', res)
//同时选择多图上传时,只校验第一张图片大小,多图递归上传需逐一校验
if (!this.tempFilePaths.length) return
this.verifyFile()
this.uploadFile(uploadCount, file.index)
}
})
// 判断是否监听上传进度变化
if (progress) {
this.uploadTask[uid].onProgressUpdate(res => this.onProgress(file, res, uploadCount))
}
},
/**校验图片格式和大小是否符合规则 */
verifyFile() {
var {
size: tempFilesSize,
type
} = this.tempFilePaths[0] //获取图片的大小单位B
this.noAllowType.map(item => {
if (type == item) {
this.$toast(`不支持上传${item}图片`)
this.tempFilePaths.shift()
}
})
if (tempFilesSize / 1024 > this.maxSize * 1024) {
this.$toast('上传图片不能大于' + this.bytesToSize(this.maxSize * 1024 * 1024) + '!')
this.tempFilePaths.shift()
}
},
onSuccess(file, res) {
//按照接口自行处理数据insertImage的src参数为接口返回的图片地址
/**
* 示例数据:
res = {
data: '{"code":0,"msg":"上传成功","data":{"path":"https://xxx.com/images/upload/1.png"}}'
}
*/
let json = JSON.parse(res.data)
if (json.errcode == 0) {
this.insertImage(json.data.url, file)
} else {
this.$toast('图片上传失败!')
}
},
onFail(file, res) {
this.$toast('图片上传失败!')
},
/**
* 监听上传进度变化的回调函数
* @param {Object} file 文件对象
* @param {Object} res 请求响应对象
* @param {Number} uploadCount 选择图片总数量
*/
onProgress(file, res, uploadCount) {
if (res.progress != 100) {
this.$toast(`正在上传图片${file.index}/${uploadCount}`)
}
const targetItem = {
...file,
progress: res.progress,
res
}
const info = {
file: targetItem
}
this.$emit('progress', info)
},
bytesToSize: function(bytes) {
if (bytes === 0) return '0 B'
var k = 1024,
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
},
setValue(value) {
if (this.editorCtx) {
this.editorCtx.setContents({
html: value,
success: () => {
this.getContents(res => {
this.onEditorInput({
detail: {
html: res.html,
text: res.text
}
})
this.$emit('update', res)
})
}
})
}
},
getContents(callback) {
//由于获取编辑器内容getContents为异步因此需要使用callback回调
this.editorCtx.getContents({
success: res => {
callback(res)
}
})
},
ifEmpty(content) {
let filterContent = content.replace(/<[^<>]+>/g, "").replace(/&nbsp;/gi, "")
if (!filterContent) return true;
let regu = "^[ ]+$";
let re = new RegExp(regu);
return re.test(filterContent);
},
save(type = 0) {
if (this.title.length < 2) {
this.$toast('请输入标题2~30个字')
return
}
this.editorCtx.getContents({
success: res => {
res.html = handleHtmlImage(res.html, true);
var imgReg = /<img.*?(?:>|\/>)/gi; //匹配所有img标签的正则表达式规则
// var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i; //匹配所有src地址的正则表达式规则
// console.log(res.html.match(srcReg));
var imgs = res.html.match(imgReg)||[]; //获取所有图片,格式为数组
console.log(this.ifEmpty(res.html),imgs);
if (this.ifEmpty(res.html)&&imgs.length==0) {
this.$toast('请输入正文')
return
}
uni.showLoading({
title: '请稍后...'
})
this.$emit('save', [this.title,res.html,type])
},
complete: res => {
this.$toast(res)
}
})
}
}
}
</script>
<style lang="scss" scoped>
@import 'iconfont.wxss';
$bg-color: #f7f7f7;
$bg-color-hover: #eaeaea;
$main-color: #5b8ff9;
.fixed-top {
position: fixed;
box-sizing: border-box;
transition: all 0.3s ease;
background-color: #FFFFFF;
z-index: 999;
&.isFixed {
top: 0;
}
.bg {
background: rgba(41, 121, 255, 0.08);
padding: 16rpx 32rpx;
}
.btn {
width: 100%;
position: relative;
border: 0 !important;
border-radius: 6rpx;
padding-left: 0;
padding-right: 0;
overflow: visible;
float: right;
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
font-size: 24rpx;
margin: 14rpx 0;
text-align: center;
}
.btn-primary {
background: #5677fc !important;
color: #fff;
}
.btn-primary:hover {
opacity: 0.8;
}
}
.fixed-top,
.fixed-top__place {
width: 100%;
height: 172rpx;
}
.flex {
display: flex;
}
.editor-container {}
.cu-editor {
box-sizing: border-box;
width: 100%;
height: 100%;
font-size: 28rpx;
line-height: 1.5;
overflow: auto;
padding: 32rpx;
min-height: unset !important;
}
.editor-img {
max-width: 98% !important;
}
.ql-active {
background-color: $bg-color-hover;
.color-circle {
border: solid 1px;
}
}
.noBgColor {
background-color: none !important;
}
.fixed-bottom {
position: fixed;
left: 0;
width: 100%;
right: 100%;
bottom: 0;
z-index: 99999;
}
.toolbar {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
border: 1rpx solid #e5e5e5;
border-left: none;
border-right: none;
background: #fff;
.iconfont {
display: inline-block;
cursor: pointer;
font-size: 40rpx;
text-align: center;
position: relative;
&.active::after {
content: '';
left: 0;
bottom: 0;
width: 100%;
position: absolute;
height: 6rpx;
border-bottom: solid 6rpx #000000;
}
}
.toolbar-item {
height: 100rpx;
line-height: 100rpx;
flex: 1;
text-align: center;
&:active {
opacity: 0.4;
}
}
.toolbar-item-header,
.toolbar-item-footer {
width: 108rpx;
text-align: center;
}
.toolbar-item-header {
border-right: solid 1rpx $uni-border-color;
}
.toolbar-item-footer {
border-left: solid 1rpx $uni-border-color;
color: #5b8ff9;
font-weight: bold;
}
}
.toolbar-content {
background-color: #ffffff;
}
.swiper-item {
box-sizing: border-box;
padding: 0 30rpx;
}
.tool-items {
background-color: $bg-color;
color: #323232;
height: 80rpx;
line-height: 80rpx;
margin: 32rpx 0;
border-radius: 16rpx;
overflow: hidden;
.tool-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
&:active {
background-color: $bg-color-hover;
}
}
.iconfont {
display: inline-block;
width: 80rpx;
height: 80rpx;
line-height: 80rpx;
cursor: pointer;
font-size: 40rpx;
text-align: center;
}
.color-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
}
.txt {
font-size: 14px;
}
}
.feature {
margin: 30rpx;
text-align: center;
.icon {
background-color: $bg-color;
width: 120rpx;
height: 120rpx;
line-height: 120rpx;
color: #323232;
margin-bottom: 10rpx;
&:active {
background-color: $bg-color-hover;
}
.iconfont {
font-size: 42rpx;
}
}
}
.scroll-view {
background-color: #fff;
}
</style>