17 changed files with 3191 additions and 334 deletions
@ -1,143 +1,498 @@
|
||||
<template> |
||||
<view class="jobInfo-detail"> |
||||
<view class="view"> |
||||
<scroll-view scroll-y class="content"> |
||||
|
||||
|
||||
<!-- 详情内容区 --> |
||||
<view class="content-wrap"> |
||||
<view class="jobInfo-title">{{ jobInfoDetail.title }}</view> |
||||
<view class="jobInfo-time">生效时间:{{ jobInfoDetail.jobDate }}</view> |
||||
|
||||
<!-- 富文本内容 --> |
||||
<view class="jobInfo-content"> |
||||
<rich-text :nodes="jobInfoDetail.context"></rich-text> |
||||
<!-- 标题输入 --> |
||||
<view class="block"> |
||||
<view> |
||||
<text style="color: #f8285a; margin-right: 4px">*</text> |
||||
标题 |
||||
</view> |
||||
<view class="value"> |
||||
<uni-easyinput |
||||
v-model="jobInfoDetail.title" |
||||
placeholder="请输入标题" |
||||
:inputBorder="false" |
||||
:clearable="false" |
||||
:autoHeight="true" |
||||
:adjust-position="true" |
||||
cursorSpacing="32" |
||||
/> |
||||
</view> |
||||
</view> |
||||
<!-- 日期选择 --> |
||||
<view class="block"> |
||||
<view> |
||||
<text style="color: #f8285a; margin-right: 4px">*</text> |
||||
日期 |
||||
</view> |
||||
<view class="value" @tap="showCalendar('jobDate')"> |
||||
<text v-if="jobInfoDetail.jobDate" class="input-value">{{ jobInfoDetail.jobDate }}</text> |
||||
<text v-else class="input-value" style="color: #808080">请选择日期</text> |
||||
<u-icon name="arrow-right"></u-icon> |
||||
</view> |
||||
</view> |
||||
<uni-calendar |
||||
ref="calendarRef" |
||||
:insert="false" |
||||
@confirm="confirmCalendar" |
||||
/> |
||||
<!-- 汇报人 --> |
||||
<view class="block"> |
||||
<view> |
||||
<text style="color: #f8285a; margin-right: 4px">*</text> |
||||
汇报人 |
||||
</view> |
||||
<view class="value"> |
||||
<uni-easyinput |
||||
v-model="jobInfoDetail.jobName" |
||||
placeholder="请输入汇报人" |
||||
:inputBorder="false" |
||||
:clearable="false" |
||||
:autoHeight="true" |
||||
:adjust-position="true" |
||||
cursorSpacing="32" |
||||
/> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 汇报内容 --> |
||||
<view class="block content-block"> |
||||
<view> |
||||
<text style="color: #f8285a; margin-right: 4px">*</text> |
||||
汇报内容 |
||||
</view> |
||||
<view class="content-wrap"> |
||||
<sp-editor |
||||
:html="jobInfoDetail.content" |
||||
:toolbar-config="{ |
||||
excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck'], |
||||
iconSize: '18px' |
||||
}" |
||||
:max-length="5000" |
||||
:min-height="300" |
||||
:placeholder="'请输入汇报内容'" |
||||
@init="initEditor" |
||||
@input="inputOver" |
||||
@upinImage="upinImage" |
||||
@overMax="overMax" |
||||
@addLink="addLink" |
||||
@exportHtml="exportHtml" |
||||
/> |
||||
</view> |
||||
</view> |
||||
</scroll-view> |
||||
<!-- 底部按钮 --> |
||||
<view class="operation"> |
||||
<button class="btn delete" @tap="handleDelete">删除</button> |
||||
<button class="btn save" @tap="handleSave">保存</button> |
||||
</view> |
||||
</view> |
||||
</template> |
||||
|
||||
<script> |
||||
import { jobInfoApi } from '@/api/jobinfo/index.js' |
||||
import { JobInfoApi } from '@/api/jobinfo/index.js' |
||||
import { uploadFile } from '@/api/system/file.js' |
||||
|
||||
export default { |
||||
// In data section |
||||
data() { |
||||
return { |
||||
id: '', // 政策ID |
||||
id: '', |
||||
// 选择器 |
||||
// 日历控件 |
||||
calendar: { |
||||
// 做表单中键的缓存用 |
||||
key: null |
||||
}, |
||||
jobInfoDetail: { |
||||
name: '', |
||||
createTime: '', |
||||
context: '' |
||||
title: '', |
||||
jobDate: '', |
||||
jobName: '靖宇', |
||||
content: '' |
||||
} |
||||
} |
||||
}, |
||||
}, |
||||
|
||||
|
||||
|
||||
// ... keep other existing methods ... |
||||
onLoad(options) { |
||||
if (options.id) { |
||||
this.id = options.id |
||||
this.getjobInfoDetail() |
||||
} |
||||
}, |
||||
// 添加 onReady 生命周期 |
||||
onReady() { |
||||
|
||||
}, |
||||
methods: { |
||||
/** |
||||
* 显示日历 |
||||
* @param {Object} key 表单中需要赋值的键 |
||||
*/ |
||||
showCalendar(key) { |
||||
this.$refs.calendarRef.open() |
||||
this.calendar = { |
||||
key |
||||
} |
||||
}, |
||||
/** |
||||
* 日历选中之后的回调 |
||||
* @param {Object} e |
||||
*/ |
||||
confirmCalendar(e) { |
||||
this.jobInfoDetail[this.calendar.key] = e.fulldate |
||||
this.closeCalendar() |
||||
}, |
||||
/** |
||||
* 关闭日历 |
||||
*/ |
||||
closeCalendar() { |
||||
this.calendar = { |
||||
key: null |
||||
} |
||||
}, |
||||
// 获取详情数据 |
||||
async getjobInfoDetail() { |
||||
try { |
||||
const res = await jobInfoApi.getjobInfo(this.id) |
||||
console.log(JobInfoApi) |
||||
const res = await JobInfoApi.getJobInfo(this.id) |
||||
if (res.code === 0) { |
||||
this.jobInfoDetail = { |
||||
...res.data, |
||||
jobDate: this.formatTime(res.data.jobDate), |
||||
// 处理富文本内容,确保样式正确显示 |
||||
context: this.formatRichText(res.data.context) |
||||
jobDate: this.formatTime(res.data.jobDate) |
||||
} |
||||
|
||||
// 使用 nextTick 确保组件已渲染 |
||||
|
||||
} |
||||
} catch (error) { |
||||
console.log(error) |
||||
|
||||
} |
||||
}, |
||||
|
||||
// 打开日期选择器 |
||||
openDatePicker() { |
||||
uni.showDatePicker({ |
||||
current: this.jobInfoDetail.jobDate, |
||||
success: (res) => { |
||||
this.jobInfoDetail.jobDate = this.formatTime(res.date) |
||||
} |
||||
}) |
||||
}, |
||||
|
||||
// 编辑器初始化完成 |
||||
initEditor(ctx) { |
||||
this.ctx = ctx |
||||
// 如果有内容,设置到编辑器 |
||||
if (this.jobInfoDetail.content) { |
||||
this.ctx.setContents({ |
||||
html: this.jobInfoDetail.content |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
// 编辑器内容变化 |
||||
inputOver(e) { |
||||
this.jobInfoDetail.content = e.html |
||||
}, |
||||
|
||||
// 上传图片 |
||||
upinImage(tempFiles, editorCtx) { |
||||
uni.showLoading({ title: '上传中...' }) |
||||
console.log('上传文件',tempFiles) |
||||
console.log('上传文件',editorCtx) |
||||
// 这里应该调用你的图片上传API |
||||
// 示例代码,需要替换为实际的上传逻辑 |
||||
tempFiles.forEach(async (item) => { |
||||
uni.showLoading({ |
||||
title: '上传中请稍后', |
||||
mask: true |
||||
}) |
||||
|
||||
console.log('上传item',item) |
||||
const res = await uploadFile({ name: `file`, filePath: item.tempFilePath }) |
||||
console.log('返回图片',res) |
||||
editorCtx.insertImage({ |
||||
src: res.data.url, |
||||
width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑 |
||||
success: function () { |
||||
uni.hideLoading() |
||||
} |
||||
}) |
||||
}) |
||||
}, |
||||
|
||||
// 超出最大长度 |
||||
overMax() { |
||||
uni.showToast({ |
||||
title: '内容超出最大长度限制', |
||||
icon: 'none' |
||||
}) |
||||
}, |
||||
|
||||
// 添加链接 |
||||
addLink(e) { |
||||
// 可以在这里处理链接添加逻辑 |
||||
console.log('添加链接', e) |
||||
}, |
||||
|
||||
// 导出HTML |
||||
exportHtml(e) { |
||||
this.jobInfoDetail.content = e.html |
||||
}, |
||||
|
||||
// 保存方法修改 |
||||
async handleSave() { |
||||
if (!this.validateForm()) return |
||||
|
||||
try { |
||||
// 获取最新的HTML内容 |
||||
if (this.ctx) { |
||||
this.ctx.getContents({ |
||||
success: async (res) => { |
||||
this.jobInfoDetail.content = res.html |
||||
|
||||
// 执行保存逻辑 |
||||
await this.saveContent() |
||||
} |
||||
}) |
||||
} else { |
||||
// 如果编辑器未初始化,直接保存 |
||||
await this.saveContent() |
||||
} |
||||
} catch (error) { |
||||
uni.hideLoading() |
||||
uni.showToast({ |
||||
title: '获取详情失败', |
||||
title: '保存失败', |
||||
icon: 'none' |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
// 抽取保存逻辑为单独方法 |
||||
async saveContent() { |
||||
uni.showLoading({ title: '保存中...' }) |
||||
|
||||
// 根据是否有ID判断是新增还是更新 |
||||
const saveApi = this.id ? |
||||
JobInfoApi.updateJobInfo : |
||||
JobInfoApi.createJobInfo |
||||
|
||||
const result = await saveApi(this.jobInfoDetail) |
||||
uni.hideLoading() |
||||
|
||||
if (result.code === 0) { |
||||
uni.showToast({ |
||||
title: '保存成功', |
||||
icon: 'success' |
||||
}) |
||||
setTimeout(() => { |
||||
uni.navigateBack() |
||||
}, 1500) |
||||
} |
||||
}, |
||||
// 删除 |
||||
handleDelete() { |
||||
if (!this.id) { |
||||
uni.navigateBack() |
||||
return |
||||
} |
||||
|
||||
uni.showModal({ |
||||
title: '提示', |
||||
content: '确定要删除此工作汇报吗?', |
||||
success: async (res) => { |
||||
if (res.confirm) { |
||||
try { |
||||
uni.showLoading({ title: '删除中...' }) |
||||
const result = await JobInfoApi.deleteJobInfo(this.id) |
||||
uni.hideLoading() |
||||
|
||||
if (result.code === 0) { |
||||
uni.showToast({ |
||||
title: '删除成功', |
||||
icon: 'success' |
||||
}) |
||||
setTimeout(() => { |
||||
uni.navigateBack() |
||||
}, 1500) |
||||
} |
||||
} catch (error) { |
||||
uni.hideLoading() |
||||
uni.showToast({ |
||||
title: '删除失败', |
||||
icon: 'none' |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
}, |
||||
|
||||
// 格式化时间 |
||||
formatTime(time) { |
||||
if (!time) return '' |
||||
const date = new Date(time) |
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` |
||||
}, |
||||
// 处理富文本内容 |
||||
formatRichText(html) { |
||||
if (!html) return '' |
||||
// 添加样式以确保图片自适应 |
||||
return html.replace(/<img/gi, '<img style="max-width:100%;height:auto;display:block;"') |
||||
}, |
||||
// 返回上一页 |
||||
handleBack() { |
||||
uni.navigateBack() |
||||
|
||||
// 表单验证 |
||||
validateForm() { |
||||
const { title, jobDate, content } = this.jobInfoDetail |
||||
if (!title) { |
||||
uni.showToast({ title: '请输入标题', icon: 'none' }) |
||||
return false |
||||
} |
||||
if (!jobDate) { |
||||
uni.showToast({ title: '请选择日期', icon: 'none' }) |
||||
return false |
||||
} |
||||
if (!content) { |
||||
uni.showToast({ title: '请输入汇报内容', icon: 'none' }) |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.jobInfo-detail { |
||||
.view { |
||||
height: 100vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
background: #fff; |
||||
|
||||
} |
||||
.content { |
||||
flex: 1; |
||||
padding: 12px; |
||||
padding-bottom: calc(156rpx + 12px); // 修改底部内边距 |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 12px; |
||||
height: calc(100vh - 156rpx - 12px); |
||||
overflow-y: auto; |
||||
background: #F9F9F9; |
||||
} |
||||
.block { |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
background-color: #fff; |
||||
padding: 12px; |
||||
border-radius: 8px; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
gap: 12px; |
||||
margin-bottom: 12px; // 添加底部间距 |
||||
|
||||
.nav-bar { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
height: 88rpx; |
||||
background: #fff; |
||||
&:last-child { |
||||
margin-bottom: 0; // 最后一个block不需要底部间距 |
||||
} |
||||
|
||||
.value { |
||||
flex: 1; |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 0 30rpx; |
||||
border-bottom: 1rpx solid #eee; |
||||
z-index: 100; |
||||
flex-flow: row nowrap; |
||||
justify-content: space-between; |
||||
gap: 4px; |
||||
|
||||
.back-btn { |
||||
font-size: 28rpx; |
||||
color: #333; |
||||
} |
||||
|
||||
.title { |
||||
.input-value { |
||||
flex: 1; |
||||
text-align: center; |
||||
font-size: 32rpx; |
||||
font-weight: bold; |
||||
padding-right: 60rpx; |
||||
text-align: right; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
cursor: auto; |
||||
display: block; |
||||
font-family: UICTFontTextStyleBody; |
||||
height: 1.4rem; |
||||
min-height: 1.4rem; |
||||
overflow: hidden; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.content-block { |
||||
flex-direction: column; |
||||
align-items: flex-start; |
||||
|
||||
.content-wrap { |
||||
padding: 30rpx 30rpx 30rpx; |
||||
|
||||
.jobInfo-title { |
||||
font-size: 36rpx; |
||||
font-weight: bold; |
||||
color: #333; |
||||
margin-bottom: 20rpx; |
||||
} |
||||
|
||||
.jobInfo-time { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
margin-bottom: 40rpx; |
||||
} |
||||
|
||||
.jobInfo-content { |
||||
font-size: 28rpx; |
||||
line-height: 1.8; |
||||
color: #333; |
||||
|
||||
:deep(img) { |
||||
max-width: 100%; |
||||
height: auto; |
||||
} |
||||
|
||||
:deep(p) { |
||||
margin-bottom: 20rpx; |
||||
} |
||||
} |
||||
width: 100%; |
||||
margin-top: 12px; |
||||
} |
||||
} |
||||
|
||||
::v-deep .uni-easyinput__content-input { |
||||
flex: 1; |
||||
text-align: right; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
cursor: auto; |
||||
display: block; |
||||
font-family: UICTFontTextStyleBody; |
||||
height: 1.4rem; |
||||
min-height: 1.4rem; |
||||
overflow: hidden; |
||||
padding-right: 0; |
||||
} |
||||
|
||||
::v-deep .uni-easyinput__placeholder-class { |
||||
flex: 1; |
||||
text-align: right; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
cursor: auto; |
||||
display: block; |
||||
font-family: UICTFontTextStyleBody; |
||||
height: 1.4rem; |
||||
min-height: 1.4rem; |
||||
font-size: 14px; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.operation { |
||||
height: 112rpx; // 调整实际按钮区域高度 |
||||
position: fixed; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 34px; |
||||
padding: 12px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
gap: 12px; |
||||
background: #FFFFFF; |
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); |
||||
margin-top: 12px; // 添加顶部间距 |
||||
|
||||
.btn { |
||||
flex: 1; |
||||
height: 88rpx; |
||||
line-height: 88rpx; |
||||
font-size: 28rpx; |
||||
border-radius: 8px; |
||||
text-align: center; |
||||
} |
||||
|
||||
.delete { |
||||
background: #F8285A; |
||||
color: #fff; |
||||
} |
||||
|
||||
.save { |
||||
background: #17C653; |
||||
color: #fff; |
||||
} |
||||
} |
||||
|
||||
::v-deep .editor-container { |
||||
border-radius: 8px; |
||||
background-color: var(--LightMode-Grey-Grey-100, #F9F9F9); |
||||
} |
||||
</style> |
@ -1,143 +0,0 @@
|
||||
<template> |
||||
<view class="jobInfo-detail"> |
||||
|
||||
|
||||
<!-- 详情内容区 --> |
||||
<view class="content-wrap"> |
||||
<view class="jobInfo-title">{{ jobInfoDetail.title }}</view> |
||||
<view class="jobInfo-time">生效时间:{{ jobInfoDetail.jobDate }}</view> |
||||
|
||||
<!-- 富文本内容 --> |
||||
<view class="jobInfo-content"> |
||||
<rich-text :nodes="jobInfoDetail.context"></rich-text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</template> |
||||
|
||||
<script> |
||||
import { jobInfoApi } from '@/api/jobinfo/index.js' |
||||
|
||||
export default { |
||||
data() { |
||||
return { |
||||
id: '', // 政策ID |
||||
jobInfoDetail: { |
||||
name: '', |
||||
createTime: '', |
||||
context: '' |
||||
} |
||||
} |
||||
}, |
||||
onLoad(options) { |
||||
if (options.id) { |
||||
this.id = options.id |
||||
this.getjobInfoDetail() |
||||
} |
||||
}, |
||||
methods: { |
||||
// 获取详情数据 |
||||
async getjobInfoDetail() { |
||||
try { |
||||
const res = await jobInfoApi.getjobInfo(this.id) |
||||
if (res.code === 0) { |
||||
this.jobInfoDetail = { |
||||
...res.data, |
||||
jobDate: this.formatTime(res.data.jobDate), |
||||
// 处理富文本内容,确保样式正确显示 |
||||
context: this.formatRichText(res.data.context) |
||||
} |
||||
} |
||||
} catch (error) { |
||||
uni.showToast({ |
||||
title: '获取详情失败', |
||||
icon: 'none' |
||||
}) |
||||
} |
||||
}, |
||||
// 格式化时间 |
||||
formatTime(time) { |
||||
if (!time) return '' |
||||
const date = new Date(time) |
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` |
||||
}, |
||||
// 处理富文本内容 |
||||
formatRichText(html) { |
||||
if (!html) return '' |
||||
// 添加样式以确保图片自适应 |
||||
return html.replace(/<img/gi, '<img style="max-width:100%;height:auto;display:block;"') |
||||
}, |
||||
// 返回上一页 |
||||
handleBack() { |
||||
uni.navigateBack() |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.jobInfo-detail { |
||||
height: 100vh; |
||||
background: #fff; |
||||
overflow-y: auto; |
||||
|
||||
.nav-bar { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
height: 88rpx; |
||||
background: #fff; |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 0 30rpx; |
||||
border-bottom: 1rpx solid #eee; |
||||
z-index: 100; |
||||
|
||||
.back-btn { |
||||
font-size: 28rpx; |
||||
color: #333; |
||||
} |
||||
|
||||
.title { |
||||
flex: 1; |
||||
text-align: center; |
||||
font-size: 32rpx; |
||||
font-weight: bold; |
||||
padding-right: 60rpx; |
||||
} |
||||
} |
||||
|
||||
.content-wrap { |
||||
padding: 30rpx 30rpx 30rpx; |
||||
|
||||
.jobInfo-title { |
||||
font-size: 36rpx; |
||||
font-weight: bold; |
||||
color: #333; |
||||
margin-bottom: 20rpx; |
||||
} |
||||
|
||||
.jobInfo-time { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
margin-bottom: 40rpx; |
||||
} |
||||
|
||||
.jobInfo-content { |
||||
font-size: 28rpx; |
||||
line-height: 1.8; |
||||
color: #333; |
||||
|
||||
:deep(img) { |
||||
max-width: 100%; |
||||
height: auto; |
||||
} |
||||
|
||||
:deep(p) { |
||||
margin-bottom: 20rpx; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,131 @@
|
||||
## 1.5.0(2024-11-01) |
||||
1. 更新弹窗中使用的示例 |
||||
## 1.4.9(2024-08-30) |
||||
1. 修复在app端和小程序端插入超链接可能会回显出部分特殊标识的bug |
||||
## 1.4.8(2024-08-16) |
||||
1. 有群友反馈,通过wangEditor添加的含有图片的富文本的宽高是在style内联样式中的,但是uni-editor不能解析img标签的内联宽高,因此我封装一个工具方法convertImgStylesToAttributes,处理一下富文本字符串即可,详见示例一 |
||||
## 1.4.7(2024-08-12) |
||||
1. 新增标题、字体、字体大小、字间距段前后距等工具子级悬浮工具栏 |
||||
2. 对于字体样式:可在组件的data中的fabTools.fontFamily中自行引入添加你本地的自定义字体(app中对于部分css自带的字体例如微软雅黑等不支持,因为app中不自带这些字体,你可能需要自行引入字体文件) |
||||
## 1.4.6(2024-08-08) |
||||
1. 修复字体工具栏无法隐藏的bug |
||||
## 1.4.5(2024-07-21) |
||||
1. 解决video图标冲突问题 |
||||
## 1.4.4(2024-07-20) |
||||
1. 新增视频插入功能 |
||||
2. 更新示例一(关于视频插入参考请见示例一) |
||||
## 1.4.3(2024-07-16) |
||||
1. 更新示例工程 |
||||
## 1.4.2(2024-07-16) |
||||
1. 更新示例工程 |
||||
## 1.4.1(2024-06-14) |
||||
1. 更新示例工程 |
||||
## 1.4.0(2024-05-31) |
||||
1. 更新示例工程 |
||||
## 1.3.9(2024-05-31) |
||||
1. 修复调色板无法正常选色的问题 |
||||
## 1.3.8(2024-05-13) |
||||
1. 更新示例三(微信小程序上使用setContents造成聚焦滚动的处理) |
||||
## 1.3.7(2024-05-10) |
||||
1. 修复添加超链接后,不触发input更新当前最新内容的bug |
||||
## 1.3.6(2024-05-09) |
||||
1. 文档迁移 |
||||
## 1.3.5(2024-05-09) |
||||
1. 所有事件携带编辑器id参数,以便循环时能区分处理 |
||||
2. 更新示例工程 |
||||
## 1.3.4(2024-05-08) |
||||
1. 更新示例工程 |
||||
2. 新增editorId参数 |
||||
## 1.3.3(2024-03-22) |
||||
1. 修复微信小程序长按无法粘贴的问题 |
||||
## 1.3.2(2024-03-14) |
||||
1. 更新了toolbar样式与配置,见文档 |
||||
2. 更新示例工程,媒体查询响应式写法 |
||||
3. 优化了只读模式效果,开启只读模式后,文章内容的超链接可正常点击并跳转 |
||||
## 1.3.1(2024-03-14) |
||||
1. 优化了只读功能,开启只读后自动隐藏工具栏 |
||||
2. 更新示例工程 |
||||
## 1.3.0(2024-03-07) |
||||
1. 新增addLink的emit事件 |
||||
## 1.2.9(2024-02-23) |
||||
1. 更新文档 |
||||
## 1.2.8(2024-02-23) |
||||
1. 新增了添加超链接的工具,toolbar中link字段,默认开启 |
||||
2. 优化了部分逻辑 |
||||
3. 更新文档、更新示例工程 |
||||
## 1.2.7(2024-02-23) |
||||
1. 更新文档,更新示例工程 |
||||
2. 添加toolbar中图标字体大小可配置项 |
||||
## 1.2.6(2024-02-22) |
||||
1. 添加导出工具按钮,可将当前已编辑的html导出至页面解析 |
||||
2. 超链接工具按钮正在尝试开发中(貌似目前官方不支持) |
||||
## 1.2.5(2024-02-19) |
||||
1. 更新示例工程(吸顶写法) |
||||
2. 完善调色板功能 |
||||
## 1.2.4(2024-02-18) |
||||
1. 修复工具栏颜色按钮底色动态切换问题 |
||||
## 1.2.3(2024-02-18) |
||||
1. 更新示例工程 |
||||
## 1.2.2(2024-02-18) |
||||
1. 删除log调试打印 |
||||
## 1.2.1(2024-02-18) |
||||
1. 修复了颜色图标不会动态切换的问题 |
||||
## 1.2.0(2024-02-18) |
||||
1. 修复选择颜色时会将所选文字删除的bug |
||||
## 1.1.9(2024-02-04) |
||||
1. 更新示例工程 |
||||
## 1.1.8(2024-02-04) |
||||
1. 文档修改 |
||||
## 1.1.7(2024-02-04) |
||||
1. 新增toolbar配置项,可自由配置工具栏工具列表 |
||||
2. 移除组件内原templates属性,默认初始化编辑器内容请看文档使用方式示例 |
||||
3. 更新文档 |
||||
## 1.1.6(2024-01-31) |
||||
1. 更好的兼容vue2了,修复在vue2下高度可能超出的问题 |
||||
2. 示例工程兼容vue2 |
||||
## 1.1.5(2024-01-30) |
||||
1. 修复工具栏字体按钮无效的问题 |
||||
## 1.1.4(2024-01-30) |
||||
1. 解决默认初始化内容时前缀空格或缩进无效的问题 |
||||
2. 解决点击工具栏高亮状态后输入内容时便失去高亮的bug |
||||
3. 更新示例工程 |
||||
## 1.1.3(2024-01-23) |
||||
1. 重写高度动态计算逻辑,现在对不同屏幕尺寸的适应性更强了 |
||||
## 1.1.2(2024-01-17) |
||||
1. 修复分割线会生成多条的问题 |
||||
## 1.1.1(2024-01-15) |
||||
1. 更新文档 |
||||
## 1.1.0(2024-01-15) |
||||
1. insertText方法在插入内容的时候会移动光标聚焦,导致焦点回滚到视口处 |
||||
2. 更新示例工程 |
||||
## 1.0.9(2024-01-04) |
||||
1. 更新文档 |
||||
## 1.0.8(2024-01-04) |
||||
1. 修复h5端官方cdn请求失败的问题,详见问答贴:https://ask.dcloud.net.cn/article/40900 |
||||
## 1.0.7(2024-01-03) |
||||
1. 移除v-bind="$attrs",该写法在微信小程序不支持 |
||||
## 1.0.6(2023-12-29) |
||||
1. 更新文档 |
||||
## 1.0.5(2023-12-29) |
||||
1. 更新了init方法,可以使用返回的editor实例尽情自定义 |
||||
2. 组件在<editor>上添加v-bind="$attrs"属性穿透,可以使用原editor参数,官方文档:https://uniapp.dcloud.net.cn/component/editor.html |
||||
## 1.0.4(2023-12-29) |
||||
1. 优化了切换文字和背景颜色是,可能会导致切换后不生效的问题 |
||||
2. 修复在部分设备上的微信小程序中可能会存在颜色版无法正常滑动的问题 |
||||
3. 更友好的交互体验:添加图标悬停字样描述、添加格式化文本弹窗确认 |
||||
4. 有插入视频的需求,暂时可能无法实现,官方给予的回复是:目前各端的eidtor组件都不能直接插入视频,编辑时可以采用视频封面占位,并在图片中保存视频信息,在预览时再还原为视频。 |
||||
## 1.0.3(2023-10-13) |
||||
1. 更新readme文档 |
||||
2. 更新调整组件示例项目,添加插件代码中部分注释 |
||||
## 1.0.2(2023-10-13) |
||||
1. 更新uni_modules规范,可一键导入组件 |
||||
2. 更新组件示例项目(包括使用uniCloud.uploadFile多选上传图片示例方法) |
||||
## 1.0.1(2023-10-12) |
||||
1. 修复小程序中自动聚焦滚动到富文本组件区域的bug |
||||
2. 略微调整了富文本上方toolbar工具栏中按钮的大小尺寸 |
||||
## 1.0.0(2023-9-19) |
||||
1. 新增字体与背景颜色板 |
||||
2. 可自定义预设内容模板 |
||||
3. 解决官方样例在小程序和app部分报错不兼容的问题 |
||||
4. 可配合云存储上传富文本中插入的图片 本质上是基于官方内置富文本editor组件改版封装,所以官方有的功能都有,官方能兼容的也都兼容 |
||||
|
@ -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> |
@ -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> |
@ -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> |
@ -0,0 +1,852 @@
|
||||
<template> |
||||
<view class="sp-editor" :style="{ '--icon-size': iconSize, '--icon-columns': iconColumns }"> |
||||
<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="toolbarList.includes('export')" class="iconfont icon-baocun" title="导出" @tap="exportHtml"></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' |
||||
|
||||
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 // 工具栏列数 |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
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: '' } |
||||
] |
||||
} |
||||
} |
||||
}, |
||||
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) |
||||
} |
||||
}, |
||||
// 导出 |
||||
exportHtml() { |
||||
this.editorCtx.getContents({ |
||||
success: (res) => { |
||||
this.$emit('exportHtml', res.html, this.editorId) |
||||
} |
||||
}) |
||||
}, |
||||
eLongpress() { |
||||
/** |
||||
* 微信小程序官方editor的长按事件有bug,需要重写覆盖,不需做任何逻辑,可见下面小程序社区问题链接 |
||||
* @tutorial https://developers.weixin.qq.com/community/develop/doc/000c04b3e1c1006f660065e4f61000 |
||||
*/ |
||||
} |
||||
} |
||||
} |
||||
</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; |
||||
} |
||||
|
||||
.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; |
||||
} |
||||
|
||||
.editor-container { |
||||
padding: 8rpx 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: #66ccff; |
||||
} |
||||
|
||||
.fab-tools { |
||||
display: flex; |
||||
padding: 0 10rpx; |
||||
box-sizing: border-box; |
||||
|
||||
.fab-sub { |
||||
width: auto; |
||||
height: auto; |
||||
margin: 10rpx; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,24 @@
|
||||
@font-face { |
||||
font-family: 'iconfont'; |
||||
src: url('data:font/ttf;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTahWRN8AAAeEAAAAHEdERUYAKQALAAAHZAAAAB5PUy8yPElI7QAAAVgAAABgY21hcOY96ckAAAHMAAABSmdhc3D//wADAAAHXAAAAAhnbHlmPxK+mgAAAyQAAAF4aGVhZCjWXYcAAADcAAAANmhoZWEHngOFAAABFAAAACRobXR4DIgAQAAAAbgAAAASbG9jYQCkALwAAAMYAAAADG1heHABGQB9AAABOAAAACBuYW1lXoIBAgAABJwAAAKCcG9zdENa1TcAAAcgAAAAOQABAAAAAQAA20yLC18PPPUACwQAAAAAAOLbjMwAAAAA4tuMzABA/78DwAMzAAAACAACAAAAAAAAAAEAAAOA/4AAXAQAAAAAAAPAAAEAAAAAAAAAAAAAAAAAAAAEAAEAAAAFAHEACgAAAAAAAgAAAAoACgAAAP8AAAAAAAAABAQAAZAABQAAAokCzAAAAI8CiQLMAAAB6wAyAQgAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZADA5hTmHgOA/4AAAAPcAIAAAAABAAAAAAAAAAAAAAAgAAEEAAAAAAAAAAQAAAAEAABAAIgAAAAAAAMAAAADAAAAHAABAAAAAABEAAMAAQAAABwABAAoAAAABgAEAAEAAuYU5h7//wAA5hTmHv//Ge8Z5gABAAAAAAAAAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApAC8AAoAQABAA8ACwAARABQAGAAoADQAQABMAFgAZABwAAABHgEUBg8BBiIuAT0BND4BMh8BFTcBESERBSEiJjURNDYzITIWFREUBgE0NjIWFREUBiImNRMyFhQGKwEiJjQ2MxcyFhQGKwEiJjQ2MwE0NjIWFREUBiImNRMiJjQ2OwEyFhQGIwciJjQ2OwEyFhQGIwJwCAgICKAHEQ8JCQ8RBxBCAV79AAMA/QAbJSUbAwAbJSX9JRMaExMaEyANExMNgA0TEw2ADRMTDYANExMNAqATGhMTGhMgDRMTDYANExMNgA0TEw2ADRMTDQGcBQ8QDwVgBAgPCcAJDwgEVU4n/wACAP4AQCUbAgAbJSUb/gAbJQJgDRMTDf3gDRMTDQGAExoTExoTwBMaExMaEwFgDRMTDf3gDRMTDQFAExoTExoTwBMaExMaEwABAIj/vwNpAzMACwAAEzMRIREzESMRIREjiJABwZCQ/j+QAzP+jwFx/IwBiP54AAAAABIA3gABAAAAAAAAABMAKAABAAAAAAABAAgATgABAAAAAAACAAcAZwABAAAAAAADAAgAgQABAAAAAAAEAAgAnAABAAAAAAAFAAsAvQABAAAAAAAGAAgA2wABAAAAAAAKACsBPAABAAAAAAALABMBkAADAAEECQAAACYAAAADAAEECQABABAAPAADAAEECQACAA4AVwADAAEECQADABAAbwADAAEECQAEABAAigADAAEECQAFABYApQADAAEECQAGABAAyQADAAEECQAKAFYA5AADAAEECQALACYBaABDAHIAZQBhAHQAZQBkACAAYgB5ACAAaQBjAG8AbgBmAG8AbgB0AABDcmVhdGVkIGJ5IGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABSAGUAZwB1AGwAYQByAABSZWd1bGFyAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABWAGUAcgBzAGkAbwBuACAAMQAuADAAAFZlcnNpb24gMS4wAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAABHZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuAABoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAABodHRwOi8vZm9udGVsbG8uY29tAAAAAAIAAAAAAAAACgAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAgECAQMFdmlkZW8GaGVhZGVyAAAAAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMABAABAAQAAAACAAAAAAAAAAEAAAAA4p8rRgAAAADi24zMAAAAAOLbjMw=') format('truetype'); |
||||
font-weight: normal; |
||||
font-style: normal; |
||||
font-display: swap; |
||||
} |
||||
|
||||
.iconfont { |
||||
font-family: "iconfont" !important; |
||||
font-size: 16px; |
||||
font-style: normal; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
} |
||||
|
||||
.icon-header:before { |
||||
content: "\e61e"; |
||||
} |
||||
|
||||
.icon-video:before { |
||||
content: "\e614"; |
||||
} |
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,83 @@
|
||||
{ |
||||
"id": "sp-editor", |
||||
"displayName": "官方富文本编辑器editor组件改良扩展优化版", |
||||
"version": "1.5.0", |
||||
"description": "基于官方的富文本编辑器editor组件,进行改良扩展优化版,添加了调色板,添加超链接等功能,可自定义扩展工具,快来试试吧~", |
||||
"keywords": [ |
||||
"富文本", |
||||
"editor", |
||||
"编辑器" |
||||
], |
||||
"repository": "", |
||||
"engines": { |
||||
}, |
||||
"dcloudext": { |
||||
"type": "component-vue", |
||||
"sale": { |
||||
"regular": { |
||||
"price": "0.00" |
||||
}, |
||||
"sourcecode": { |
||||
"price": "0.00" |
||||
} |
||||
}, |
||||
"contact": { |
||||
"qq": "" |
||||
}, |
||||
"declaration": { |
||||
"ads": "无", |
||||
"data": "插件不采集任何数据", |
||||
"permissions": "无" |
||||
}, |
||||
"npmurl": "" |
||||
}, |
||||
"uni_modules": { |
||||
"dependencies": [], |
||||
"encrypt": [], |
||||
"platforms": { |
||||
"cloud": { |
||||
"tcb": "y", |
||||
"aliyun": "y", |
||||
"alipay": "n" |
||||
}, |
||||
"client": { |
||||
"Vue": { |
||||
"vue2": "y", |
||||
"vue3": "y" |
||||
}, |
||||
"App": { |
||||
"app-vue": "y", |
||||
"app-nvue": "y" |
||||
}, |
||||
"H5-mobile": { |
||||
"Safari": "y", |
||||
"Android Browser": "y", |
||||
"微信浏览器(Android)": "y", |
||||
"QQ浏览器(Android)": "y" |
||||
}, |
||||
"H5-pc": { |
||||
"Chrome": "y", |
||||
"IE": "y", |
||||
"Edge": "y", |
||||
"Firefox": "y", |
||||
"Safari": "y" |
||||
}, |
||||
"小程序": { |
||||
"微信": "y", |
||||
"阿里": "u", |
||||
"百度": "u", |
||||
"字节跳动": "u", |
||||
"QQ": "u", |
||||
"钉钉": "u", |
||||
"快手": "u", |
||||
"飞书": "u", |
||||
"京东": "u" |
||||
}, |
||||
"快应用": { |
||||
"华为": "u", |
||||
"联盟": "u" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
# sp-editor |
||||
|
||||
### 视频插入功能 |
||||
|
||||
`于2024-7-20日v1.4.4版本更新视频插入功能(属实是鸽🕊了太久了)` |
||||
|
||||
- 实现方案:先以图片占位,在导出时,将携带视频链接的图片转换成视频标签。 |
||||
- 更多请参考示例一 |
||||
- 如果该插件有帮助到您,还望能点赞好评一下,谢谢!🌟 |
||||
|
||||
### 文档迁移 |
||||
|
||||
> 防止文档失效,提供下列五个地址,内容一致 |
||||
|
||||
- [地址一](https://sonvee.github.io/sv-app-docs/docs-github/src/plugins/sp-editor/sp-editor.html) |
||||
- [地址二](https://sv-app-docs.pages.dev/src/plugins/sp-editor/sp-editor.html) |
||||
- [地址三](https://sv-app-docs.4everland.app/src/plugins/sp-editor/sp-editor.html) |
||||
- [地址四](https://sv-app-docs.vercel.app/src/plugins/sp-editor/sp-editor.html) (需要梯子) |
||||
- [地址五](https://static-mp-74bfcbac-6ba6-4f39-8513-8831390ff75a.next.bspapp.com/docs-uni/src/plugins/sp-editor/sp-editor.html) (有IP限制) |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,132 @@
|
||||
// 标识必须独一无二 - 标识是为了使用insertText插入标识文本后,查找到标识所在delta位置的索引
|
||||
export const linkFlag = '#-*=*-*=*-*=*@-link超链接标识link-@*=*-*=*-*=*-#' |
||||
|
||||
export function addLink(editorCtx, attr, callback) { |
||||
// 先插入一段文本内容
|
||||
editorCtx.insertText({ |
||||
text: linkFlag |
||||
}) |
||||
// 获取全文delta内容
|
||||
editorCtx.getContents({ |
||||
success(res) { |
||||
let options = res.delta.ops |
||||
const findex = options.findIndex(item => { |
||||
return item.insert && typeof item.insert !== 'object' && item.insert?.indexOf(linkFlag) !== -1 |
||||
}) |
||||
// 根据标识查找到插入的位置
|
||||
if (findex > -1) { |
||||
const findOption = options[findex] |
||||
const findAttributes = findOption.attributes |
||||
// 将该findOption分成三部分:前内容 要插入的link 后内容
|
||||
const [prefix, suffix] = findOption.insert.split(linkFlag); |
||||
const handleOps = [] |
||||
// 前内容
|
||||
if (prefix) { |
||||
const prefixOps = findAttributes ? { |
||||
insert: prefix, |
||||
attributes: findAttributes |
||||
} : { |
||||
insert: prefix |
||||
} |
||||
handleOps.push(prefixOps) |
||||
} |
||||
// 插入的link
|
||||
const linkOps = { |
||||
insert: attr.text, |
||||
attributes: { |
||||
link: attr.href, |
||||
textDecoration: attr.textDecoration || 'none', // 下划线
|
||||
color: attr.color || '#007aff' |
||||
} |
||||
} |
||||
handleOps.push(linkOps) |
||||
// 后内容
|
||||
if (suffix) { |
||||
const suffixOps = findAttributes ? { |
||||
insert: suffix, |
||||
attributes: findAttributes |
||||
} : { |
||||
insert: suffix |
||||
} |
||||
handleOps.push(suffixOps) |
||||
} |
||||
// 删除原options[findex]并在findex位置插入上述三个ops
|
||||
options.splice(findex, 1); |
||||
options.splice(findex, 0, ...handleOps); |
||||
// 最后重新初始化内容,注意该方法会导致光标重置到最开始位置
|
||||
editorCtx.setContents({ |
||||
delta: { |
||||
ops: options |
||||
} |
||||
}) |
||||
// 所以最后建议使富文本光标失焦,让用户手动聚焦光标
|
||||
editorCtx.blur() |
||||
|
||||
// 后续回调操作
|
||||
if (callback) callback() |
||||
} |
||||
} |
||||
}) |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 将含有特殊图片形式视频的富文本转换成正常视频的富文本 |
||||
* @param {String} html 要进行处理的富文本字符串 |
||||
* @returns {String} 返回处理结果 |
||||
*/ |
||||
export function handleHtmlWithVideo(html) { |
||||
// 正则表达式用于匹配img标签中带有alt属性且alt属性值为视频链接的模式
|
||||
const regex = /<img\s+src="[^"]*"\s+alt="([^"]*)"[^>]*>/g |
||||
// 使用replace方法和一个函数回调来替换匹配到的内容
|
||||
return html.replace(regex, (match, videoUrl) => { |
||||
// 替换为video标签,并添加controls属性以便用户可以控制播放
|
||||
return `<video width="80%" controls><source src="${videoUrl}" type="video/mp4"></video>` |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* 将img标签中内联style属性中的宽高样式提取出标签width与height属性 |
||||
* @param {Object} html 要处理的富文本字符串 |
||||
* @returns {Object} 返回处理结果 |
||||
*/ |
||||
export function convertImgStylesToAttributes(html) { |
||||
return html.replace(/<img\s+([^>]+)\s*>/g, function(match, attributes) { |
||||
// 分割属性
|
||||
const attrs = attributes.split(/\s+/); |
||||
|
||||
// 找到style属性的位置
|
||||
const styleIndex = attrs.findIndex(attr => attr.startsWith('style=')); |
||||
if (styleIndex === -1) return match; // 如果没有找到style属性,则返回原样
|
||||
|
||||
// 提取style属性值
|
||||
const styleAttr = attrs.splice(styleIndex, 1)[0]; |
||||
const style = styleAttr.match(/"([^"]*)"/)[1]; |
||||
|
||||
// 解析 style 属性
|
||||
const styleObj = {}; |
||||
style.split(';').forEach(function(part) { |
||||
if (part) { |
||||
const [name, value] = part.split(':'); |
||||
styleObj[name.trim()] = value.trim(); |
||||
} |
||||
}); |
||||
|
||||
// 创建新的 img 标签
|
||||
let newTag = '<img'; |
||||
if (styleObj.width) { |
||||
newTag += ` width="${styleObj.width}"`; |
||||
} |
||||
if (styleObj.height) { |
||||
newTag += ` height="${styleObj.height}"`; |
||||
} |
||||
|
||||
// 添加原有的属性,包括修改过的style属性
|
||||
newTag += ` ${styleAttr} ${attrs.join(' ')}`; |
||||
|
||||
// 关闭 img 标签
|
||||
newTag += '>'; |
||||
|
||||
return newTag; |
||||
}); |
||||
} |
Loading…
Reference in new issue