1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-06-12 13:19:31 +08:00

v1.0.0-beta 源码正式开源

This commit is contained in:
小莫唐尼
2022-12-06 15:08:29 +08:00
commit 636ae7b169
461 changed files with 116817 additions and 0 deletions
@@ -0,0 +1,11 @@
// 以下项目可以删减或更换顺序,但不能添加或更改名字
export default {
// 普通标签的菜单项
node: ['大小', '斜体', '粗体', '下划线', '居中', '缩进', '上移', '下移', '删除'],
// 图片的菜单项
img: ['换图', '宽度', '超链接', '预览图', '禁用预览', '上移', '下移', '删除'],
// 链接的菜单项
link: ['更换链接', '上移', '下移', '删除'],
// 音视频的菜单项
media: ['封面', '循环', '自动播放', '上移', '下移', '删除']
}
@@ -0,0 +1,532 @@
/**
* @fileoverview editable 插件
*/
import config from './config'
import Parser from '../parser'
function Editable (vm) {
this.vm = vm
this.editHistory = [] // 历史记录
this.editI = -1 // 历史记录指针
vm._mask = [] // 蒙版被点击时进行的操作
vm._setData = function (path, val) {
const paths = path.split('.')
let target = vm
for (let i = 0; i < paths.length - 1; i++) {
target = target[paths[i]]
}
vm.$set(target, paths.pop(), val)
}
/**
* @description 移动历史记录指针
* @param {Number} num 移动距离
*/
const move = num => {
setTimeout(() => {
const item = this.editHistory[this.editI + num]
if (item) {
this.editI += num
vm._setData(item.key, item.value)
}
}, 200)
}
vm.undo = () => move(-1) // 撤销
vm.redo = () => move(1) // 重做
/**
* @description 更新记录
* @param {String} path 更新内容路径
* @param {*} oldVal 旧值
* @param {*} newVal 新值
* @param {Boolean} set 是否更新到视图
* @private
*/
vm._editVal = (path, oldVal, newVal, set) => {
// 当前指针后的内容去除
while (this.editI < this.editHistory.length - 1) {
this.editHistory.pop()
}
// 最多存储 30 条操作记录
while (this.editHistory.length > 30) {
this.editHistory.pop()
this.editI--
}
const last = this.editHistory[this.editHistory.length - 1]
if (!last || last.key !== path) {
if (last) {
// 去掉上一次的新值
this.editHistory.pop()
this.editI--
}
// 存入这一次的旧值
this.editHistory.push({
key: path,
value: oldVal
})
this.editI++
}
// 存入本次的新值
this.editHistory.push({
key: path,
value: newVal
})
this.editI++
// 更新到视图
if (set) {
vm._setData(path, newVal)
}
}
/**
* @description 获取菜单项
* @private
*/
vm._getItem = function (node, up, down) {
let items
let i
if (node.name === 'img') {
items = config.img.slice(0)
if (!vm.getSrc) {
i = items.indexOf('换图')
if (i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('超链接')
if (i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('预览图')
if (i !== -1) {
items.splice(i, 1)
}
}
i = items.indexOf('禁用预览')
if (i !== -1 && node.attrs.ignore) {
items[i] = '启用预览'
}
} else if (node.name === 'a') {
items = config.link.slice(0)
if (!vm.getSrc) {
i = items.indexOf('更换链接')
if (i !== -1) {
items.splice(i, 1)
}
}
} else if (node.name === 'video' || node.name === 'audio') {
items = config.media.slice(0)
i = items.indexOf('封面')
if (!vm.getSrc && i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('循环')
if (node.attrs.loop && i !== -1) {
items[i] = '不循环'
}
i = items.indexOf('自动播放')
if (node.attrs.autoplay && i !== -1) {
items[i] = '不自动播放'
}
} else {
items = config.node.slice(0)
}
if (!up) {
i = items.indexOf('上移')
if (i !== -1) {
items.splice(i, 1)
}
}
if (!down) {
i = items.indexOf('下移')
if (i !== -1) {
items.splice(i, 1)
}
}
return items
}
/**
* @description 显示 tooltip
* @param {object} obj
* @private
*/
vm._tooltip = function (obj) {
vm.$set(vm, 'tooltip', {
top: obj.top,
items: obj.items
})
vm._tooltipcb = obj.success
}
/**
* @description 显示滚动条
* @param {object} obj
* @private
*/
vm._slider = function (obj) {
vm.$set(vm, 'slider', {
min: obj.min,
max: obj.max,
value: obj.value,
top: obj.top
})
vm._slideringcb = obj.changing
vm._slidercb = obj.change
}
/**
* @description 点击蒙版
* @private
*/
vm._maskTap = function () {
// 隐藏所有悬浮窗
while (vm._mask.length) {
(vm._mask.pop())()
}
if (vm.tooltip) {
vm.$set(vm, 'tooltip', null)
}
if (vm.slider) {
vm.$set(vm, 'slider', null)
}
}
/**
* @description 插入节点
* @param {Object} node
*/
function insert (node) {
if (vm._edit) {
vm._edit.insert(node)
} else {
const nodes = vm.nodes.slice(0)
nodes.push(node)
vm._editVal('nodes', vm.nodes, nodes, true)
}
}
/**
* @description 在光标处插入指定 html 内容
* @param {String} html 内容
*/
vm.insertHtml = html => {
this.inserting = true
const arr = new Parser(vm).parse(html)
this.inserting = undefined
for (let i = 0; i < arr.length; i++) {
insert(arr[i])
}
}
/**
* @description 在光标处插入图片
*/
vm.insertImg = function () {
vm.getSrc && vm.getSrc('img').then(src => {
if (typeof src === 'string') {
src = [src]
}
const parser = new Parser(vm)
for (let i = 0; i < src.length; i++) {
insert({
name: 'img',
attrs: {
src: parser.getUrl(src[i])
}
})
}
}).catch(() => { })
}
/**
* @description 在光标处插入一个链接
*/
vm.insertLink = function () {
vm.getSrc && vm.getSrc('link').then(url => {
insert({
name: 'a',
attrs: {
href: url
},
children: [{
type: 'text',
text: url
}]
})
}).catch(() => { })
}
/**
* @description 在光标处插入一个表格
* @param {Number} rows 行数
* @param {Number} cols 列数
*/
vm.insertTable = function (rows, cols) {
const table = {
name: 'table',
attrs: {
style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
},
children: []
}
for (let i = 0; i < rows; i++) {
const tr = {
name: 'tr',
attrs: {},
children: []
}
for (let j = 0; j < cols; j++) {
tr.children.push({
name: 'td',
attrs: {
style: 'padding:2px;border:1px solid gray'
},
children: [{
type: 'text',
text: ''
}]
})
}
table.children.push(tr)
}
insert(table)
}
/**
* @description 插入视频/音频
* @param {Object} node
*/
function insertMedia (node) {
if (typeof node.src === 'string') {
node.src = [node.src]
}
const parser = new Parser(vm)
// 拼接主域名
for (let i = 0; i < node.src.length; i++) {
node.src[i] = parser.getUrl(node.src[i])
}
insert({
name: 'div',
attrs: {
style: 'text-align:center'
},
children: [node]
})
}
/**
* @description 在光标处插入一个视频
*/
vm.insertVideo = function () {
vm.getSrc && vm.getSrc('video').then(src => {
insertMedia({
name: 'video',
attrs: {
controls: 'T'
},
children: [],
src,
// #ifdef APP-PLUS
html: `<video src="${src}" style="width:100%;height:100%"></video>`
// #endif
})
}).catch(() => { })
}
/**
* @description 在光标处插入一个音频
*/
vm.insertAudio = function () {
vm.getSrc && vm.getSrc('audio').then(attrs => {
let src
if (attrs.src) {
src = attrs.src
attrs.src = undefined
} else {
src = attrs
attrs = {}
}
attrs.controls = 'T'
insertMedia({
name: 'audio',
attrs,
children: [],
src
})
}).catch(() => { })
}
/**
* @description 在光标处插入一段文本
*/
vm.insertText = function () {
insert({
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
})
}
/**
* @description 清空内容
*/
vm.clear = function () {
vm._maskTap()
vm._edit = undefined
vm.$set(vm, 'nodes', [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}])
}
/**
* @description 获取编辑后的 html
*/
vm.getContent = function () {
let html = '';
// 递归遍历获取
(function traversal (nodes, table) {
for (let i = 0; i < nodes.length; i++) {
let item = nodes[i]
if (item.type === 'text') {
html += item.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>').replace(/\xa0/g, '&nbsp;') // 编码实体
} else {
if (item.name === 'img') {
item.attrs.i = ''
// 还原被转换的 svg
if ((item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"')
continue
}
} else if (item.name === 'video' || item.name === 'audio') {
// 还原 video 和 audio 的 source
item = JSON.parse(JSON.stringify(item))
if (item.src.length > 1) {
item.children = []
for (let j = 0; j < item.src.length; j++) {
item.children.push({
name: 'source',
attrs: {
src: item.src[j]
}
})
}
} else {
item.attrs.src = item.src[0]
}
} else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') {
// 还原滚动层
item = item.children[0]
}
// 还原 table
if (item.name === 'table') {
item = JSON.parse(JSON.stringify(item))
table = item.attrs
if ((item.attrs.style || '').includes('display:grid')) {
item.attrs.style = item.attrs.style.split('display:grid')[0]
const children = [{
name: 'tr',
attrs: {},
children: []
}]
for (let j = 0; j < item.children.length; j++) {
item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '')
if (item.children[j].r !== children.length) {
children.push({
name: 'tr',
attrs: {},
children: [item.children[j]]
})
} else {
children[children.length - 1].children.push(item.children[j])
}
}
item.children = children
}
}
html += '<' + item.name
for (const attr in item.attrs) {
let val = item.attrs[attr]
if (!val) continue
if (val === 'T' || val === true) {
// bool 型省略值
html += ' ' + attr
continue
} else if (item.name[0] === 't' && attr === 'style' && table) {
// 取消为了显示 table 添加的 style
val = val.replace(/;*display:table[^;]*/, '')
if (table.border) {
val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '')
}
if (table.cellpadding) {
val = val.replace(/padding[^;]+;*/g, '')
}
if (!val) continue
}
html += ' ' + attr + '="' + val.replace(/"/g, '&quot;') + '"'
}
html += '>'
if (item.children) {
traversal(item.children, table)
html += '</' + item.name + '>'
}
}
}
})(vm.nodes)
// 其他插件处理
for (let i = vm.plugins.length; i--;) {
if (vm.plugins[i].onGetContent) {
html = vm.plugins[i].onGetContent(html) || html
}
}
return html
}
}
Editable.prototype.onUpdate = function (content, config) {
if (this.vm.editable) {
this.vm._maskTap()
config.entities.amp = '&'
if (!this.inserting) {
this.vm._edit = undefined
if (!content) {
setTimeout(() => {
this.vm.$set(this.vm, 'nodes', [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}])
}, 0)
}
}
}
}
Editable.prototype.onParse = function (node) {
// 空白单元格可编辑
if (this.vm.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
node.children.push({
type: 'text',
text: ''
})
}
}
export default Editable
@@ -0,0 +1,203 @@
/**
* @fileoverview emoji 插件
*/
const reg = /\[(\S+?)\]/g
const data = {
笑脸: '😄',
生病: '😷',
破涕为笑: '😂',
吐舌: '😝',
脸红: '😳',
恐惧: '😱',
失望: '😔',
无语: '😒',
眨眼: '😉',
: '😎',
: '😭',
痴迷: '😍',
: '😘',
思考: '🤔',
困惑: '😕',
颠倒: '🙃',
: '🤑',
惊讶: '😲',
白眼: '🙄',
叹气: '😤',
睡觉: '😴',
书呆子: '🤓',
愤怒: '😡',
面无表情: '😑',
张嘴: '😮',
量体温: '🤒',
呕吐: '🤮',
光环: '😇',
幽灵: '👻',
外星人: '👽',
机器人: '🤖',
捂眼镜: '🙈',
捂耳朵: '🙉',
捂嘴: '🙊',
婴儿: '👶',
男孩: '👦',
女孩: '👧',
男人: '👨',
女人: '👩',
老人: '👴',
老妇人: '👵',
警察: '👮',
王子: '🤴',
公主: '🤴',
举手: '🙋',
跑步: '🏃',
家庭: '👪',
眼睛: '👀',
鼻子: '👃',
耳朵: '👂',
舌头: '👅',
: '👄',
: '❤️',
心碎: '💔',
雪人: '☃️',
情书: '💌',
大便: '💩',
闹钟: '⏰',
眼镜: '👓',
雨伞: '☂️',
音乐: '🎵',
话筒: '🎤',
游戏机: '🎮',
喇叭: '📢',
耳机: '🎧',
礼物: '🎁',
电话: '📞',
电脑: '💻',
打印机: '🖨️',
手电筒: '🔦',
灯泡: '💡',
书本: '📖',
信封: '✉️',
药丸: '💊',
口红: '💄',
手机: '📱',
相机: '📷',
电视: '📺',
: '🀄',
垃圾桶: '🚮',
厕所: '🚾',
感叹号: '❗',
: '🈲',
: '🉑',
彩虹: '🌈',
旋风: '🌀',
雷电: '⚡',
雪花: '❄️',
星星: '⭐',
水滴: '💧',
玫瑰: '🌹',
加油: '💪',
: '👈',
: '👉',
: '👆',
: '👇',
手掌: '🖐️',
好的: '👌',
: '👍',
: '👎',
胜利: '✌',
拳头: '👊',
挥手: '👋',
鼓掌: '👏',
猴子: '🐒',
: '🐶',
: '🐺',
: '🐱',
老虎: '🐯',
: '🐎',
独角兽: '🦄',
斑马: '🦓',
鹿: '🦌',
: '🐮',
: '🐷',
: '🐏',
长颈鹿: '🦒',
大象: '🐘',
老鼠: '🐭',
蝙蝠: '🦇',
刺猬: '🦔',
熊猫: '🐼',
鸽子: '🕊️',
鸭子: '🦆',
兔子: '🐇',
老鹰: '🦅',
青蛙: '🐸',
: '🐍',
: '🐉',
鲸鱼: '🐳',
海豚: '🐬',
足球: '⚽',
棒球: '⚾',
篮球: '🏀',
排球: '🏐',
橄榄球: '🏉',
网球: '🎾',
骰子: '🎲',
鸡腿: '🍗',
蛋糕: '🎂',
啤酒: '🍺',
饺子: '🥟',
汉堡: '🍔',
薯条: '🍟',
意大利面: '🍝',
干杯: '🥂',
筷子: '🥢',
糖果: '🍬',
奶瓶: '🍼',
爆米花: '🍿',
邮局: '🏤',
医院: '🏥',
银行: '🏦',
酒店: '🏨',
学校: '🏫',
城堡: '🏰',
火车: '🚂',
高铁: '🚄',
地铁: '🚇',
公交: '🚌',
救护车: '🚑',
消防车: '🚒',
警车: '🚓',
出租车: '🚕',
汽车: '🚗',
货车: '🚛',
自行车: '🚲',
摩托: '🛵',
红绿灯: '🚥',
帆船: '⛵',
游轮: '🛳️',
轮船: '⛴️',
飞机: '✈️',
直升机: '🚁',
缆车: '🚠',
警告: '⚠️',
禁止: '⛔'
}
function Emoji () {
}
Emoji.prototype.onUpdate = function (content) {
return content.replace(reg, ($, $1) => {
if (data[$1]) return data[$1]
return $
})
}
Emoji.prototype.onGetContent = function (content) {
for (const item in data) {
content = content.replace(new RegExp(data[item], 'g'), '[' + item + ']')
}
return content
}
export default Emoji
@@ -0,0 +1,5 @@
export default {
copyByLongPress: true, // 是否需要长按代码块时显示复制代码内容菜单
showLanguageName: true, // 是否在代码块右上角显示语言的名称
showLineNumber: true // 是否显示行号
}
@@ -0,0 +1,96 @@
/**
* @fileoverview highlight 插件
* Include prismjs (https://prismjs.com)
*/
import prism from './prism.min'
import config from './config'
import Parser from '../parser'
function Highlight (vm) {
this.vm = vm
}
Highlight.prototype.onParse = function (node, vm) {
if (node.name === 'pre') {
if (vm.options.editable) {
node.attrs.class = (node.attrs.class || '') + ' hl-pre'
return
}
let i
for (i = node.children.length; i--;) {
if (node.children[i].name === 'code') break
}
if (i === -1) return
const code = node.children[i]
let className = code.attrs.class + ' ' + node.attrs.class
i = className.indexOf('language-')
if (i === -1) {
i = className.indexOf('lang-')
if (i === -1) {
className = 'language-text'
i = 9
} else {
i += 5
}
} else {
i += 9
}
let j
for (j = i; j < className.length; j++) {
if (className[j] === ' ') break
}
const lang = className.substring(i, j)
if (code.children.length) {
const text = this.vm.getText(code.children).replace(/&amp;/g, '&')
if (!text) return
if (node.c) {
node.c = undefined
}
if (prism.languages[lang]) {
code.children = (new Parser(this.vm).parse(
// 加一层 pre 保留空白符
'<pre>' + prism.highlight(text, prism.languages[lang], lang).replace(/token /g, 'hl-') + '</pre>'))[0].children
}
node.attrs.class = 'hl-pre'
code.attrs.class = 'hl-code'
if (config.showLanguageName) {
node.children.push({
name: 'div',
attrs: {
class: 'hl-language',
style: 'user-select:none'
},
children: [{
type: 'text',
text: lang
}]
})
}
if (config.copyByLongPress) {
node.attrs.style += (node.attrs.style || '') + ';user-select:none'
node.attrs['data-content'] = text
vm.expose()
}
if (config.showLineNumber) {
const line = text.split('\n').length; const children = []
for (let k = line; k--;) {
children.push({
name: 'span',
attrs: {
class: 'span'
}
})
}
node.children.push({
name: 'span',
attrs: {
class: 'line-numbers-rows'
},
children
})
}
}
}
}
export default Highlight
File diff suppressed because one or more lines are too long
@@ -0,0 +1,138 @@
const data = {
name: 'imgcache',
prefix: 'imgcache_'
}
function ImgCache (vm) {
this.vm = vm // 保存实例在其他周期使用
this.i = 0 // 用于标记第几张图
vm.imgCache = {
get list () {
return uni
.getStorageInfoSync()
.keys.filter((key) => key.startsWith(data.prefix))
.map((key) => key.split(data.prefix)[1])
},
get (url) {
return uni.getStorageSync(data.prefix + url)
},
delete (url) {
const path = uni.getStorageSync(data.prefix + url)
if (!path) return false
plus.io.resolveLocalFileSystemURL(path, (entry) => {
entry.remove()
})
uni.removeStorageSync(data.prefix + url)
return true
},
async add (url) {
const filename = await download(url)
if (filename) {
uni.setStorageSync(data.prefix + url, filename)
return 'file://' + plus.io.convertLocalFileSystemURL(filename)
}
return null
},
clear () {
uni
.getStorageInfoSync()
.keys.filter((key) => key.startsWith(data.prefix))
.forEach((key) => {
uni.removeStorageSync(key)
})
plus.io.resolveLocalFileSystemURL(`_doc/${data.name}/`, (entry) => {
entry.removeRecursively(
(entry) => {
console.log(`${data.name}缓存删除成功`, entry)
},
(e) => {
console.log(`${data.name}缓存删除失败`, e)
}
)
})
}
}
}
// #ifdef APP-PLUS
ImgCache.prototype.onParse = function (node, parser) {
// 启用本插件 && 解析图片标签 && 拥有src属性 && 是网络图片
if (
this.vm.ImgCache &&
node.name === 'img' &&
node.attrs.src &&
/^https?:\/\//.test(node.attrs.src)
) {
const src = node.attrs.src
node.attrs.src = ''
node.attrs.i = this.vm.imgList.length + this.i++
parser.expose()
async function getUrl (path) {
if (await resolveFile(path)) return path
const filename = await download(src)
filename && uni.setStorageSync(data.prefix + src, filename)
return filename
}
uni.getStorage({
key: data.prefix + src,
success: async (res) => {
const path = await getUrl(res.data)
const url = path
? 'file://' + plus.io.convertLocalFileSystemURL(path)
: src
node.attrs.src = url
this.vm.imgList[node.attrs.i] = path || src
},
fail: async () => {
const path = await getUrl()
const url = path
? 'file://' + plus.io.convertLocalFileSystemURL(path)
: src
node.attrs.src = url
this.vm.imgList[node.attrs.i] = path || src
}
})
}
}
const taskQueue = new Set()
function download (url) {
return new Promise((resolve) => {
if (taskQueue.has(url)) return
taskQueue.add(url)
const suffix = /.+\.(jpg|jpeg|png|bmp|gif|webp)/.exec(url)
const name = `${makeid(8)}_${Date.now()}${suffix ? '.' + suffix[1] : ''}`
const task = plus.downloader.createDownload(
url,
{ filename: `_doc/${data.name}/${name}` },
(download, status) => {
taskQueue.delete(url)
resolve(status === 200 ? download.filename : null)
}
)
task.start()
})
}
// 判断文件存在
function resolveFile (url) {
return new Promise((resolve) => {
plus.io.resolveLocalFileSystemURL(url, resolve, () => resolve(null))
})
}
// 生成uuid
function makeid (length) {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length))
}
return result
}
// #endif
export default ImgCache
@@ -0,0 +1,34 @@
/**
* @fileoverview markdown 插件
* Include marked (https://github.com/markedjs/marked)
* Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css)
*/
import marked from './marked.min'
let index = 0
function Markdown (vm) {
this.vm = vm
vm._ids = {}
}
Markdown.prototype.onUpdate = function (content) {
if (this.vm.markdown) {
return marked(content)
}
}
Markdown.prototype.onParse = function (node, vm) {
if (vm.options.markdown) {
// 中文 id 需要转换,否则无法跳转
if (vm.options.useAnchor && node.attrs && /[\u4e00-\u9fa5]/.test(node.attrs.id)) {
const id = 't' + index++
this.vm._ids[node.attrs.id] = id
node.attrs.id = id
}
if (node.name === 'p' || node.name === 'table' || node.name === 'tr' || node.name === 'th' || node.name === 'td' || node.name === 'blockquote' || node.name === 'pre' || node.name === 'code') {
node.attrs.class = `md-${node.name} ${node.attrs.class || ''}`
}
}
}
export default Markdown
File diff suppressed because one or more lines are too long
@@ -0,0 +1,579 @@
<template>
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="(editable?'min-height:200px;':'')+containerStyle" @tap="_containTap">
<slot v-if="!nodes[0]" />
<!-- #ifndef APP-PLUS-NVUE -->
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable,editable,placeholder,'nodes']" name="span" />
<!-- #endif -->
<!-- #ifdef APP-PLUS-NVUE -->
<web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
<!-- #endif -->
<view v-if="tooltip" class="_tooltip_contain" :style="'top:'+tooltip.top+'px'">
<view class="_tooltip">
<view v-for="(item, index) in tooltip.items" v-bind:key="index" class="_tooltip_item" :data-i="index" @tap="_tooltipTap">{{item}}</view>
</view>
</view>
<view v-if="slider" class="_slider" :style="'top:'+slider.top+'px'">
<slider :value="slider.value" :min="slider.min" :max="slider.max" handle-size="14" block-size="14" show-value activeColor="white" style="padding:3px" @changing="_sliderChanging" @change="_sliderChange" />
</view>
</view>
</template>
<script>
/**
* mp-html v2.4.0
* @description 富文本组件
* @tutorial https://github.com/jin-yufeng/mp-html
* @property {String} container-style 容器的样式
* @property {String} content 用于渲染的 html 字符串
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
* @property {String} domain 主域名,用于拼接链接
* @property {String} error-img 图片出错时的占位图链接
* @property {Boolean} lazy-load 是否开启图片懒加载
* @property {string} loading-img 图片加载过程中的占位图链接
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
* @property {Boolean | String} selectable 是否开启长按复制
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
* @property {Object} tag-style 标签的默认样式
* @property {Boolean | Number} use-anchor 是否使用锚点链接
* @event {Function} load dom 结构加载完毕时触发
* @event {Function} ready 所有图片加载完毕时触发
* @event {Function} imgtap 图片被点击时触发
* @event {Function} linktap 链接被点击时触发
* @event {Function} play 音视频播放时触发
* @event {Function} error 媒体加载出错时触发
*/
// #ifndef APP-PLUS-NVUE
import node from './node/node'
// #endif
import Parser from './parser'
import markdown from './markdown/index.js'
import emoji from './emoji/index.js'
import highlight from './highlight/index.js'
import style from './style/index.js'
import imgCache from './img-cache/index.js'
import editable from './editable/index.js'
const plugins=[markdown,emoji,highlight,style,imgCache,editable,]
// #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom')
// #endif
export default {
name: 'mp-html',
data() {
return {
tooltip: null,
slider: null,
nodes: [],
// #ifdef APP-PLUS-NVUE
height: 3
// #endif
}
},
props: {
editable: Boolean,
placeholder: String,
ImgCache: Boolean,
markdown: Boolean,
containerStyle: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
copyLink: {
type: [Boolean, String],
default: true
},
domain: String,
errorImg: {
type: String,
default: ''
},
lazyLoad: {
type: [Boolean, String],
default: false
},
loadingImg: {
type: String,
default: ''
},
pauseVideo: {
type: [Boolean, String],
default: true
},
previewImg: {
type: [Boolean, String],
default: true
},
scrollTable: [Boolean, String],
selectable: [Boolean, String],
setTitle: {
type: [Boolean, String],
default: true
},
showImgMenu: {
type: [Boolean, String],
default: true
},
tagStyle: Object,
useAnchor: [Boolean, Number]
},
// #ifdef VUE3
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
// #endif
// #ifndef APP-PLUS-NVUE
components: {
node
},
// #endif
watch: {
editable(val) {
this.setContent(val ? this.content : this.getContent())
if (!val)
this._maskTap()
},
content (content) {
this.setContent(content)
}
},
created () {
this.plugins = []
for (let i = plugins.length; i--;) {
this.plugins.push(new plugins[i](this))
}
},
mounted () {
if ((this.content || this.editable) && !this.nodes.length) {
this.setContent(this.content)
}
},
beforeDestroy () {
this._hook('onDetached')
},
methods: {
_containTap() {
if (!this._lock && !this.slider) {
this._edit = undefined
this._maskTap()
}
},
_tooltipTap(e) {
this._tooltipcb(e.currentTarget.dataset.i)
this.$set(this, 'tooltip', null)
},
_sliderChanging(e) {
this._slideringcb(e.detail.value)
},
_sliderChange(e) {
this._slidercb(e.detail.value)
},
/**
* @description 将锚点跳转的范围限定在一个 scroll-view 内
* @param {Object} page scroll-view 所在页面的示例
* @param {String} selector scroll-view 的选择器
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
*/
in (page, selector, scrollTop) {
// #ifndef APP-PLUS-NVUE
if (page && selector && scrollTop) {
this._in = {
page,
selector,
scrollTop
}
}
// #endif
},
/**
* @description 锚点跳转
* @param {String} id 要跳转的锚点 id
* @param {Number} offset 跳转位置的偏移量
* @returns {Promise}
*/
navigateTo (id, offset) {
id = this._ids[decodeURI(id)] || id
return new Promise((resolve, reject) => {
if (!this.useAnchor) {
reject(Error('Anchor is disabled'))
return
}
offset = offset || parseInt(this.useAnchor) || 0
// #ifdef APP-PLUS-NVUE
if (!id) {
dom.scrollToElement(this.$refs.web, {
offset
})
resolve()
} else {
this._navigateTo = {
resolve,
reject,
offset
}
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
}
// #endif
// #ifndef APP-PLUS-NVUE
let deep = ' '
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
deep = '>>>'
// #endif
const selector = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this._in ? this._in.page : this)
// #endif
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
if (this._in) {
selector.select(this._in.selector).scrollOffset()
.select(this._in.selector).boundingClientRect()
} else {
// 获取 scroll-view 的位置和滚动距离
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
}
selector.exec(res => {
if (!res[0]) {
reject(Error('Label not found'))
return
}
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
if (this._in) {
// scroll-view 跳转
this._in.page[this._in.scrollTop] = scrollTop
} else {
// 页面跳转
uni.pageScrollTo({
scrollTop,
duration: 300
})
}
resolve()
})
// #endif
})
},
/**
* @description 获取文本内容
* @return {String}
*/
getText (nodes) {
let text = '';
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === 'text') {
text += node.text.replace(/&amp;/g, '&')
} else if (node.name === 'br') {
text += '\n'
} else {
// 块级标签前后加换行
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
if (isBlock && text && text[text.length - 1] !== '\n') {
text += '\n'
}
// 递归获取子节点的文本
if (node.children) {
traversal(node.children)
}
if (isBlock && text[text.length - 1] !== '\n') {
text += '\n'
} else if (node.name === 'td' || node.name === 'th') {
text += '\t'
}
}
}
})(nodes || this.nodes)
return text
},
/**
* @description 获取内容大小和位置
* @return {Promise}
*/
getRect () {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
})
},
/**
* @description 暂停播放媒体
*/
pauseMedia () {
for (let i = (this._videos || []).length; i--;) {
this._videos[i].pause()
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置媒体播放速率
* @param {Number} rate 播放速率
*/
setPlaybackRate (rate) {
this.playbackRate = rate
for (let i = (this._videos || []).length; i--;) {
this._videos[i].playbackRate(rate)
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置内容
* @param {String} content html 内容
* @param {Boolean} append 是否在尾部追加
*/
setContent (content, append) {
if (!append || !this.imgList) {
this.imgList = []
}
const nodes = new Parser(this).parse(content)
// #ifdef APP-PLUS-NVUE
if (this._ready) {
this._set(nodes, append)
}
// #endif
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
// #ifndef APP-PLUS-NVUE
this._videos = []
this.$nextTick(() => {
this._hook('onLoad')
this.$emit('load')
})
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
let height
const callback = rect => {
// 350ms 总高度无变化就触发 ready 事件
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback)
}, 350)
}
}
this.getRect().then(callback)
} else {
// 未设置懒加载,等待所有图片加载完毕
if (!this.imgList._unloadimgs) {
this.getRect(rect => {
this.$emit('ready', rect)
})
}
}
// #endif
},
/**
* @description 调用插件钩子函数
*/
_hook (name) {
for (let i = plugins.length; i--;) {
if (this.plugins[i][name]) {
this.plugins[i][name]()
}
}
},
// #ifdef APP-PLUS-NVUE
/**
* @description 设置内容
*/
_set (nodes, append) {
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes) + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
},
/**
* @description 接收到 web-view 消息
*/
_onMessage (e) {
const message = e.detail.data[0]
switch (message.action) {
// web-view 初始化完毕
case 'onJSBridgeReady':
this._ready = true
if (this.nodes) {
this._set(this.nodes)
}
break
// 内容 dom 加载完毕
case 'onLoad':
this.height = message.height
this._hook('onLoad')
this.$emit('load')
break
// 所有图片加载完毕
case 'onReady':
this.getRect().then(res => {
this.$emit('ready', res)
}).catch(() => { })
break
// 总高度发生变化
case 'onHeightChange':
this.height = message.height
break
// 图片点击
case 'onImgTap':
this.$emit('imgtap', message.attrs)
if (this.previewImg) {
uni.previewImage({
current: parseInt(message.attrs.i),
urls: this.imgList
})
}
break
// 链接点击
case 'onLinkTap': {
const href = message.attrs.href
this.$emit('linktap', message.attrs)
if (href) {
// 锚点跳转
if (href[0] === '#') {
if (this.useAnchor) {
dom.scrollToElement(this.$refs.web, {
offset: message.offset
})
}
} else if (href.includes('://')) {
// 打开外链
if (this.copyLink) {
plus.runtime.openWeb(href)
}
} else {
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href
})
}
})
}
}
break
}
case 'onPlay':
this.$emit('play')
break
// 获取到锚点的偏移量
case 'getOffset':
if (typeof message.offset === 'number') {
dom.scrollToElement(this.$refs.web, {
offset: message.offset + this._navigateTo.offset
})
this._navigateTo.resolve()
} else {
this._navigateTo.reject(Error('Label not found'))
}
break
// 点击
case 'onClick':
this.$emit('tap')
this.$emit('click')
break
// 出错
case 'onError':
this.$emit('error', {
source: message.source,
attrs: message.attrs
})
}
}
// #endif
}
}
</script>
<style>
/* #ifndef APP-PLUS-NVUE */
/* 根节点样式 */
._root {
padding: 1px 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
/* 长按复制 */
._select {
user-select: text;
}
/* #endif */
/* 提示条 */
._tooltip_contain {
position: absolute;
right: 20px;
left: 20px;
text-align: center;
}
._tooltip {
box-sizing: border-box;
display: inline-block;
width: auto;
max-width: 100%;
height: 30px;
padding: 0 3px;
overflow: scroll;
font-size: 14px;
line-height: 30px;
white-space: nowrap;
}
._tooltip_item {
display: inline-block;
width: auto;
padding: 0 2vw;
line-height: 30px;
background-color: black;
color: white;
}
/* 图片宽度滚动条 */
._slider {
position: absolute;
left: 20px;
width: 220px;
}
._tooltip,
._slider {
background-color: black;
border-radius: 3px;
opacity: 0.75;
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,129 @@
/**
* @fileoverview style 插件
*/
// #ifndef APP-PLUS-NVUE
import Parser from './parser'
// #endif
function Style () {
this.styles = []
}
// #ifndef APP-PLUS-NVUE
Style.prototype.onParse = function (node, vm) {
// 获取样式
if (node.name === 'style' && node.children.length && node.children[0].type === 'text') {
this.styles = this.styles.concat(new Parser().parse(node.children[0].text))
} else if (node.name) {
// 匹配样式(对非文本标签)
// 存储不同优先级的样式 name < class < id < 后代
let matched = ['', '', '', '']
for (let i = 0, len = this.styles.length; i < len; i++) {
const item = this.styles[i]
let res = match(node, item.key || item.list[item.list.length - 1])
let j
if (res) {
// 后代选择器
if (!item.key) {
j = item.list.length - 2
for (let k = vm.stack.length; j >= 0 && k--;) {
// 子选择器
if (item.list[j] === '>') {
// 错误情况
if (j < 1 || j > item.list.length - 2) break
if (match(vm.stack[k], item.list[j - 1])) {
j -= 2
} else {
j++
}
} else if (match(vm.stack[k], item.list[j])) {
j--
}
}
res = 4
}
if (item.key || j < 0) {
// 添加伪类
if (item.pseudo && node.children) {
let text
item.style = item.style.replace(/content:([^;]+)/, (_, $1) => {
text = $1.replace(/['"]/g, '')
// 处理 attr 函数
.replace(/attr\((.+?)\)/, (_, $1) => node.attrs[$1.trim()] || '')
// 编码 \xxx
.replace(/\\(\w{4})/, (_, $1) => String.fromCharCode(parseInt($1, 16)))
return ''
})
const pseudo = {
name: 'span',
attrs: {
style: item.style
},
children: [{
type: 'text',
text
}]
}
if (item.pseudo === 'before') {
node.children.unshift(pseudo)
} else {
node.children.push(pseudo)
}
} else {
matched[res - 1] += item.style + (item.style[item.style.length - 1] === ';' ? '' : ';')
}
}
}
}
matched = matched.join('')
if (matched.length > 2) {
node.attrs.style = matched + (node.attrs.style || '')
}
}
}
/**
* @description 匹配样式
* @param {object} node 要匹配的标签
* @param {string|string[]} keys 选择器
* @returns {number} 0:不匹配;1name 匹配;2class 匹配;3id 匹配
*/
function match (node, keys) {
function matchItem (key) {
if (key[0] === '#') {
// 匹配 id
if (node.attrs.id && node.attrs.id.trim() === key.substr(1)) return 3
} else if (key[0] === '.') {
// 匹配 class
key = key.substr(1)
const selectors = (node.attrs.class || '').split(' ')
for (let i = 0; i < selectors.length; i++) {
if (selectors[i].trim() === key) return 2
}
} else if (node.name === key) {
// 匹配 name
return 1
}
return 0
}
// 多选择器交集
if (keys instanceof Array) {
let res = 0
for (let j = 0; j < keys.length; j++) {
const tmp = matchItem(keys[j])
// 任意一个不匹配就失败
if (!tmp) return 0
// 优先级最大的一个作为最终优先级
if (tmp > res) {
res = tmp
}
}
return res
}
return matchItem(keys)
}
// #endif
export default Style
@@ -0,0 +1,175 @@
const blank = {
' ': true,
'\n': true,
'\t': true,
'\r': true,
'\f': true
}
function Parser () {
this.styles = []
this.selectors = []
}
/**
* @description 解析 css 字符串
* @param {string} content css 内容
*/
Parser.prototype.parse = function (content) {
new Lexer(this).parse(content)
return this.styles
}
/**
* @description 解析到一个选择器
* @param {string} name 名称
*/
Parser.prototype.onSelector = function (name) {
// 不支持的选择器
if (name.includes('[') || name.includes('*') || name.includes('@')) return
const selector = {}
// 伪类
if (name.includes(':')) {
const info = name.split(':')
const pseudo = info.pop()
if (pseudo === 'before' || pseudo === 'after') {
selector.pseudo = pseudo
name = info[0]
} else return
}
// 分割交集选择器
function splitItem (str) {
const arr = []
let i, start
for (i = 1, start = 0; i < str.length; i++) {
if (str[i] === '.' || str[i] === '#') {
arr.push(str.substring(start, i))
start = i
}
}
if (!arr.length) {
return str
} else {
arr.push(str.substring(start, i))
return arr
}
}
// 后代选择器
if (name.includes(' ')) {
selector.list = []
const list = name.split(' ')
for (let i = 0; i < list.length; i++) {
if (list[i].length) {
// 拆分子选择器
const arr = list[i].split('>')
for (let j = 0; j < arr.length; j++) {
selector.list.push(splitItem(arr[j]))
if (j < arr.length - 1) {
selector.list.push('>')
}
}
}
}
} else {
selector.key = splitItem(name)
}
this.selectors.push(selector)
}
/**
* @description 解析到选择器内容
* @param {string} content 内容
*/
Parser.prototype.onContent = function (content) {
// 并集选择器
for (let i = 0; i < this.selectors.length; i++) {
this.selectors[i].style = content
}
this.styles = this.styles.concat(this.selectors)
this.selectors = []
}
/**
* @description css 词法分析器
* @param {object} handler 高层处理器
*/
function Lexer (handler) {
this.selector = ''
this.style = ''
this.handler = handler
}
Lexer.prototype.parse = function (content) {
this.i = 0
this.content = content
this.state = this.blank
for (let len = content.length; this.i < len; this.i++) {
this.state(content[this.i])
}
}
Lexer.prototype.comment = function () {
this.i = this.content.indexOf('*/', this.i) + 1
if (!this.i) {
this.i = this.content.length
}
}
Lexer.prototype.blank = function (c) {
if (!blank[c]) {
if (c === '/' && this.content[this.i + 1] === '*') {
this.comment()
return
}
this.selector += c
this.state = this.name
}
}
Lexer.prototype.name = function (c) {
if (c === '/' && this.content[this.i + 1] === '*') {
this.comment()
return
}
if (c === '{' || c === ',' || c === ';') {
this.handler.onSelector(this.selector.trimEnd())
this.selector = ''
if (c !== '{') {
while (blank[this.content[++this.i]]);
}
if (this.content[this.i] === '{') {
this.floor = 1
this.state = this.val
} else {
this.selector += this.content[this.i]
}
} else if (blank[c]) {
this.selector += ' '
} else {
this.selector += c
}
}
Lexer.prototype.val = function (c) {
if (c === '/' && this.content[this.i + 1] === '*') {
this.comment()
return
}
if (c === '{') {
this.floor++
} else if (c === '}') {
this.floor--
if (!this.floor) {
this.handler.onContent(this.style)
this.style = ''
this.state = this.blank
return
}
}
this.style += c
}
export default Parser
@@ -0,0 +1 @@
"use strict";function t(t){for(var e=Object.create(null),n=t.attributes.length;n--;)e[t.attributes[n].name]=t.attributes[n].value;return e}function e(){a[1]&&(this.src=a[1],this.onerror=null),this.onclick=null,this.ontouchstart=null,uni.postMessage({data:{action:"onError",source:"img",attrs:t(this)}})}function n(){window.unloadimgs-=1,0===window.unloadimgs&&uni.postMessage({data:{action:"onReady"}})}function o(r,s,c){for(var d=0;d<r.length;d++)!function(d){var u=r[d],l=void 0;if(u.type&&"node"!==u.type)l=document.createTextNode(u.text.replace(/&amp;/g,"&"));else{var g=u.name;"svg"===g&&(c="http://www.w3.org/2000/svg"),"html"!==g&&"body"!==g||(g="div"),l=c?document.createElementNS(c,g):document.createElement(g);for(var p in u.attrs)l.setAttribute(p,u.attrs[p]);if(u.children&&o(u.children,l,c),"img"===g){if(window.unloadimgs+=1,l.onload=n,l.onerror=n,!l.src&&l.getAttribute("data-src")&&(l.src=l.getAttribute("data-src")),u.attrs.ignore||(l.onclick=function(e){e.stopPropagation(),uni.postMessage({data:{action:"onImgTap",attrs:t(this)}})}),a[2]){var h=new Image;h.src=l.src,l.src=a[2],h.onload=function(){l.src=this.src},h.onerror=function(){l.onerror()}}l.onerror=e}else if("a"===g)l.addEventListener("click",function(e){e.stopPropagation(),e.preventDefault();var n,o=this.getAttribute("href");o&&"#"===o[0]&&(n=(document.getElementById(o.substr(1))||{}).offsetTop),uni.postMessage({data:{action:"onLinkTap",attrs:t(this),offset:n}})},!0);else if("video"===g||"audio"===g)i.push(l),u.attrs.autoplay||u.attrs.controls||l.setAttribute("controls","true"),l.onplay=function(){if(uni.postMessage({data:{action:"onPlay"}}),a[3])for(var t=0;t<i.length;t++)i[t]!==this&&i[t].pause()},l.onerror=function(){uni.postMessage({data:{action:"onError",source:g,attrs:t(this)}})};else if("table"===g&&a[4]&&!l.style.cssText.includes("inline")){var f=document.createElement("div");f.style.overflow="auto",f.appendChild(l),l=f}else"svg"===g&&(c=void 0)}s.appendChild(l)}(d)}document.addEventListener("UniAppJSBridgeReady",function(){document.body.onclick=function(){return uni.postMessage({data:{action:"onClick"}})},uni.postMessage({data:{action:"onJSBridgeReady"}})});var a,i=[];window.setContent=function(t,e,n){var r=document.getElementById("content");e[0]&&(document.body.style.cssText=e[0]),e[5]||(r.style.userSelect="none"),n||(r.innerHTML="",i=[]),a=e,window.unloadimgs=0;var s=document.createDocumentFragment();o(t,s),r.appendChild(s);var c=r.scrollHeight;uni.postMessage({data:{action:"onLoad",height:c}}),window.unloadimgs||uni.postMessage({data:{action:"onReady",height:c}}),clearInterval(window.timer),window.timer=setInterval(function(){r.scrollHeight!==c&&(c=r.scrollHeight,uni.postMessage({data:{action:"onHeightChange",height:c}}))},350)},window.onunload=function(){clearInterval(window.timer)};
@@ -0,0 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function t(e,t){return n.call(e,t)}var i=[],a=function(e,n){var t={options:{timestamp:+new Date},name:e,arg:n};if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){if("postMessage"===e){var a={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(a):window.__dcloud_weex_.postMessage(JSON.stringify(a))}var o={type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(o):window.__dcloud_weex_.postMessageToService(JSON.stringify(o))}if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:t,pageId:""},"*");if(0===i.length){var r=plus.webview.currentWebview();if(!r)throw new Error("plus.webview.currentWebview() is undefined");var d=r.parent(),s="";s=d?d.id:r.id,i.push(s)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}},"__uniapp__service");else{var w=JSON.stringify(t);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(w,",").concat(JSON.stringify(i),");"))}},o={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;a("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("redirectTo",{url:encodeURI(n)})},getEnv:function(e){window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};a("postMessage",e.data||{})}},r=/uni-app/i.test(navigator.userAgent),d=/Html5Plus/i.test(navigator.userAgent),s=/complete|loaded|interactive/;var w=window.my&&navigator.userAgent.indexOf("AlipayClient")>-1;var u=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var g=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var v=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);for(var l,_=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},f=[function(e){if(r||d)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&s.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),o},function(e){if(v)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(w){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(u)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){return document.addEventListener("DOMContentLoaded",e),o}],m=0;m<f.length&&!(l=f[m](_));m++);l||(l={});var E="undefined"!=typeof uni?uni:{};if(!E.navigateTo)for(var b in l)t(l,b)&&(E[b]=l[b]);return E.webView=l,E}));
@@ -0,0 +1 @@
<style>.hl-code,.hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.hl-pre{padding:1em;margin:.5em 0;overflow:auto}.hl-pre{background:#2d2d2d}.hl-block-comment,.hl-cdata,.hl-comment,.hl-doctype,.hl-prolog{color:#999}.hl-punctuation{color:#ccc}.hl-attr-name,.hl-deleted,.hl-namespace,.hl-tag{color:#e2777a}.hl-function-name{color:#6196cc}.hl-boolean,.hl-function,.hl-number{color:#f08d49}.hl-class-name,.hl-constant,.hl-property,.hl-symbol{color:#f8c555}.hl-atrule,.hl-builtin,.hl-important,.hl-keyword,.hl-selector{color:#cc99cd}.hl-attr-value,.hl-char,.hl-regex,.hl-string,.hl-variable{color:#7ec699}.hl-entity,.hl-operator,.hl-url{color:#67cdcc}.hl-bold,.hl-important{font-weight:700}.hl-italic{font-style:italic}.hl-entity{cursor:help}.hl-inserted{color:green}</style><style>.md-p{margin-block-start:1em;margin-block-end:1em}.md-blockquote,.md-table{margin-bottom:16px}.md-table{box-sizing:border-box;width:100%;overflow:auto;border-spacing:0;border-collapse:collapse}.md-tr{background-color:#fff;border-top:1px solid #c6cbd1}.md-table .md-tr:nth-child(2n){background-color:#f6f8fa}.md-td,.md-th{padding:6px 13px!important;border:1px solid #dfe2e5}.md-th{font-weight:600}.md-blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.md-code{padding:.2em .4em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.md-pre .md-code{padding:0;font-size:100%;background:0 0;border:0}.hl-pre{position:relative}.hl-code{overflow:auto;display:block}.hl-language{font-size:12px;font-weight:600;position:absolute;right:8px;text-align:right;top:3px}.hl-pre{padding-top:1.5em}.hl-pre{font-size:14px;padding-left:3.8em;counter-reset:linenumber}.line-numbers-rows{position:absolute;pointer-events:none;top:1.5em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows .span{display:block;counter-increment:linenumber}.line-numbers-rows .span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}._address,._article,._aside,._body,._caption,._center,._cite,._footer,._header,._html,._nav,._pre,._section{display:block}._video{width:300px;height:225px;display:inline-block;background-color:#000}</style><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>