moment-detail.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <template>
  2. <view class="app-page">
  3. <view v-if="loading !== 'success'" class="loading-wrap">
  4. <tm-skeleton model="card"></tm-skeleton>
  5. <tm-skeleton model="card"></tm-skeleton>
  6. <tm-skeleton model="card"></tm-skeleton>
  7. </view>
  8. <block v-else>
  9. <view class="moment-card">
  10. <view class="card flex flex-start">
  11. <view class="avatar" style="flex-shrink: 0;">
  12. <image style="width: 80rpx;height: 80rpx;border-radius: 50%;" :src="moment.spec.user.avatar" />
  13. </view>
  14. <view class="nickname" style="margin-left: 12rpx;">
  15. <view style="font-size: 30rpx;font-weight: bold;color: #333333;">
  16. {{ moment.spec.user.displayName }}
  17. </view>
  18. <view style="margin-top: 6rpx;font-size: 24rpx;color: #666;">
  19. {{ {d: moment.spec.releaseTime, f: 'yyyy年MM月dd日 星期w'} | formatTime }}
  20. </view>
  21. </view>
  22. </view>
  23. <view v-if="moment.spec.tags && moment.spec.tags.length!==0" class="card flex flex-wrap flex-start"
  24. style="padding-top:12rpx;padding-bottom:12rpx;">
  25. <text class="text-size-m">标签列表:</text>
  26. <tm-tags v-for="(tag,tagIndex) in moment.spec.tags" :key="tagIndex" :color="randomTagColor()"
  27. size="m" model="text">
  28. {{ tag }}
  29. </tm-tags>
  30. </view>
  31. <view class="card" style="padding:0">
  32. <mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain"
  33. :loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true"
  34. :tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
  35. :content="moment.spec.newHtml" :markdown="true" :showLineNumber="true" :showLanguageName="true"
  36. :copyByLongPress="true" />
  37. </view>
  38. <view v-if="moment.images && moment.images.length!==0" class="card">
  39. <view class="card-head text-size-m">
  40. 图片附件:
  41. </view>
  42. <view class="images" :class="['images-'+moment.images.length]">
  43. <view class="image-item" v-for="(image,mediumIndex) in moment.images" :key="mediumIndex">
  44. <image mode="aspectFill" style="width: 100%;height: 100%;border-radius: 6rpx;"
  45. :src="image.url" @click="handlePreview(mediumIndex,moment.images)" />
  46. </view>
  47. </view>
  48. </view>
  49. <view v-if="moment.audios && moment.audios.length!==0" class="card">
  50. <view class="card-head text-size-m">
  51. 音频附件:
  52. </view>
  53. <view
  54. style="display: flex; flex-direction: column; gap: 12rpx 0;padding: 0 24rpx;padding-right:28rpx;">
  55. <audio v-for="(audio,index) in moment.audios" :controls="true" :key="index" :id="audio.url"
  56. :poster="bloggerInfo.avatar"
  57. :name="'来自' + (startConfig.title||bloggerInfo.nickname) + '的声音'"
  58. :author="bloggerInfo.nickname" :src="audio.url"></audio>
  59. </view>
  60. </view>
  61. <view v-if="moment.videos && moment.videos.length!==0" class="card">
  62. <view class="card-head text-size-m">
  63. 视频附件:
  64. </view>
  65. <view style="margin-top:24rpx;width:100%;display: flex; flex-direction: column; gap: 12rpx 0;">
  66. <video style="width:100%;height: 400rpx;border-radius: 12rpx;"
  67. v-for="(video,index) in moment.videos" :key="index" :src="video.url"
  68. :id="'video_' + video.id" :show-mute-btn="true" :controls="true"
  69. :show-center-play-btn="true" :enable-progress-gesture="true" @play="onVideoPlay(video.id)"
  70. @pause="onVideoPause(video.id)" @ended="onVideoEnded(video.id)"></video>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 返回顶部 -->
  75. <tm-flotbutton :width="90" :offset="[16, 80]" icon="icon-angle-up" color="bg-gradient-light-blue-accent"
  76. @click="fnToTopPage()"></tm-flotbutton>
  77. </block>
  78. </view>
  79. </template>
  80. <script>
  81. import MarkdownConfig from '@/common/markdown/markdown.config.js';
  82. import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
  83. import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
  84. import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
  85. import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
  86. import tmTags from '@/tm-vuetify/components/tm-tags/tm-tags.vue';
  87. import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue';
  88. import {
  89. getAppConfigs
  90. } from '@/config/index.js'
  91. import HaloTokenConfig from "@/config/uhalo.config";
  92. import {
  93. getRandomNumberByRange
  94. } from "@/utils/random.js";
  95. import {
  96. generateUUID
  97. } from '@/utils/uuid.js';
  98. export default {
  99. components: {
  100. tmSkeleton,
  101. tmFlotbutton,
  102. tmButton,
  103. tmEmpty,
  104. mpHtml,
  105. tmTags
  106. },
  107. data() {
  108. return {
  109. loading: 'loading',
  110. markdownConfig: MarkdownConfig,
  111. queryParams: {
  112. name: null
  113. },
  114. moment: null,
  115. tagColors: ['orange', 'green', 'red', 'blue'],
  116. videoContexts: {},
  117. currentVideoId: null
  118. };
  119. },
  120. computed: {
  121. haloConfigs() {
  122. return this.$tm.vx.getters().getConfigs;
  123. },
  124. calcUrl() {
  125. return url => {
  126. if (this.$utils.checkIsUrl(url)) {
  127. return url;
  128. }
  129. return getApp().globalData.baseApiUrl + url;
  130. };
  131. },
  132. // 获取博主信息
  133. bloggerInfo() {
  134. const blogger = this.haloConfigs.authorConfig.blogger;
  135. blogger.avatar = this.$utils.checkAvatarUrl(blogger.avatar, true);
  136. return blogger;
  137. },
  138. calcUseTagRandomColor() {
  139. return this.haloConfigs.pageConfig.momentConfig.useTagRandomColor;
  140. },
  141. },
  142. onLoad(e) {
  143. this.fnSetPageTitle('瞬间加载中...');
  144. this.queryParams.name = e.name;
  145. this.fnGetData();
  146. },
  147. onPullDownRefresh() {
  148. this.videoContexts = {};
  149. this.currentVideoId = null;
  150. this.fnGetData();
  151. },
  152. onShareAppMessage() {
  153. return {
  154. path: '/pagesA/moment-detail/moment-detail?name=' + this.moment.metadata.name,
  155. title: this.moment.spec.title,
  156. }
  157. },
  158. onShareTimeline() {
  159. return {
  160. title: this.moment.spec.title,
  161. query: {
  162. name: this.moment.metadata.name
  163. }
  164. }
  165. },
  166. methods: {
  167. fnGetData() {
  168. this.loading = 'loading';
  169. this.$httpApi.v2
  170. .getMomentByName(this.queryParams.name)
  171. .then(res => {
  172. console.log('获取详情', res);
  173. let _tempResult = res;
  174. this.fnSetPageTitle('瞬间详情');
  175. _tempResult.spec.user = {
  176. displayName: this.bloggerInfo.nickname,
  177. avatar: this.$utils.checkAvatarUrl(this.bloggerInfo.avatar)
  178. }
  179. _tempResult.spec.content.medium.map(medium => {
  180. medium.url = this.$utils.checkThumbnailUrl(medium.url, true)
  181. })
  182. _tempResult.spec.newHtml = this.removeTagLinksCompletely(_tempResult.spec.content.html, '')
  183. _tempResult['images'] = _tempResult.spec.content.medium
  184. .filter(x => x.type === 'PHOTO')
  185. _tempResult['videos'] = _tempResult.spec.content.medium
  186. .filter(x => x.type === 'VIDEO').map(_tempResult => {
  187. _tempResult.id = generateUUID()
  188. return _tempResult;
  189. })
  190. _tempResult['audios'] = _tempResult.spec.content.medium
  191. .filter(x => x.type === 'AUDIO')
  192. this.moment = _tempResult;
  193. this.loading = 'success';
  194. this.$nextTick(() => {
  195. this.createVideoContexts(_tempResult.videos);
  196. })
  197. })
  198. .catch(err => {
  199. console.log("错误", err)
  200. this.loading = 'error';
  201. })
  202. .finally(() => {
  203. uni.hideLoading();
  204. uni.stopPullDownRefresh();
  205. });
  206. },
  207. handlePreview(index, list) {
  208. uni.previewImage({
  209. current: index,
  210. urls: list.map(item => item.url)
  211. })
  212. },
  213. removeTagLinksCompletely(htmlString) {
  214. const regex = /<a\s+(?:[^>]*?\s+)?class=(['"])[^'"]*?\btag\b[^'"]*?\1[^>]*?>.*?<\/a>/gi;
  215. const newHtml = htmlString.replace(regex, '');
  216. return newHtml
  217. },
  218. randomTagColor() {
  219. if (!this.calcUseTagRandomColor) return "blue";
  220. const randomIndex = getRandomNumberByRange(0, this.tagColors.length);
  221. return this.tagColors[randomIndex];
  222. },
  223. createVideoContexts(videos) {
  224. this.stopAllVideos()
  225. videos.forEach(item => {
  226. this.videoContexts[item.id] = uni.createVideoContext(`video_${item.id}`, this);
  227. })
  228. },
  229. onVideoPlay(videoId) {
  230. this.currentVideoId = videoId;
  231. this.stopAllVideos(videoId)
  232. },
  233. onVideoPause(videoId) {
  234. if (this.currentVideoId == videoId) {
  235. this.currentVideoId = null;
  236. }
  237. },
  238. onVideoEnded(videoId) {
  239. this.currentVideoId = null;
  240. },
  241. stopAllVideos(excludesVideoId = null) {
  242. Object.keys(this.videoContexts).forEach(videoId => {
  243. if (!excludesVideoId || excludesVideoId != videoId) {
  244. const videoContext = this.videoContexts[videoId]
  245. videoContext?.pause();
  246. }
  247. });
  248. }
  249. }
  250. };
  251. </script>
  252. <style lang="scss" scoped>
  253. .app-page {
  254. width: 100vw;
  255. min-height: 100vh;
  256. display: flex;
  257. flex-direction: column;
  258. box-sizing: border-box;
  259. background-color: #fafafd;
  260. padding-bottom: 24rpx;
  261. }
  262. .loading-wrap {
  263. padding: 0 24rpx;
  264. height: inherit;
  265. background-color: #fff;
  266. }
  267. .moment-card {
  268. display: flex;
  269. flex-direction: column;
  270. box-sizing: border-box;
  271. gap: 24rpx;
  272. padding: 24rpx;
  273. }
  274. .card {
  275. width: 100%;
  276. box-sizing: border-box;
  277. border-radius: 12rpx;
  278. background-color: #ffff;
  279. box-shadow: 0rpx 2rpx 24rpx rgba(0, 0, 0, 0.03);
  280. overflow: hidden;
  281. padding: 24rpx;
  282. &-head {
  283. position: relative;
  284. // padding-left: 24rpx;
  285. margin-bottom: 6rpx;
  286. &:before {
  287. display: none;
  288. box-sizing: border-box;
  289. content: "";
  290. width: 6rpx;
  291. height: 24rpx;
  292. border-radius: 24rpx;
  293. background: #2196F3;
  294. position: absolute;
  295. left: 0;
  296. top: 6rpx;
  297. overflow: hidden;
  298. }
  299. }
  300. }
  301. .images {
  302. display: flex;
  303. flex-wrap: wrap;
  304. align-items: flex-start;
  305. padding: 24rpx;
  306. .image-item {
  307. box-sizing: border-box;
  308. border-radius: 24rpx;
  309. padding: 6rpx;
  310. width: 33%;
  311. height: 200rpx
  312. }
  313. &-1 {
  314. >.image-item {
  315. width: 100%;
  316. height: 350rpx
  317. }
  318. }
  319. &-2 {
  320. >.image-item {
  321. width: 50%;
  322. height: 250rpx
  323. }
  324. }
  325. }
  326. ::v-deep .uni-audio-default {
  327. width: 100%;
  328. border-radius: 12rpx;
  329. }
  330. </style>