details.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <template>
  2. <view class="art-page">
  3. <view class="art-nav" :style="{ paddingTop: statusBar + 'px' }">
  4. <view class="art-nav-row">
  5. <view class="art-back" @tap="goBack">
  6. <text class="art-back-icon">←</text>
  7. </view>
  8. <text class="art-nav-title">{{ navTitle }}</text>
  9. <view class="art-nav-placeholder" />
  10. </view>
  11. </view>
  12. <web-view
  13. v-if="pageUrl"
  14. class="art-webview"
  15. :style="{ height: bodyHeight + 'px' }"
  16. :webview-styles="webviewStyles"
  17. :src="pageUrl"
  18. />
  19. <scroll-view
  20. v-else
  21. scroll-y
  22. class="art-scroll"
  23. :style="{ height: bodyHeight + 'px' }"
  24. >
  25. <view class="art-body" :style="{ paddingBottom: safeBottom + 'px' }">
  26. <view v-if="loading" class="art-state">
  27. <text class="art-state-txt">{{ copy.loading }}</text>
  28. </view>
  29. <view v-else-if="!hasContent" class="art-state">
  30. <text class="art-state-txt">{{ copy.empty }}</text>
  31. </view>
  32. <view v-else class="art-card">
  33. <text v-if="article.title && showHeadTitle" class="art-head-title">{{ article.title }}</text>
  34. <view v-if="metaLine" class="art-meta">
  35. <text class="art-meta-txt">{{ metaLine }}</text>
  36. </view>
  37. <rich-text
  38. class="art-rich"
  39. :nodes="richNodes"
  40. />
  41. </view>
  42. </view>
  43. </scroll-view>
  44. </view>
  45. </template>
  46. <script>
  47. import {
  48. fetchArticleDetails,
  49. formatArticleRichContent,
  50. isExternalArticleUrl
  51. } from '@/features/article/article-gateway'
  52. export default {
  53. data() {
  54. return {
  55. statusBar: 0,
  56. bodyHeight: 600,
  57. safeBottom: 0,
  58. articleId: '',
  59. externalUrl: '',
  60. navTitle: '',
  61. loading: true,
  62. article: {},
  63. richHtml: '',
  64. richNodes: [],
  65. webviewStyles: {
  66. progress: { color: '#4a8cff' }
  67. }
  68. }
  69. },
  70. computed: {
  71. copy() {
  72. const t = this.$t.bind(this)
  73. return {
  74. loading: t('article.loading'),
  75. loadFailed: t('article.loadFailed'),
  76. empty: t('article.empty')
  77. }
  78. },
  79. pageUrl() {
  80. if (this.externalUrl) return this.externalUrl
  81. const url = this.article && this.article.url
  82. return isExternalArticleUrl(url) ? String(url).trim() : ''
  83. },
  84. hasContent() {
  85. return !!(this.richHtml || (this.richNodes && this.richNodes.length))
  86. },
  87. showHeadTitle() {
  88. return this.navTitle !== (this.article.title || '')
  89. },
  90. metaLine() {
  91. const parts = []
  92. if (this.article.createtime_text) parts.push(this.article.createtime_text)
  93. if (this.article.views != null && this.article.views !== '') {
  94. parts.push(`${this.article.views} ${this.$t('article.views')}`)
  95. }
  96. return parts.join(' · ')
  97. }
  98. },
  99. onLoad(option) {
  100. const sys = uni.getSystemInfoSync()
  101. this.statusBar = sys.statusBarHeight || 0
  102. this.safeBottom = sys.safeAreaInsets ? sys.safeAreaInsets.bottom : 0
  103. const navH = this.statusBar + uni.upx2px(88)
  104. this.bodyHeight = (sys.windowHeight || 667) - navH
  105. let title = option.title || ''
  106. try {
  107. title = decodeURIComponent(title)
  108. } catch (_) {}
  109. this.navTitle = title || this.copy.loading
  110. const directUrl = option.url || ''
  111. if (isExternalArticleUrl(directUrl)) {
  112. this.externalUrl = String(directUrl).trim()
  113. this.loading = false
  114. return
  115. }
  116. this.articleId = option.id || ''
  117. if (isExternalArticleUrl(this.articleId)) {
  118. this.externalUrl = String(this.articleId).trim()
  119. this.loading = false
  120. return
  121. }
  122. this.loadArticle()
  123. },
  124. methods: {
  125. goBack() {
  126. if (getCurrentPages().length > 1) {
  127. uni.navigateBack({ delta: 1 })
  128. } else {
  129. uni.switchTab({
  130. url: '/pages/mine/index',
  131. fail: () => uni.reLaunch({ url: '/pages/index/index' })
  132. })
  133. }
  134. },
  135. oss(src, w, h) {
  136. return this.$appKit && this.$appKit.oss ? this.$appKit.oss(src, w, h) : src
  137. },
  138. applyRichContent(html) {
  139. const rich = formatArticleRichContent(html, this.oss.bind(this))
  140. this.richHtml = rich.html
  141. // 优先 HTML 字符串(富文本兼容更好);nodes 作兜底
  142. this.richNodes = rich.html ? rich.html : rich.nodes
  143. },
  144. async loadArticle() {
  145. if (!this.articleId) {
  146. this.loading = false
  147. this.$appKit.msg(this.copy.loadFailed)
  148. return
  149. }
  150. this.loading = true
  151. try {
  152. const data = await fetchArticleDetails(this.articleId)
  153. this.article = data || {}
  154. if (data.title) {
  155. this.navTitle = data.title
  156. }
  157. if (isExternalArticleUrl(data.url)) {
  158. this.loading = false
  159. return
  160. }
  161. this.applyRichContent(data.content)
  162. } catch (err) {
  163. this.$appKit.msg((err && err.message) || this.copy.loadFailed)
  164. } finally {
  165. this.loading = false
  166. }
  167. }
  168. }
  169. }
  170. </script>
  171. <style scoped>
  172. @import './article-shared.css';
  173. .art-head-title {
  174. display: block;
  175. font-size: 36rpx;
  176. font-weight: 700;
  177. color: #1a1d26;
  178. line-height: 1.4;
  179. margin-bottom: 16rpx;
  180. }
  181. .art-meta {
  182. margin-bottom: 24rpx;
  183. padding-bottom: 20rpx;
  184. border-bottom: 1rpx solid #eef1f6;
  185. }
  186. .art-meta-txt {
  187. font-size: 24rpx;
  188. color: #8a8d9e;
  189. }
  190. .art-rich {
  191. display: block;
  192. width: 100%;
  193. font-size: 30rpx;
  194. color: #3d4150;
  195. line-height: 1.65;
  196. word-break: break-word;
  197. }
  198. </style>