学员端小程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

366 lines
11 KiB

1 year ago
  1. <template>
  2. <view id="_root" :class="(selectable?'_select ':'')+'_root'">
  3. <slot v-if="!nodes[0]" />
  4. <!-- #ifndef APP-PLUS-NVUE -->
  5. <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu]" />
  6. <!-- #endif -->
  7. <!-- #ifdef APP-PLUS-NVUE -->
  8. <web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
  9. <!-- #endif -->
  10. </view>
  11. </template>
  12. <script>
  13. import props from './props.js';
  14. /**
  15. * mp-html v2.0.4
  16. * @description 富文本组件
  17. * @tutorial https://github.com/jin-yufeng/mp-html
  18. * @property {String} bgColor 背景颜色只适用与APP-PLUS-NVUE
  19. * @property {String} content 用于渲染的富文本字符串默认 true
  20. * @property {Boolean} copyLink 是否允许外部链接被点击时自动复制
  21. * @property {String} domain 主域名用于拼接链接
  22. * @property {String} errorImg 图片出错时的占位图链接
  23. * @property {Boolean} lazyLoad 是否开启图片懒加载默认 true
  24. * @property {string} loadingImg 图片加载过程中的占位图链接
  25. * @property {Boolean} pauseVideo 是否在播放一个视频时自动暂停其它视频默认 true
  26. * @property {Boolean} previewImg 是否允许图片被点击时自动预览默认 true
  27. * @property {Boolean} scrollTable 是否给每个表格添加一个滚动层使其能单独横向滚动
  28. * @property {Boolean} selectable 是否开启长按复制
  29. * @property {Boolean} setTitle 是否将 title 标签的内容设置到页面标题默认 true
  30. * @property {Boolean} showImgMenu 是否允许图片被长按时显示菜单默认 true
  31. * @property {Object} tagStyle 标签的默认样式
  32. * @property {Boolean | Number} useAnchor 是否使用锚点链接
  33. *
  34. * @event {Function} load dom 结构加载完毕时触发
  35. * @event {Function} ready 所有图片加载完毕时触发
  36. * @event {Function} imgTap 图片被点击时触发
  37. * @event {Function} linkTap 链接被点击时触发
  38. * @event {Function} error 媒体加载出错时触发
  39. */
  40. const plugins=[]
  41. const parser = require('./parser')
  42. // #ifndef APP-PLUS-NVUE
  43. import node from './node/node'
  44. // #endif
  45. // #ifdef APP-PLUS-NVUE
  46. const dom = weex.requireModule('dom')
  47. // #endif
  48. export default {
  49. name: 'mp-html',
  50. data() {
  51. return {
  52. nodes: [],
  53. // #ifdef APP-PLUS-NVUE
  54. height: 0
  55. // #endif
  56. }
  57. },
  58. mixins:[props],
  59. // #ifndef APP-PLUS-NVUE
  60. components: {
  61. node
  62. },
  63. // #endif
  64. watch: {
  65. content(content) {
  66. this.setContent(content)
  67. }
  68. },
  69. created() {
  70. this.plugins = []
  71. for (let i = plugins.length; i--;)
  72. this.plugins.push(new plugins[i](this))
  73. },
  74. mounted() {
  75. if (this.content && !this.nodes.length)
  76. this.setContent(this.content)
  77. },
  78. beforeDestroy() {
  79. this._hook('onDetached')
  80. clearInterval(this._timer)
  81. },
  82. methods: {
  83. /**
  84. * @description 将锚点跳转的范围限定在一个 scroll-view
  85. * @param {Object} page scroll-view 所在页面的示例
  86. * @param {String} selector scroll-view 的选择器
  87. * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
  88. */
  89. in(page, selector, scrollTop) {
  90. // #ifndef APP-PLUS-NVUE
  91. if (page && selector && scrollTop)
  92. this._in = {
  93. page,
  94. selector,
  95. scrollTop
  96. }
  97. // #endif
  98. },
  99. /**
  100. * @description 锚点跳转
  101. * @param {String} id 要跳转的锚点 id
  102. * @param {Number} offset 跳转位置的偏移量
  103. * @returns {Promise}
  104. */
  105. navigateTo(id, offset) {
  106. return new Promise((resolve, reject) => {
  107. if (!this.useAnchor)
  108. return reject('Anchor is disabled')
  109. offset = offset || parseInt(this.useAnchor) || 0
  110. // #ifdef APP-PLUS-NVUE
  111. if (!id) {
  112. dom.scrollToElement(this.$refs.web, {
  113. offset
  114. })
  115. resolve()
  116. } else {
  117. this._navigateTo = {
  118. resolve,
  119. reject,
  120. offset
  121. }
  122. this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
  123. }
  124. // #endif
  125. // #ifndef APP-PLUS-NVUE
  126. let deep = ' '
  127. // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
  128. deep = '>>>'
  129. // #endif
  130. const selector = uni.createSelectorQuery()
  131. // #ifndef MP-ALIPAY
  132. .in(this._in ? this._in.page : this)
  133. // #endif
  134. .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
  135. if (this._in)
  136. selector.select(this._in.selector).scrollOffset()
  137. .select(this._in.selector).boundingClientRect() // 获取 scroll-view 的位置和滚动距离
  138. else
  139. selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
  140. selector.exec(res => {
  141. if (!res[0])
  142. return reject('Label not found')
  143. const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
  144. if (this._in)
  145. // scroll-view 跳转
  146. this._in.page[this._in.scrollTop] = scrollTop
  147. else
  148. // 页面跳转
  149. uni.pageScrollTo({
  150. scrollTop,
  151. duration: 300
  152. })
  153. resolve()
  154. })
  155. // #endif
  156. })
  157. },
  158. /**
  159. * @description 获取文本内容
  160. * @return {String}
  161. */
  162. getText() {
  163. let text = '';
  164. (function traversal(nodes) {
  165. for (let i = 0; i < nodes.length; i++) {
  166. const node = nodes[i]
  167. if (node.type == 'text')
  168. text += node.text.replace(/&amp;/g, '&')
  169. else if (node.name == 'br')
  170. text += '\n'
  171. else {
  172. // 块级标签前后加换行
  173. 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')
  174. if (isBlock && text && text[text.length - 1] != '\n')
  175. text += '\n'
  176. // 递归获取子节点的文本
  177. if (node.children)
  178. traversal(node.children)
  179. if (isBlock && text[text.length - 1] != '\n')
  180. text += '\n'
  181. else if (node.name == 'td' || node.name == 'th')
  182. text += '\t'
  183. }
  184. }
  185. })(this.nodes)
  186. return text
  187. },
  188. /**
  189. * @description 获取内容大小和位置
  190. * @return {Promise}
  191. */
  192. getRect() {
  193. return new Promise((resolve, reject) => {
  194. uni.createSelectorQuery()
  195. // #ifndef MP-ALIPAY
  196. .in(this)
  197. // #endif
  198. .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject('Root label not found'))
  199. })
  200. },
  201. /**
  202. * @description 设置内容
  203. * @param {String} content html 内容
  204. * @param {Boolean} append 是否在尾部追加
  205. */
  206. setContent(content, append) {
  207. if (!append || !this.imgList)
  208. this.imgList = []
  209. const nodes = new parser(this).parse(content)
  210. // #ifdef APP-PLUS-NVUE
  211. if (this._ready)
  212. this._set(nodes, append)
  213. // #endif
  214. this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
  215. // #ifndef APP-PLUS-NVUE
  216. this._videos = []
  217. this.$nextTick(() => {
  218. this._hook('onLoad')
  219. this.$emit('load')
  220. })
  221. // 等待图片加载完毕
  222. let height
  223. clearInterval(this._timer)
  224. this._timer = setInterval(() => {
  225. this.getRect().then(rect => {
  226. // 350ms 总高度无变化就触发 ready 事件
  227. if (rect.height == height) {
  228. this.$emit('ready', rect)
  229. clearInterval(this._timer)
  230. }
  231. height = rect.height
  232. }).catch(() => { })
  233. }, 350)
  234. // #endif
  235. },
  236. /**
  237. * @description 调用插件钩子函数
  238. */
  239. _hook(name) {
  240. for (let i = plugins.length; i--;)
  241. if (this.plugins[i][name])
  242. this.plugins[i][name]()
  243. },
  244. // #ifdef APP-PLUS-NVUE
  245. /**
  246. * @description 设置内容
  247. */
  248. _set(nodes, append) {
  249. this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes) + ',' + JSON.stringify([this.bgColor, this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
  250. },
  251. /**
  252. * @description 接收到 web-view 消息
  253. */
  254. _onMessage(e) {
  255. const message = e.detail.data[0]
  256. switch (message.action) {
  257. // web-view 初始化完毕
  258. case 'onJSBridgeReady':
  259. this._ready = true
  260. if (this.nodes)
  261. this._set(this.nodes)
  262. break
  263. // 内容 dom 加载完毕
  264. case 'onLoad':
  265. this.height = message.height
  266. this._hook('onLoad')
  267. this.$emit('load')
  268. break
  269. // 所有图片加载完毕
  270. case 'onReady':
  271. this.getRect().then(res => {
  272. this.$emit('ready', res)
  273. }).catch(() => { })
  274. break
  275. // 总高度发生变化
  276. case 'onHeightChange':
  277. this.height = message.height
  278. break
  279. // 图片点击
  280. case 'onImgTap':
  281. this.$emit('imgTap', message.attrs)
  282. if (this.previewImg)
  283. uni.previewImage({
  284. current: parseInt(message.attrs.i),
  285. urls: this.imgList
  286. })
  287. break
  288. // 链接点击
  289. case 'onLinkTap':
  290. const href = message.attrs.href
  291. this.$emit('linkTap', message.attrs)
  292. if (href) {
  293. // 锚点跳转
  294. if (href[0] == '#') {
  295. if (this.useAnchor)
  296. dom.scrollToElement(this.$refs.web, {
  297. offset: message.offset
  298. })
  299. }
  300. // 打开外链
  301. else if (href.includes('://')) {
  302. if (this.copyLink)
  303. plus.runtime.openWeb(href)
  304. }
  305. else
  306. uni.navigateTo({
  307. url: href,
  308. fail() {
  309. wx.switchTab({
  310. url: href
  311. })
  312. }
  313. })
  314. }
  315. break
  316. // 获取到锚点的偏移量
  317. case 'getOffset':
  318. if (typeof message.offset == 'number') {
  319. dom.scrollToElement(this.$refs.web, {
  320. offset: message.offset + this._navigateTo.offset
  321. })
  322. this._navigateTo.resolve()
  323. } else
  324. this._navigateTo.reject('Label not found')
  325. break
  326. // 点击
  327. case 'onClick':
  328. this.$emit('tap')
  329. break
  330. // 出错
  331. case 'onError':
  332. this.$emit('error', {
  333. source: message.source,
  334. attrs: message.attrs
  335. })
  336. }
  337. }
  338. // #endif
  339. }
  340. }
  341. </script>
  342. <style>
  343. /* #ifndef APP-PLUS-NVUE */
  344. /* 根节点样式 */
  345. ._root {
  346. overflow: auto;
  347. -webkit-overflow-scrolling: touch;
  348. }
  349. /* 长按复制 */
  350. ._select {
  351. user-select: text;
  352. }
  353. /* #endif */
  354. </style>