update:ai page

master
xhhing 3 weeks ago
parent 0880661727
commit 1a8a62f598

@ -29,7 +29,7 @@ export default {
// //
fit_journey_basic_address: 'http://127.0.0.1:8086',//TODO: fit_journey_basic_address: 'http://127.0.0.1:8086',//TODO:
fit_journey_community_address: 'http://47.122.61.54:8082',//TODO: fit_journey_community_address: 'http://47.122.61.54:8082',//TODO:
fit_journey_ai_address: 'http://127.0.0.1:8081',//TODO fit_journey_ai_address: 'http://47.122.61.54:8081',//TODO
fit_journey_login_address: 'http://47.122.61.54:8084',//TODO: fit_journey_login_address: 'http://47.122.61.54:8084',//TODO:
fit_journey_recipe_address: 'http://47.122.61.54:8087',//TODO: fit_journey_recipe_address: 'http://47.122.61.54:8087',//TODO:
fit_journey_exercise_address: 'http://47.122.61.54:8080',//TODO: fit_journey_exercise_address: 'http://47.122.61.54:8080',//TODO:

@ -1,25 +1,30 @@
<template> <template>
<!-- 背景图片--> <!-- 背景图片-->
<image class="background" src="@/static/homepages/puppy_chat/pictures/chat_image.png"></image> <image class="background" src="@/static/homepages/puppy_chat/pictures/chat_image.png"></image>
<scroll-view scroll-y="true" class="chat_scroll" :scroll-into-view="scrollInto">
<view class="chat"> <view class="chat">
<!-- 渲染聊天消息气泡 --> <!-- 渲染聊天消息气泡 -->
<view v-for="(message, index) in send_messages" :key="index" :class="['chat_area', message.sender === 'user' ? 'user_message' : 'ai_message']"> <view v-for="(message, index) in send_messages" :id="'s'+index" :key="index" :class="['chat_area', message.sender === 'user' ? 'user_message' : 'ai_message']">
<!-- 头像部分放在气泡外部 --> <!-- 头像部分放在气泡外部 -->
<view v-if="message.sender === 'ai'" class="avatar_container ai_avatar_container"> <view v-if="message.sender === 'ai'" class="avatar_container ai_avatar_container">
<image class="ai_avatar" src="@/static/homepages/puppy_chat/pictures/ai_avatar.png"></image> <image class="ai_avatar" src="@/static/homepages/puppy_chat/pictures/ai_avatar.png"></image>
</view> </view>
<view v-if="message.sender === 'user'" class="avatar_container user_avatar_container">
<image class="user_avatar" src="@/static/homepages/puppy_chat/pictures/user_avatar.png"></image>
</view> <!-- 消息内容 -->
<view :class="['message_content', message.sender === 'user' ? 'user_message_content' : 'ai_message_content']">
<!-- 消息内容 --> <!-- <text>{{ message.message }}</text> -->
<view class="message_content"> <zero-markdown-view :markdown="message.message"></zero-markdown-view>
<text>{{ message.message }}</text> </view>
</view>
</view> <view v-if="message.sender === 'user'" class="avatar_container user_avatar_container">
</view> <image class="user_avatar" src="@/static/homepages/puppy_chat/pictures/user_avatar.png"></image>
</view>
</view>
</view>
</scroll-view>
@ -44,6 +49,7 @@ export default {
send_messages: [], // send_messages: [], //
conversation_id: 'e97ec0f2-2a94-42e7-b6db-b04883d349e8',//id conversation_id: 'e97ec0f2-2a94-42e7-b6db-b04883d349e8',//id
answer: '', // AI answer: '', // AI
scrollInto:"", // scroll-into-view
}; };
}, },
onLoad() { onLoad() {
@ -73,7 +79,7 @@ export default {
}); });
this.send_messages.push({ this.send_messages.push({
sender: 'ai', // sender: 'ai', //
message: ":" +"我是fit journey的专属AI小助手有什么问题都可以问我哦~" message:"我是fit journey的专属AI小助手有什么问题都可以问我哦~"
}); });
@ -81,11 +87,17 @@ export default {
}, },
methods: { methods: {
setScroll(){
//TODO:
const len = this.send_messages.length;
this.scrollInto = 's'+(len-1);
console.log('scrollInto'+this.scrollInto)
},
handleSendMessage() { handleSendMessage() {
// //
const userMessageCount = this.send_messages.filter(msg => msg.sender === 'user').length; const userMessageCount = this.send_messages.filter(msg => msg.sender === 'user').length;
if (userMessageCount >= 1) { if (userMessageCount >= 3) {
// 2 // 2
uni.showToast({ uni.showToast({
title: "AI 可是要交钱的哦,最多发送1条消息,刷新之后再试试吧~", title: "AI 可是要交钱的哦,最多发送1条消息,刷新之后再试试吧~",
@ -97,16 +109,18 @@ export default {
// //
this.send_messages.push({ this.send_messages.push({
sender: 'user', // sender: 'user', //
message: ":" + this.input_message message: this.input_message
}); });
this.send_messages.push({ this.send_messages.push({
sender: 'ai', // sender: 'ai', //
message: ":正在分析您的问题等待puppy一会呢" message: "正在分析您的问题等待puppy一会呢"
}); });
this.send_message=this.input_message; this.send_message=this.input_message;
this.input_message = ""; // this.input_message = ""; //
this.setScroll() //
// AI // AI
this.fetchAIResponse(); this.fetchAIResponse();
} else { } else {
@ -122,7 +136,6 @@ export default {
const app = getApp(); const app = getApp();
console.log(app.globalData.token); console.log(app.globalData.token);
console.log(app.globalData.fit_journey_ai_address); console.log(app.globalData.fit_journey_ai_address);
// //
uni.request({ uni.request({
url: app.globalData.fit_journey_ai_address + '/ai/chat-with-no-stream',//TODO: url: app.globalData.fit_journey_ai_address + '/ai/chat-with-no-stream',//TODO:
@ -144,8 +157,9 @@ export default {
this.send_messages.pop();// this.send_messages.pop();//
this.send_messages.push({ this.send_messages.push({
sender: 'ai', // AI sender: 'ai', // AI
message:':'+ this.answer message:this.answer
}); });
this.setScroll() //
}, },
error: (res) => { error: (res) => {
console.log("请求失败!请求数据是", res); console.log("请求失败!请求数据是", res);

@ -7,15 +7,23 @@
} }
.chat_scroll{
position: absolute;
width: 100%;
top: 12%;
height: 65vh;
padding: 10px;
}
.chat { .chat {
position: absolute; position: absolute;
width: 100%; width: 100%;
top: 12%; // top: 12%;
height: 79%; height: 100%;
z-index: -1; // z-index: -1;
padding: 10px; // padding: 10px;
overflow-y: auto; // overflow-y: auto;
-webkit-overflow-scrolling: touch; // -webkit-overflow-scrolling: touch;
display: flex; display: flex;
flex-direction: column; /* 消息从上往下显示 */ flex-direction: column; /* 消息从上往下显示 */
@ -23,7 +31,6 @@
.chat_area { .chat_area {
margin-bottom: 10px; margin-bottom: 10px;
padding: 10px; padding: 10px;
border-radius: 15px;
max-width: 75%; max-width: 75%;
word-wrap: break-word; word-wrap: break-word;
display: flex; display: flex;
@ -31,7 +38,7 @@
align-items: center; /* 内容垂直居中 */ align-items: center; /* 内容垂直居中 */
} }
.user_message { .user_message {
background-color: #7879F1; /* 用户消息气泡背景色 */ /*background-color: #7879F1; */
color: white; color: white;
margin-top: 2%; /* 让用户的消息气泡靠右 */ margin-top: 2%; /* 让用户的消息气泡靠右 */
margin-right: 5%; /* AI消息靠左 */ margin-right: 5%; /* AI消息靠左 */
@ -40,7 +47,7 @@
word-break: break-word; /* 自动换行 */ word-break: break-word; /* 自动换行 */
} }
.ai_message { .ai_message {
background-color: #e5e5ea; /* AI消息气泡背景色 */ /*background-color: #e5e5ea;*/
color: black; color: black;
margin-left: 2%; /* 用户消息靠右 */ margin-left: 2%; /* 用户消息靠右 */
margin-right: auto; /* 让用户的消息气泡靠左 */ margin-right: auto; /* 让用户的消息气泡靠左 */
@ -48,10 +55,9 @@
word-break: break-word; /* 自动换行 */ word-break: break-word; /* 自动换行 */
} }
.avatar_container { .avatar_container {
display: flex;
right: 10%; right: 10%;
justify-content: center; height: 100%;
align-items: center; padding-top: 30rpx;
} }
.ai_avatar_container { .ai_avatar_container {
margin-right: 10px; /* AI头像与气泡的间距 */ margin-right: 10px; /* AI头像与气泡的间距 */
@ -68,6 +74,14 @@
.message_content { .message_content {
max-width: 80%; /* 限制消息气泡的最大宽度 */ max-width: 80%; /* 限制消息气泡的最大宽度 */
} }
.user_message_content{
background-color: #7879F1;
border-radius: 15px;
}
.ai_message_content{
background-color: #e5e5ea;
border-radius: 15px;
}
.input_box { .input_box {
position: fixed; position: fixed;
width: 100%; width: 100%;

@ -0,0 +1,15 @@
## 2.0.52024-04-24
## 流式输出代码块解决方案
## 2.0.42023-12-06
### 长按复制代码改为点击代码块复制
## 2.0.32023-10-30
doc: 文档说明
## 2.0.22023-10-30
- 新增长按复制代码-仅小程序可用
- 新增代码块语言显示
## 2.0.12023-10-27
##支持vue2,vue3
## 2.0.02022-11-01
使用mp-html自带的插件,重新生成uniapp包,大幅减少插件体积
## 1.0.02022-09-13
首次发布

@ -0,0 +1,5 @@
export default {
copyByClickCode: true, // 点击代码块复制
showLanguageName: true, // 是否在代码块右上角显示语言的名称
showLineNumber: false // 是否显示行号
}

@ -0,0 +1,109 @@
/**
* @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'
code.attrs.style ='display:block;overflow: auto;'
if (config.showLanguageName) {
node.children.push({
name: 'div',
attrs: {
class: 'hl-language',
style: 'user-select:none;position:absolute;top:0;right:2px;font-size:10px;'
},
children: [{
type: 'text',
text: lang
}]
})
}
if (config.copyByClickCode) {
node.attrs.style += (node.attrs.style || '') + ';user-select:none;'
node.attrs['data-content'] = text
node.children.push({
name: 'div',
attrs: {
class: 'hl-copy',
style: 'user-select:none;position:absolute;top:0;right:3px;font-size:10px;'
},
// children: [{
// type: 'text',
// text: '复制'
// }]
})
vm.expose()
// console.log('vm',node,vm)
}
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,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,503 @@
<template>
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
<slot v-if="!nodes[0]" />
<!-- #ifndef APP-PLUS-NVUE -->
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" 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>
</template>
<script>
/**
* mp-html v2.4.2
* @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 highlight from './highlight/index.js'
import style from './style/index.js'
const plugins=[markdown,highlight,style,]
// #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom')
// #endif
export default {
name: 'mp-html',
data () {
return {
nodes: [],
// #ifdef APP-PLUS-NVUE
height: 3
// #endif
}
},
props: {
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: {
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.nodes.length) {
this.setContent(this.content)
}
},
beforeDestroy () {
this._hook('onDetached')
},
methods: {
/**
* @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 = 0
const callback = rect => {
if (!rect || !rect.height) rect = {}
// 350ms ready
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback).catch(callback)
}, 350)
}
}
this.getRect().then(callback).catch(callback)
} else {
//
if (!this.imgList._unloadimgs) {
this.getRect().then(rect => {
this.$emit('ready', rect)
}).catch(() => {
this.$emit('ready', {})
})
}
}
// #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).replace(/%22/g, '') + ',' + 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(() => {
this.$emit('ready', {})
})
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 */
</style>

@ -0,0 +1,678 @@
<template>
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
<block v-for="(n, i) in childs" v-bind:key="i">
<!-- 图片 -->
<!-- 占位图 -->
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
<!-- 显示图片 -->
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
<img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
<!-- 表格中的图片使用 rich-text 防止大小不正确 -->
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style,src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
<!-- #endif -->
<!-- #ifndef H5 || APP-PLUS -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifdef APP-PLUS && VUE3 -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- 文本 -->
<!-- #ifdef MP-WEIXIN -->
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
<text v-else-if="n.text" decode>{{n.text}}</text>
<!-- #endif -->
<text v-else-if="n.name==='br'">\n</text>
<!-- 链接 -->
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
</view>
<!-- 视频 -->
<!-- #ifdef APP-PLUS -->
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" @vplay.stop="play" />
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
<!-- 音频 -->
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<node :childs="tr.children" :opts="opts" />
</view>
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
<node :childs="td.children" :opts="opts" />
</view>
</view>
</block>
</view>
</view>
<!-- 富文本 -->
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" @tap.stop="codeLongTap(n)"/>
<!-- #endif -->
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" @tap.stop="codeLongTap(n)"/>
<!-- #endif -->
<!-- 继续递归 -->
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
</view>
<node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts"/>
</block>
</view>
</template>
<script module="handler" lang="wxs">
//
var inlineTags = {
abbr: true,
b: true,
big: true,
code: true,
del: true,
em: true,
i: true,
ins: true,
label: true,
q: true,
small: true,
span: true,
strong: true,
sub: true,
sup: true
}
/**
* @description 判断是否为行内标签
*/
module.exports = {
isInline: function (tagName, style) {
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
}
}
</script>
<script>
import node from './node'
export default {
name: 'node',
options: {
// #ifdef MP-WEIXIN
virtualHost: true,
// #endif
// #ifdef MP-TOUTIAO
addGlobalClass: false
// #endif
},
data () {
return {
ctrl: {},
// #ifdef MP-WEIXIN
isiOS: uni.getSystemInfoSync().system.includes('iOS')
// #endif
}
},
props: {
name: String,
attrs: {
type: Object,
default () {
return {}
}
},
childs: Array,
opts: Array
},
components: {
// #ifndef (H5 || APP-PLUS) && VUE3
node
// #endif
},
mounted () {
this.$nextTick(() => {
for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
})
// #ifdef H5 || APP-PLUS
if (this.opts[0]) {
let i
for (i = this.childs.length; i--;) {
if (this.childs[i].name === 'img') break
}
if (i !== -1) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
})
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
this.$set(this.ctrl, 'load', 1)
this.observer.disconnect()
}
})
}
}
// #endif
},
beforeDestroy () {
// #ifdef H5 || APP-PLUS
if (this.observer) {
this.observer.disconnect()
}
// #endif
},
methods:{
codeLongTap(e){
if(e.attrs.class=='hl-pre'){
uni.setClipboardData({
data: e.attrs['data-content'],
showToast:false,
success: () => {
uni.showToast({
title: '代码复制成功',
duration: 1000
});
},
fail: (err) => {
console.log('err', err);
}
});
}
},
// codeLongTap(e){
// console.log('codeLongTap',e.attrs);
// if(e.attrs.class=='hl-pre'){
// uni.showActionSheet({
// itemList: [''],
// success: function (res) {
// uni.setClipboardData({
// data: e.attrs['data-content'],
// showToast:false,
// success: () => {
// uni.showToast({
// title: '',
// duration: 1000
// });
// },
// fail: (err) => {
// console.log('err', err);
// }
// });
// },
// fail: function (res) {
// console.log(res.errMsg);
// }
// });
// }
// },
// #ifdef MP-WEIXIN
toJSON () { return this },
// #endif
/**
* @description 播放视频事件
* @param {Event} e
*/
play (e) {
this.root.$emit('play')
// #ifndef APP-PLUS
if (this.root.pauseVideo) {
let flag = false
const id = e.target.id
for (let i = this.root._videos.length; i--;) {
if (this.root._videos[i].id === id) {
flag = true
} else {
this.root._videos[i].pause() //
}
}
//
if (!flag) {
const ctx = uni.createVideoContext(id
// #ifndef MP-BAIDU
, this
// #endif
)
ctx.id = id
if (this.root.playbackRate) {
ctx.playbackRate(this.root.playbackRate)
}
this.root._videos.push(ctx)
}
}
// #endif
},
/**
* @description 图片点击事件
* @param {Event} e
*/
imgTap (e) {
const node = this.childs[e.currentTarget.dataset.i]
if (node.a) {
this.linkTap(node.a)
return
}
if (node.attrs.ignore) return
// #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif
this.root.$emit('imgtap', node.attrs)
//
if (this.root.previewImg) {
uni.previewImage({
// #ifdef MP-WEIXIN
showmenu: this.root.showImgMenu,
// #endif
// #ifdef MP-ALIPAY
enablesavephoto: this.root.showImgMenu,
enableShowPhotoDownload: this.root.showImgMenu,
// #endif
current: parseInt(node.attrs.i),
urls: this.root.imgList
})
}
},
/**
* @description 图片长按
*/
imgLongTap (e) {
// #ifdef APP-PLUS
const attrs = this.childs[e.currentTarget.dataset.i].attrs
if (this.opts[3] && !attrs.ignore) {
uni.showActionSheet({
itemList: ['保存图片'],
success: () => {
const save = path => {
uni.saveImageToPhotosAlbum({
filePath: path,
success () {
uni.showToast({
title: '保存成功'
})
}
})
}
if (this.root.imgList[attrs.i].startsWith('http')) {
uni.downloadFile({
url: this.root.imgList[attrs.i],
success: res => save(res.tempFilePath)
})
} else {
save(this.root.imgList[attrs.i])
}
}
})
}
// #endif
},
/**
* @description 图片加载完成事件
* @param {Event} e
*/
imgLoad (e) {
const i = e.currentTarget.dataset.i
/* #ifndef H5 || (APP-PLUS && VUE2) */
if (!this.childs[i].w) {
//
this.$set(this.ctrl, i, e.detail.width)
} else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
//
this.$set(this.ctrl, i, 1)
}
this.checkReady()
},
/**
* @description 检查是否所有图片加载完毕
*/
checkReady () {
if (this.root && !this.root.lazyLoad) {
this.root._unloadimgs -= 1
if (!this.root._unloadimgs) {
setTimeout(() => {
this.root.getRect().then(rect => {
this.root.$emit('ready', rect)
}).catch(() => {
this.root.$emit('ready', {})
})
}, 350)
}
}
},
/**
* @description 链接点击事件
* @param {Event} e
*/
linkTap (e) {
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
const attrs = node.attrs || e
const href = attrs.href
this.root.$emit('linktap', Object.assign({
innerText: this.root.getText(node.children || []) //
}, attrs))
if (href) {
if (href[0] === '#') {
//
this.root.navigateTo(href.substring(1)).catch(() => { })
} else if (href.split('?')[0].includes('://')) {
//
if (this.root.copyLink) {
// #ifdef H5
window.open(href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openWeb(href)
// #endif
}
} else {
//
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href,
fail () { }
})
}
})
}
}
},
/**
* @description 错误事件
* @param {Event} e
*/
mediaError (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
//
if (node.name === 'video' || node.name === 'audio') {
let index = (this.ctrl[i] || 0) + 1
if (index > node.src.length) {
index = 0
}
if (index < node.src.length) {
this.$set(this.ctrl, i, index)
return
}
} else if (node.name === 'img') {
// #ifdef H5 && VUE3
if (this.opts[0] && !this.ctrl.load) return
// #endif
//
if (this.opts[2]) {
this.$set(this.ctrl, i, -1)
}
this.checkReady()
}
if (this.root) {
this.root.$emit('error', {
source: node.name,
attrs: node.attrs,
// #ifndef H5 && VUE3
errMsg: e.detail.errMsg
// #endif
})
}
}
}
}
</script>
<style>/deep/ .hl-code,/deep/ .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}/deep/ .hl-pre{padding:1em;margin:.5em 0;overflow:auto}/deep/ .hl-pre{background:#2d2d2d}/deep/ .hl-block-comment,/deep/ .hl-cdata,/deep/ .hl-comment,/deep/ .hl-doctype,/deep/ .hl-prolog{color:#999}/deep/ .hl-punctuation{color:#ccc}/deep/ .hl-attr-name,/deep/ .hl-deleted,/deep/ .hl-namespace,/deep/ .hl-tag{color:#e2777a}/deep/ .hl-function-name{color:#6196cc}/deep/ .hl-boolean,/deep/ .hl-function,/deep/ .hl-number{color:#f08d49}/deep/ .hl-class-name,/deep/ .hl-constant,/deep/ .hl-property,/deep/ .hl-symbol{color:#f8c555}/deep/ .hl-atrule,/deep/ .hl-builtin,/deep/ .hl-important,/deep/ .hl-keyword,/deep/ .hl-selector{color:#cc99cd}/deep/ .hl-attr-value,/deep/ .hl-char,/deep/ .hl-regex,/deep/ .hl-string,/deep/ .hl-variable{color:#7ec699}/deep/ .hl-entity,/deep/ .hl-operator,/deep/ .hl-url{color:#67cdcc}/deep/ .hl-bold,/deep/ .hl-important{font-weight:700}/deep/ .hl-italic{font-style:italic}/deep/ .hl-entity{cursor:help}/deep/ .hl-inserted{color:green}/deep/ .md-p {
margin-block-start: 1em;
margin-block-end: 1em;
}
/deep/.hl-copy{
color:#cccccc;
}
/deep/ .md-table,
/deep/ .md-blockquote {
margin-bottom: 16px;
}
/deep/ .md-table {
box-sizing: border-box;
width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
}
/deep/ .md-tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.md-table .md-tr:nth-child(2n) {
background-color: #f6f8fa;
}
/deep/ .md-th,
/deep/ .md-td {
padding: 6px 13px !important;
border: 1px solid #dfe2e5;
}
/deep/ .md-th {
font-weight: 600;
}
/deep/ .md-blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
/deep/ .md-code {
padding: 0.2em 0.4em;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
/deep/ .md-pre .md-code {
padding: 0;
font-size: 100%;
background: transparent;
border: 0;
}
/* a 标签默认效果 */
._a {
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
/* a 标签点击态效果 */
._hover {
text-decoration: underline;
opacity: 0.7;
}
/* 图片默认效果 */
._img {
max-width: 100%;
-webkit-touch-callout: none;
}
/* 内部样式 */
._block {
display: block;
}
._b,
._strong {
font-weight: bold;
}
._code {
font-family: monospace;
}
._del {
text-decoration: line-through;
}
._em,
._i {
font-style: italic;
}
._h1 {
font-size: 2em;
}
._h2 {
font-size: 1.5em;
}
._h3 {
font-size: 1.17em;
}
._h5 {
font-size: 0.83em;
}
._h6 {
font-size: 0.67em;
}
._h1,
._h2,
._h3,
._h4,
._h5,
._h6 {
display: block;
font-weight: bold;
}
._image {
height: 1px;
}
._ins {
text-decoration: underline;
}
._li {
display: list-item;
}
._ol {
list-style-type: decimal;
}
._ol,
._ul {
display: block;
padding-left: 40px;
margin: 1em 0;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
._thead,
._tbody,
._tfoot {
display: table-row-group;
}
._tr {
display: table-row;
}
._td,
._th {
display: table-cell;
vertical-align: middle;
}
._th {
font-weight: bold;
text-align: center;
}
._ul {
list-style-type: disc;
}
._ul ._ul {
margin: 0;
list-style-type: circle;
}
._ul ._ul ._ul {
list-style-type: square;
}
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #ifdef APP-PLUS */
._video {
width: 300px;
height: 225px;
}
/* #endif */
</style>

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,177 @@
<template>
<view class="zero-markdown-view">
<mp-html :key="mpkey" :selectable="selectable" :scroll-table='scrollTable' :tag-style="tagStyle"
:markdown="true" :content="html">
</mp-html>
</view>
</template>
<script>
import mpHtml from '../mp-html/mp-html';
export default {
name: 'zero-markdown-view',
components: {
mpHtml
},
props: {
markdown: {
type: String,
default: ''
},
selectable: {
type: [Boolean, String],
default: true
},
scrollTable: {
type: Boolean,
default: true
},
themeColor: {
type: String,
default: '#007AFF'
},
codeBgColor: {
type: String,
default: '#2d2d2d'
},
},
data() {
return {
html: '',
tagStyle: '',
mpkey: 'zero'
};
},
watch: {
markdown: function(val) {
this.html = this.markdown
}
},
created() {
this.initTagStyle();
},
mounted() {
this.html = this.markdown
},
methods: {
initTagStyle() {
const themeColor = this.themeColor
const codeBgColor = this.codeBgColor
let zeroStyle = {
p: `
margin:5px 5px;
font-size: 15px;
line-height:1.75;
letter-spacing:0.2em;
word-spacing:0.1em;
`,
//
h1: `
margin:25px 0;
font-size: 24px;
text-align: center;
font-weight: bold;
color: ${themeColor};
padding:3px 10px 1px;
border-bottom: 2px solid ${themeColor};
border-top-right-radius:3px;
border-top-left-radius:3px;
`,
//
h2: `
margin:40px 0 20px 0;
font-size: 20px;
text-align:center;
color:${themeColor};
font-weight:bolder;
padding-left:10px;
// border:1px solid ${themeColor};
`,
//
h3: `
margin:30px 0 10px 0;
font-size: 18px;
color: ${themeColor};
padding-left:10px;
border-left:3px solid ${themeColor};
`,
//
blockquote: `
margin:15px 0;
font-size:15px;
color: #777777;
border-left: 4px solid #dddddd;
padding: 0 10px;
`,
//
ul: `
margin: 10px 0;
color: #555;
`,
li: `
margin: 5px 0;
color: #555;
`,
//
a: `
// color: ${themeColor};
`,
//
strong: `
font-weight: border;
color: ${themeColor};
`,
//
em: `
color: ${themeColor};
letter-spacing:0.3em;
`,
// 线
hr: `
height:1px;
padding:0;
border:none;
// border-top:medium solid #333;
text-align:center;
background-image:linear-gradient(to right,rgba(248,57,41,0),${themeColor},rgba(248,57,41,0));
`,
//
table: `
border-spacing:0;
overflow:auto;
min-width:100%;
margin:10px 0;
border-collapse: collapse;
`,
th: `
border: 1px solid #202121;
color: #555;
`,
td: `
color:#555;
border: 1px solid #555555;
`,
pre: `
border-radius: 5px;
white-space: pre;
background: ${codeBgColor};
font-size:12px;
position: relative;
`,
}
this.tagStyle = zeroStyle
},
}
};
</script>
<style lang="scss">
.zero-markdown-view {
padding: 15rpx;
position: relative;
}
</style>

@ -0,0 +1,86 @@
{
"id": "zero-markdown-view",
"displayName": "zero-markdown-view(markdown解析)",
"version": "2.0.5",
"description": "一行代码即可实现markdown解析,支持自定义主题色,支持vue2,vue3.",
"keywords": [
"markdown",
"markdown解析",
"代码块",
"代码高亮",
"mp-html"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"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": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,134 @@
# zero-markdown-view
## 一. 重要更新说明
### v2.0.4
- 新增点击代码块复制代码-仅小程序可用
### v2.0.1
- 兼容vue2,vue3
### v2.0.0
- 省去了 npm install marked
- 省去了 npm install highlight.js
- 使用mp-html自带的插件,重新生成uniapp包,大幅减少插件体积
传送门: [优化思路及详细过程](https://juejin.cn/post/7160995270476431373/) https://juejin.cn/post/7160995270476431373/
## 二. 使用方法
**符合`easycom`组件模式, 导入 `uni_modules` 后直接使用即可 **
```html
<template>
<view class="container">
<!-- 默认用法 直接传入md文本即可-->
<zero-markdown-view :markdown="content"></zero-markdown-view>
</view>
</template>
<script>
export default {
data() {
return {
content: "\n# 一级标题\n\n## 二级标题\n\n### 三级标题\n\n### 1.2 无序列表\n\n无序列表的使用在符号`-`后加空格使用。如下:\n- 无序列表 1\n- 无序列表 2\n - 无序列表 2.1\n - 无序列表 2.2\n\n**由于微信原因,最多支持到二级列表**。\n\n### 1.3 有序列表\n\n1. 有序列表 1\n2. 有序列表 2\n\n\n### 1.4 粗体和斜体\n\n**这个是粗体**\n\n_这个是斜体_\n\n**_这个是粗体加斜体_**\n\n\n### 1.5 链接\n\n对于该论述欢迎读者查阅之前发过的文章[你是《未来世界的幸存者》么?](https://mp.weixin.qq.com/s/s5IhxV2ooX3JN_X416nidA)\n\n### 1.6 引用\n\n> ### 一级引用示例\n> \n> 读一本好书,就是在和高尚的人谈话。 **——歌德**\n\n### 1.7 分割线\n\n可以在一行中用三个以上的减号来建立一个分隔线同时需要在分隔线的上面空一行。如下\n\n---\n\n### 1.8 删除线\n\n删除线的使用在需要删除的文字前后各使用两个`~`,如下:\n\n~~这是要被删除的内容。~~\n\n### 1.9 表格\n\n| 姓名 | 年龄 | 工作 |\n| :--------- | :--: | -----------: |\n| 作者 | 18 | web |\n| zerojs | 20 | 前端 |\n| 太菜了 | 22 | 躺平 |\n\n\n## 2. 特殊语法\n\n### 2.1 脚注\n\n脚注与链接的区别如下所示\n\n```markdown\n链接[文字](链接)\n脚注[文字](脚注解释 \"脚注名字\")\n```\n### 2.2 代码块\n\n```js\nconsole.log(\"1\");\n\nsetTimeout(function () {\n console.log(\"2\");\n process.nextTick(function () {\n console.log(\"3\");\n });\n new Promise(function (resolve) {\n console.log(\"4\");\n resolve();\n }).then(function () {\n console.log(\"5\");\n });\n});\n```\n\ndiff 不能同时和其他语言的高亮同时显示,且需要调整代码主题为微信代码主题以外的代码主题才能看到 diff 效果,使用效果如下:\n\n```diff\n+ 新增项\n- 删除项\n```\n\n**其他主题不带行号,可自定义是否换行,代码大小与当前编辑器一致**\n\n\n\n## 3 其他语法\n\n### 3.1 HTML\n\n支持原生 HTML 语法,请写内联样式,如下:\n\n<span style=\"display:block;text-align:right;color:orangered;\">橙色居右</span>\n<span style=\"display:block;text-align:center;color:orangered;\">橙色居中</span>\n\n### 3.2 UML\n\n不支持推荐使用开源工具`https://draw.io/`制作后再导入图片"
}
},
created() {
},
computed: {
// 流式输出时代码块处理 , 这时候请使用 msgContent 传入组件中
msgContent() {
if (!this.content) {
return
}
let htmlString = ''
// 判断markdown中代码块标识符的数量是否为偶数
if (this.content.split("```").length % 2) {
let content = this.content
if (content[content.length - 1] != '\n') {
content += '\n'
}
htmlString = content
} else {
htmlString = this.content
}
return htmlString
}
},
methods: {
},
}
</script>
<style lang="scss" scoped>
</style>
```
## 三. 参数说明
|参数 |类型 |默认值 |描述 |
|-- |-- |-- |-- |
|markdown |String | |markdown文本 |
|themeColor |String |'#007AFF' |主题色 |
|codeBgColor|String |'#2d2d2d' |代码块背景色 (不建议修改) |
## 四. 注意事项
### 关于代码块流式输出闪烁,可以尝试 给代码块后增加 `\n`
```javascript
computed: {
// 流式输出时代码块处理 , 这时候请使用 msgContent 传入组件中
msgContent() {
if (!this.content) {
return
}
let htmlString = ''
// 判断markdown中代码块标识符的数量是否为偶数
if (this.content.split("```").length % 2) {
let content = this.content
if (content[content.length - 1] != '\n') {
content += '\n'
}
htmlString = content
} else {
htmlString = this.content
}
return htmlString
}
},
```
### 如何关闭点击代码块复制功能?
找到组件文件夹 `zero-markdown-view`-`mp-html`-`highlight`-`config.js`
**把 `copyByClickCode` 改成 false 即可**
```
export default {
copyByClickCode: true, // 点击代码块复制
showLanguageName: true, // 是否在代码块右上角显示语言的名称
showLineNumber: false // 是否显示行号
}
```
### 感谢 mp-html 插件
插件地址: [https://ext.dcloud.net.cn/plugin?id=805](https://ext.dcloud.net.cn/plugin?id=805)
文档地址: [https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart)
插件预览:
![code](https://img.zerojs.cn/mweb/we_code.jpg)
> 小程序搜索: zerojs零技术
> 预览的小程序不一定能及时更新当前插件
Loading…
Cancel
Save