add editor
parent
ea162d0ee9
commit
eaad940502
|
|
@ -1,4 +1,3 @@
|
|||
location ^~ /h5/ {
|
||||
root h5
|
||||
try_files $uri $uri/ /index.html
|
||||
location /h5 {
|
||||
try_files $uri $uri/h5 /h5/index.html?$query_string;
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# 富文本编辑器插件
|
||||
uniapp 富文本编辑器插件
|
||||
|
||||
## 兼容性
|
||||
|微信小程序|H5|APP|
|
||||
|:--:|:--:|:--:|
|
||||
|√|√ |x|
|
||||
|
||||
## 使用方式
|
||||
在 `script` 中引用组件
|
||||
```js
|
||||
import myeditor from "@/components/robin-editor/editor.vue"
|
||||
export default {
|
||||
components: {myeditor}
|
||||
}
|
||||
```
|
||||
在 `template` 中使用组件
|
||||
```html
|
||||
<myeditor class="editor"
|
||||
@cancel="hideEditor"
|
||||
@save="saveEditor"
|
||||
v-model="html"
|
||||
:imageUploader="uploadImg"
|
||||
:muiltImage="true">
|
||||
</myeditor>
|
||||
```
|
||||
|
||||
## Demo
|
||||
https://github.com/health901/uniapp-editor-demo
|
||||
|
||||
## 属性说明
|
||||
|属性|类型|默认值|说明|
|
||||
|--|--|--|--|
|
||||
|v-model|String| |富文本,双向绑定|
|
||||
|imageUploader|function(img,callback)| |上传图片处理函数 接受参数 img:本地图片地址,callback:上传成功回调传入图片链接|
|
||||
|muiltImage|Boolean|false|是否支持多图上传|
|
||||
|compressImage|Boolean|true|图片上传是否压缩|
|
||||
|previewMode|Boolean|false|预览模式,不可编辑|
|
||||
|autoHideToolbar|Boolean|false|失去焦点时自动隐藏工具栏|
|
||||
|tools|Array|['bold', 'italic', 'underline', 'strike', 'align-left', 'align-center', 'align-right', 'remove', 'font', 'color', 'backgroundColor','image', 'clear', 'preview']|工具栏|
|
||||
|
||||
### 工具栏
|
||||
|名称|值|
|
||||
|--|--|
|
||||
|加粗|`bold`|
|
||||
|斜体|`italic`|
|
||||
|下划线|`underline`|
|
||||
|删除线|`strike`|
|
||||
|右对齐|`align-left`|
|
||||
|居中|`align-center`|
|
||||
|左对齐|`align-right`|
|
||||
|清除格式|`remove`|
|
||||
|字体大小|`font`|
|
||||
|字体颜色|`color`|
|
||||
|背景色|`backgroundColor`|
|
||||
|插入图片|`image`|
|
||||
|清空|`clear`|
|
||||
|预览|`preview`|
|
||||
|插入日期|`date`|
|
||||
|列表|`list-check`,`list-ordered`,`list-bullet`|
|
||||
|上下标|`sub`,`super`|
|
||||
|撤销,恢复撤销|`undo`,`redo`|
|
||||
|缩进|`indent`,`outdent`|
|
||||
|分割线|`divider`|
|
||||
|标题|`h1`,`h2`,`h3`,`h4`,`h5`,`h6`|
|
||||
|书写方向|`rtl`|
|
||||
|
||||
## 事件说明
|
||||
|事件|说明|参数|
|
||||
|--|--|--|
|
||||
|cancel|点击取消按钮|
|
||||
|save|点击保存按钮|e={html,text,delta}|
|
||||
|
||||
## 依赖
|
||||
|组件|链接|备注|
|
||||
|---|--|--|
|
||||
|Popup 弹出层<sup>[[1]](#注)</sup>|https://ext.dcloud.net.cn/plugin?id=329|uni-ui库|
|
||||
|Transition动画|https://ext.dcloud.net.cn/plugin?id=1231|uni-ui库,Popup依赖|
|
||||
|颜色选择器ColorPicker<sup>[[2]](#注)</sup>|https://ext.dcloud.net.cn/plugin?id=1237|字体颜色,背景色|
|
||||
|
||||
|
||||
## 注
|
||||
|
||||
1. 修改:新增动画结束事件
|
||||
2. 修改:添加按钮,支持预设颜色值
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
<template>
|
||||
<view class="content">
|
||||
<topbar class="head" @cancel="cancel" @save="confirm"></topbar>
|
||||
<view class="color-picker">
|
||||
<view class="color-name">{{ colorName }}</view>
|
||||
<view class="show-view" :style="{ background: colorName }"></view>
|
||||
<view class="hue-view" @touchstart="pickHue" @touchmove="pickHue">
|
||||
<text class="anchor" :style="{ left: hueView.anchorLeft + 'px' }"></text>
|
||||
</view>
|
||||
<view class="color-view" @touchstart="pickColor" @touchmove="pickColor" :style="{ backgroundColor: 'hsl(' + hueView.H + ', 100%, 50%)' }">
|
||||
<text class="anchor" :style="{ top: colorView.anchorTop + 'px', left: colorView.anchorLeft + 'px' }"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import topbar from '@/components/robin-editor/header.vue'
|
||||
export default {
|
||||
components: {
|
||||
topbar
|
||||
},
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hueView: {},
|
||||
colorView: {},
|
||||
colorName: '',
|
||||
hueLeft: 0.5, // 色相选择器初始位置 [0, 1]
|
||||
anchorTop: 0.5, // 颜色选择器初始 top [0, 1]
|
||||
anchorLeft: 0.5, // 颜色选择器初始 left [0, 1]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function(newval, oldvar) {
|
||||
if (!oldvar && newval) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.show) {
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const reg = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})*$/
|
||||
if (this.color !== '' && reg.test(this.color)) {
|
||||
this.getColorOffset()
|
||||
}
|
||||
Promise.all([this.getHueViewOffset(), this.getColorViewOffset()]).then(() => {
|
||||
this.colorName = this.getColorString() // 根据 HLS 计算 RGB 字符串
|
||||
})
|
||||
},
|
||||
getHueViewOffset() { // 获取色相选择区域尺寸
|
||||
return new Promise(resolve => uni.createSelectorQuery().in(this).select('.hue-view').boundingClientRect(
|
||||
data => {
|
||||
this.hueView = {
|
||||
...data,
|
||||
anchorLeft: data.width * this.hueLeft,
|
||||
H: this.hueLeft * 360,
|
||||
}
|
||||
resolve()
|
||||
}).exec())
|
||||
},
|
||||
getColorViewOffset() { // 获取颜色选择区域尺寸
|
||||
return new Promise(resolve => uni.createSelectorQuery().in(this).select('.color-view').boundingClientRect(
|
||||
data => {
|
||||
this.colorView = {
|
||||
...data,
|
||||
anchorTop: data.height * this.anchorTop,
|
||||
anchorLeft: data.width * this.anchorLeft,
|
||||
S: this.anchorLeft,
|
||||
L: (1 - this.anchorLeft * 0.5) - this.anchorTop / (this.anchorLeft + 1)
|
||||
}
|
||||
resolve()
|
||||
}).exec())
|
||||
},
|
||||
getColorString() { // 获取 RGB 颜色字符串
|
||||
const arr = hslToRgb(this.hueView.anchorLeft / this.hueView.width, this.colorView.S, this.colorView.L)
|
||||
const r = arr[0].toString(16).length === 1 ? `0${arr[0].toString(16)}` : arr[0].toString(16)
|
||||
const g = arr[1].toString(16).length === 1 ? `0${arr[1].toString(16)}` : arr[1].toString(16)
|
||||
const b = arr[2].toString(16).length === 1 ? `0${arr[2].toString(16)}` : arr[2].toString(16)
|
||||
return `#${r.toUpperCase()}${g.toUpperCase()}${b.toUpperCase()}`
|
||||
},
|
||||
getColorOffset() {
|
||||
var color = this.color.substr(1)
|
||||
color = color.length == 6 ? color : color.charAt(0) + color.charAt(0) + color.charAt(1) + color.charAt(
|
||||
1) + color.charAt(2) + color.charAt(2)
|
||||
const r = parseInt("0x" + color.substr(0, 2))
|
||||
const g = parseInt("0x" + color.substr(2, 2))
|
||||
const b = parseInt("0x" + color.substr(4, 2))
|
||||
const arr = rgbToHsl(r, g, b)
|
||||
this.hueLeft = arr[0]
|
||||
this.anchorLeft = arr[1]
|
||||
this.anchorTop = ((1 - arr[1] * 0.5) - arr[2]) * (arr[1] + 1)
|
||||
},
|
||||
pickColor(e) { // 选择颜色
|
||||
const top = e.touches[0].clientY - this.colorView.top
|
||||
const left = e.touches[0].clientX - this.colorView.left
|
||||
if (top < 0) {
|
||||
this.colorView.anchorTop = 0
|
||||
} else if (top > this.colorView.height) {
|
||||
this.colorView.anchorTop = this.colorView.height
|
||||
} else {
|
||||
this.colorView.anchorTop = top
|
||||
}
|
||||
if (left < 0) {
|
||||
this.colorView.anchorLeft = 0
|
||||
} else if (left > this.colorView.width) {
|
||||
this.colorView.anchorLeft = this.colorView.width
|
||||
} else {
|
||||
this.colorView.anchorLeft = e.touches[0].clientX - this.colorView.left
|
||||
}
|
||||
this.colorView.S = this.colorView.anchorLeft / this.colorView.width
|
||||
this.colorView.L = this.floor((1 - this.colorView.S * 0.5) - this.colorView.anchorTop / this.colorView.height /
|
||||
(this.colorView.S + 1))
|
||||
this.colorName = this.getColorString() // 根据 HLS 计算 RGB 字符串
|
||||
},
|
||||
pickHue(e) { // 选择色相
|
||||
if (e.touches[0].clientX >= this.hueView.left && e.touches[0].clientX <= this.hueView.right) {
|
||||
this.hueView.anchorLeft = e.touches[0].clientX - this.hueView.left
|
||||
this.hueView.H = this.hueView.anchorLeft / this.hueView.width * 360
|
||||
this.colorName = this.getColorString() // 根据 HLS 计算 RGB 字符串
|
||||
}
|
||||
},
|
||||
floor(num) {
|
||||
return num < 0.09 ? 0 : num
|
||||
},
|
||||
confirm() {
|
||||
this.$emit('confirm', {
|
||||
color: this.colorName
|
||||
})
|
||||
},
|
||||
cancel(){
|
||||
this.$emit('cancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) { // HSL 转 RGB 方法
|
||||
var r, g, b;
|
||||
if (s == 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
var hue2rgb = function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255, g /= 255, b /= 255;
|
||||
var max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b);
|
||||
var h, s, l = (max + min) / 2;
|
||||
if (max == min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
var d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
var round = function(n, l) {
|
||||
return Math.round(n * Math.pow(10, l)) / Math.pow(10, l)
|
||||
}
|
||||
return [round(h, 3), round(s, 3), round(l, 3)];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
|
||||
.head {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.color-name {
|
||||
margin: 23rpx;
|
||||
font-size: 45rpx;
|
||||
font-weight: bold;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
.show-view {
|
||||
height: 56rpx;
|
||||
width: 567rpx;
|
||||
}
|
||||
|
||||
.hue-view {
|
||||
width: 567rpx;
|
||||
height: 56rpx;
|
||||
margin: 12rpx 0;
|
||||
position: relative;
|
||||
background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
|
||||
|
||||
.anchor {
|
||||
width: 12rpx;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: #FFFFFF;
|
||||
transform: translate(-50%);
|
||||
box-shadow: 0 0 2rpx rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.color-view {
|
||||
width: 567rpx;
|
||||
height: 345rpx;
|
||||
position: relative;
|
||||
margin-bottom: 12upx;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(to right, white, transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: linear-gradient(to top, black, transparent);
|
||||
}
|
||||
|
||||
.anchor {
|
||||
z-index: 1;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
border: 4rpx solid #FFFFFF;
|
||||
background: rgba(0, 0, 0, .3);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,86 +1,19 @@
|
|||
<template>
|
||||
<view class="cu-editor">
|
||||
<u-modal
|
||||
:show="show"
|
||||
showCancelButton
|
||||
@confirm="confirm"
|
||||
@cancel="close"
|
||||
>
|
||||
<view class="editor-wrapper">
|
||||
<view class="btn-content">
|
||||
<view v-for="(group, groupIndex) in btns" :key="groupIndex" class="btn-group">
|
||||
<view v-for="item in group" :key="item.action" class="btn-item" @click="handleBtn(item)">
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <view class="btn-group">
|
||||
<view class="btn-item">
|
||||
<text>标题</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>列表</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>图片</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-group">
|
||||
<view class="btn-item">
|
||||
<text>字体颜色</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>背景色</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>字体大小</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>缩进</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-group">
|
||||
<view class="btn-item">
|
||||
<text>粗体</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>斜体</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>下划线</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>删除线</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-group">
|
||||
<view class="btn-item">
|
||||
<text>左对齐</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>居中</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>右对齐</text>
|
||||
</view>
|
||||
<view class="btn-item">
|
||||
<text>两端对齐</text>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
<view v-if="btn && options" class="btn-option">
|
||||
<view class="title">{{ btn.name }}</view>
|
||||
<view class="options">
|
||||
<view v-for="item in options" :key="item.value" class="option" :style="item.style" @click="handleOption(item)">{{ item.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<editor id="editor" :placeholder="placeholder" @ready="onEditorReady" />
|
||||
</view>
|
||||
</u-modal>
|
||||
</view>
|
||||
<u-popup :show="show">
|
||||
<robin-editor
|
||||
v-model="value"
|
||||
class="editor"
|
||||
@cancel="close"
|
||||
@save="confirm"
|
||||
:imageUploader="uploadImage"
|
||||
/>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RobinEditor from '@/components/robin-editor/editor'
|
||||
export default {
|
||||
components: { RobinEditor },
|
||||
name: 'CuEditor',
|
||||
props: {
|
||||
placeholder: {
|
||||
|
|
@ -90,84 +23,11 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
btns: [
|
||||
[
|
||||
{ name: '标题', type: 'format', action: 'header', options: [
|
||||
{ name: 'H1', value: 'H1' },
|
||||
{ name: 'H2', value: 'H2' },
|
||||
{ name: 'H3', value: 'H3' },
|
||||
]},
|
||||
{ name: '列表', type: 'format', action: 'list', options: [
|
||||
{ name: '有序列表', value: 'ordered' },
|
||||
{ name: '无序列表', value: 'bullet ' },
|
||||
]},
|
||||
{ name: '图片', type: 'image', action: 'insert'},
|
||||
],
|
||||
// [
|
||||
// { name: '字体颜色', type: 'format', action: 'color', options: [
|
||||
// { name: '黑色', value: '#000000', style: { color: '#000000' } },
|
||||
// { name: '红色', value: '#FF0000', style: { color: '#FF0000' } },
|
||||
// { name: '绿色', value: '#008000', style: { color: '#008000' } },
|
||||
// { name: '白色', value: '#ffffff', style: { color: '#ffffff', background: 'grey' } },
|
||||
// ]},
|
||||
// { name: '背景色', type: 'format', action: 'backgroundColor', options: [
|
||||
// { name: '黑色', value: '#000000', style: { background: '#000000', color: '#ffffff' } },
|
||||
// { name: '红色', value: '#FF0000', style: { background: '#FF0000', color: '#ffffff' } },
|
||||
// { name: '绿色', value: '#008000', style: { background: '#008000', color: '#ffffff' } },
|
||||
// { name: '白色', value: '#ffffff', style: { background: '#ffffff', color: '#ffffff' } },
|
||||
// ]},
|
||||
// { name: '缩进', type: 'format', action: 'indent', options: [
|
||||
// { name: '增加缩进', value: '+1' },
|
||||
// { name: '减少缩进', value: '-1' },
|
||||
// ]},
|
||||
// ],
|
||||
],
|
||||
options: [],
|
||||
btn: '',
|
||||
option: '',
|
||||
value: '',
|
||||
show: true,
|
||||
editor: ''
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onEditorReady() {
|
||||
uni.createSelectorQuery().in(this).select('#editor').context((res) => {
|
||||
this.editor = res.context
|
||||
if (this.value) {
|
||||
this.editor.setContents({ html: this.value })
|
||||
}
|
||||
}).exec()
|
||||
},
|
||||
handleBtn(e) {
|
||||
if (e.type == 'image') {
|
||||
return uni.chooseImage({
|
||||
success: (res) => {
|
||||
res.tempFilePaths.forEach(item => this.editorAction('image', { width: '100%', height: '100%', alt: '无法显示图片' }, item))
|
||||
// const files = res.tempFiles
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!e.options) {
|
||||
this.editorAction(e.type, e.action)
|
||||
} else {
|
||||
this.btn = e
|
||||
this.options = e.options
|
||||
}
|
||||
},
|
||||
handleOption(e) {
|
||||
const value = e.value
|
||||
this.editorAction(this.btn.type, this.btn.action, value)
|
||||
},
|
||||
editorAction(type, action, value) {
|
||||
if (type == 'format') {
|
||||
this.editor.format(action, value)
|
||||
} else if (type == 'image') {
|
||||
this.editor.insertImage({ src: value, action })
|
||||
} else if (action == 'removeFormat') {
|
||||
this.editor.removeFormat()
|
||||
}
|
||||
},
|
||||
open(value) {
|
||||
this.value = value
|
||||
this.show = true
|
||||
|
|
@ -175,71 +35,22 @@ export default {
|
|||
close() {
|
||||
this.show = false
|
||||
},
|
||||
confirm() {
|
||||
if (this.editor) {
|
||||
this.editor.getContents({
|
||||
success: (res) => {
|
||||
this.$emit('confirm', res)
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
confirm(e) {
|
||||
this.$emit('confirm', e)
|
||||
this.close()
|
||||
},
|
||||
uploadImage(file, callback) {
|
||||
this.$ajax.upload('/admin-api/upload_image', {
|
||||
params: {'full-url': 1},
|
||||
name: 'file',
|
||||
filePath: file.path,
|
||||
custom: { toast: false }
|
||||
}).then(res => {
|
||||
if (res.status == 0) {
|
||||
callback(res.data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.cu-editor ::v-deep .u-modal__content {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.editor-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: scroll;
|
||||
}
|
||||
.btn-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.btn-item {
|
||||
margin-right: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.btn-item:active {
|
||||
background: grey;
|
||||
color: white;
|
||||
}
|
||||
.btn-option {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.btn-option .title {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.btn-option .options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn-option .option {
|
||||
padding: 0 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.btn-option .option:active {
|
||||
background: grey;
|
||||
color: white;
|
||||
}
|
||||
#editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
@font-face {
|
||||
font-family: "iconfont";
|
||||
src: url('~./editor-icon.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-redo:before {
|
||||
content: "\e627";
|
||||
}
|
||||
|
||||
.icon-undo:before {
|
||||
content: "\e633";
|
||||
}
|
||||
|
||||
.icon-indent:before {
|
||||
content: "\eb28";
|
||||
}
|
||||
|
||||
.icon-outdent:before {
|
||||
content: "\e6e8";
|
||||
}
|
||||
|
||||
.icon-fontsize:before {
|
||||
content: "\e6fd";
|
||||
}
|
||||
|
||||
.icon-format-header-1:before {
|
||||
content: "\e860";
|
||||
}
|
||||
|
||||
.icon-format-header-4:before {
|
||||
content: "\e863";
|
||||
}
|
||||
|
||||
.icon-format-header-5:before {
|
||||
content: "\e864";
|
||||
}
|
||||
|
||||
.icon-format-header-6:before {
|
||||
content: "\e865";
|
||||
}
|
||||
|
||||
.icon-clearup:before {
|
||||
content: "\e64d";
|
||||
}
|
||||
|
||||
.icon-preview:before {
|
||||
content: "\e631";
|
||||
}
|
||||
|
||||
.icon-date:before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.icon-fontbgcolor:before {
|
||||
content: "\e678";
|
||||
}
|
||||
|
||||
.icon-clearedformat:before {
|
||||
content: "\e67e";
|
||||
}
|
||||
|
||||
.icon-font:before {
|
||||
content: "\e684";
|
||||
}
|
||||
|
||||
.icon-723bianjiqi_duanhouju:before {
|
||||
content: "\e65f";
|
||||
}
|
||||
|
||||
.icon-722bianjiqi_duanqianju:before {
|
||||
content: "\e660";
|
||||
}
|
||||
|
||||
.icon-text_color:before {
|
||||
content: "\e72c";
|
||||
}
|
||||
|
||||
.icon-format-header-2:before {
|
||||
content: "\e75c";
|
||||
}
|
||||
|
||||
.icon-format-header-3:before {
|
||||
content: "\e75d";
|
||||
}
|
||||
|
||||
.icon--checklist:before {
|
||||
content: "\e664";
|
||||
}
|
||||
|
||||
.icon-baocun:before {
|
||||
content: "\ec09";
|
||||
}
|
||||
|
||||
.icon-line-height:before {
|
||||
content: "\e7f8";
|
||||
}
|
||||
|
||||
.icon-quanping:before {
|
||||
content: "\ec13";
|
||||
}
|
||||
|
||||
.icon-direction-rtl:before {
|
||||
content: "\e66e";
|
||||
}
|
||||
|
||||
.icon-direction-ltr:before {
|
||||
content: "\e66d";
|
||||
}
|
||||
|
||||
.icon-selectall:before {
|
||||
content: "\e62b";
|
||||
}
|
||||
|
||||
.icon-fuzhi:before {
|
||||
content: "\ec7a";
|
||||
}
|
||||
|
||||
.icon-shanchu:before {
|
||||
content: "\ec7b";
|
||||
}
|
||||
|
||||
.icon-bianjisekuai:before {
|
||||
content: "\ec7c";
|
||||
}
|
||||
|
||||
.icon-fengexian:before {
|
||||
content: "\ec7f";
|
||||
}
|
||||
|
||||
.icon-dianzan:before {
|
||||
content: "\ec80";
|
||||
}
|
||||
|
||||
.icon-charulianjie:before {
|
||||
content: "\ec81";
|
||||
}
|
||||
|
||||
.icon-charutupian:before {
|
||||
content: "\ec82";
|
||||
}
|
||||
|
||||
.icon-wuxupailie:before {
|
||||
content: "\ec83";
|
||||
}
|
||||
|
||||
.icon-juzhongduiqi:before {
|
||||
content: "\ec84";
|
||||
}
|
||||
|
||||
.icon-yinyong:before {
|
||||
content: "\ec85";
|
||||
}
|
||||
|
||||
.icon-youxupailie:before {
|
||||
content: "\ec86";
|
||||
}
|
||||
|
||||
.icon-youduiqi:before {
|
||||
content: "\ec87";
|
||||
}
|
||||
|
||||
.icon-zitidaima:before {
|
||||
content: "\ec88";
|
||||
}
|
||||
|
||||
.icon-xiaolian:before {
|
||||
content: "\ec89";
|
||||
}
|
||||
|
||||
.icon-zitijiacu:before {
|
||||
content: "\ec8a";
|
||||
}
|
||||
|
||||
.icon-zitishanchuxian:before {
|
||||
content: "\ec8b";
|
||||
}
|
||||
|
||||
.icon-zitishangbiao:before {
|
||||
content: "\ec8c";
|
||||
}
|
||||
|
||||
.icon-zitibiaoti:before {
|
||||
content: "\ec8d";
|
||||
}
|
||||
|
||||
.icon-zitixiahuaxian:before {
|
||||
content: "\ec8e";
|
||||
}
|
||||
|
||||
.icon-zitixieti:before {
|
||||
content: "\ec8f";
|
||||
}
|
||||
|
||||
.icon-zitiyanse:before {
|
||||
content: "\ec90";
|
||||
}
|
||||
|
||||
.icon-zuoduiqi:before {
|
||||
content: "\ec91";
|
||||
}
|
||||
|
||||
.icon-zitiyulan:before {
|
||||
content: "\ec92";
|
||||
}
|
||||
|
||||
.icon-zitixiabiao:before {
|
||||
content: "\ec93";
|
||||
}
|
||||
|
||||
.icon-zuoyouduiqi:before {
|
||||
content: "\ec94";
|
||||
}
|
||||
|
||||
.icon-duigoux:before {
|
||||
content: "\ec9e";
|
||||
}
|
||||
|
||||
.icon-guanbi:before {
|
||||
content: "\eca0";
|
||||
}
|
||||
|
||||
.icon-shengyin_shiti:before {
|
||||
content: "\eca5";
|
||||
}
|
||||
|
||||
.icon-Character-Spacing:before {
|
||||
content: "\e964";
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,488 @@
|
|||
<template>
|
||||
<view class="wrapper">
|
||||
<topbar class="header" @cancel="cancel" @save="save" :labelConfirm="labelConfirm" :labelCancel="labelCancel"></topbar>
|
||||
<view :style="'height:' + editorHeight + 'px;'" class="container" v-if="!previewMode" v-show="!showPreview">
|
||||
<editor
|
||||
v-if="!previewMode"
|
||||
v-show="!showPreview"
|
||||
id="editor"
|
||||
class="ql-container"
|
||||
placeholder="开始输入..."
|
||||
showImgSize
|
||||
showImgToolbar
|
||||
showImgResize
|
||||
@statuschange="onStatusChange"
|
||||
:read-only="readOnly"
|
||||
@ready="onEditorReady"
|
||||
></editor>
|
||||
</view>
|
||||
<view class="toolbar" @tap="format" v-if="!showPreview" v-show="keyboardHeight || !autoHideToolbar" :style="'bottom:' + (isIOS ? keyboardHeight : 0) + 'px'">
|
||||
<block v-for="(t, i) in tools" :key="i">
|
||||
<view v-if="t == 'bold'" :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold" data-label="加粗"></view>
|
||||
<view v-if="t == 'italic'" :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" data-name="italic" data-label="斜体"></view>
|
||||
<view v-if="t == 'underline'" :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian" data-name="underline" data-label="下滑线"></view>
|
||||
<view v-if="t == 'strike'" :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian" data-name="strike" data-label="删除线"></view>
|
||||
<view
|
||||
v-if="t == 'align-left'"
|
||||
:class="formats.align === 'left' || !formats.align ? 'ql-active' : ''"
|
||||
class="iconfont icon-zuoduiqi"
|
||||
data-name="align"
|
||||
data-value="left"
|
||||
data-label="居左"
|
||||
></view>
|
||||
<view
|
||||
v-if="t == 'align-center'"
|
||||
:class="formats.align === 'center' ? 'ql-active' : ''"
|
||||
class="iconfont icon-juzhongduiqi"
|
||||
data-name="align"
|
||||
data-value="center"
|
||||
data-label="居中"
|
||||
></view>
|
||||
<view
|
||||
v-if="t == 'align-right'"
|
||||
:class="formats.align === 'right' ? 'ql-active' : ''"
|
||||
class="iconfont icon-youduiqi"
|
||||
data-name="align"
|
||||
data-value="right"
|
||||
data-label="居右"
|
||||
></view>
|
||||
<view
|
||||
v-if="t == 'align-justify'"
|
||||
:class="formats.align === 'justify' ? 'ql-active' : ''"
|
||||
class="iconfont icon-zuoyouduiqi"
|
||||
data-name="align"
|
||||
data-value="justify"
|
||||
data-label="平铺"
|
||||
></view>
|
||||
<!-- <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.previewarginBottom ? 'ql-active' : ''" class="iconfont icon-723bianjiqi_duanhouju"
|
||||
data-name="marginBottom" data-value="20px"></view> -->
|
||||
<view v-if="t == 'remove'" class="iconfont icon-clearedformat" @tap.stop="removeFormat"></view>
|
||||
<picker v-if="t == 'font'" class="iconfont" mode="selector" :range="fontSizeRange" @change="fontSize"><view class="icon-fontsize"></view></picker>
|
||||
<view
|
||||
v-if="t == 'color'"
|
||||
:style="fontColor != '#FFFFFF' ? 'color:' + formats.color : ''"
|
||||
class="iconfont icon-text_color"
|
||||
data-name="color"
|
||||
@tap.stop="openColor"
|
||||
></view>
|
||||
<view
|
||||
v-if="t == 'backgroundColor'"
|
||||
:style="bgColor ? 'color:' + formats.backgroundColor : ''"
|
||||
class="iconfont icon-fontbgcolor"
|
||||
data-name="backgroundColor"
|
||||
@tap.stop="openColor"
|
||||
></view>
|
||||
<view v-if="t == 'image'" class="iconfont icon-charutupian" @tap.stop="insertImage"></view>
|
||||
<view v-if="t == 'clear'" class="iconfont icon-shanchu" @tap.stop="clear"></view>
|
||||
<view v-if="t == 'preview'" class="iconfont icon-preview" @tap.stop="preview"></view>
|
||||
<view v-if="t == 'date'" class="iconfont icon-date" @tap="insertDate"></view>
|
||||
<view v-if="t == 'list-check'" class="iconfont icon-checklist" data-name="list" data-value="check"></view>
|
||||
<view
|
||||
v-if="t == 'list-ordered'"
|
||||
:class="formats.list === 'ordered' ? 'ql-active' : ''"
|
||||
class="iconfont icon-youxupailie"
|
||||
data-name="list"
|
||||
data-value="ordered"
|
||||
></view>
|
||||
<view v-if="t == 'list-bullet'" :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie" data-name="list" data-value="bullet"></view>
|
||||
<view v-if="t == 'undo'" class="iconfont icon-undo" @tap="undo"></view>
|
||||
<view v-if="t == 'redo'" class="iconfont icon-redo" @tap="redo"></view>
|
||||
<view v-if="t == 'outdent'" class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
|
||||
<view v-if="t == 'indent'" class="iconfont icon-indent" data-name="indent" data-value="+1"></view>
|
||||
<view v-if="t == 'divider'" class="iconfont icon-fengexian" @tap="insertDivider"></view>
|
||||
<view v-if="t == 'h1'" :class="formats.header === 1 ? 'ql-active' : ''" class="iconfont icon-format-header-1" data-name="header" :data-value="1"></view>
|
||||
<view v-if="t == 'h2'" :class="formats.header === 2 ? 'ql-active' : ''" class="iconfont icon-format-header-2" data-name="header" :data-value="2"></view>
|
||||
<view v-if="t == 'h3'" :class="formats.header === 3 ? 'ql-active' : ''" class="iconfont icon-format-header-3" data-name="header" :data-value="3"></view>
|
||||
<view v-if="t == 'h4'" :class="formats.header === 4 ? 'ql-active' : ''" class="iconfont icon-format-header-4" data-name="header" :data-value="4"></view>
|
||||
<view v-if="t == 'h5'" :class="formats.header === 5 ? 'ql-active' : ''" class="iconfont icon-format-header-5" data-name="header" :data-value="5"></view>
|
||||
<view v-if="t == 'h6'" :class="formats.header === 6 ? 'ql-active' : ''" class="iconfont icon-format-header-6" data-name="header" :data-value="6"></view>
|
||||
<view v-if="t == 'sub'" :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao" data-name="script" data-value="sub"></view>
|
||||
<view v-if="t == 'super'" :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao" data-name="script" data-value="super"></view>
|
||||
<view
|
||||
v-if="t == 'rtl'"
|
||||
:class="formats.direction === 'rtl' ? 'ql-active' : ''"
|
||||
class="iconfont icon-direction-rtl"
|
||||
data-name="direction"
|
||||
:data-value="formats.direction === 'rtl' ? '' : 'rtl'"
|
||||
></view>
|
||||
</block>
|
||||
</view>
|
||||
<uni-popup ref="popup" type="bottom" @transed="colorPop">
|
||||
<colorPicker :color="color" :show="showColor" @confirm="colorChanged" @cancel="colorPopClose"></colorPicker>
|
||||
</uni-popup>
|
||||
<view class="preview" v-show="showPreview"><rich-text :nodes="htmlData" class="previewNodes"></rich-text></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import colorPicker from '@/components/colorPicker.vue';
|
||||
import uniPopup from '@/components/uni-popup/uni-popup.vue';
|
||||
import topbar from './header.vue';
|
||||
export default {
|
||||
components: {
|
||||
colorPicker,
|
||||
uniPopup,
|
||||
topbar
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String
|
||||
},
|
||||
imageUploader: {
|
||||
type: Function
|
||||
},
|
||||
muiltImage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
compressImage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
previewMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoHideToolbar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tools: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strike',
|
||||
'align-left',
|
||||
'align-center',
|
||||
'align-right',
|
||||
'remove',
|
||||
'font',
|
||||
'color',
|
||||
'backgroundColor',
|
||||
'image',
|
||||
'clear',
|
||||
'preview'
|
||||
];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
readOnly: false,
|
||||
formats: {},
|
||||
fontColor: '#000000',
|
||||
bgColor: '',
|
||||
color: '',
|
||||
colorPickerName: '',
|
||||
showColor: false,
|
||||
fontSizeRange: [10, 12, 14, 16, 18, 24, 32],
|
||||
showPreview: false,
|
||||
htmlData: '',
|
||||
html: '',
|
||||
keyboardHeight: 0,
|
||||
editorHeight: 0,
|
||||
isIOS: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value: function(newvar) {
|
||||
this.html = newvar;
|
||||
},
|
||||
html: function(newvar) {
|
||||
if (this.previewMode) {
|
||||
this.previewData(this.html);
|
||||
}
|
||||
if (this.editorCtx) {
|
||||
this.editorCtx.setContents({
|
||||
html: this.html
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.html = this.value;
|
||||
},
|
||||
mounted: function() {
|
||||
const platform = uni.getSystemInfoSync().platform;
|
||||
this.isIOS = platform === 'ios';
|
||||
if (this.previewMode) {
|
||||
this.previewData(this.html);
|
||||
}
|
||||
let keyboardHeight = 0;
|
||||
this.updatePosition(0);
|
||||
uni.onKeyboardHeightChange(res => {
|
||||
if (res.height === keyboardHeight) return;
|
||||
const duration = res.height > 0 ? res.duration * 1000 : 0;
|
||||
keyboardHeight = res.height;
|
||||
setTimeout(() => {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
success: () => {
|
||||
this.updatePosition(keyboardHeight);
|
||||
this.editorCtx && this.editorCtx.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}, duration);
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
labelConfirm: function() {
|
||||
return this.showPreview ? '关闭' : '保存';
|
||||
},
|
||||
labelCancel: function() {
|
||||
return this.showPreview ? '' : '取消';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePosition(keyboardHeight) {
|
||||
const { windowHeight, windowWidth, platform } = uni.getSystemInfoSync();
|
||||
const rpx = windowWidth / 750;
|
||||
let topbarHeight = 85 * rpx;
|
||||
//#ifdef H5
|
||||
topbarHeight += 44;
|
||||
//#endif
|
||||
const toolbarHeight = (70 * Math.ceil(this.tools.length / 15) + 1) * rpx;
|
||||
|
||||
const bodyHeight = windowHeight - topbarHeight;
|
||||
this.keyboardHeight = keyboardHeight;
|
||||
this.editorHeight = keyboardHeight > 0 ? bodyHeight - keyboardHeight - toolbarHeight : this.autoHideToolbar ? bodyHeight : bodyHeight - toolbarHeight;
|
||||
},
|
||||
openColor(e) {
|
||||
var name = e.currentTarget.dataset.name;
|
||||
var color = this.formats[name];
|
||||
this.colorPickerName = name;
|
||||
if (name == 'backgroundColor' && !color) {
|
||||
color = '#FFFFFF';
|
||||
}
|
||||
if (name == 'color' && !color) {
|
||||
color = '#000000';
|
||||
}
|
||||
this.color = color;
|
||||
this.$refs.popup.open({
|
||||
type: 'bottom'
|
||||
});
|
||||
},
|
||||
colorPop(e) {
|
||||
this.showColor = e.show;
|
||||
},
|
||||
colorPopClose() {
|
||||
this.$refs.popup.close();
|
||||
},
|
||||
colorChanged(e) {
|
||||
let label = '';
|
||||
switch (this.colorPickerName) {
|
||||
case 'backgroundColor':
|
||||
if (e.color == '#FFFFFF') {
|
||||
e.color = '';
|
||||
}
|
||||
this.bgColor = e.color;
|
||||
label = '背景色';
|
||||
break;
|
||||
case 'color':
|
||||
this.fontColor = e.color;
|
||||
label = '颜色';
|
||||
break;
|
||||
}
|
||||
this.colorPopClose();
|
||||
this._format(this.colorPickerName, e.color, label + e.color);
|
||||
},
|
||||
readOnlyChange() {
|
||||
this.readOnly = !this.readOnly;
|
||||
},
|
||||
onEditorReady() {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#editor')
|
||||
.context(res => {
|
||||
this.editorCtx = res.context;
|
||||
if (this.html) {
|
||||
this.editorCtx.setContents({
|
||||
html: this.html
|
||||
});
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
undo() {
|
||||
this.editorCtx.undo();
|
||||
this.toast('撤销');
|
||||
},
|
||||
redo() {
|
||||
this.editorCtx.redo();
|
||||
this.toast('重做');
|
||||
},
|
||||
format(e) {
|
||||
let { name, value, label } = e.target.dataset;
|
||||
if (!name) return;
|
||||
this._format(name, value, label);
|
||||
},
|
||||
_format(name, value, label) {
|
||||
this.editorCtx.format(name, value);
|
||||
this.toast(label);
|
||||
},
|
||||
toast(label) {
|
||||
uni.showToast({
|
||||
duration: 600,
|
||||
icon: 'none',
|
||||
title: label
|
||||
});
|
||||
},
|
||||
onStatusChange(e) {
|
||||
const formats = e.detail;
|
||||
this.formats = formats;
|
||||
},
|
||||
insertDivider() {
|
||||
this.editorCtx.insertDivider({
|
||||
success: function() {
|
||||
this.toast('插入分割线');
|
||||
}
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
this.editorCtx.clear({
|
||||
success: res => {
|
||||
this.toast('清空');
|
||||
}
|
||||
});
|
||||
},
|
||||
removeFormat() {
|
||||
this.editorCtx.removeFormat();
|
||||
this.toast('清除格式');
|
||||
},
|
||||
insertDate() {
|
||||
const date = new Date();
|
||||
const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||
this.editorCtx.insertText({
|
||||
text: formatDate
|
||||
});
|
||||
this.toast('插入日期');
|
||||
},
|
||||
insertImage() {
|
||||
let params = {};
|
||||
params.count = this.muiltImage ? 9 : 1;
|
||||
params.sizeType = this.compressImage ? ['compressed'] : ['original'];
|
||||
uni.chooseImage({
|
||||
...params,
|
||||
success: res => {
|
||||
res.tempFiles.map(path => {
|
||||
this.imageUploader(path, url => {
|
||||
this.editorCtx.insertImage({
|
||||
src: url,
|
||||
alt: '图像'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fontSize(e) {
|
||||
const index = e.detail.value;
|
||||
const fz = this.fontSizeRange[index] + 'px';
|
||||
this._format('fontSize', fz, '字体大小:' + fz);
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
save() {
|
||||
if (this.showPreview) {
|
||||
if (this.previewMode) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.showPreview = false;
|
||||
}
|
||||
} else {
|
||||
this.editorCtx.getContents({
|
||||
success: res => {
|
||||
this.$emit('save', res);
|
||||
this.$emit('input', res.html);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
previewData: function(html) {
|
||||
this.htmlData = html.replace(/\<img/gi, '<img style="max-width:100%;height:auto"');
|
||||
this.showPreview = true;
|
||||
},
|
||||
preview: function() {
|
||||
this.editorCtx.getContents({
|
||||
success: res => {
|
||||
this.previewData(res.html);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './editor-icon.css';
|
||||
|
||||
.wrapper {
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
|
||||
.ql-container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
overflow: auto;
|
||||
padding: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
line-height: 50rpx;
|
||||
|
||||
.iconfont {
|
||||
display: inline-block;
|
||||
padding: 10rpx 0;
|
||||
width: 50rpx;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
margin-top: 90rpx;
|
||||
|
||||
.previewNodes {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.ql-active {
|
||||
color: #06c;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<view class="head">
|
||||
<view class="btn left" @tap="cancel" v-if="labelCancel">{{labelCancel}}</view>
|
||||
<view class="btn right" @tap="save" v-if="labelConfirm">{{labelConfirm}}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props:{
|
||||
labelCancel:{
|
||||
type:String,
|
||||
default:"取消"
|
||||
},
|
||||
labelConfirm:{
|
||||
type:String,
|
||||
default:"确定"
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
cancel:function(){
|
||||
this.$emit('cancel')
|
||||
},
|
||||
save:function(){
|
||||
this.$emit('save')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-bottom: 1px #eee solid;
|
||||
// box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
.btn {
|
||||
display: block;
|
||||
width: 150upx;
|
||||
height: 80upx;
|
||||
line-height: 80upx;
|
||||
font-size: 30upx;
|
||||
color: #666;
|
||||
padding-left: 20upx;
|
||||
text-align: center;
|
||||
&.left{
|
||||
float: left;
|
||||
}
|
||||
&.right{
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
<template>
|
||||
<view v-if="showPopup" class="uni-popup" @touchmove.stop.prevent="clear">
|
||||
<uni-transition :mode-class="['fade']" :styles="maskClass" :duration="duration" :show="showTrans" @click="onTap" />
|
||||
<uni-transition :mode-class="ani" :styles="transClass" :duration="duration" :show="showTrans" @click="onTap">
|
||||
<view class="uni-popup__wrapper-box" @click.stop="clear">
|
||||
<slot />
|
||||
</view>
|
||||
</uni-transition>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uniTransition from '../uni-transition/uni-transition.vue'
|
||||
|
||||
/**
|
||||
* PopUp 弹出层
|
||||
* @description 弹出层组件,为了解决遮罩弹层的问题
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
|
||||
* @property {String} type = [top|center|bottom] 弹出方式
|
||||
* @value top 顶部弹出
|
||||
* @value center 中间弹出
|
||||
* @value bottom 底部弹出
|
||||
* @property {Boolean} animation = [ture|false] 是否开启动画
|
||||
* @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
|
||||
* @event {Function} change 打开关闭弹窗触发,e={show: false}
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'UniPopup',
|
||||
components: {
|
||||
uniTransition
|
||||
},
|
||||
props: {
|
||||
// 开启动画
|
||||
animation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
|
||||
type: {
|
||||
type: String,
|
||||
default: 'center'
|
||||
},
|
||||
// maskClick
|
||||
maskClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
duration: 300,
|
||||
ani: [],
|
||||
showPopup: false,
|
||||
showTrans: false,
|
||||
maskClass: {
|
||||
'position': 'fixed',
|
||||
'bottom': 0,
|
||||
'top': 0,
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
'backgroundColor': 'rgba(0, 0, 0, 0.4)'
|
||||
},
|
||||
transClass: {
|
||||
'position': 'fixed',
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
type: {
|
||||
handler: function(newVal) {
|
||||
switch (this.type) {
|
||||
case 'top':
|
||||
this.ani = ['slide-top']
|
||||
this.transClass = {
|
||||
'position': 'fixed',
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
}
|
||||
break
|
||||
case 'bottom':
|
||||
this.ani = ['slide-bottom']
|
||||
this.transClass = {
|
||||
'position': 'fixed',
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
'bottom': 0
|
||||
}
|
||||
break
|
||||
case 'center':
|
||||
this.ani = ['zoom-out', 'fade']
|
||||
this.transClass = {
|
||||
'position': 'fixed',
|
||||
/* #ifndef APP-NVUE */
|
||||
'display': 'flex',
|
||||
'flexDirection': 'column',
|
||||
/* #endif */
|
||||
'bottom': 0,
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
'top': 0,
|
||||
'justifyContent': 'center',
|
||||
'alignItems': 'center'
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.animation) {
|
||||
this.duration = 300
|
||||
} else {
|
||||
this.duration = 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clear(e) {
|
||||
// TODO nvue 取消冒泡
|
||||
e.stopPropagation()
|
||||
},
|
||||
open() {
|
||||
this.showPopup = true
|
||||
this.$nextTick(() => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => {
|
||||
this.showTrans = true
|
||||
}, 50);
|
||||
setTimeout(()=>{
|
||||
this.$emit('transed', {
|
||||
show: true
|
||||
})
|
||||
},this.duration)
|
||||
})
|
||||
this.$emit('change', {
|
||||
show: true
|
||||
})
|
||||
},
|
||||
close(type) {
|
||||
this.showTrans = false
|
||||
this.$nextTick(() => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => {
|
||||
this.$emit('change', {
|
||||
show: false
|
||||
})
|
||||
this.showPopup = false
|
||||
setTimeout(()=>{
|
||||
this.$emit('transed', {
|
||||
show: false
|
||||
})
|
||||
},this.duration)
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
onTap() {
|
||||
if (!this.maskClick) return
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.uni-popup {
|
||||
position: fixed;
|
||||
/* #ifdef H5 */
|
||||
top: var(--window-top);
|
||||
/* #endif */
|
||||
/* #ifndef H5 */
|
||||
top: 0;
|
||||
/* #endif */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* #ifndef APP-NVUE */
|
||||
z-index: 99;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-popup__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: $uni-bg-color-mask;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mask-ani {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
.uni-top-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.uni-bottom-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.uni-center-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.uni-popup__wrapper {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: translateY(-500px);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: translateY(500px);
|
||||
}
|
||||
|
||||
.center {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* #endif */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: scale(1.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.uni-popup__wrapper-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-ani {
|
||||
// transition: transform 0.3s;
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
|
||||
.uni-top-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.uni-bottom-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.uni-center-content {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
<template>
|
||||
<view v-if="isShow" ref="ani" class="uni-transition" :class="[ani.in]" :style="'transform:' +transform+';'+stylesObject"
|
||||
@click="change">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef APP-NVUE
|
||||
const animation = uni.requireNativePlugin('animation');
|
||||
// #endif
|
||||
/**
|
||||
* Transition 过渡动画
|
||||
* @description 简单过渡动画组件
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
|
||||
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
|
||||
* @property {Array} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
|
||||
* @value fade 渐隐渐出过渡
|
||||
* @value slide-top 由上至下过渡
|
||||
* @value slide-right 由右至左过渡
|
||||
* @value slide-bottom 由下至上过渡
|
||||
* @value slide-left 由左至右过渡
|
||||
* @value zoom-in 由小到大过渡
|
||||
* @value zoom-out 由大到小过渡
|
||||
* @property {Number} duration 过渡动画持续时间
|
||||
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
|
||||
*/
|
||||
export default {
|
||||
name: 'uniTransition',
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modeClass: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
transform: '',
|
||||
ani: { in: '',
|
||||
active: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.open()
|
||||
} else {
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
stylesObject() {
|
||||
let styles = {
|
||||
...this.styles,
|
||||
'transition-duration': this.duration / 1000 + 's'
|
||||
}
|
||||
let transfrom = ''
|
||||
for (let i in styles) {
|
||||
let line = this.toLine(i)
|
||||
transfrom += line + ':' + styles[i] + ';'
|
||||
}
|
||||
return transfrom
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// this.timer = null
|
||||
// this.nextTick = (time = 50) => new Promise(resolve => {
|
||||
// clearTimeout(this.timer)
|
||||
// this.timer = setTimeout(resolve, time)
|
||||
// return this.timer
|
||||
// });
|
||||
},
|
||||
methods: {
|
||||
change() {
|
||||
this.$emit('click', {
|
||||
detail: this.isShow
|
||||
})
|
||||
},
|
||||
open() {
|
||||
clearTimeout(this.timer)
|
||||
this.isShow = true
|
||||
this.transform = ''
|
||||
this.ani.in = ''
|
||||
for (let i in this.getTranfrom(false)) {
|
||||
if (i === 'opacity') {
|
||||
this.ani.in = 'fade-in'
|
||||
} else {
|
||||
this.transform += `${this.getTranfrom(false)[i]} `
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this._animation(true)
|
||||
}, 50)
|
||||
})
|
||||
|
||||
},
|
||||
close(type) {
|
||||
clearTimeout(this.timer)
|
||||
this._animation(false)
|
||||
},
|
||||
_animation(type) {
|
||||
let styles = this.getTranfrom(type)
|
||||
// #ifdef APP-NVUE
|
||||
if(!this.$refs['ani']) return
|
||||
animation.transition(this.$refs['ani'].ref, {
|
||||
styles,
|
||||
duration: this.duration, //ms
|
||||
timingFunction: 'ease',
|
||||
needLayout: false,
|
||||
delay: 0 //ms
|
||||
}, () => {
|
||||
if (!type) {
|
||||
this.isShow = false
|
||||
}
|
||||
this.$emit('change', {
|
||||
detail: this.isShow
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.transform = ''
|
||||
for (let i in styles) {
|
||||
if (i === 'opacity') {
|
||||
this.ani.in = `fade-${type?'out':'in'}`
|
||||
} else {
|
||||
this.transform += `${styles[i]} `
|
||||
}
|
||||
}
|
||||
this.timer = setTimeout(() => {
|
||||
if (!type) {
|
||||
this.isShow = false
|
||||
}
|
||||
this.$emit('change', {
|
||||
detail: this.isShow
|
||||
})
|
||||
|
||||
}, this.duration)
|
||||
// #endif
|
||||
|
||||
},
|
||||
getTranfrom(type) {
|
||||
let styles = {
|
||||
transform: ''
|
||||
}
|
||||
this.modeClass.forEach((mode) => {
|
||||
switch (mode) {
|
||||
case 'fade':
|
||||
styles.opacity = type ? 1 : 0
|
||||
break;
|
||||
case 'slide-top':
|
||||
styles.transform += `translateY(${type?'0':'-100%'}) `
|
||||
break;
|
||||
case 'slide-right':
|
||||
styles.transform += `translateX(${type?'0':'100%'}) `
|
||||
break;
|
||||
case 'slide-bottom':
|
||||
styles.transform += `translateY(${type?'0':'100%'}) `
|
||||
break;
|
||||
case 'slide-left':
|
||||
styles.transform += `translateX(${type?'0':'-100%'}) `
|
||||
break;
|
||||
case 'zoom-in':
|
||||
styles.transform += `scale(${type?1:0.8}) `
|
||||
break;
|
||||
case 'zoom-out':
|
||||
styles.transform += `scale(${type?1:1.2}) `
|
||||
break;
|
||||
}
|
||||
})
|
||||
return styles
|
||||
},
|
||||
_modeClassArr(type) {
|
||||
let mode = this.modeClass
|
||||
if (typeof(mode) !== "string") {
|
||||
let modestr = ''
|
||||
mode.forEach((item) => {
|
||||
modestr += (item + '-' + type + ',')
|
||||
})
|
||||
return modestr.substr(0, modestr.length - 1)
|
||||
} else {
|
||||
return mode + '-' + type
|
||||
}
|
||||
},
|
||||
// getEl(el) {
|
||||
// console.log(el || el.ref || null);
|
||||
// return el || el.ref || null
|
||||
// },
|
||||
toLine(name) {
|
||||
return name.replace(/([A-Z])/g, "-$1").toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-transition {
|
||||
transition-timing-function: ease;
|
||||
transition-duration: 0.3s;
|
||||
transition-property: transform, opacity;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.slide-top-in {
|
||||
/* transition-property: transform, opacity; */
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.slide-top-active {
|
||||
transform: translateY(0);
|
||||
/* opacity: 1; */
|
||||
}
|
||||
|
||||
.slide-right-in {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-right-active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slide-bottom-in {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.slide-bottom-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.slide-left-in {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-left-active {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.zoom-in-in {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.zoom-out-active {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.zoom-out-in {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "robin-editor",
|
||||
"name": "微信小程序富文本编辑器",
|
||||
"version": "1.1.6",
|
||||
"description": "基于原生editor组件的富文本编辑器组件,支持颜色选择,插入图片",
|
||||
"keywords": [
|
||||
"编辑器",
|
||||
"富文本",
|
||||
"小程序"
|
||||
]
|
||||
}
|
||||
|
|
@ -12,6 +12,10 @@ export default {
|
|||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else if (e.reLaunch) {
|
||||
uni.reLaunch({
|
||||
url: e.reLaunch
|
||||
})
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
|
|
|
|||
|
|
@ -232,10 +232,6 @@ export default {
|
|||
.page {
|
||||
padding: 0 20px;
|
||||
background: white;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-text {
|
||||
color: #303133;
|
||||
|
|
@ -251,6 +247,6 @@ export default {
|
|||
}
|
||||
.button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,6 +4,9 @@ module.exports = () => {
|
|||
// 全局配置
|
||||
uni.$u.http.setConfig((config) => {
|
||||
config.baseURL = process.env.VUE_APP_BASE_API
|
||||
config.custom = {
|
||||
toast: true
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue