node.vue 14 KB

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