更新富文本编辑器
This commit is contained in:
15
uni_modules/sv-editor/components/common/config.js
Normal file
15
uni_modules/sv-editor/components/common/config.js
Normal file
File diff suppressed because one or more lines are too long
261
uni_modules/sv-editor/components/common/file-handler.js
Normal file
261
uni_modules/sv-editor/components/common/file-handler.js
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 以下方法出自 image-tools
|
||||
* @see https://ext.dcloud.net.cn/plugin?id=123
|
||||
*/
|
||||
|
||||
function getLocalFilePath(path) {
|
||||
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf(
|
||||
'_downloads') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('file://') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('/storage/emulated/0/') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('/') === 0) {
|
||||
let localFilePath = plus.io.convertAbsoluteFileSystem(path)
|
||||
if (localFilePath !== path) {
|
||||
return localFilePath
|
||||
} else {
|
||||
path = path.substr(1)
|
||||
}
|
||||
}
|
||||
return '_www/' + path
|
||||
}
|
||||
|
||||
function dataUrlToBase64(str) {
|
||||
let array = str.split(',')
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
let index = 0
|
||||
|
||||
function getNewFileId() {
|
||||
return Date.now() + String(index++)
|
||||
}
|
||||
|
||||
function biggerThan(v1, v2) {
|
||||
let v1Array = v1.split('.')
|
||||
let v2Array = v2.split('.')
|
||||
let update = false
|
||||
for (let index = 0; index < v2Array.length; index++) {
|
||||
let diff = v1Array[index] - v2Array[index]
|
||||
if (diff !== 0) {
|
||||
update = diff > 0
|
||||
break
|
||||
}
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
export function pathToBase64(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (typeof window === 'object' && 'document' in window) {
|
||||
if (typeof FileReader === 'function') {
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', path, true)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onload = function() {
|
||||
if (this.status === 200) {
|
||||
let fileReader = new FileReader()
|
||||
fileReader.onload = function(e) {
|
||||
resolve(e.target.result)
|
||||
}
|
||||
fileReader.onerror = reject
|
||||
fileReader.readAsDataURL(this.response)
|
||||
}
|
||||
}
|
||||
xhr.onerror = reject
|
||||
xhr.send()
|
||||
return
|
||||
}
|
||||
let canvas = document.createElement('canvas')
|
||||
let c2x = canvas.getContext('2d')
|
||||
let img = new Image
|
||||
img.onload = function() {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
c2x.drawImage(img, 0, 0)
|
||||
resolve(canvas.toDataURL())
|
||||
canvas.height = canvas.width = 0
|
||||
}
|
||||
img.onerror = reject
|
||||
img.src = path
|
||||
return
|
||||
}
|
||||
if (typeof plus === 'object') {
|
||||
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
|
||||
entry.file(function(file) {
|
||||
let fileReader = new plus.io.FileReader()
|
||||
fileReader.onload = function(data) {
|
||||
resolve(data.target.result)
|
||||
}
|
||||
fileReader.onerror = function(error) {
|
||||
reject(error)
|
||||
}
|
||||
fileReader.readAsDataURL(file)
|
||||
}, function(error) {
|
||||
reject(error)
|
||||
})
|
||||
}, function(error) {
|
||||
reject(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
|
||||
wx.getFileSystemManager().readFile({
|
||||
filePath: path,
|
||||
encoding: 'base64',
|
||||
success: function(res) {
|
||||
resolve('data:image/png;base64,' + res.data)
|
||||
},
|
||||
fail: function(error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
reject(new Error('not support'))
|
||||
})
|
||||
}
|
||||
|
||||
export function base64ToPath(base64) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (typeof window === 'object' && 'document' in window) {
|
||||
base64 = base64.split(',')
|
||||
let type = base64[0].match(/:(.*?);/)[1]
|
||||
let str = atob(base64[1])
|
||||
let n = str.length
|
||||
let array = new Uint8Array(n)
|
||||
while (n--) {
|
||||
array[n] = str.charCodeAt(n)
|
||||
}
|
||||
return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], {
|
||||
type: type
|
||||
})))
|
||||
}
|
||||
let extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/)
|
||||
if (extName) {
|
||||
extName = extName[1]
|
||||
} else {
|
||||
reject(new Error('base64 error'))
|
||||
}
|
||||
let fileName = getNewFileId() + '.' + extName
|
||||
if (typeof plus === 'object') {
|
||||
let basePath = '_doc'
|
||||
let dirPath = 'uniapp_temp'
|
||||
let filePath = basePath + '/' + dirPath + '/' + fileName
|
||||
if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) {
|
||||
plus.io.resolveLocalFileSystemURL(basePath, function(entry) {
|
||||
entry.getDirectory(dirPath, {
|
||||
create: true,
|
||||
exclusive: false,
|
||||
}, function(entry) {
|
||||
entry.getFile(fileName, {
|
||||
create: true,
|
||||
exclusive: false,
|
||||
}, function(entry) {
|
||||
entry.createWriter(function(writer) {
|
||||
writer.onwrite = function() {
|
||||
resolve(filePath)
|
||||
}
|
||||
writer.onerror = reject
|
||||
writer.seek(0)
|
||||
writer.writeAsBinary(dataUrlToBase64(base64))
|
||||
}, reject)
|
||||
}, reject)
|
||||
}, reject)
|
||||
}, reject)
|
||||
return
|
||||
}
|
||||
let bitmap = new plus.nativeObj.Bitmap(fileName)
|
||||
bitmap.loadBase64Data(base64, function() {
|
||||
bitmap.save(filePath, {}, function() {
|
||||
bitmap.clear()
|
||||
resolve(filePath)
|
||||
}, function(error) {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
}, function(error) {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
|
||||
let filePath = wx.env.USER_DATA_PATH + '/' + fileName
|
||||
wx.getFileSystemManager().writeFile({
|
||||
filePath: filePath,
|
||||
data: dataUrlToBase64(base64),
|
||||
encoding: 'base64',
|
||||
success: function() {
|
||||
resolve(filePath)
|
||||
},
|
||||
fail: function(error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
reject(new Error('not support'))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 本方法为本人自己写的,建议还是使用上述的pathToBase64方法
|
||||
* @description 图片地址转换为base64格式图片
|
||||
* @param {string} url 图片地址 网络地址 本地相对路径
|
||||
* @param {string} type base64图片类型 默认png
|
||||
*/
|
||||
export function urlToBase64(url, type = 'png') {
|
||||
let promises
|
||||
|
||||
// 网络地址 或者h5端本地相对路径 可使用request方式
|
||||
promises = new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
success: (res) => {
|
||||
const base64 = `data:image/${type};base64,${uni.arrayBufferToBase64(res.data)}`
|
||||
resolve(base64);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// #ifdef APP
|
||||
if (!url.startsWith('http')) {
|
||||
// app真机本地相对路径
|
||||
promises = new Promise((resolve, reject) => {
|
||||
// 使用compressImage获取到安卓本地路径file:///...
|
||||
uni.compressImage({
|
||||
src: url,
|
||||
quality: 100,
|
||||
success: (res) => {
|
||||
const tempUrl = res.tempFilePath
|
||||
plus.io.resolveLocalFileSystemURL(tempUrl, (entry) => {
|
||||
entry.file((e) => {
|
||||
let fileReader = new plus.io.FileReader();
|
||||
fileReader.onload = (r) => {
|
||||
resolve(r.target.result)
|
||||
}
|
||||
fileReader.readAsDataURL(e)
|
||||
})
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
return promises
|
||||
}
|
||||
179
uni_modules/sv-editor/components/common/parse.js
Normal file
179
uni_modules/sv-editor/components/common/parse.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 富文本解析工具
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import config from './config.js'
|
||||
|
||||
/**
|
||||
* 将含有封面占位图形式的视频富文本转换成正常视频的富文本
|
||||
* @param {String} richText 要进行处理的富文本字符串
|
||||
* @returns {String} 返回处理结果
|
||||
*/
|
||||
export function parseHtmlWithVideo(richText) {
|
||||
// 正则表达式匹配<img>标签及其属性
|
||||
const imgRegex = /<img\s+([^>]+)>/gi;
|
||||
// 正则表达式匹配data-custom属性中的url值
|
||||
const customUrlRegex = /\bdata-custom="[^"]*url=([^&"]+)/i;
|
||||
|
||||
return richText.replace(imgRegex, (match, attrs) => {
|
||||
// 查找data-custom属性中的url值
|
||||
const urlMatch = attrs.match(customUrlRegex);
|
||||
if (urlMatch) {
|
||||
// 获取data-custom中的url
|
||||
const videoUrl = urlMatch[1];
|
||||
|
||||
// 解析出所有属性
|
||||
const attrArray = attrs.split(/\s+/).filter(attr => attr.trim() !== '');
|
||||
|
||||
// 过滤掉src属性和data-custom属性
|
||||
const newAttrs = attrArray.filter(attr => !attr.startsWith('src=') && !attr.startsWith('data-custom='))
|
||||
.join(' ');
|
||||
|
||||
// 构建新的video标签,保留原有的其他属性,但去除src和data-custom
|
||||
return `<video controls ${newAttrs}><source src="${videoUrl}" /></video>`;
|
||||
}
|
||||
// 如果没有匹配到data-custom中的url,则保持原样
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带有视频的富文本逆向转换
|
||||
* @description 可自定义处理封面
|
||||
* @param {Promise} richText 要转换的富文本
|
||||
* @param {Function<Promise>} customCallback 自定义处理封面回调,需要return封面图片资源,自带参数为视频地址
|
||||
* @returns {Promise} 转换后的富文本 注意异步处理
|
||||
*/
|
||||
export async function replaceVideoWithImageRender(richText, customCallback) {
|
||||
|
||||
// 正则表达式用于匹配 <video> 标签以及其内部的 <source> 标签
|
||||
const videoRegex = /<video\s+([^>]+)>(.*?)<\/video>/gi;
|
||||
|
||||
// 找到所有的 <video> 标签
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = videoRegex.exec(richText)) !== null) {
|
||||
matches.push(match);
|
||||
}
|
||||
|
||||
// 并行处理每个 <video> 标签,生成对应的缩略图
|
||||
const replacements = await Promise.all(
|
||||
matches.map(async (match) => {
|
||||
const [fullMatch, attributes, content] = match;
|
||||
|
||||
// 匹配 <source> 标签中的 src 属性
|
||||
const sourceRegex = /<source\s+[^>]*src="([^">]+)"/i;
|
||||
const matchSource = content.match(sourceRegex);
|
||||
|
||||
let videoUrl = '';
|
||||
if (matchSource && matchSource.length > 1) {
|
||||
videoUrl = matchSource[1];
|
||||
}
|
||||
|
||||
// 生成视频封面图
|
||||
let thumbnailRes
|
||||
if (customCallback) thumbnailRes = await customCallback(videoUrl) // 自定义封面处理
|
||||
if (!thumbnailRes) thumbnailRes = config.video_thumbnail // 无效值则默认封面处理
|
||||
|
||||
// 过滤掉不需要的属性,例如 controls
|
||||
const filteredAttributes = attributes
|
||||
.split(/\s+/)
|
||||
.filter(attr => !attr.startsWith('controls'))
|
||||
.join(' ');
|
||||
|
||||
// 构建新的 img 标签,继承 video 的属性(除了 controls)并添加 data-custom 属性
|
||||
const imgTag = `<img ${filteredAttributes} src="${thumbnailRes}" data-custom="url=${videoUrl}" />`;
|
||||
|
||||
return { fullMatch, imgTag };
|
||||
}));
|
||||
|
||||
// 使用 replacements 替换原始的 <video> 标签
|
||||
let result = richText;
|
||||
for (const { fullMatch, imgTag } of replacements) {
|
||||
result = result.replace(fullMatch, imgTag);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出富文本中的图片和视频
|
||||
* @param {String} richText 要解析的富文本
|
||||
* @returns {Array} 图片和视频数组
|
||||
*/
|
||||
export function parseImagesAndVideos(richText) {
|
||||
// 创建一个空数组用于存储图片和视频信息
|
||||
const result = [];
|
||||
|
||||
// 正则表达式匹配 <img> 标签及其属性
|
||||
const imgRegex = /<img\s+[^>]*>/gi;
|
||||
// 匹配属性名和值的正则表达式,改进后的版本可以处理属性名中包含连字符的情况
|
||||
const attrRegex = /(\w+(-\w+)*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))/gi;
|
||||
|
||||
// 找到所有的 <img> 标签
|
||||
const matches = richText.match(imgRegex);
|
||||
// 如果没有找到任何 <img> 标签,返回空数组
|
||||
if (!matches) return [];
|
||||
|
||||
// 遍历所有的 <img> 标签
|
||||
matches.forEach(match => {
|
||||
// 创建一个对象用于存储单个图片或视频的信息
|
||||
const ivInfo = {};
|
||||
// 使用正则表达式匹配每个 <img> 标签的属性
|
||||
let attrsMatch;
|
||||
while ((attrsMatch = attrRegex.exec(match)) !== null) {
|
||||
// 属性名
|
||||
const name = attrsMatch[1].toLowerCase();
|
||||
// 属性值可能存在于第三、第四或第五个捕获组中
|
||||
let value = attrsMatch[3] || attrsMatch[4] || attrsMatch[5] || '';
|
||||
|
||||
// 去除属性值两端可能存在的引号
|
||||
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
||||
value = value.substring(1, value.length - 1);
|
||||
}
|
||||
|
||||
// 将属性名和值添加到 ivInfo 对象中
|
||||
ivInfo[name] = value;
|
||||
}
|
||||
// 将单个图片或视频信息添加到数组中
|
||||
result.push(ivInfo);
|
||||
});
|
||||
|
||||
// 返回包含所有图片和视频信息的数组
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出富文本中的图片
|
||||
* @param {String} richText 要解析的富文本
|
||||
* @returns {Array} 图片数组
|
||||
*/
|
||||
export function parseImages(richText) {
|
||||
let result = []
|
||||
const ivList = parseImagesAndVideos(richText)
|
||||
ivList.forEach(item => {
|
||||
if (!item['data-custom'] || !item['data-custom'].startsWith('url')) {
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出富文本中的视频
|
||||
* @param {String} richText 要解析的富文本
|
||||
* @returns {Array} 视频数组
|
||||
*/
|
||||
export function parseVideos(richText) {
|
||||
let result = []
|
||||
const ivList = parseImagesAndVideos(richText)
|
||||
ivList.forEach(item => {
|
||||
if (item['data-custom'] && item['data-custom'].startsWith('url')) {
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
101
uni_modules/sv-editor/components/common/store.js
Normal file
101
uni_modules/sv-editor/components/common/store.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 插件内全局状态管理
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
// #ifdef VUE3
|
||||
import { reactive } from 'vue';
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE2
|
||||
import Vue from 'vue';
|
||||
// #endif
|
||||
|
||||
// 定义state状态
|
||||
let state = null
|
||||
|
||||
// #ifdef VUE3
|
||||
// 定义响应式状态
|
||||
state = reactive({
|
||||
curEID: '',
|
||||
formats: {},
|
||||
isReadOnly: false,
|
||||
firstInstanceFlag: '' // 首次实例化标志,禁止手动更改
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE2
|
||||
// 定义响应式状态
|
||||
state = Vue.observable({
|
||||
curEID: '',
|
||||
formats: {},
|
||||
isReadOnly: false,
|
||||
firstInstanceFlag: '' // 首次实例化标志,禁止手动更改
|
||||
})
|
||||
// #endif
|
||||
|
||||
// 定义方法
|
||||
function getEditor(eid) {
|
||||
return state[`${eid}-ctx`];
|
||||
};
|
||||
|
||||
function setEditor(eid, ctx) {
|
||||
state[`${eid}-ctx`] = ctx
|
||||
// #ifdef MP-WEIXIN
|
||||
state[`${eid}-ctx`].id = eid
|
||||
// #endif
|
||||
}
|
||||
|
||||
function getEID() {
|
||||
return state.curEID
|
||||
};
|
||||
|
||||
function setEID(eid) {
|
||||
state.curEID = eid
|
||||
}
|
||||
|
||||
function getFormats() {
|
||||
return state.formats
|
||||
}
|
||||
|
||||
function setFormats(formats) {
|
||||
state.formats = formats
|
||||
}
|
||||
|
||||
function getReadOnly() {
|
||||
return state.isReadOnly
|
||||
}
|
||||
|
||||
function setReadOnly(readOnly) {
|
||||
state.isReadOnly = readOnly
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
// 重置所有状态
|
||||
state = {}
|
||||
state.curEID = ''
|
||||
state.formats = {}
|
||||
state.isReadOnly = false
|
||||
state.firstInstanceFlag = '' // 首次实例化标志,禁止手动更改
|
||||
}
|
||||
|
||||
// 定义options对象
|
||||
const options = {
|
||||
state,
|
||||
actions: {
|
||||
getEditor,
|
||||
setEditor,
|
||||
getEID,
|
||||
setEID,
|
||||
getFormats,
|
||||
setFormats,
|
||||
getReadOnly,
|
||||
setReadOnly,
|
||||
destroy
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
export default options
|
||||
208
uni_modules/sv-editor/components/common/tool-list.js
Normal file
208
uni_modules/sv-editor/components/common/tool-list.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 工具栏
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
export const toolList = [
|
||||
{ title: '样式', name: 'style', icon: 'icon-zitiyanse' },
|
||||
{ title: '表情', name: 'emoji', icon: 'icon-xiaolian' },
|
||||
{ title: '撤销', name: 'undo', icon: 'icon-shangyibu1' },
|
||||
{ title: '重做', name: 'redo', icon: 'icon-xiayibu1' },
|
||||
{ title: '更多', name: 'more', icon: 'icon-icon_tianjia' },
|
||||
{ title: '扩展', name: 'setting', icon: 'icon-bianji' },
|
||||
]
|
||||
|
||||
export const styleToolList = [
|
||||
{ title: '标题', name: 'header', value: 2, icon: 'icon-zitibiaoti' },
|
||||
{ title: '分割线', name: 'divider', icon: 'icon-fengexian' },
|
||||
{ title: '粗体', name: 'bold', icon: 'icon-zitijiacu' },
|
||||
{ title: '斜体', name: 'italic', icon: 'icon-zitixieti' },
|
||||
{ title: '下划线', name: 'underline', icon: 'icon-zitixiahuaxian' },
|
||||
{ title: '删除线', name: 'strike', icon: 'icon-zitishanchuxian' },
|
||||
{ title: '左对齐', name: 'align', value: 'left', icon: 'icon-zuoduiqi' },
|
||||
{ title: '居中', name: 'align', value: 'center', icon: 'icon-juzhongduiqi' },
|
||||
{ title: '右对齐', name: 'align', value: 'right', icon: 'icon-youduiqi' },
|
||||
{ title: '有序列表', name: 'list', value: 'ordered', icon: 'icon-youxupailie' },
|
||||
{ title: '无序列表', name: 'list', value: 'bullet', icon: 'icon-wuxupailie' },
|
||||
{ title: '上标', name: 'script', value: 'super', icon: 'icon-zitishangbiao' },
|
||||
{ title: '左缩进', name: 'indent', value: '+1', icon: 'icon-zuosuojin' },
|
||||
{ title: '右缩进', name: 'indent', value: '-1', icon: 'icon-yousuojin' },
|
||||
{ title: '下标', name: 'script', value: 'sub', icon: 'icon-ziti-xiabiao' },
|
||||
{ title: '文字颜色', name: 'color', icon: 'icon-wenziyanse' },
|
||||
{ title: '背景颜色', name: 'backgroundColor', icon: 'icon-beijingyanse' },
|
||||
{ title: '清除格式', name: 'removeformat', icon: 'icon-qingchugeshi' },
|
||||
]
|
||||
|
||||
export const moreToolList = [
|
||||
{ title: '添加图片', name: 'image', value: 'popup', icon: 'icon-charutupian' },
|
||||
{ title: '添加视频', name: 'video', value: 'popup', icon: 'icon-shexiangji' },
|
||||
{ title: '添加链接', name: 'link', value: 'popup', icon: 'icon-charulianjie' },
|
||||
{ title: '添加附件', name: 'attachment', value: 'popup', icon: 'icon-huixingzhen' },
|
||||
{ title: '提及', name: 'at', value: 'popup', icon: 'icon-at' },
|
||||
{ title: '话题', name: 'topic', value: 'popup', icon: 'icon-huati' },
|
||||
{ title: '清空', name: 'clear', value: 'button', icon: 'icon-shanchu' },
|
||||
]
|
||||
|
||||
export const emojiToolList = [
|
||||
'😊', // 笑笑
|
||||
'😃', // 大笑
|
||||
'😄', // 开心果
|
||||
'😁', // 嘲讽
|
||||
'😆', // 爆笑
|
||||
'😅', // 出汗笑
|
||||
'🤣', // 滚地大笑
|
||||
'😂', // 泪流满面
|
||||
'🙂', // 轻松愉快
|
||||
'🙃', // 上下翻白眼
|
||||
'😉', // 鬼鬼祟祟
|
||||
'😌', // 安慰
|
||||
'😍', // 心动
|
||||
'🥰', // 深情
|
||||
'😘', // 吻
|
||||
'😗', // 接吻
|
||||
'😙', // 亲吻
|
||||
'😚', // 亲吻
|
||||
'😋', // 哇塞
|
||||
'😛', // 舌头外伸
|
||||
'😝', // 舌头吐出
|
||||
'😜', // 顽皮
|
||||
'🤪', // 疯狂
|
||||
'😎', // 自豪
|
||||
'🤓', // 学究
|
||||
'🧐', // 思考
|
||||
'😏', // 狡猾
|
||||
'😒', // 不高兴
|
||||
'😞', // 不开心
|
||||
'😔', // 抒发情绪
|
||||
'😟', // 担忧
|
||||
'😕', // 困惑
|
||||
'🙁', // 小失望
|
||||
'☹️️', // 不好意思
|
||||
'😣', // 苦恼
|
||||
'😖', // 愤怒
|
||||
'😫', // 累
|
||||
'😩', // 悲伤
|
||||
'😤', // 生气
|
||||
'😠', // 生气
|
||||
'😡', // 极端愤怒
|
||||
'🤬', // 发飙
|
||||
'🤯', // 爆炸头脑
|
||||
'😳', // 吃惊
|
||||
'😱', // 惊吓
|
||||
'😨', // 恐惧
|
||||
'😰', // 慌张
|
||||
'😢', // 哭泣
|
||||
'😭', // 大哭
|
||||
'😓', // 受挫
|
||||
'🤗', // 给力
|
||||
'🤔', // 思考
|
||||
'🤭', // 戴口罩捂嘴笑
|
||||
'🤫', // 戴口罩做鬼脸
|
||||
'🤥', // 说谎
|
||||
'😬', // 格格不入
|
||||
'😴', // 睡觉
|
||||
'🤤', // 垂涎欲滴
|
||||
'🥳', // 庆祝
|
||||
'🥺', // 求求你
|
||||
'😈', // 恶魔
|
||||
'👿', // 恶灵
|
||||
'🤡', // 小丑
|
||||
'👻', // 鬼魂
|
||||
'👽', // 外星人
|
||||
'👾', // 游戏角色
|
||||
'🤖', // 机器人
|
||||
'😺', // 笑猫
|
||||
'😸', // 大笑猫
|
||||
'😹', // 开心猫
|
||||
'😻', // 心动猫
|
||||
'😼', // 傲娇猫
|
||||
'😽', // 亲吻猫
|
||||
'🙀', // 惊吓猫
|
||||
'😿', // 哭猫
|
||||
'😾' // 生气猫
|
||||
]
|
||||
|
||||
export const colorList = [
|
||||
'#000000',
|
||||
'#222222',
|
||||
'#444444',
|
||||
'#666666',
|
||||
'#999999',
|
||||
'#cccccc',
|
||||
'#eeeeee',
|
||||
'#ffffff',
|
||||
|
||||
'#c92a2a',
|
||||
'#e03131',
|
||||
'#f03e3e',
|
||||
'#fa5252',
|
||||
'#ff6b6b',
|
||||
'#ff8787',
|
||||
'#ffa8a8',
|
||||
'#ffc9c9',
|
||||
|
||||
'#a61e4d',
|
||||
'#c2255c',
|
||||
'#d6336c',
|
||||
'#e64980',
|
||||
'#f06595',
|
||||
'#f783ac',
|
||||
'#faa2c1',
|
||||
'#fcc2d7',
|
||||
|
||||
'#862e9c',
|
||||
'#9c36b5',
|
||||
'#ae3ec9',
|
||||
'#be4bdb',
|
||||
'#cc5de8',
|
||||
'#da77f2',
|
||||
'#e599f7',
|
||||
'#eebefa',
|
||||
|
||||
'#5f3dc4',
|
||||
'#6741d9',
|
||||
'#7048e8',
|
||||
'#7950f2',
|
||||
'#845ef7',
|
||||
'#9775fa',
|
||||
'#b197fc',
|
||||
'#d0bfff',
|
||||
|
||||
'#0b7285',
|
||||
'#0c8599',
|
||||
'#1098ad',
|
||||
'#15aabf',
|
||||
'#22b8cf',
|
||||
'#3bc9db',
|
||||
'#66d9e8',
|
||||
'#99e9f2',
|
||||
|
||||
'#087f5b',
|
||||
'#099268',
|
||||
'#0ca678',
|
||||
'#12b886',
|
||||
'#20c997',
|
||||
'#38d9a9',
|
||||
'#63e6be',
|
||||
'#96f2d7',
|
||||
|
||||
'#5c940d',
|
||||
'#66a80f',
|
||||
'#74b816',
|
||||
'#82c91e',
|
||||
'#94d82d',
|
||||
'#a9e34b',
|
||||
'#c0eb75',
|
||||
'#ffec99',
|
||||
|
||||
'#d9480f',
|
||||
'#e8590c',
|
||||
'#f76707',
|
||||
'#fd7e14',
|
||||
'#ff922b',
|
||||
'#ffa94d',
|
||||
'#ffc078',
|
||||
'#ffd8a8'
|
||||
]
|
||||
412
uni_modules/sv-editor/components/common/utils.js
Normal file
412
uni_modules/sv-editor/components/common/utils.js
Normal file
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* 通用工具api
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import store from './store.js'
|
||||
|
||||
/**
|
||||
* 添加图片
|
||||
* @param {Function} uploadFunc 文件上传函数(开发者自行调用上传接口上传本地图片至服务器后获取服务器图片真实地址,需要return包含地址的数组)
|
||||
* @param {Object} options 图片配置项
|
||||
* @property {String} options.srcFiled 图片地址字段名,默认无时使用数组元素本身
|
||||
* @property {String} options.alt 图像无法显示时的替代文本
|
||||
* @property {String} options.width 图片宽度(pixels/百分比)为空时自适应图片本身宽度,默认空(不建议100%,预留一点空隙以便用户编辑)
|
||||
* @property {String} options.height 图片高度 (pixels/百分比)为空时自适应图片本身高度,默认空
|
||||
* @property {String} options.extClass 添加到图片 img 标签上的类名
|
||||
* @property {String} options.data 被序列化为 v1=1;v2=2 的格式挂在属性 data-custom 上
|
||||
* @returns {Array|Promise} 上传的文件数组
|
||||
*/
|
||||
export async function addImage(uploadFunc, options = {}) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 服务器上传图片
|
||||
if (!uploadFunc) return
|
||||
const upRes = await uploadFunc(editorCtx)
|
||||
if (!upRes || !upRes?.length) return
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
editorCtx.insertText({ text: '\n' })
|
||||
upRes?.forEach((item) => {
|
||||
editorCtx.insertImage({
|
||||
...options,
|
||||
src: options.srcFiled ? item[options.srcFiled] : item,
|
||||
})
|
||||
})
|
||||
// 建议加个换行,虽然会导致input回调再次触发,不过问题不大
|
||||
editorCtx.insertText({ text: '\n' })
|
||||
})
|
||||
|
||||
return upRes
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加视频
|
||||
* @description uni-editor暂不支持插入视频,此处使用视频封面占位,将视频地址作为属性挂在至data-custom上,携带视频的富文本需要使用专用的api进行解析导出。注:建议后端配合返回视频封面图片地址,或者使用固定的网络图片作为封面。
|
||||
* @param {Function} uploadFunc 文件上传函数(开发者自行调用上传接口上传本地视频至服务器后获取服务器视频真实地址,需要return包含地址的数组)
|
||||
* @param {Object} options 视频封面图片配置项
|
||||
* @property {String} options.imageFiled 视频封面图片地址字段名,默认imagePath
|
||||
* @property {String} options.videoFiled 视频真实地址字段名,默认videoPath
|
||||
* @property {String} options.alt 视频封面图片无法显示时的替代文本
|
||||
* @property {String} options.width 视频封面图片宽度(pixels/百分比)默认空,但是要注意,不设置width的话,video标签默认宽度为300px
|
||||
* @property {String} options.height 视频封面图片高度 (pixels/百分比)默认空
|
||||
* @property {String} options.extClass 添加到视频封面图片 img 标签上的类名
|
||||
* @property {String} options.data 警告:视频地址已存入data-custom中,请勿使用此参数导致视频地址被覆盖
|
||||
* @returns {Array|Promise} 上传的文件数组
|
||||
*/
|
||||
export async function addVideo(uploadFunc, options = {}) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 服务器上传视频
|
||||
if (!uploadFunc) return
|
||||
const upRes = await uploadFunc(editorCtx)
|
||||
console.log(upRes);
|
||||
if (!upRes || !upRes?.length) return
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
editorCtx.insertText({ text: '\n' })
|
||||
upRes?.forEach((item) => {
|
||||
editorCtx.insertImage({
|
||||
...options,
|
||||
src: item[options.imageFiled || 'imagePath'],
|
||||
data: { url: item[options.videoFiled || 'videoPath'] },
|
||||
})
|
||||
})
|
||||
// 建议加个换行,虽然会导致input回调再次触发,不过问题不大
|
||||
editorCtx.insertText({ text: '\n' })
|
||||
})
|
||||
|
||||
return upRes
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加链接
|
||||
* @param {Object} options 链接配置项
|
||||
* @property {String} options.link 链接地址
|
||||
* @property {String} options.text 链接文本 空缺时使用link
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#007aff
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加链接成功后回调
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function addLink(options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
...options,
|
||||
link: options.link,
|
||||
text: ` ${options.text || options.link} `, // 前后各加一个空格
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加附件
|
||||
* @param {Function} uploadFunc 文件上传函数(开发者自行调用上传接口上传本地附件至服务器后获取服务器附件真实地址,需要return包含地址的对象)
|
||||
* @param {Object} options 附件配置项
|
||||
* @property {String} options.srcFiled 附件地址字段名,默认path
|
||||
* @property {String} options.link 附件地址 注:临时地址会自动转成about:blank导致无效
|
||||
* @property {String} options.text 附件文本 空缺时使用link
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#34d19d
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加附件成功后回调
|
||||
* @returns {Object|Promise} 上传的文件对象
|
||||
*/
|
||||
export async function addAttachment(uploadFunc, options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 服务器上传附件
|
||||
if (!uploadFunc) return
|
||||
const upRes = await uploadFunc(editorCtx)
|
||||
if (!upRes) return
|
||||
|
||||
const link = upRes[options.srcFiled || 'path'] || options.link
|
||||
if (!link) return
|
||||
const text = ` 📄${upRes.text || options.text || upRes.file?.name || link } ` // 加上附件图标前置,并前后各加一个空格
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
color: '#34d19d',
|
||||
...options,
|
||||
text,
|
||||
link,
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
|
||||
return upRes
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加提及
|
||||
* @param {Object} options 提及配置项
|
||||
* @property {String} options.username 用户名称
|
||||
* @property {String} options.userid 用户id
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#66ccff
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加链接成功后回调
|
||||
*/
|
||||
export async function addAt(options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
color: '#66ccff',
|
||||
...options,
|
||||
link: `@${options.userid}`, // 添加特殊前缀,后续便于解析标识
|
||||
text: ` @${options.username} `, // 前后各加一个空格
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加话题
|
||||
* @param {Object} options 话题配置项
|
||||
* @property {String} options.link 话题链接
|
||||
* @property {String} options.topic 话题名称
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#909399
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加链接成功后回调
|
||||
*/
|
||||
export async function addTopic(options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
color: '#909399',
|
||||
...options,
|
||||
link: `#${options.link}`, // 添加特殊前缀,后续便于解析标识
|
||||
text: ` #${options.topic}# `, // 前后各加一个空格
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 标识必须独一无二 - 标识是为了使用insertText插入标识文本后,查找到标识所在delta位置的索引
|
||||
* 注:因为做了一次insertText操作,所有可能会有linkFlag标识字样闪一下的副作用,没有办法避免
|
||||
*/
|
||||
export const linkFlag = '🔗添加链接中, 请稍后...🔗' // 建议语义化该标识,否则闪烁的时候可能会有点尴尬
|
||||
/**
|
||||
* 插入链接
|
||||
* @description uni-editor暂不支持插入链接,此api使用delta替换链接本文标识方式实现,因硬性原因会导致光标失焦
|
||||
* @param {Object} editorCtx 编辑器上下文
|
||||
* @param {Object} attr 链接属性
|
||||
* @property {String} attr.link 链接地址 注:临时地址会自动转成about:blank导致无效
|
||||
* @property {String} attr.text 链接文本 空缺时使用link
|
||||
* @property {String} attr.textDecoration 下划线
|
||||
* @property {String} attr.color 颜色 默认#007aff
|
||||
* @property {Object} attr.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Object} callback 成功回调
|
||||
*/
|
||||
export function insertLink(editorCtx, attr, callback) {
|
||||
// 先插入一段文本内容
|
||||
editorCtx.insertText({ text: linkFlag })
|
||||
// 必须先失焦,否则光标会移至开始位置
|
||||
editorCtx.blur()
|
||||
// 获取全文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 || attr.link,
|
||||
attributes: {
|
||||
link: attr.link,
|
||||
textDecoration: attr.textDecoration || 'none', // 下划线
|
||||
color: attr.color || '#007aff',
|
||||
...attr.style
|
||||
}
|
||||
}
|
||||
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.removeFormat()
|
||||
editorCtx.format('color', 'inherit')
|
||||
|
||||
// 后续回调操作
|
||||
if (callback) callback()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 尽量消除键盘带来的影响;重要:核心功能!!!
|
||||
* @param {Function} callback 回调
|
||||
* @param {Object} options 配置项
|
||||
* @property {String} options.mode 可选:setInputMode:通过控制ql-editor的inputmode属性控制键盘 [H5 APP] | loseFocus:通过blur失焦隐藏键盘 [MP-WEIXIN] | hideKeyboard:通过hideKeyboard隐藏键盘 | setReadOnly:通过控制读写隐藏键盘
|
||||
* @property {Number} options.delay 延时(毫秒)默认50
|
||||
*/
|
||||
export function noKeyboardEffect(callback, options) {
|
||||
let defaultOpt = { delay: 50 }
|
||||
|
||||
// #ifdef APP
|
||||
const isIOS = uni.getSystemInfoSync().platform == 'ios'
|
||||
defaultOpt.mode = isIOS ? 'loseFocus' : 'setInputMode' // iOS使用setInputMode无效
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
defaultOpt.mode = 'setInputMode'
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
defaultOpt.mode = 'loseFocus'
|
||||
// #endif
|
||||
|
||||
const opt = Object.assign(defaultOpt, options)
|
||||
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 通过 uni.hideKeyboard() 隐藏键盘,但是会导致键盘闪烁
|
||||
// 微信小程序好像无法正常隐藏键盘
|
||||
if (opt.mode == 'hideKeyboard') {
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
uni.hideKeyboard()
|
||||
}, opt.delay)
|
||||
}
|
||||
|
||||
// 通过控制编辑器失焦来隐藏键盘,但是会导致键盘闪烁
|
||||
// 只推荐微信小程序使用(也是无可奈何)
|
||||
if (opt.mode == 'loseFocus') {
|
||||
callback()
|
||||
editorCtx.blur()
|
||||
}
|
||||
|
||||
// 通过控制编辑器读写模式进行屏蔽焦点,虽然隐藏了键盘,但是也失焦了
|
||||
// 微信小程序中当只读时是无法使用api去修改内容的
|
||||
if (opt.mode == 'setReadOnly') {
|
||||
store.actions.setReadOnly(true)
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
store.actions.setReadOnly(false)
|
||||
}, opt.delay)
|
||||
}
|
||||
|
||||
// 使用renderjs给ql-editor节点设置inputmode属性来控制键盘是否弹出
|
||||
// 设置none时将会阻止键盘弹出,设置remove将会恢复,完美适配H5、App(Android),但是不支持App(iOS)和微信小程序
|
||||
if (opt.mode == 'setInputMode') {
|
||||
// #ifdef APP || H5
|
||||
// 以下严格处理异步与延时操作,缺一不可
|
||||
editorCtx.changeInputMode('none')
|
||||
setTimeout(() => {
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
editorCtx.changeInputMode('remove')
|
||||
}, opt.delay)
|
||||
}, opt.delay)
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 版权信息
|
||||
*/
|
||||
import packageConfig from '../../package.json'
|
||||
export function copyrightPrint() {
|
||||
/* 标题样式 */
|
||||
const styleTitle1 = `font-size:16px;font-weight:700;color:#ff4500;`
|
||||
const styleTitle2 = `font-style:oblique;font-size:14px;color:#fb7299;`
|
||||
const styleContent = `color:#66ccff;`
|
||||
/* 版权信息 */
|
||||
const title1 = ` 📝 sv-editor v${packageConfig.version} `
|
||||
const title2 = 'by Sonve'
|
||||
const content = `
|
||||
版权声明:
|
||||
1. 本插件免费开源,还望保留此版权声明在控制台输出
|
||||
2. 如需借鉴源码,还望注明出处
|
||||
3. 未经授权您不得以任何形式转载、售卖本插件,或以其他形式侵犯版权及附属权利
|
||||
4. 作者将保留对此插件版权信息的最终解释权
|
||||
🏠 地址: https://ext.dcloud.net.cn/plugin?id=21184
|
||||
😸 Gitee: https://gitee.com/Sonve/sv-editor
|
||||
💬 微信: s1051399604
|
||||
🐧 QQ群: ① 852637893 ② 816646292
|
||||
`
|
||||
console.log(`%c${title1}%c${title2}%c${content}`, styleTitle1, styleTitle2, styleContent)
|
||||
}
|
||||
|
||||
export function noAuthorization(name) {
|
||||
/* 标题样式 */
|
||||
const styleTitle1 = `font-size:16px;font-weight:700;color:#e6a23c;`
|
||||
const styleTitle2 = `font-style:oblique;font-size:14px;color:#fb7299;`
|
||||
const styleContent = `color:#f56c6c;`
|
||||
/* 授权信息 */
|
||||
const title1 = ` ⛔ sv-editor ${name} `
|
||||
const title2 = 'by Sonve'
|
||||
const content = `
|
||||
提示:您还未获取插件特殊扩展功能授权,可联系作者获取
|
||||
💬 微信: s1051399604 | 🐧 QQ群: ① 852637893 ② 816646292
|
||||
🏠 插件地址: https://ext.dcloud.net.cn/plugin?id=21184
|
||||
`
|
||||
console.log(`%c${title1}%c${title2}%c${content}`, styleTitle1, styleTitle2, styleContent)
|
||||
}
|
||||
Reference in New Issue
Block a user