会员权益
This commit is contained in:
825
uni_modules/sp-editor/components/sp-editor/color-picker.vue
Normal file
825
uni_modules/sp-editor/components/sp-editor/color-picker.vue
Normal file
@@ -0,0 +1,825 @@
|
||||
<template>
|
||||
<view v-if="show" class="t-wrapper" @touchmove.stop.prevent="moveHandle">
|
||||
<view class="t-mask" :class="{ active: active }" @click.stop="close"></view>
|
||||
<view class="t-box" :class="{ active: active }">
|
||||
<view class="t-header">
|
||||
<view class="t-header-button" @click="close">取消</view>
|
||||
<view class="t-header-button" @click="confirm">确认</view>
|
||||
</view>
|
||||
<view class="t-color__box" :style="{ background: 'rgb(' + bgcolor.r + ',' + bgcolor.g + ',' + bgcolor.b + ')' }">
|
||||
<view
|
||||
class="t-background boxs"
|
||||
@touchstart="touchstart($event, 0)"
|
||||
@touchmove="touchmove($event, 0)"
|
||||
@touchend="touchend($event, 0)"
|
||||
>
|
||||
<view class="t-color-mask"></view>
|
||||
<view class="t-pointer" :style="{ top: site[0].top - 8 + 'px', left: site[0].left - 8 + 'px' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="t-control__box">
|
||||
<view class="t-control__color">
|
||||
<view
|
||||
class="t-control__color-content"
|
||||
:style="{ background: 'rgba(' + rgba.r + ',' + rgba.g + ',' + rgba.b + ',' + rgba.a + ')' }"
|
||||
></view>
|
||||
</view>
|
||||
<view class="t-control-box__item">
|
||||
<view
|
||||
class="t-controller boxs"
|
||||
@touchstart="touchstart($event, 1)"
|
||||
@touchmove="touchmove($event, 1)"
|
||||
@touchend="touchend($event, 1)"
|
||||
>
|
||||
<view class="t-hue">
|
||||
<view class="t-circle" :style="{ left: site[1].left - 12 + 'px' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="t-controller boxs"
|
||||
@touchstart="touchstart($event, 2)"
|
||||
@touchmove="touchmove($event, 2)"
|
||||
@touchend="touchend($event, 2)"
|
||||
>
|
||||
<view class="t-transparency">
|
||||
<view class="t-circle" :style="{ left: site[2].left - 12 + 'px' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="t-result__box">
|
||||
<view v-if="mode" class="t-result__item">
|
||||
<view class="t-result__box-input">{{ hex }}</view>
|
||||
<view class="t-result__box-text">HEX</view>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="t-result__item">
|
||||
<view class="t-result__box-input">{{ rgba.r }}</view>
|
||||
<view class="t-result__box-text">R</view>
|
||||
</view>
|
||||
<view class="t-result__item">
|
||||
<view class="t-result__box-input">{{ rgba.g }}</view>
|
||||
<view class="t-result__box-text">G</view>
|
||||
</view>
|
||||
<view class="t-result__item">
|
||||
<view class="t-result__box-input">{{ rgba.b }}</view>
|
||||
<view class="t-result__box-text">B</view>
|
||||
</view>
|
||||
<view class="t-result__item">
|
||||
<view class="t-result__box-input">{{ rgba.a }}</view>
|
||||
<view class="t-result__box-text">A</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="t-result__item t-select" @click="select">
|
||||
<view class="t-result__box-input">
|
||||
<view>切换</view>
|
||||
<view>模式</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="t-alternative">
|
||||
<view class="t-alternative__item" v-for="(item, index) in colorList" :key="index">
|
||||
<view
|
||||
class="t-alternative__item-content"
|
||||
:style="{ background: 'rgba(' + item.r + ',' + item.g + ',' + item.b + ',' + item.a + ')' }"
|
||||
@click="selectColor(item)"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
color: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
spareColor: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
active: false,
|
||||
// rgba 颜色
|
||||
rgba: {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
},
|
||||
// hsb 颜色
|
||||
hsb: {
|
||||
h: 0,
|
||||
s: 0,
|
||||
b: 0
|
||||
},
|
||||
site: [
|
||||
{
|
||||
top: 0,
|
||||
left: 0
|
||||
},
|
||||
{
|
||||
left: 0
|
||||
},
|
||||
{
|
||||
left: 0
|
||||
}
|
||||
],
|
||||
index: 0,
|
||||
bgcolor: {
|
||||
r: 255,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
},
|
||||
hex: '#000000',
|
||||
mode: true,
|
||||
colorList: [
|
||||
{
|
||||
r: 244,
|
||||
g: 67,
|
||||
b: 54,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 233,
|
||||
g: 30,
|
||||
b: 99,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 156,
|
||||
g: 39,
|
||||
b: 176,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 103,
|
||||
g: 58,
|
||||
b: 183,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 63,
|
||||
g: 81,
|
||||
b: 181,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 33,
|
||||
g: 150,
|
||||
b: 243,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 3,
|
||||
g: 169,
|
||||
b: 244,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
g: 188,
|
||||
b: 212,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
g: 150,
|
||||
b: 136,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 76,
|
||||
g: 175,
|
||||
b: 80,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 139,
|
||||
g: 195,
|
||||
b: 74,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 205,
|
||||
g: 220,
|
||||
b: 57,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 255,
|
||||
g: 235,
|
||||
b: 59,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 255,
|
||||
g: 193,
|
||||
b: 7,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 255,
|
||||
g: 152,
|
||||
b: 0,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 255,
|
||||
g: 87,
|
||||
b: 34,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 121,
|
||||
g: 85,
|
||||
b: 72,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 158,
|
||||
g: 158,
|
||||
b: 158,
|
||||
a: 1
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0.5
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.ready()
|
||||
},
|
||||
methods: {
|
||||
ready() {
|
||||
this.rgba = this.color
|
||||
if (this.spareColor.length !== 0) {
|
||||
this.colorList = this.spareColor
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init() {
|
||||
// hsb 颜色
|
||||
this.hsb = this.rgbToHex(this.rgba)
|
||||
// this.setColor();
|
||||
this.setValue(this.rgba)
|
||||
},
|
||||
moveHandle() {},
|
||||
open() {
|
||||
this.show = true
|
||||
this.$nextTick(() => {
|
||||
this.init()
|
||||
setTimeout(() => {
|
||||
this.active = true
|
||||
setTimeout(() => {
|
||||
this.getSelectorQuery()
|
||||
}, 350)
|
||||
}, 50)
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.active = false
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.show = false
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
confirm() {
|
||||
this.close()
|
||||
this.$emit('confirm', {
|
||||
rgba: this.rgba,
|
||||
hex: this.hex
|
||||
})
|
||||
},
|
||||
// 选择模式
|
||||
select() {
|
||||
this.mode = !this.mode
|
||||
},
|
||||
// 常用颜色选择
|
||||
selectColor(item) {
|
||||
this.setColorBySelect(item)
|
||||
},
|
||||
touchstart(e, index) {
|
||||
const { pageX, pageY, clientX, clientY } = e.touches[0]
|
||||
// 部分机型可能没有pageX或clientX,因此此处需要做兼容
|
||||
this.moveX = clientX || pageX
|
||||
this.moveY = clientY || pageY
|
||||
this.setPosition(this.moveX, this.moveY, index)
|
||||
},
|
||||
touchmove(e, index) {
|
||||
const { pageX, pageY, clientX, clientY } = e.touches[0]
|
||||
this.moveX = clientX || pageX
|
||||
this.moveY = clientY || pageY
|
||||
this.setPosition(this.moveX, this.moveY, index)
|
||||
},
|
||||
touchend(e, index) {},
|
||||
/**
|
||||
* 设置位置
|
||||
*/
|
||||
setPosition(x, y, index) {
|
||||
this.index = index
|
||||
const { top, left, width, height } = this.position[index]
|
||||
// 设置最大最小值
|
||||
|
||||
this.site[index].left = Math.max(0, Math.min(parseInt(x - left), width))
|
||||
if (index === 0) {
|
||||
this.site[index].top = Math.max(0, Math.min(parseInt(y - top), height))
|
||||
// 设置颜色
|
||||
this.hsb.s = parseInt((100 * this.site[index].left) / width)
|
||||
this.hsb.b = parseInt(100 - (100 * this.site[index].top) / height)
|
||||
this.setColor()
|
||||
this.setValue(this.rgba)
|
||||
} else {
|
||||
this.setControl(index, this.site[index].left)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 设置 rgb 颜色
|
||||
*/
|
||||
setColor() {
|
||||
const rgb = this.HSBToRGB(this.hsb)
|
||||
this.rgba.r = rgb.r
|
||||
this.rgba.g = rgb.g
|
||||
this.rgba.b = rgb.b
|
||||
},
|
||||
/**
|
||||
* 设置二进制颜色
|
||||
* @param {Object} rgb
|
||||
*/
|
||||
setValue(rgb) {
|
||||
this.hex = '#' + this.rgbToHex(rgb)
|
||||
},
|
||||
setControl(index, x) {
|
||||
const { top, left, width, height } = this.position[index]
|
||||
|
||||
if (index === 1) {
|
||||
this.hsb.h = parseInt((360 * x) / width)
|
||||
this.bgcolor = this.HSBToRGB({
|
||||
h: this.hsb.h,
|
||||
s: 100,
|
||||
b: 100
|
||||
})
|
||||
this.setColor()
|
||||
} else {
|
||||
this.rgba.a = (x / width).toFixed(1)
|
||||
}
|
||||
this.setValue(this.rgba)
|
||||
},
|
||||
/**
|
||||
* rgb 转 二进制 hex
|
||||
* @param {Object} rgb
|
||||
*/
|
||||
rgbToHex(rgb) {
|
||||
let hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16)]
|
||||
hex.map(function (str, i) {
|
||||
if (str.length == 1) {
|
||||
hex[i] = '0' + str
|
||||
}
|
||||
})
|
||||
return hex.join('')
|
||||
},
|
||||
setColorBySelect(getrgb) {
|
||||
const { r, g, b, a } = getrgb
|
||||
let rgb = {}
|
||||
rgb = {
|
||||
r: r ? parseInt(r) : 0,
|
||||
g: g ? parseInt(g) : 0,
|
||||
b: b ? parseInt(b) : 0,
|
||||
a: a ? a : 0
|
||||
}
|
||||
this.rgba = rgb
|
||||
this.hsb = this.rgbToHsb(rgb)
|
||||
this.changeViewByHsb()
|
||||
},
|
||||
changeViewByHsb() {
|
||||
const [a, b, c] = this.position
|
||||
this.site[0].left = parseInt((this.hsb.s * a.width) / 100)
|
||||
this.site[0].top = parseInt(((100 - this.hsb.b) * a.height) / 100)
|
||||
this.setColor(this.hsb.h)
|
||||
this.setValue(this.rgba)
|
||||
this.bgcolor = this.HSBToRGB({
|
||||
h: this.hsb.h,
|
||||
s: 100,
|
||||
b: 100
|
||||
})
|
||||
|
||||
this.site[1].left = (this.hsb.h / 360) * b.width
|
||||
this.site[2].left = this.rgba.a * c.width
|
||||
},
|
||||
/**
|
||||
* hsb 转 rgb
|
||||
* @param {Object} 颜色模式 H(hues)表示色相,S(saturation)表示饱和度,B(brightness)表示亮度
|
||||
*/
|
||||
HSBToRGB(hsb) {
|
||||
let rgb = {}
|
||||
let h = Math.round(hsb.h)
|
||||
let s = Math.round((hsb.s * 255) / 100)
|
||||
let v = Math.round((hsb.b * 255) / 100)
|
||||
if (s == 0) {
|
||||
rgb.r = rgb.g = rgb.b = v
|
||||
} else {
|
||||
let t1 = v
|
||||
let t2 = ((255 - s) * v) / 255
|
||||
let t3 = ((t1 - t2) * (h % 60)) / 60
|
||||
if (h == 360) h = 0
|
||||
if (h < 60) {
|
||||
rgb.r = t1
|
||||
rgb.b = t2
|
||||
rgb.g = t2 + t3
|
||||
} else if (h < 120) {
|
||||
rgb.g = t1
|
||||
rgb.b = t2
|
||||
rgb.r = t1 - t3
|
||||
} else if (h < 180) {
|
||||
rgb.g = t1
|
||||
rgb.r = t2
|
||||
rgb.b = t2 + t3
|
||||
} else if (h < 240) {
|
||||
rgb.b = t1
|
||||
rgb.r = t2
|
||||
rgb.g = t1 - t3
|
||||
} else if (h < 300) {
|
||||
rgb.b = t1
|
||||
rgb.g = t2
|
||||
rgb.r = t2 + t3
|
||||
} else if (h < 360) {
|
||||
rgb.r = t1
|
||||
rgb.g = t2
|
||||
rgb.b = t1 - t3
|
||||
} else {
|
||||
rgb.r = 0
|
||||
rgb.g = 0
|
||||
rgb.b = 0
|
||||
}
|
||||
}
|
||||
return {
|
||||
r: Math.round(rgb.r),
|
||||
g: Math.round(rgb.g),
|
||||
b: Math.round(rgb.b)
|
||||
}
|
||||
},
|
||||
rgbToHsb(rgb) {
|
||||
let hsb = {
|
||||
h: 0,
|
||||
s: 0,
|
||||
b: 0
|
||||
}
|
||||
let min = Math.min(rgb.r, rgb.g, rgb.b)
|
||||
let max = Math.max(rgb.r, rgb.g, rgb.b)
|
||||
let delta = max - min
|
||||
hsb.b = max
|
||||
hsb.s = max != 0 ? (255 * delta) / max : 0
|
||||
if (hsb.s != 0) {
|
||||
if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta
|
||||
else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta
|
||||
else hsb.h = 4 + (rgb.r - rgb.g) / delta
|
||||
} else hsb.h = -1
|
||||
hsb.h *= 60
|
||||
if (hsb.h < 0) hsb.h = 0
|
||||
hsb.s *= 100 / 255
|
||||
hsb.b *= 100 / 255
|
||||
return hsb
|
||||
},
|
||||
getSelectorQuery() {
|
||||
const views = uni.createSelectorQuery().in(this)
|
||||
views
|
||||
.selectAll('.boxs')
|
||||
.boundingClientRect((data) => {
|
||||
if (!data || data.length === 0) {
|
||||
setTimeout(() => this.getSelectorQuery(), 20)
|
||||
return
|
||||
}
|
||||
this.position = data
|
||||
// this.site[0].top = data[0].height;
|
||||
// this.site[0].left = 0;
|
||||
// this.site[1].left = data[1].width;
|
||||
// this.site[2].left = data[2].width;
|
||||
this.setColorBySelect(this.rgba)
|
||||
})
|
||||
.exec()
|
||||
},
|
||||
hex2Rgb(hexColor, alpha = 1) {
|
||||
const color = hexColor.slice(1)
|
||||
const r = parseInt(color.slice(0, 2), 16)
|
||||
const g = parseInt(color.slice(2, 4), 16)
|
||||
const b = parseInt(color.slice(4, 6), 16)
|
||||
return {
|
||||
r: r,
|
||||
g: g,
|
||||
b: b,
|
||||
a: alpha
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
spareColor(newVal) {
|
||||
this.colorList = newVal
|
||||
},
|
||||
color(newVal) {
|
||||
this.ready()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.t-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.t-box {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 30upx 0;
|
||||
padding-top: 0;
|
||||
background: #fff;
|
||||
transition: all 0.3s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.t-box.active {
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
.t-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100upx;
|
||||
border-bottom: 1px #eee solid;
|
||||
box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.t-header-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 150upx;
|
||||
height: 100upx;
|
||||
font-size: 30upx;
|
||||
color: #666;
|
||||
padding-left: 20upx;
|
||||
}
|
||||
|
||||
.t-header-button:last-child {
|
||||
justify-content: flex-end;
|
||||
padding-right: 20upx;
|
||||
}
|
||||
|
||||
.t-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: -1;
|
||||
transition: all 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.t-mask.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.t-color__box {
|
||||
position: relative;
|
||||
height: 400upx;
|
||||
background: rgb(255, 0, 0);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
margin: 0 20upx;
|
||||
margin-top: 20upx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.t-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.t-color-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 400upx;
|
||||
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
.t-pointer {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: -8px;
|
||||
z-index: 2;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 1px #fff solid;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.t-show-color {
|
||||
width: 100upx;
|
||||
height: 50upx;
|
||||
}
|
||||
|
||||
.t-control__box {
|
||||
margin-top: 50upx;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-left: 20upx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.t-control__color {
|
||||
flex-shrink: 0;
|
||||
width: 100upx;
|
||||
height: 100upx;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
|
||||
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
|
||||
background-size: 36upx 36upx;
|
||||
background-position: 0 0, 18upx 18upx;
|
||||
border: 1px #eee solid;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.t-control__color-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.t-control-box__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 30upx;
|
||||
}
|
||||
|
||||
.t-controller {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
|
||||
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
|
||||
background-size: 32upx 32upx;
|
||||
background-position: 0 0, 16upx 16upx;
|
||||
}
|
||||
|
||||
.t-hue {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
||||
}
|
||||
|
||||
.t-transparency {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0));
|
||||
}
|
||||
|
||||
.t-circle {
|
||||
position: absolute;
|
||||
/* right: -10px; */
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.t-result__box {
|
||||
margin-top: 20upx;
|
||||
padding: 10upx;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.t-result__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10upx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.t-result__box-input {
|
||||
padding: 10upx 0;
|
||||
width: 100%;
|
||||
font-size: 28upx;
|
||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
color: #999;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.t-result__box-text {
|
||||
margin-top: 10upx;
|
||||
font-size: 28upx;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.t-select {
|
||||
flex-shrink: 0;
|
||||
width: 150upx;
|
||||
padding: 0 30upx;
|
||||
}
|
||||
|
||||
.t-select .t-result__box-input {
|
||||
border-radius: 10upx;
|
||||
border: none;
|
||||
color: #999;
|
||||
box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.t-select .t-result__box-input:active {
|
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.t-alternative {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
/* justify-content: space-between; */
|
||||
width: 100%;
|
||||
padding-right: 10upx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.t-alternative__item {
|
||||
margin-left: 12upx;
|
||||
margin-top: 10upx;
|
||||
width: 50upx;
|
||||
height: 50upx;
|
||||
border-radius: 10upx;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
|
||||
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
|
||||
background-size: 36upx 36upx;
|
||||
background-position: 0 0, 18upx 18upx;
|
||||
border: 1px #eee solid;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.t-alternative__item-content {
|
||||
width: 50upx;
|
||||
height: 50upx;
|
||||
background: rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.t-alternative__item:active {
|
||||
transition: all 0.3s;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
140
uni_modules/sp-editor/components/sp-editor/fab-tool.vue
Normal file
140
uni_modules/sp-editor/components/sp-editor/fab-tool.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="fab-tool">
|
||||
<view id="toolfab">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="fab-tool-content" :style="placementStyle" id="placementfab">
|
||||
<slot name="content" v-if="visible"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'auto' // 'auto' | 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
placementHeight: '0',
|
||||
placementType: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal) {
|
||||
const { screenWidth } = uni.getSystemInfoSync()
|
||||
|
||||
this.$nextTick(() => {
|
||||
let placementWidth = 0
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#placementfab')
|
||||
.boundingClientRect((res) => {
|
||||
this.placementHeight = -res.height + 'px'
|
||||
placementWidth = res.width
|
||||
})
|
||||
.exec()
|
||||
// 开启自动模式后
|
||||
if (this.placement == 'auto') {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#toolfab')
|
||||
.boundingClientRect((res) => {
|
||||
let leftRemain = res.left
|
||||
let rightRemain = screenWidth - leftRemain
|
||||
if (rightRemain > placementWidth) {
|
||||
this.placementType = 'bottom-start'
|
||||
} else if (leftRemain > placementWidth) {
|
||||
this.placementType = 'bottom-end'
|
||||
} else {
|
||||
this.placementType = 'bottom-center'
|
||||
}
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.placementType = this.placement
|
||||
},
|
||||
computed: {
|
||||
placementStyle() {
|
||||
let position = {}
|
||||
switch (this.placementType) {
|
||||
case 'top-start':
|
||||
position = {
|
||||
top: this.placementHeight,
|
||||
left: 0
|
||||
}
|
||||
break
|
||||
case 'top-center':
|
||||
position = {
|
||||
top: this.placementHeight,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)'
|
||||
}
|
||||
break
|
||||
case 'top-end':
|
||||
position = {
|
||||
top: this.placementHeight,
|
||||
right: 0
|
||||
}
|
||||
break
|
||||
case 'bottom-start':
|
||||
position = {
|
||||
bottom: this.placementHeight,
|
||||
left: 0
|
||||
}
|
||||
break
|
||||
case 'bottom-center':
|
||||
position = {
|
||||
bottom: this.placementHeight,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)'
|
||||
}
|
||||
break
|
||||
case 'bottom-end':
|
||||
position = {
|
||||
bottom: this.placementHeight,
|
||||
right: 0
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return position
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.fab-tool {
|
||||
position: relative;
|
||||
|
||||
.fab-tool-content {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
background-color: #ffffff;
|
||||
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.05), 2px 2px 4px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
152
uni_modules/sp-editor/components/sp-editor/link-edit.vue
Normal file
152
uni_modules/sp-editor/components/sp-editor/link-edit.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<view class="link-edit-container" v-if="showPopup">
|
||||
<view class="link-edit">
|
||||
<view class="title">添加链接</view>
|
||||
<view class="edit">
|
||||
<view class="description">
|
||||
链接描述:
|
||||
<input v-model="descVal" type="text" class="input" placeholder="请输入链接描述" />
|
||||
</view>
|
||||
<view class="address">
|
||||
链接地址:
|
||||
<input v-model="addrVal" type="text" class="input" placeholder="请输入链接地址" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="control">
|
||||
<view class="cancel" @click="close">取消</view>
|
||||
<view class="confirm" @click="onConfirm">确认</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mask"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
descVal: '',
|
||||
addrVal: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.showPopup = true
|
||||
this.$emit('open')
|
||||
},
|
||||
close() {
|
||||
this.showPopup = false
|
||||
this.descVal = ''
|
||||
this.addrVal = ''
|
||||
this.$emit('close')
|
||||
},
|
||||
onConfirm() {
|
||||
if (!this.descVal) {
|
||||
uni.showToast({
|
||||
title: '请输入链接描述',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!this.addrVal) {
|
||||
uni.showToast({
|
||||
title: '请输入链接地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$emit('confirm', {
|
||||
text: this.descVal,
|
||||
href: this.addrVal
|
||||
})
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.link-edit-container {
|
||||
.link-edit {
|
||||
width: 80%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #ffffff;
|
||||
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.05), 2px 2px 4px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 999;
|
||||
font-size: 14px;
|
||||
|
||||
.title {
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit {
|
||||
padding: 24rpx;
|
||||
border-top: 1px solid #eeeeee;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
box-sizing: border-box;
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #eeeeee;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.uni-input-placeholder {
|
||||
color: #dddddd;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.address {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.control {
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
|
||||
.cancel {
|
||||
flex: 1;
|
||||
color: #dd524d;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.confirm {
|
||||
border-left: 1px solid #eeeeee;
|
||||
flex: 1;
|
||||
color: #007aff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
z-index: 998;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
681
uni_modules/sp-editor/components/sp-editor/sp-editor.vue
Normal file
681
uni_modules/sp-editor/components/sp-editor/sp-editor.vue
Normal file
@@ -0,0 +1,681 @@
|
||||
<template>
|
||||
<view class="sp-editor" :style="{ '--icon-size': iconSize, '--icon-columns': iconColumns, '--theme-color': themeColor }">
|
||||
<view class="sp-editor-toolbar" v-if="!readOnly" @tap="format">
|
||||
<!-- 标题栏 -->
|
||||
<fab-tool v-if="toolbarList.includes('header')" :visible="curFab == 'header'">
|
||||
<view :class="formats.header ? 'ql-active' : ''" class="iconfont icon-header" title="标题" data-name="header" @click.stop="fabTap('header')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'header')">
|
||||
<view v-for="item in fabTools.header" :key="item.value">
|
||||
<view v-if="toolbarList.includes(item.name)" class="fab-sub" :class="[formats.header === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]" :title="item.title" data-name="header" :data-value="item.value"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<view v-if="toolbarList.includes('bold')" :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" title="加粗" data-name="bold"></view>
|
||||
<view v-if="toolbarList.includes('italic')" :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" title="斜体" data-name="italic"></view>
|
||||
<view v-if="toolbarList.includes('underline')" :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian" title="下划线" data-name="underline"></view>
|
||||
<view v-if="toolbarList.includes('strike')" :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian" title="删除线" data-name="strike"></view>
|
||||
<!-- 对齐方式 -->
|
||||
<fab-tool v-if="toolbarList.includes('align')" :visible="curFab == 'align'">
|
||||
<view :class="formats.align ? 'ql-active' : ''" class="iconfont icon-zuoyouduiqi" title="对齐方式" data-name="align" @click.stop="fabTap('align')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'align')">
|
||||
<view v-for="item in fabTools.align" :key="item.value">
|
||||
<view v-if="toolbarList.includes(item.name)" class="fab-sub" :class="[formats.align === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]" :title="item.title" data-name="align" :data-value="item.value"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<!-- 行间距 -->
|
||||
<fab-tool v-if="toolbarList.includes('lineHeight')" :visible="curFab == 'lineHeight'">
|
||||
<view :class="formats.lineHeight ? 'ql-active' : ''" class="iconfont icon-line-height" title="行间距" data-name="lineHeight" @click.stop="fabTap('lineHeight')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'lineHeight')">
|
||||
<view v-for="item in fabTools.lineHeight" :key="item.value">
|
||||
<view class="fab-sub" :class="[formats.lineHeight === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]" :title="item.title" data-name="lineHeight" :data-value="item.value">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<!-- 字间距 -->
|
||||
<fab-tool v-if="toolbarList.includes('letterSpacing')" :visible="curFab == 'letterSpacing'">
|
||||
<view :class="formats.letterSpacing ? 'ql-active' : ''" class="iconfont icon-Character-Spacing" title="字间距" data-name="letterSpacing" @click.stop="fabTap('letterSpacing')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'letterSpacing')">
|
||||
<view v-for="item in fabTools.space" :key="item.value">
|
||||
<view class="fab-sub" :class="[
|
||||
formats.letterSpacing === item.value ? 'ql-active' : '',
|
||||
item.icon ? 'iconfont' : '',
|
||||
item.icon
|
||||
]" :title="item.title" data-name="letterSpacing" :data-value="item.value">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<!-- 段前距 -->
|
||||
<fab-tool v-if="toolbarList.includes('marginTop')" :visible="curFab == 'marginTop'">
|
||||
<view :class="formats.marginTop ? 'ql-active' : ''" class="iconfont icon-722bianjiqi_duanqianju" title="段前距" data-name="marginTop" @click.stop="fabTap('marginTop')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'marginTop')">
|
||||
<view v-for="item in fabTools.space" :key="item.value">
|
||||
<view class="fab-sub" :class="[formats.marginTop === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]" :title="item.title" data-name="marginTop" :data-value="item.value">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<!-- 段后距 -->
|
||||
<fab-tool v-if="toolbarList.includes('marginBottom')" :visible="curFab == 'marginBottom'">
|
||||
<view :class="formats.marginBottom ? 'ql-active' : ''" class="iconfont icon-723bianjiqi_duanhouju" title="段后距" data-name="marginBottom" @click.stop="fabTap('marginBottom')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'marginBottom')">
|
||||
<view v-for="item in fabTools.space" :key="item.value">
|
||||
<view class="fab-sub" :class="[
|
||||
formats.marginBottom === item.value ? 'ql-active' : '',
|
||||
item.icon ? 'iconfont' : '',
|
||||
item.icon
|
||||
]" :title="item.title" data-name="marginBottom" :data-value="item.value">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<!-- 字体栏 -->
|
||||
<fab-tool v-if="toolbarList.includes('fontFamily')" :visible="curFab == 'fontFamily'">
|
||||
<view :class="formats.fontFamily ? 'ql-active' : ''" class="iconfont icon-font" title="字体" data-name="fontFamily" @click.stop="fabTap('fontFamily')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'fontFamily')">
|
||||
<view v-for="item in fabTools.fontFamily" :key="item.value">
|
||||
<view class="fab-sub" :class="[formats.fontFamily === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]" :title="item.title" data-name="fontFamily" :data-value="item.value">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<!-- 字体大小栏 -->
|
||||
<fab-tool v-if="toolbarList.includes('fontSize')" :visible="curFab == 'fontSize'">
|
||||
<view :class="formats.fontSize ? 'ql-active' : ''" class="iconfont icon-fontsize" title="字号" data-name="fontSize" @click.stop="fabTap('fontSize')"></view>
|
||||
<template #content>
|
||||
<view class="fab-tools" @click.stop="fabTapSub($event, 'fontSize')">
|
||||
<view v-for="item in fabTools.fontSize" :key="item.value">
|
||||
<view class="fab-sub" :class="[formats.fontSize === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]" :title="item.title" data-name="fontSize" :data-value="item.value">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</fab-tool>
|
||||
<view v-if="toolbarList.includes('color')" :style="{ color: formats.color ? textColor : 'initial' }" class="iconfont icon-text_color" title="文字颜色" data-name="color" :data-value="textColor"></view>
|
||||
<view v-if="toolbarList.includes('backgroundColor')" :style="{ color: formats.backgroundColor ? backgroundColor : 'initial' }" class="iconfont icon-fontbgcolor" title="背景颜色" data-name="backgroundColor" :data-value="backgroundColor"></view>
|
||||
<view v-if="toolbarList.includes('date')" class="iconfont icon-date" title="日期" @tap="insertDate"></view>
|
||||
<view v-if="toolbarList.includes('listCheck')" class="iconfont icon--checklist" title="待办" data-name="list" data-value="check"></view>
|
||||
<view v-if="toolbarList.includes('listOrdered')" :class="formats.list === 'ordered' ? 'ql-active' : ''" class="iconfont icon-youxupailie" title="有序列表" data-name="list" data-value="ordered"></view>
|
||||
<view v-if="toolbarList.includes('listBullet')" :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie" title="无序列表" data-name="list" data-value="bullet"></view>
|
||||
<view v-if="toolbarList.includes('divider')" class="iconfont icon-fengexian" title="分割线" @click="insertDivider"></view>
|
||||
<view v-if="toolbarList.includes('indentDec')" class="iconfont icon-outdent" title="减少缩进" data-name="indent" data-value="-1"></view>
|
||||
<view v-if="toolbarList.includes('indentInc')" class="iconfont icon-indent" title="增加缩进" data-name="indent" data-value="+1"></view>
|
||||
<view v-if="toolbarList.includes('scriptSub')" :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao" title="下标" data-name="script" data-value="sub"></view>
|
||||
<view v-if="toolbarList.includes('scriptSuper')" :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao" title="上标" data-name="script" data-value="super"></view>
|
||||
<view v-if="toolbarList.includes('direction')" :class="formats.direction === 'rtl' ? 'ql-active' : ''" class="iconfont icon-direction-rtl" title="文本方向" data-name="direction" data-value="rtl"></view>
|
||||
<view v-if="toolbarList.includes('image')" class="iconfont icon-charutupian" title="图片" @tap="insertImage"></view>
|
||||
<view v-if="toolbarList.includes('video')" class="iconfont icon-video" title="视频" @tap="insertVideo"></view>
|
||||
<view v-if="toolbarList.includes('link')" class="iconfont icon-charulianjie" title="超链接" @tap="insertLink"></view>
|
||||
<view v-if="toolbarList.includes('undo')" class="iconfont icon-undo" title="撤销" @tap="undo"></view>
|
||||
<view v-if="toolbarList.includes('redo')" class="iconfont icon-redo" title="重做" @tap="redo"></view>
|
||||
<view v-if="toolbarList.includes('removeFormat')" class="iconfont icon-clearedformat" title="清除格式" @tap="removeFormat"></view>
|
||||
<view v-if="toolbarList.includes('clear')" class="iconfont icon-shanchu" title="清空" @tap="clear"></view>
|
||||
<view v-if="toolbarConfig.showFullscreen" class="iconfont icon-quanping" title="全屏" @tap="fullscreen"></view>
|
||||
<view v-if="toolbarList.includes('export')" class="submit-footer" title="导出" @tap="exportHtml">
|
||||
<view class="footer-btn">保存</view>
|
||||
<view class="footer-space"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义功能组件 -->
|
||||
<!-- 调色板 -->
|
||||
<color-picker v-if="toolbarList.includes('color') || toolbarList.includes('backgroundColor')" ref="colorPickerRef" :color="defaultColor" @confirm="confirmColor"></color-picker>
|
||||
<!-- 添加链接的操作弹窗 -->
|
||||
<link-edit v-if="toolbarList.includes('link') && !readOnly" ref="linkEditRef" @confirm="confirmLink"></link-edit>
|
||||
<view class="sp-editor-wrapper" @longpress="eLongpress">
|
||||
<editor :id="editorId" class="ql-editor editor-container" :class="{ 'ql-image-overlay-none': readOnly }" show-img-size show-img-toolbar show-img-resize :placeholder="placeholder" :read-only="readOnly" @statuschange="onStatusChange" @ready="onEditorReady" @input="onEditorInput"></editor>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorPicker from './color-picker.vue'
|
||||
import LinkEdit from './link-edit.vue'
|
||||
import FabTool from './fab-tool.vue'
|
||||
import { addLink, linkFlag } from '../../utils'
|
||||
import { mapState } from "vuex"
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColorPicker,
|
||||
LinkEdit,
|
||||
FabTool
|
||||
},
|
||||
props: {
|
||||
// 编辑器id可传入,以便循环组件使用,防止id重复
|
||||
editorId: {
|
||||
type: String,
|
||||
default: 'editor'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '写点什么吧 ~'
|
||||
},
|
||||
// 是否只读
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 最大字数限制,-1不限
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
// 工具栏配置
|
||||
toolbarConfig: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
keys: [], // 要显示的工具,优先级最大
|
||||
excludeKeys: [], // 除这些指定的工具外,其他都显示
|
||||
iconSize: '18px', // 工具栏字体大小
|
||||
iconColumns: 10, // 工具栏列数
|
||||
showFullscreen: false, // 是否存在全屏
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
toolbarConfig: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(newToolbar) {
|
||||
/**
|
||||
* 若工具栏配置中keys存在,则以keys为准
|
||||
* 否则以excludeKeys向toolbarAllList中排查
|
||||
* 若keys与excludeKeys皆为空,则以toolbarAllList为准
|
||||
*/
|
||||
if (newToolbar.keys?.length > 0) {
|
||||
this.toolbarList = newToolbar.keys
|
||||
} else {
|
||||
this.toolbarList =
|
||||
newToolbar.excludeKeys?.length > 0 ?
|
||||
this.toolbarAllList.filter((item) => !newToolbar.excludeKeys.includes(item)) :
|
||||
this.toolbarAllList
|
||||
}
|
||||
this.iconSize = newToolbar.iconSize || '18px'
|
||||
this.iconColumns = newToolbar.iconColumns || 10
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formats: {},
|
||||
curFab: '', // 当前悬浮工具栏
|
||||
fabXY: {},
|
||||
textColor: '',
|
||||
backgroundColor: '',
|
||||
curColor: '',
|
||||
defaultColor: { r: 0, g: 0, b: 0, a: 1 }, // 调色板默认颜色
|
||||
iconSize: '20px', // 工具栏图标字体大小
|
||||
iconColumns: 10, // 工具栏列数
|
||||
toolbarList: [],
|
||||
toolbarAllList: [
|
||||
'header', // 标题
|
||||
'H1', // 一级标题
|
||||
'H2', // 二级标题
|
||||
'H3', // 三级标题
|
||||
'H4', // 四级标题
|
||||
'H5', // 五级标题
|
||||
'H6', // 六级标题
|
||||
'bold', // 加粗
|
||||
'italic', // 斜体
|
||||
'underline', // 下划线
|
||||
'strike', // 删除线
|
||||
'align', // 对齐方式
|
||||
'alignLeft', // 左对齐
|
||||
'alignCenter', // 居中对齐
|
||||
'alignRight', // 右对齐
|
||||
'alignJustify', // 两端对齐
|
||||
'lineHeight', // 行间距
|
||||
'letterSpacing', // 字间距
|
||||
'marginTop', // 段前距
|
||||
'marginBottom', // 段后距
|
||||
'fontFamily', // 字体
|
||||
'fontSize', // 字号
|
||||
'color', // 文字颜色
|
||||
'backgroundColor', // 背景颜色
|
||||
'date', // 日期
|
||||
'listCheck', // 待办
|
||||
'listOrdered', // 有序列表
|
||||
'listBullet', // 无序列表
|
||||
'indentInc', // 增加缩进
|
||||
'indentDec', // 减少缩进
|
||||
'divider', // 分割线
|
||||
'scriptSub', // 下标
|
||||
'scriptSuper', // 上标
|
||||
'direction', // 文本方向
|
||||
'image', // 图片
|
||||
'video', // 视频
|
||||
'link', // 超链接
|
||||
'undo', // 撤销
|
||||
'redo', // 重做
|
||||
'removeFormat', // 清除格式
|
||||
'clear', // 清空
|
||||
'export' // 导出
|
||||
],
|
||||
fabTools: {
|
||||
header: [
|
||||
{ title: '一级标题', name: 'H1', value: 1, icon: 'icon-format-header-1' },
|
||||
{ title: '二级标题', name: 'H2', value: 2, icon: 'icon-format-header-2' },
|
||||
{ title: '三级标题', name: 'H3', value: 3, icon: 'icon-format-header-3' },
|
||||
{ title: '四级标题', name: 'H4', value: 4, icon: 'icon-format-header-4' },
|
||||
{ title: '五级标题', name: 'H5', value: 5, icon: 'icon-format-header-5' },
|
||||
{ title: '六级标题', name: 'H6', value: 6, icon: 'icon-format-header-6' }
|
||||
],
|
||||
fontFamily: [
|
||||
{ title: '宋体', name: '宋', value: '宋体', icon: '' },
|
||||
{ title: '黑体', name: '黑', value: '黑体', icon: '' },
|
||||
{ title: '楷体', name: '楷', value: '楷体', icon: '' },
|
||||
{ title: '仿宋', name: '仿', value: '仿宋', icon: '' },
|
||||
{ title: '华文隶书', name: '隶', value: 'STLiti', icon: '' },
|
||||
{ title: '华文行楷', name: '行', value: 'STXingkai', icon: '' },
|
||||
{ title: '幼圆', name: '圆', value: 'YouYuan', icon: '' }
|
||||
],
|
||||
fontSize: [
|
||||
{ title: '12', name: '12', value: '12px', icon: '' },
|
||||
{ title: '14', name: '14', value: '14px', icon: '' },
|
||||
{ title: '16', name: '16', value: '16px', icon: '' },
|
||||
{ title: '18', name: '18', value: '18px', icon: '' },
|
||||
{ title: '20', name: '20', value: '20px', icon: '' },
|
||||
{ title: '22', name: '22', value: '22px', icon: '' },
|
||||
{ title: '24', name: '24', value: '24px', icon: '' }
|
||||
],
|
||||
align: [
|
||||
{ title: '左对齐', name: 'alignLeft', value: 'left', icon: 'icon-zuoduiqi' },
|
||||
{ title: '居中对齐', name: 'alignCenter', value: 'center', icon: 'icon-juzhongduiqi' },
|
||||
{ title: '右对齐', name: 'alignRight', value: 'right', icon: 'icon-youduiqi' },
|
||||
{ title: '两端对齐', name: 'alignJustify', value: 'justify', icon: 'icon-zuoyouduiqi' }
|
||||
],
|
||||
lineHeight: [
|
||||
{ title: '1倍', name: '1', value: '1', icon: '' },
|
||||
{ title: '1.5倍', name: '1.5', value: '1.5', icon: '' },
|
||||
{ title: '2倍', name: '2', value: '2', icon: '' },
|
||||
{ title: '2.5倍', name: '2.5', value: '2.5', icon: '' },
|
||||
{ title: '3倍', name: '3', value: '3', icon: '' }
|
||||
],
|
||||
// 字间距/段前距/段后距
|
||||
space: [
|
||||
{ title: '0.5倍', name: '0.5', value: '0.5em', icon: '' },
|
||||
{ title: '1倍', name: '1', value: '1em', icon: '' },
|
||||
{ title: '1.5倍', name: '1.5', value: '1.5em', icon: '' },
|
||||
{ title: '2倍', name: '2', value: '2em', icon: '' },
|
||||
{ title: '2.5倍', name: '2.5', value: '2.5em', icon: '' },
|
||||
{ title: '3倍', name: '3', value: '3em', icon: '' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
themeColor: state => state.app.themeColor,
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onEditorReady() {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#' + this.editorId)
|
||||
.context((res) => {
|
||||
this.editorCtx = res.context
|
||||
this.$emit('init', this.editorCtx, this.editorId)
|
||||
})
|
||||
.exec()
|
||||
},
|
||||
undo() {
|
||||
this.editorCtx.undo()
|
||||
},
|
||||
redo() {
|
||||
this.editorCtx.redo()
|
||||
},
|
||||
format(e) {
|
||||
let { name, value } = e.target.dataset
|
||||
if (!name) return
|
||||
switch (name) {
|
||||
case 'color':
|
||||
case 'backgroundColor':
|
||||
this.curColor = name
|
||||
this.showPicker()
|
||||
break
|
||||
default:
|
||||
this.editorCtx.format(name, value)
|
||||
break
|
||||
}
|
||||
},
|
||||
// 悬浮工具点击
|
||||
fabTap(fabType) {
|
||||
if (this.curFab != fabType) {
|
||||
this.curFab = fabType
|
||||
} else {
|
||||
this.curFab = ''
|
||||
}
|
||||
},
|
||||
// 悬浮工具子集点击
|
||||
fabTapSub(e, fabType) {
|
||||
this.format(e)
|
||||
this.fabTap(fabType)
|
||||
},
|
||||
showPicker() {
|
||||
switch (this.curColor) {
|
||||
case 'color':
|
||||
this.defaultColor = this.textColor ?
|
||||
this.$refs.colorPickerRef.hex2Rgb(this.textColor) : { r: 0, g: 0, b: 0, a: 1 }
|
||||
break
|
||||
case 'backgroundColor':
|
||||
this.defaultColor = this.backgroundColor ?
|
||||
this.$refs.colorPickerRef.hex2Rgb(this.backgroundColor) : { r: 0, g: 0, b: 0, a: 0 }
|
||||
break
|
||||
}
|
||||
this.$refs.colorPickerRef.open()
|
||||
},
|
||||
confirmColor(e) {
|
||||
switch (this.curColor) {
|
||||
case 'color':
|
||||
this.textColor = e.hex
|
||||
this.editorCtx.format('color', this.textColor)
|
||||
break
|
||||
case 'backgroundColor':
|
||||
this.backgroundColor = e.hex
|
||||
this.editorCtx.format('backgroundColor', this.backgroundColor)
|
||||
break
|
||||
}
|
||||
},
|
||||
onStatusChange(e) {
|
||||
if (e.detail.color) {
|
||||
this.textColor = e.detail.color
|
||||
}
|
||||
if (e.detail.backgroundColor) {
|
||||
this.backgroundColor = e.detail.backgroundColor
|
||||
}
|
||||
this.formats = e.detail
|
||||
},
|
||||
insertDivider() {
|
||||
this.editorCtx.insertDivider()
|
||||
},
|
||||
clear() {
|
||||
uni.showModal({
|
||||
title: '清空编辑器',
|
||||
content: '确定清空编辑器吗?',
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
this.editorCtx.clear()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
removeFormat() {
|
||||
uni.showModal({
|
||||
title: '文本格式化',
|
||||
content: '确定要清除所选择部分文本块格式吗?',
|
||||
showCancel: true,
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
this.editorCtx.removeFormat()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
insertDate() {
|
||||
const date = new Date()
|
||||
const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
|
||||
this.editorCtx.insertText({ text: formatDate })
|
||||
},
|
||||
insertLink() {
|
||||
this.$refs.linkEditRef.open()
|
||||
},
|
||||
/**
|
||||
* 确认添加链接
|
||||
* @param {Object} e { text: '链接描述', href: '链接地址' }
|
||||
*/
|
||||
confirmLink(e) {
|
||||
this.$refs.linkEditRef.close()
|
||||
addLink(this.editorCtx, e, () => {
|
||||
// 修复添加超链接后,不触发input更新当前最新内容的bug,这里做一下手动更新
|
||||
this.editorCtx.getContents({
|
||||
success: (res) => {
|
||||
this.$emit('input', { html: res.html, text: res.text }, this.editorId)
|
||||
}
|
||||
})
|
||||
})
|
||||
this.$emit('addLink', e, this.editorId)
|
||||
},
|
||||
insertImage() {
|
||||
// #ifdef APP-PLUS || H5
|
||||
uni.chooseImage({
|
||||
// count: 1, // 默认9
|
||||
success: (res) => {
|
||||
const { tempFiles } = res
|
||||
// 将文件和编辑器示例抛出,由开发者自行上传和插入图片
|
||||
this.$emit('upinImage', tempFiles, this.editorCtx, this.editorId)
|
||||
},
|
||||
fail() {
|
||||
uni.showToast({
|
||||
title: '未授权访问相册权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
|
||||
uni.chooseMedia({
|
||||
// count: 1, // 默认9
|
||||
mediaType: ['image'],
|
||||
success: (res) => {
|
||||
// 同上chooseImage处理
|
||||
const { tempFiles } = res
|
||||
this.$emit('upinImage', tempFiles, this.editorCtx, this.editorId)
|
||||
},
|
||||
fail() {
|
||||
uni.showToast({
|
||||
title: '未授权访问相册权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
insertVideo() {
|
||||
uni.chooseVideo({
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (res) => {
|
||||
const { tempFilePath } = res
|
||||
// 将文件和编辑器示例抛出,由开发者自行上传和插入图片
|
||||
this.$emit('upinVideo', tempFilePath, this.editorCtx, this.editorId)
|
||||
},
|
||||
fail() {
|
||||
uni.showToast({
|
||||
title: '未授权访问媒体权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onEditorInput(e) {
|
||||
// 注意不要使用getContents获取html和text,会导致重复触发onStatusChange从而失去toolbar工具的高亮状态
|
||||
// 复制粘贴的时候detail会为空,此时应当直接return
|
||||
if (Object.keys(e.detail).length <= 0) return
|
||||
const { html, text } = e.detail
|
||||
// 识别到标识立即return
|
||||
if (text.indexOf(linkFlag) !== -1) return
|
||||
|
||||
const maxlength = parseInt(this.maxlength)
|
||||
const textStr = text.replace(/[ \t\r\n]/g, '')
|
||||
if (textStr.length > maxlength && maxlength != -1) {
|
||||
uni.showModal({
|
||||
content: `超过${maxlength}字数啦~`,
|
||||
confirmText: '确定',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
this.$emit('overMax', { html, text }, this.editorId)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.$emit('input', { html, text }, this.editorId)
|
||||
}
|
||||
},
|
||||
eLongpress() {
|
||||
/**
|
||||
* 微信小程序官方editor的长按事件有bug,需要重写覆盖,不需做任何逻辑,可见下面小程序社区问题链接
|
||||
* @tutorial https://developers.weixin.qq.com/community/develop/doc/000c04b3e1c1006f660065e4f61000
|
||||
*/
|
||||
},
|
||||
// 全屏
|
||||
fullscreen() {
|
||||
this.$emit('fullscreen', this.editorId)
|
||||
},
|
||||
// 导出
|
||||
exportHtml() {
|
||||
this.editorCtx.getContents({
|
||||
success: (res) => {
|
||||
this.$emit('exportHtml', res.html, this.editorId)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 获取数据
|
||||
getHtml() {
|
||||
return new Promise(resolve => {
|
||||
this.editorCtx.getContents({
|
||||
success: (res) => {
|
||||
resolve(res.html)
|
||||
},
|
||||
fail: () => {
|
||||
reject("")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/uni_modules/sp-editor/icons/editor-icon.css';
|
||||
@import '@/uni_modules/sp-editor/icons/custom-icon.css';
|
||||
|
||||
.sp-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.sp-editor-toolbar {
|
||||
box-sizing: border-box;
|
||||
padding: calc(var(--icon-size) / 4) 0;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--icon-columns), 1fr);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: calc(var(--icon-size) * 1.8);
|
||||
cursor: pointer;
|
||||
font-size: var(--icon-size);
|
||||
}
|
||||
|
||||
.sp-editor-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding-bottom: 112rpx;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
padding: 16rpx;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ql-image-overlay-none {
|
||||
::v-deep .ql-image-overlay {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .ql-editor.ql-blank::before {
|
||||
font-style: normal;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
::v-deep .ql-container {
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.ql-active {
|
||||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
.fab-tools {
|
||||
display: flex;
|
||||
padding: 0 10rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.fab-sub {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 96;
|
||||
background: #ffffff;
|
||||
padding: 12rpx 16rpx;
|
||||
border-top: 1rpx solid #F6F7FB;
|
||||
}
|
||||
|
||||
.submit-footer .footer-btn {
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
line-height: 44rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--theme-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-footer .footer-space {
|
||||
width: 100%;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user