|                                                                                                                                                                                                                                                                                                                                                                                                  |  | <template>	<view class="yingbing-computed" :style="{'padding-left': slide + 'px', 'padding-right': slide + 'px'}">		<view :class="'page_' + index" v-for="(item, index) in pages" v-html="getContent(item.content)"></view>	</view></template>
<script>	const htmlChars = ['㊦', '㊟', '㊨', '㊧']	export default {		inject: ['getFontSize', 'getFontFamily', 'getLineHeight', 'getTopGap', 'getBottomGap', 'getSlide', 'getHeaderShow', 'getFooterShow', 'getTotalChapter', 'getSplit', 'getPageType'],		props: {			windowHeight: {				type: [Number, String],				default: 0			},			windowWidth: {				type: [Number, String],				default: 0			}		},		computed: {			fontSize () {				return this.getFontSize()			},			fontFamily () {				return this.getFontFamily()			},			lineHeight () {				return this.getLineHeight()			},			topGap () {				return this.getTopGap()			},			bottomGap () {				return this.getBottomGap()			},			slide () {				return this.getSlide()			},			totalChapter () {				return this.getTotalChapter()			},			split () {				return this.getSplit()			},			pageType () {				return this.getPageType()			},			//展示头部
			headerShow () {				return this.pageType == 'scroll' ? this.getHeaderShow() : typeof this.chapter.headerShow == 'boolean' ? this.chapter.headerShow : this.getHeaderShow()//判断是否显示头部
			},			//展示底部
			footerShow () {				return this.pageType == 'scroll' ? this.getFooterShow() : typeof this.chapter.footerShow == 'boolean' ? this.chapter.footerShow : this.getFooterShow()//判断是否显示头部
			},			contentWidth () {				return this.windowWidth - (2 * this.slide)			},			contentHeight () {				return this.windowHeight - this.topGap - this.bottomGap - (this.headerShow ? 30 : 0) - (this.footerShow ? 30 : 0)			}		},		data () {			return {				pages: [],//渲染页面数组
				chapter: {},//章节内容临时存储
				contents: [],//内容转化数组
				startTags: [],//临时存储被切割的开始标签
				success: null,//成功回调
				fail: null,//失败回调
			}		},		methods: {			//外层包裹div,设置公共样式(为了兼容微信小程序)
			getContent (content) {				return `<div class="yingbing-computed-content-html" style="font-family:${this.fontFamily};font-size:${this.fontSize}px;line-height:${this.lineHeight}px;">` + content.replace('<whole-render', '<div').replace('<\/whole-render>', '<\/div>') + '</div>'			},			start ({chapter, success, fail}) {				this.chapter = chapter//记录章节内容
				this.success = success				this.fail = fail				if ( !this.chapter.content ) {					this.handleSuccess()					return				}				let content = this.chapter.content				const fullStyle = 'box-sizing:border-box;overflow:hidden;height:' + this.contentHeight + 'px'//full-box的样式
				content = content.replace(/<full-box.*?>/g, function(item){				    if(item.includes('style=')){				        return item.replace(/style="(.*?)(?=\")/, 'style="' + fullStyle +';$1');				    }else{				        return item.replace(/\<full-box/, '<full-box style="'+fullStyle+'" $1');				    }				})//给所有full-box添加全部高度
				content = content.replace(/<full-box/g, '<whole-render')//将full-box标签替换为whole-render
				content = content.replace(/<\/full-box>/g, '</whole-render>')//将full-box标签替换为whole-render
				const imgClassName = 'yingbing-img'//img类名
				const imgStyle = 'max-height:' + (this.contentHeight - this.lineHeight) + 'px'//img样式
				content = content.replace(/<img.*?>/g, function(item){				    if(item.includes('class=')){				        return item.replace(/class="(.*?)(?=\")/, 'class="$1 '+imgClassName);				    }else{				        return item.replace('<img', '<img class="'+imgClassName+'"');				    }				})//给所有img添加class[yingbing-img] 设置img公共样式(为了兼容微信小程序)
				content = content.replace(/<img.*?>/g, function(item){					if(item.includes('style=')){					    return item.replace(/style="(.*?)(?=\")/, 'style="$1;'+imgStyle);					}else{					    return item.replace('<img', '<img style="'+imgStyle+'"');					}				})//给所有img添加style 设置img公共样式
				content = content.replace(/\r|\n/g, '<br/>')//将\n|\r替换为换行符
				content = content.replace(/\t/g, '')//将\t删除
				this.contents = this.contentToArr(content)//初始化内容,并将文本内容转为数组
				this.computedPage()			},			//将文本内容转为数组
			contentToArr (content) {				let temp = null				let originContent = content				const wholeRegTags= ['whole-render', 'full-box', 'button', 'video', 'audio', 'iframe']//需要整块渲染的标签
				const singleRegTags = ['img', 'hr', 'input', 'video', 'audio', 'iframe']//非闭合标签
				const wrapTags = [], wholeTags = [], endTags = [], startTags = []				//将整块渲染的标签替换为㊟符号保存
				const wholeTagReg = new RegExp(`<(${wholeRegTags.join('|')})>*([\\s\\S]*?)<\/(${wholeRegTags.join('|')})>|<(${singleRegTags.join('|')})[^>]+>`)				while ( (temp = originContent.match(wholeTagReg)) != null ) {					wholeTags.push(temp[0])					originContent = originContent.substring(0, temp.index) + htmlChars[1] + originContent.substring(temp.index + temp[0].length)				}				//将换行标签替换为㊦符号保存
				const wrapTagReg = new RegExp(`<br[^>]+>`)				while ( (temp = originContent.match(wrapTagReg)) != null ) {					wrapTags.push(temp[0])					originContent = originContent.substring(0, temp.index) + htmlChars[0] + originContent.substring(temp.index + temp[0].length)				}				//将结束标签替换为㊨符号保存
				const endTagReg = new RegExp(`<\/[^>]+>`)//结束标签
				while ( (temp = originContent.match(endTagReg)) != null ) {					endTags.push(temp[0])					originContent = originContent.substring(0, temp.index) + htmlChars[2] + originContent.substring(temp.index + temp[0].length)				}				//将开始标签替换为㊧符号保存
				const startTagReg = new RegExp(`<[^>]+>`)//开始标签
				while ( (temp = originContent.match(startTagReg)) != null ) {					startTags.push(temp[0])					originContent = originContent.substring(0, temp.index) + htmlChars[3] + originContent.substring(temp.index + temp[0].length)				}				//将字符内容分割为数组
				const arr = this.split ? [] : originContent.split('')				if ( arr.length == 0 ) {//如果传入了分隔符
					let chars = ''//临时字符串
					for ( let i = 0; i < originContent.length; i++ ) {						const char = originContent.charAt(i)						if ( htmlChars.includes(char) ) {//如果是标签
							if ( chars ) arr.push(chars)//直接将先前存储的字符push进数组
							arr.push(char)//再将标签push进数组
							chars = ''//清空临时字符串
						} else if ( this.split.indexOf(char) > -1 ) {//如果是分隔符
							chars += char//将分隔符加入字符串
							arr.push(chars)//将字符串push进数组
							chars = ''//清空临时字符串
						} else {//其余字符先存着
							chars += char						}					}				}				return arr.map((char, key) => {					if ( htmlChars.indexOf(char) == -1 ) return char//非标签内容直接返回
					else {//标签内容转化为真实标签再返回
						let index = -1						for ( let j = 0; j < key + 1; j++ ) if ( arr[j] == char ) index++						const tags = char == '㊦' ? wrapTags : char == '㊟' ? wholeTags : char == '㊨' ? endTags : startTags						return index > -1 ? char + ':' + tags[index] : char					}				})			},			//计算页面
			computedPage () {				const start = this.pages.length > 0 ? this.pages[this.pages.length-1].end : 0//获取字符开始位置
				//新增页面
				this.pages.push( Object.assign({}, this.chapter, {					content: '',					contents: [],					type: 'text',					start: start,					end: 0,				}) )				//给页面添加内容
				this.addText(start)			},			//增加内容
			async addText (start) {				let text = '',//渲染内容临时存储
				end = 0,//结束位置
				lt = this.startTags.length,//开始标签数量
				rt = 0//结束标签数量
				if ( this.startTags.length > 0 ) text += this.startTags.map(t => t.split('+')[1]).join('')//如果存在被切割的开始标签添加到内容最前面
				//截取字符 渲染
				for ( let i = start; i < this.contents.length; i++ ) {					end = i//记录结束位置
					const char = this.contents[i]					text += char//拼接文本
					if ( htmlChars.findIndex(c => char.indexOf(c)) > -1 ) {//如果是标签内容
						if ( char.indexOf(htmlChars[0] + ':') > -1 && lt == rt ) break;//如是换行标签并且开始标签和结束标签数量一致时直接开始渲染
						if ( char.indexOf(htmlChars[3] + ':') > -1 ) lt++//开始标签+1
						if ( char.indexOf(htmlChars[2] + ':') > -1 ) rt++//结束标签+1
						if ( lt == rt ) break;//如果开始标签数量等于结束标签则直接渲染
					}				}				this.startTags = []				this.pages[this.pages.length-1].end = end//记录结束位置
				const handle = () => {					this.getRect().then(data => {//计算当前高度
						if ( data.height <= this.contentHeight ) {//如果内容高度小于或等于窗口高度
							if ( end < this.contents.length - 1 ) {//如果内容还没有渲染完
								const subheight = this.contentHeight - data.height//页面剩余高度
								if ( subheight > this.lineHeight ) this.addText(end+1)//当前剩余高度大于行高,则继续增加内容
								else {									this.pages[this.pages.length-1].end = end + 1//如果是增加内容时完成本页计算,结束位置要加1
									this.computedPage()//否则开始计算下一页
								}							} else {								this.pages[this.pages.length-1].end = end + 1//如果是增加内容时完成章节计算,结束位置要加1
								this.handleSuccess()//页面计算完毕开始返回
							}						} else {							//如果内容高度大于窗口高度开始减少内容
							this.subText()						}					})				}				if ( /<img/.test(text)) {					const regex = /<img[^>]*src="([^"]+)"[^>]*>/g;					let match;					while ((match = regex.exec(text)) !== null) {						const res = await this.getImageInfo(match[1])						text = this.replaceImgSrc(text, match[1], res.path)					}				}				this.pages[this.pages.length-1].content += text.replace(new RegExp(`(${htmlChars.join('|')}):`, 'g'), '')// 开始渲染内容
				this.$nextTick(function () { handle() })			},			replaceImgSrc(html, oldSrc, newSrc) {			  const regex = /(?<=<img[^>]*src=")[^"]*(?=")/g;			  return html.replace(regex, match => {			    if (match === oldSrc) {			      return newSrc;			    }			    return match;			  });			},			getImageInfo (src) {				return new Promise((resolve, reject) => {					uni.getImageInfo({						src: src,						success: res => {							resolve(res)						},						fail: err => {							reject(err)						}					})				})			},			//减少内容
			subText () {				const page = this.pages[this.pages.length - 1]//获取最后一页渲染数据
				let contents = this.contentToArr(page.content),//将当前页内容转化为数组
				end = page.end,//结束位置减1
				index = 0,//字符索引
				endTags = []//结束字符临时存储
				contents.reverse()//反转文本内容集合
				const handle = () => {					let lts = [],//存储开始标签
					rts = []//存储结束标签
					end--//结束位置减1
					const char = contents[index]//获取文本内容最后一位
					if ( char.indexOf(htmlChars[2] + ':') > -1 ) {//如果最后一位是结束标签
						let startIndex = 0//存储结束标签对应开始标签的位置
						for ( let i = index; i < contents.length; i++ ) {//循环字符集合,找到结束标签对应的开始标签
							const char1 = contents[i]							if ( char1.indexOf(htmlChars[2] + ':') > -1 ) rts.unshift(char1)//如果标签是结束标签加入rts集合
							if ( char1.indexOf(htmlChars[3] + ':') > -1 ) lts.push(char1)//如果标签是开始标签加入lts集合
							if ( lts.length == rts.length ) break//如果开始标签数量等于结束标签时中段循环
							startIndex = i						}						if ( lts.length > 0 ) this.startTags.push(startIndex + '+' + lts[lts.length-1])//将结束最后一位结束标签对应的开始标签加入startTags
						endTags.unshift(startIndex + '+' + char)//存储结束标签,用于最后的拼接
					}					if ( char.indexOf(htmlChars[3] + ':') > -1 ) {//如果最后一位是开始标签
						const startIndex = this.startTags.findIndex(t => t.indexOf(index + '+') > -1)//获取当前开始标签在startTags中的位置
						const endIndex = endTags.findIndex(t => t.indexOf(index + '+') > -1)//获取当前开始标签在endTags中对应的结束标签位置
						this.startTags.splice(startIndex, 1)//删除记录的开始标签
						endTags.splice(endIndex, 1)//删除记录的当前开始标签对应的结束标签
					}					let contents1 = JSON.parse(JSON.stringify(contents.slice(index)))//获取文本集合,并从索引位置开始截取
					contents1 = contents1.slice(1)//去掉最后一位
					contents1.reverse()//反转文本
					const text = contents1.join('') + endTags.map(t => t.split('+')[1]).join('')//拼接结束标签
					this.pages[this.pages.length - 1].content = text.replace(new RegExp(`(${htmlChars.join('|')}):`, 'g'), '')//渲染文本
					this.pages[this.pages.length - 1].end = end + 1//记录结束位置
					this.$nextTick(function () {						this.getRect().then(data => {							if ( data.height > this.contentHeight ) {//如果内容高度大于窗口高度
								index++//索引+1
								handle()//继续减少文本内容
							} else {								if ( end < this.contents.length - 1 ) this.computedPage()//如果内容还没有渲染完毕,继续计算下一页
								else this.handleSuccess()//页面计算完毕开始返回
							}						})					})				}				handle()			},			//成功回调
			handleSuccess () {				const slots1 = this.chapter.frontSlots || []//获取章节前置插槽
				const slots2 = this.chapter.backSlots || []//获取章节后置插槽
				slots1.reverse()//反转前置插槽
				//插入前置插槽
				slots1.forEach(name => {					const start = this.pages.length > 0 ? this.pages[0].start : 2					this.pages.unshift(Object.assign({}, this.chapter, {						type: 'slot',						content: name,						start: start - 2,						end: start - 1,					}))				})				//插入后置插槽
				slots2.forEach(name => {					const end = this.pages.length > 0 ? this.pages[this.pages.length - 1].end : -1					this.pages.push(Object.assign({}, this.chapter, {						type: 'slot',						content: name,						start: end + 1,						end: end + 2,					}))				})				this.pages = this.pages.map((p, key) => {					const total = this.pages.length					const current = key + 1					const rate = 1 / this.totalChapter					const progress = this.totalChapter ? (rate * (current / total)) + ((p.index - 1) * rate) : 0					return Object.assign({}, p, {total: total, current: current, progress: progress * 100}, p.type == 'text' ? { contents: this.contents.slice(p.start, p.end).map(char => char.replace(new RegExp(`(${htmlChars.join('|')}):`), ''))} : {})				})				this.success && this.success(this.pages)				this.success = null				this.fail = null				this.pages = []				this.startTags = []				this.contents = []				this.chapter = {}			},			//获取内容尺寸
			getRect () {				return new Promise(resolve => {					uni.createSelectorQuery().in(this).select('.page_' + (this.pages.length - 1)).boundingClientRect(data => {						resolve(data)					}).exec();				})			}		}	}</script>
<style scoped>	.yingbing-computed {		position: absolute;		top: -100vh;		left: -100vw;		width: 100%;		height: 100%;		visibility: hidden;		box-sizing: border-box;		display: flex;		flex-direction: column;	}	/deep/ .yingbing-img {		max-width: 100%!important;		box-sizing: border-box!important;	}</style>
 |