Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

626 строки
17 KiB

  1. <template>
  2. <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
  3. <block v-for="(n, i) in nodes" v-bind:key="i">
  4. <!-- 图片 -->
  5. <!-- 占位图 -->
  6. <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" />
  7. <!-- 显示图片 -->
  8. <!-- #ifdef H5 || (APP-PLUS && VUE2) -->
  9. <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" />
  10. <!-- #endif -->
  11. <!-- #ifndef H5 || (APP-PLUS && VUE2) -->
  12. <!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
  13. <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" />
  14. <!-- #endif -->
  15. <!-- #ifdef APP-HARMONY -->
  16. <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]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
  17. <!-- #endif -->
  18. <!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
  19. <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':(n.m||'scaleToFill'))" :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" />
  20. <!-- #endif -->
  21. <!-- #ifdef MP-KUAISHOU -->
  22. <image v-else-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" :lazy-load="opts[0]" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap"></image>
  23. <!-- #endif -->
  24. <!-- #ifdef APP-PLUS && VUE3 -->
  25. <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':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
  26. <!-- #endif -->
  27. <!-- 文本 -->
  28. <!-- #ifdef MP-WEIXIN -->
  29. <text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
  30. <!-- #endif -->
  31. <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
  32. <text v-else-if="n.text" decode>{{n.text}}</text>
  33. <!-- #endif -->
  34. <text v-else-if="n.name==='br'">{{'\n'}}</text>
  35. <!-- 链接 -->
  36. <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">
  37. <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
  38. </view>
  39. <!-- 视频 -->
  40. <!-- #ifdef APP-PLUS -->
  41. <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" :data-i="i" @vplay.stop="play" />
  42. <!-- #endif -->
  43. <!-- #ifndef APP-PLUS -->
  44. <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" @pause="mediaEvent" @fullscreenchange="mediaEvent" @error="mediaError" />
  45. <!-- #endif -->
  46. <!-- #ifdef H5 || APP-PLUS -->
  47. <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
  48. <embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
  49. <!-- #endif -->
  50. <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
  51. <!-- 音频 -->
  52. <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" @pause="mediaEvent" @error="mediaError" />
  53. <!-- #endif -->
  54. <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">
  55. <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
  56. <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
  57. <node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
  58. <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
  59. <view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
  60. <node :childs="tr.children" :opts="opts" />
  61. </view>
  62. <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
  63. <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
  64. <node :childs="td.children" :opts="opts" />
  65. </view>
  66. </view>
  67. </block>
  68. </view>
  69. </view>
  70. <!-- 富文本 -->
  71. <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
  72. <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]" />
  73. <!-- #endif -->
  74. <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
  75. <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]" />
  76. <!-- #endif -->
  77. <!-- 继续递归 -->
  78. <view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
  79. <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" />
  80. </view>
  81. <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
  82. </block>
  83. </view>
  84. </template>
  85. <script module="handler" lang="wxs">
  86. // 行内标签列表
  87. var inlineTags = {
  88. abbr: true,
  89. b: true,
  90. big: true,
  91. code: true,
  92. del: true,
  93. em: true,
  94. i: true,
  95. ins: true,
  96. label: true,
  97. q: true,
  98. small: true,
  99. span: true,
  100. strong: true,
  101. sub: true,
  102. sup: true
  103. }
  104. /**
  105. * @description 判断是否为行内标签
  106. */
  107. module.exports = {
  108. isInline: function (tagName, style) {
  109. return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
  110. }
  111. }
  112. </script>
  113. <script>
  114. import node from './node'
  115. export default {
  116. name: 'node',
  117. options: {
  118. // #ifdef MP-WEIXIN
  119. virtualHost: true,
  120. // #endif
  121. // #ifdef MP-TOUTIAO
  122. addGlobalClass: false
  123. // #endif
  124. },
  125. data () {
  126. return {
  127. ctrl: {},
  128. nodes: [],
  129. // #ifdef MP-WEIXIN
  130. isiOS: (uni.canIUse('getDeviceInfo') ? uni.getDeviceInfo() : uni.getSystemInfoSync()).system.includes('iOS')
  131. // #endif
  132. }
  133. },
  134. props: {
  135. name: String,
  136. attrs: {
  137. type: Object,
  138. default () {
  139. return {}
  140. }
  141. },
  142. childs: Array,
  143. opts: Array
  144. },
  145. watch: {
  146. childs: {
  147. handler (nodes) {
  148. // 列表缩短会刷新整个列表,因此进行空填充
  149. while (this.nodes.length > nodes.length) {
  150. nodes.push({})
  151. }
  152. this.nodes = nodes
  153. },
  154. immediate: true
  155. }
  156. },
  157. components: {
  158. // #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
  159. node
  160. // #endif
  161. },
  162. mounted () {
  163. this.$nextTick(() => {
  164. for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
  165. })
  166. // #ifdef H5 || APP-PLUS
  167. if (this.opts[0]) {
  168. let i
  169. for (i = this.childs.length; i--;) {
  170. if (this.childs[i].name === 'img') break
  171. }
  172. if (i !== -1) {
  173. this.observer = uni.createIntersectionObserver(this).relativeToViewport({
  174. top: 500,
  175. bottom: 500
  176. })
  177. this.observer.observe('._img', res => {
  178. if (res.intersectionRatio) {
  179. this.$set(this.ctrl, 'load', 1)
  180. this.observer.disconnect()
  181. }
  182. })
  183. }
  184. }
  185. // #endif
  186. },
  187. beforeDestroy () {
  188. // #ifdef H5 || APP-PLUS
  189. if (this.observer) {
  190. this.observer.disconnect()
  191. }
  192. // #endif
  193. },
  194. methods:{
  195. // #ifdef MP-WEIXIN
  196. toJSON () { return this },
  197. // #endif
  198. /**
  199. * @description 播放视频事件
  200. * @param {Event} e
  201. */
  202. play (e) {
  203. const i = e.currentTarget.dataset.i
  204. const node = this.childs[i]
  205. this.root.$emit('play', {
  206. source: node.name,
  207. attrs: {
  208. ...node.attrs,
  209. src: node.src[this.ctrl[i] || 0]
  210. }
  211. })
  212. // #ifndef APP-PLUS
  213. if (this.root.pauseVideo) {
  214. let flag = false
  215. const id = e.target.id
  216. for (let i = this.root._videos.length; i--;) {
  217. if (this.root._videos[i].id === id) {
  218. flag = true
  219. } else {
  220. this.root._videos[i].pause() // 自动暂停其他视频
  221. }
  222. }
  223. // 将自己加入列表
  224. if (!flag) {
  225. const ctx = uni.createVideoContext(id
  226. // #ifndef MP-BAIDU
  227. , this
  228. // #endif
  229. )
  230. ctx.id = id
  231. if (this.root.playbackRate) {
  232. ctx.playbackRate(this.root.playbackRate)
  233. }
  234. this.root._videos.push(ctx)
  235. }
  236. }
  237. // #endif
  238. },
  239. /**
  240. * @description 音视频其他事件
  241. * @param {Event} e
  242. */
  243. mediaEvent (e) {
  244. const i = e.currentTarget.dataset.i
  245. const node = this.childs[i]
  246. this.root.$emit(e.type, {
  247. ...e.detail,
  248. source: node.name,
  249. attrs: {
  250. ...node.attrs,
  251. src: node.src[this.ctrl[i] || 0]
  252. }
  253. })
  254. },
  255. /**
  256. * @description 图片点击事件
  257. * @param {Event} e
  258. */
  259. imgTap (e) {
  260. const node = this.childs[e.currentTarget.dataset.i]
  261. if (node.a) {
  262. this.linkTap(node.a)
  263. return
  264. }
  265. if (node.attrs.ignore) return
  266. // #ifdef H5 || APP-PLUS
  267. node.attrs.src = node.attrs.src || node.attrs['data-src']
  268. // #endif
  269. // #ifndef APP-HARMONY
  270. this.root.$emit('imgtap', node.attrs)
  271. // #endif
  272. // #ifdef APP-HARMONY
  273. this.root.$emit('imgtap', {
  274. ...node.attrs
  275. })
  276. // #endif
  277. // 自动预览图片
  278. if (this.root.previewImg) {
  279. uni.previewImage({
  280. // #ifdef MP-WEIXIN
  281. showmenu: this.root.showImgMenu,
  282. // #endif
  283. // #ifdef MP-ALIPAY
  284. enablesavephoto: this.root.showImgMenu,
  285. enableShowPhotoDownload: this.root.showImgMenu,
  286. // #endif
  287. current: parseInt(node.attrs.i),
  288. urls: this.root.imgList
  289. })
  290. }
  291. },
  292. /**
  293. * @description 图片长按
  294. */
  295. imgLongTap (e) {
  296. // #ifdef APP-PLUS
  297. const attrs = this.childs[e.currentTarget.dataset.i].attrs
  298. if (this.opts[3] && !attrs.ignore) {
  299. uni.showActionSheet({
  300. itemList: ['保存图片'],
  301. success: () => {
  302. const save = path => {
  303. uni.saveImageToPhotosAlbum({
  304. filePath: path,
  305. success () {
  306. uni.showToast({
  307. title: '保存成功'
  308. })
  309. }
  310. })
  311. }
  312. if (this.root.imgList[attrs.i].startsWith('http')) {
  313. uni.downloadFile({
  314. url: this.root.imgList[attrs.i],
  315. success: res => save(res.tempFilePath)
  316. })
  317. } else {
  318. save(this.root.imgList[attrs.i])
  319. }
  320. }
  321. })
  322. }
  323. // #endif
  324. },
  325. /**
  326. * @description 图片加载完成事件
  327. * @param {Event} e
  328. */
  329. imgLoad (e) {
  330. const i = e.currentTarget.dataset.i
  331. /* #ifndef H5 || (APP-PLUS && VUE2) */
  332. if (!this.childs[i].w) {
  333. // 设置原宽度
  334. this.$set(this.ctrl, i, e.detail.width)
  335. } else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
  336. // 加载完毕,取消加载中占位图
  337. this.$set(this.ctrl, i, 1)
  338. }
  339. this.checkReady()
  340. },
  341. /**
  342. * @description 检查是否所有图片加载完毕
  343. */
  344. checkReady () {
  345. if (this.root && !this.root.lazyLoad) {
  346. this.root._unloadimgs -= 1
  347. if (!this.root._unloadimgs) {
  348. setTimeout(() => {
  349. this.root.getRect().then(rect => {
  350. this.root.$emit('ready', rect)
  351. }).catch(() => {
  352. this.root.$emit('ready', {})
  353. })
  354. }, 350)
  355. }
  356. }
  357. },
  358. /**
  359. * @description 链接点击事件
  360. * @param {Event} e
  361. */
  362. linkTap (e) {
  363. const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
  364. const attrs = node.attrs || e
  365. const href = attrs.href
  366. this.root.$emit('linktap', Object.assign({
  367. innerText: this.root.getText(node.children || []) // 链接内的文本内容
  368. }, attrs))
  369. if (href) {
  370. if (href[0] === '#') {
  371. // 跳转锚点
  372. this.root.navigateTo(href.substring(1)).catch(() => { })
  373. } else if (href.split('?')[0].includes('://')) {
  374. // 复制外部链接
  375. if (this.root.copyLink) {
  376. // #ifdef H5
  377. window.open(href)
  378. // #endif
  379. // #ifdef MP
  380. uni.setClipboardData({
  381. data: href,
  382. success: () =>
  383. uni.showToast({
  384. title: '链接已复制'
  385. })
  386. })
  387. // #endif
  388. // #ifdef APP-PLUS
  389. plus.runtime.openWeb(href)
  390. // #endif
  391. }
  392. } else {
  393. // 跳转页面
  394. uni.navigateTo({
  395. url: href,
  396. fail () {
  397. uni.switchTab({
  398. url: href,
  399. fail () { }
  400. })
  401. }
  402. })
  403. }
  404. }
  405. },
  406. /**
  407. * @description 错误事件
  408. * @param {Event} e
  409. */
  410. mediaError (e) {
  411. const i = e.currentTarget.dataset.i
  412. const node = this.childs[i]
  413. // 加载其他源
  414. if (node.name === 'video' || node.name === 'audio') {
  415. let index = (this.ctrl[i] || 0) + 1
  416. if (index > node.src.length) {
  417. index = 0
  418. }
  419. if (index < node.src.length) {
  420. this.$set(this.ctrl, i, index)
  421. return
  422. }
  423. } else if (node.name === 'img') {
  424. // #ifdef H5 && VUE3
  425. if (this.opts[0] && !this.ctrl.load) return
  426. // #endif
  427. // 显示错误占位图
  428. if (this.opts[2]) {
  429. this.$set(this.ctrl, i, -1)
  430. }
  431. this.checkReady()
  432. }
  433. if (this.root) {
  434. this.root.$emit('error', {
  435. source: node.name,
  436. attrs: node.attrs,
  437. // #ifndef H5 && VUE3
  438. errMsg: e.detail.errMsg
  439. // #endif
  440. })
  441. }
  442. }
  443. }
  444. }
  445. </script>
  446. <style>
  447. /* a 标签默认效果 */
  448. ._a {
  449. padding: 1.5px 0 1.5px 0;
  450. color: #366092;
  451. word-break: break-all;
  452. }
  453. /* a 标签点击态效果 */
  454. ._hover {
  455. text-decoration: underline;
  456. opacity: 0.7;
  457. }
  458. /* 图片默认效果 */
  459. ._img {
  460. max-width: 100%;
  461. -webkit-touch-callout: none;
  462. }
  463. /* 内部样式 */
  464. ._block {
  465. display: block;
  466. }
  467. ._b,
  468. ._strong {
  469. font-weight: bold;
  470. }
  471. ._code {
  472. font-family: monospace;
  473. }
  474. ._del {
  475. text-decoration: line-through;
  476. }
  477. ._em,
  478. ._i {
  479. font-style: italic;
  480. }
  481. ._h1 {
  482. font-size: 2em;
  483. }
  484. ._h2 {
  485. font-size: 1.5em;
  486. }
  487. ._h3 {
  488. font-size: 1.17em;
  489. }
  490. ._h5 {
  491. font-size: 0.83em;
  492. }
  493. ._h6 {
  494. font-size: 0.67em;
  495. }
  496. ._h1,
  497. ._h2,
  498. ._h3,
  499. ._h4,
  500. ._h5,
  501. ._h6 {
  502. display: block;
  503. font-weight: bold;
  504. }
  505. ._image {
  506. height: 1px;
  507. }
  508. ._ins {
  509. text-decoration: underline;
  510. }
  511. ._li {
  512. display: list-item;
  513. }
  514. ._ol {
  515. list-style-type: decimal;
  516. }
  517. ._ol,
  518. ._ul {
  519. display: block;
  520. padding-left: 40px;
  521. margin: 1em 0;
  522. }
  523. ._q::before {
  524. content: '"';
  525. }
  526. ._q::after {
  527. content: '"';
  528. }
  529. ._sub {
  530. font-size: smaller;
  531. vertical-align: sub;
  532. }
  533. ._sup {
  534. font-size: smaller;
  535. vertical-align: super;
  536. }
  537. ._thead,
  538. ._tbody,
  539. ._tfoot {
  540. display: table-row-group;
  541. }
  542. ._tr {
  543. display: table-row;
  544. }
  545. ._td,
  546. ._th {
  547. display: table-cell;
  548. vertical-align: middle;
  549. }
  550. ._th {
  551. font-weight: bold;
  552. text-align: center;
  553. }
  554. ._ul {
  555. list-style-type: disc;
  556. }
  557. ._ul ._ul {
  558. margin: 0;
  559. list-style-type: circle;
  560. }
  561. ._ul ._ul ._ul {
  562. list-style-type: square;
  563. }
  564. ._abbr,
  565. ._b,
  566. ._code,
  567. ._del,
  568. ._em,
  569. ._i,
  570. ._ins,
  571. ._label,
  572. ._q,
  573. ._span,
  574. ._strong,
  575. ._sub,
  576. ._sup {
  577. display: inline;
  578. }
  579. /* #ifdef APP-PLUS */
  580. ._video {
  581. width: 300px;
  582. height: 225px;
  583. }
  584. /* #endif */
  585. </style>