ParseNodeRender.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <template>
  2. <!-- 图片 -->
  3. <image
  4. v-if="node.tag === 'img'"
  5. :id="node.attrs?.id"
  6. :class="'_img ' + (node.attrs?.class || '')"
  7. :style="node.attrs?.style || {}"
  8. :src="node.attrs?.src || ''"
  9. mode="widthFix"
  10. @click="preview(node.attrs?.src as string)"
  11. />
  12. <!-- 换行 -->
  13. <text v-else-if="node.tag === 'br'">\n</text>
  14. <!-- 链接 -->
  15. <view
  16. v-else-if="node.tag === 'a'"
  17. :id="node.attrs?.id"
  18. :class="(node.attrs?.href ? '_a ' : '') + (node.attrs?.class || '')"
  19. hover-class="_hover"
  20. :style="'display:inline;' + (node.attrs?.style || '')"
  21. @tap.stop="linkTap"
  22. >
  23. <ParseNodeRender
  24. v-for="(child, index) in node.children"
  25. :key="index"
  26. :node="child"
  27. />
  28. </view>
  29. <!-- 视频 -->
  30. <video
  31. v-else-if="node.tag === 'video'"
  32. :id="node.attrs?.id"
  33. :class="node.attrs?.class || ''"
  34. :style="node.attrs?.style || {}"
  35. :autoplay="Boolean(node.attrs?.autoplay || false)"
  36. :controls="Boolean(node.attrs?.controls || true)"
  37. :loop="Boolean(node.attrs?.loop || false)"
  38. :muted="Boolean(node.attrs?.muted || false)"
  39. :object-fit="node.attrs?.['object-fit'] || 'contain'"
  40. :poster="node.attrs?.poster as string || ''"
  41. :src="node.attrs?.src as string || ''"
  42. />
  43. <!-- 音频 -->
  44. <audio
  45. v-else-if="node.tag === 'audio'"
  46. :id="node.attrs?.id"
  47. :class="node.attrs?.class || ''"
  48. :style="node.attrs?.style || {}"
  49. :author="node.attrs?.author || ''"
  50. :controls="Boolean(node.attrs?.controls || true)"
  51. :loop="Boolean(node.attrs?.loop || false)"
  52. :name="node.attrs?.name || ''"
  53. :poster="node.attrs?.poster || ''"
  54. :src="node.attrs?.src as string || ''"
  55. />
  56. <!-- 嵌入小程序内容 -->
  57. <view v-else-if="node.tag === 'inject-mp'">
  58. <InjectMPRender :type="node.attrs?.type as string || ''" v-bind="node.attrs" />
  59. </view>
  60. <!-- 其他标签 -->
  61. <view
  62. v-else-if="node.tag !== 'text'"
  63. :id="node.attrs?.id"
  64. :data-tag="node.tag"
  65. :class="node.attrs?.class || ''"
  66. :style="style"
  67. >
  68. <ParseNodeRender
  69. v-for="(child, index) in node.children"
  70. :key="index"
  71. :node="child"
  72. />
  73. </view>
  74. <!-- 文本 -->
  75. <text
  76. v-else
  77. :style="style"
  78. :class="node.attrs?.class || ''"
  79. >
  80. {{ node.attrs?.content }}
  81. </text>
  82. </template>
  83. <script setup lang="ts">
  84. import { computed, inject, ref, type Ref } from 'vue';
  85. import ParseNodeRender from './ParseNodeRender.vue';
  86. import type { ParseNode } from './Parse';
  87. import InjectMPRender from '@/common/components/rich/InjectMPRender.vue';
  88. const props = withDefaults(defineProps<{
  89. node: ParseNode;
  90. }>(), {
  91. });
  92. const tagStyle = inject<Ref<Record<string, string>>>('tagStyle', ref({}));
  93. const style = computed(() =>
  94. [
  95. (props.node.attrs?.style || ''),
  96. (tagStyle.value[props.node.tag] || ''),
  97. isInline.value ? 'display:inline;' : '',
  98. ].join(';'),
  99. );
  100. const isInline = computed(() => ['span','a','large','small'].includes(props.node.tag));
  101. // 链接点击事件
  102. const linkTap = (e: any) => {
  103. const href = props.node.attrs?.href as string;
  104. if (href) {
  105. if (href[0] === '#') {
  106. // 跳转锚点
  107. // 实现锚点跳转逻辑
  108. } else if (href.includes('://')) {
  109. // 外部链接
  110. uni.showModal({
  111. title: '打开链接',
  112. content: href,
  113. success: (res) => {
  114. if (res.confirm) {
  115. // #ifdef H5
  116. window.open(href);
  117. // #endif
  118. // #ifdef MP
  119. uni.setClipboardData({
  120. data: href,
  121. success: () => {
  122. uni.showToast({
  123. title: '链接已复制',
  124. duration: 2000
  125. });
  126. }
  127. });
  128. // #endif
  129. // #ifdef APP-PLUS
  130. plus.runtime.openWeb(href);
  131. // #endif
  132. }
  133. }
  134. });
  135. } else {
  136. // 跳转页面
  137. uni.navigateTo({
  138. url: href,
  139. fail: () => {
  140. uni.switchTab({
  141. url: href,
  142. fail: () => {}
  143. });
  144. }
  145. });
  146. }
  147. }
  148. };
  149. function preview(url: string) {
  150. if (url) {
  151. uni.previewImage({
  152. urls: [url],
  153. })
  154. }
  155. }
  156. defineOptions({
  157. options: {
  158. inheritAttrs: false,
  159. virtualHost: true,
  160. }
  161. })
  162. </script>
  163. <style scoped>
  164. /* a 标签默认效果 */
  165. ._a {
  166. padding: 1.5px 0;
  167. color: #366092;
  168. word-break: break-all;
  169. }
  170. /* a 标签点击态效果 */
  171. ._hover {
  172. text-decoration: underline;
  173. opacity: 0.7;
  174. }
  175. /* 图片默认效果 */
  176. ._img {
  177. max-width: 100%;
  178. -webkit-touch-callout: none;
  179. }
  180. /* 视频默认效果 */
  181. ._video {
  182. width: 300px;
  183. height: 225px;
  184. }
  185. </style>