西藏巴青项目

index.vue 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <template>
  2. <view :class="pageRootClass" class="tab-page lrd-page">
  3. <view v-if="loading" class="lrd-state">
  4. <text class="text-body lrd-state__txt">{{ $t('livestockResourceDetailPage.loading') }}</text>
  5. </view>
  6. <view v-else-if="loadError" class="lrd-state">
  7. <text class="text-body lrd-state__txt">{{ loadError }}</text>
  8. </view>
  9. <scroll-view v-else scroll-y class="lrd-scroll" enable-back-to-top>
  10. <view class="lrd-inner">
  11. <text class="lrd-title">{{ displayTitle }}</text>
  12. <view class="lrd-meta">
  13. <text v-if="typeLabel" class="lrd-meta__tag text-body">{{ typeLabel }}</text>
  14. <text v-if="publishTime" class="lrd-meta__date text-body">{{ publishTime }}</text>
  15. </view>
  16. <view v-if="showCoverSection" class="lrd-section">
  17. <text class="lrd-section__label text-title">{{ $t('newsDetailPage.coverTitle') }}</text>
  18. <view v-if="hasCover" class="lrd-cover-wrap" role="button" @click="onPreviewCover">
  19. <image class="lrd-cover" :src="coverSrc" mode="aspectFit" />
  20. </view>
  21. <text v-else class="text-body lrd-muted">{{ $t('newsDetailPage.noCover') }}</text>
  22. </view>
  23. <view class="lrd-section">
  24. <text class="lrd-section__label text-title">{{ $t('newsDetailPage.introTitle') }}</text>
  25. <text class="text-body lrd-intro">{{ introText }}</text>
  26. </view>
  27. <view v-if="showVideoSection" class="lrd-section">
  28. <text class="lrd-section__label text-title">{{ $t('livestockResourceDetailPage.videoTitle') }}</text>
  29. <view v-if="videoSrc" class="lrd-video-wrap">
  30. <video
  31. class="lrd-video"
  32. :src="videoSrc"
  33. :poster="hasCover ? coverSrc : ''"
  34. controls
  35. object-fit="contain"
  36. :enable-progress-gesture="true"
  37. :show-center-play-btn="true"
  38. />
  39. </view>
  40. <text v-else class="text-body lrd-muted">{{ $t('livestockResourceDetailPage.noVideo') }}</text>
  41. </view>
  42. <view v-if="detailRows.length" class="lrd-section">
  43. <text class="lrd-section__label text-title">{{ $t('livestockResourceDetailPage.sectionInfo') }}</text>
  44. <view v-for="(row, idx) in detailRows" :key="idx" class="lrd-kv">
  45. <text class="lrd-kv__label text-body">{{ row.label }}</text>
  46. <text class="lrd-kv__value text-body">{{ row.value }}</text>
  47. </view>
  48. </view>
  49. </view>
  50. </scroll-view>
  51. </view>
  52. </template>
  53. <script>
  54. import tabPage from '@/mixins/tabPage'
  55. import { getLivestockResourceDetail } from '@/api/livestockResource'
  56. import { resolveResourceUrl } from '@/utils/resourceUrl'
  57. import {
  58. buildLivestockDetailRows,
  59. formatPublishTime,
  60. resolveDetailCover,
  61. resolveDetailVideo
  62. } from '@/utils/livestockResourceDetail'
  63. const TAB_TYPE_CODES = ['004002', '004006', '004007', '004008']
  64. const TAB_I18N_KEYS = ['t1', 't2', 't3', 't4']
  65. export default {
  66. mixins: [tabPage],
  67. data() {
  68. return {
  69. navTitleKey: 'livestockResourceDetailPage.navTitle',
  70. resourceId: '',
  71. sourceType: 0,
  72. typeCode: '',
  73. detail: null,
  74. loading: false,
  75. loadError: '',
  76. coverRaw: '',
  77. coverSrc: '',
  78. videoSrc: ''
  79. }
  80. },
  81. computed: {
  82. displayTitle() {
  83. const name = this.detail && (this.detail.resourceName || this.detail.title)
  84. return name || this.$t('livestockResourceDetailPage.titleFallback')
  85. },
  86. typeLabel() {
  87. const code = (this.detail && this.detail.resourceType) || this.typeCode
  88. const idx = TAB_TYPE_CODES.indexOf(code)
  89. if (idx >= 0) {
  90. return this.$t(`livestockResourcesPage.tabs.${TAB_I18N_KEYS[idx]}`)
  91. }
  92. return code || ''
  93. },
  94. publishTime() {
  95. return formatPublishTime(this.detail && this.detail.publishTime)
  96. },
  97. introText() {
  98. return (this.detail && this.detail.introduction) || this.$t('livestockResourceDetailPage.noIntro')
  99. },
  100. isVideoCourse() {
  101. const rt = (this.detail && this.detail.resourceType) || this.typeCode
  102. return rt === '004008'
  103. },
  104. hasCover() {
  105. return !!(this.coverRaw || '').trim()
  106. },
  107. /** 视频课程:封面作 poster,有视频时不单独占一大块封面区 */
  108. showCoverSection() {
  109. if (!this.hasCover) {
  110. return !this.isVideoCourse
  111. }
  112. if (this.isVideoCourse && this.videoSrc) {
  113. return false
  114. }
  115. return true
  116. },
  117. showVideoSection() {
  118. return this.isVideoCourse
  119. },
  120. detailRows() {
  121. return buildLivestockDetailRows(this.detail, this.sourceType, (key) => this.$t(key))
  122. }
  123. },
  124. onLoad(query) {
  125. const q = query || {}
  126. this.resourceId = this.decodeQuery(q, 'id')
  127. this.typeCode = this.decodeQuery(q, 'typeCode')
  128. const st = parseInt(this.decodeQuery(q, 'sourceType'), 10)
  129. this.sourceType = st === 1 || st === 2 ? st : 0
  130. this.loadDetail()
  131. },
  132. onShow() {
  133. const title = this.displayTitle
  134. const p = uni.setNavigationBarTitle({
  135. title: title === this.$t('livestockResourceDetailPage.titleFallback') ? this.$t(this.navTitleKey) : title
  136. })
  137. if (p && typeof p.catch === 'function') {
  138. p.catch(() => {})
  139. }
  140. },
  141. methods: {
  142. decodeQuery(q, key) {
  143. const raw = q && q[key]
  144. if (raw == null || raw === '') {
  145. return ''
  146. }
  147. try {
  148. return decodeURIComponent(String(raw))
  149. } catch (e) {
  150. return String(raw)
  151. }
  152. },
  153. loadDetail() {
  154. if (!this.resourceId || !this.sourceType) {
  155. this.loadError = this.$t('livestockResourceDetailPage.invalidParams')
  156. return Promise.resolve()
  157. }
  158. this.loading = true
  159. this.loadError = ''
  160. return getLivestockResourceDetail(this.resourceId, this.sourceType)
  161. .then((res) => {
  162. this.detail = res.data || null
  163. if (!this.detail) {
  164. this.loadError = this.$t('livestockResourceDetailPage.notFound')
  165. return
  166. }
  167. const cover = resolveDetailCover(this.detail, this.sourceType)
  168. this.coverRaw = cover || ''
  169. this.coverSrc = cover ? resolveResourceUrl(cover) : ''
  170. const video = resolveDetailVideo(this.detail)
  171. this.videoSrc = video ? resolveResourceUrl(video) : ''
  172. if (this.detail.resourceType) {
  173. this.typeCode = this.detail.resourceType
  174. }
  175. uni.setNavigationBarTitle({ title: this.displayTitle })
  176. })
  177. .catch((e) => {
  178. this.detail = null
  179. this.loadError = (e && e.message) || this.$t('livestockResourceDetailPage.loadFail')
  180. })
  181. .finally(() => {
  182. this.loading = false
  183. })
  184. },
  185. onPreviewCover() {
  186. if (!this.coverSrc) return
  187. uni.previewImage({ urls: [this.coverSrc], current: 0 })
  188. }
  189. }
  190. }
  191. </script>
  192. <style lang="scss" scoped>
  193. @import '@/styles/morandi.scss';
  194. @import '@/styles/tab-page.scss';
  195. .lrd-page {
  196. display: flex;
  197. flex-direction: column;
  198. min-width: 0;
  199. min-height: 100%;
  200. box-sizing: border-box;
  201. background: $morandi-bg-page;
  202. }
  203. .lrd-scroll {
  204. flex: 1;
  205. min-height: 0;
  206. min-width: 0;
  207. }
  208. .lrd-inner {
  209. display: flex;
  210. flex-direction: column;
  211. align-items: stretch;
  212. gap: 28rpx;
  213. min-width: 0;
  214. padding: 40rpx 32rpx 48rpx;
  215. box-sizing: border-box;
  216. }
  217. .lrd-state {
  218. flex: 1;
  219. display: flex;
  220. align-items: center;
  221. justify-content: center;
  222. padding: 48rpx 24rpx;
  223. }
  224. .lrd-state__txt {
  225. color: $morandi-text-muted;
  226. text-align: center;
  227. }
  228. .lrd-title {
  229. display: block;
  230. width: 100%;
  231. text-align: center;
  232. font-size: 38rpx;
  233. font-weight: 700;
  234. line-height: 1.45;
  235. color: #111827;
  236. word-break: break-word;
  237. overflow-wrap: anywhere;
  238. }
  239. .lrd-meta {
  240. display: flex;
  241. flex-direction: row;
  242. flex-wrap: wrap;
  243. align-items: center;
  244. justify-content: center;
  245. gap: 12rpx 20rpx;
  246. width: 100%;
  247. }
  248. .lrd-meta__tag {
  249. padding: 6rpx 16rpx;
  250. border-radius: 999rpx;
  251. font-size: 22rpx;
  252. line-height: 1.4;
  253. color: #15803d;
  254. background: rgba(34, 197, 94, 0.12);
  255. border: 1rpx solid rgba(34, 197, 94, 0.25);
  256. }
  257. .lrd-meta__date {
  258. font-size: 24rpx;
  259. line-height: 1.5;
  260. color: $morandi-text-muted;
  261. }
  262. .lrd-section {
  263. display: flex;
  264. flex-direction: column;
  265. gap: 16rpx;
  266. min-width: 0;
  267. padding: 24rpx;
  268. box-sizing: border-box;
  269. border-radius: 16rpx;
  270. background: $morandi-bg-card;
  271. border: 1rpx solid $morandi-border;
  272. }
  273. .lrd-section__label {
  274. font-size: 30rpx;
  275. font-weight: 600;
  276. color: $morandi-text;
  277. }
  278. .lrd-muted {
  279. font-size: 26rpx;
  280. line-height: 1.5;
  281. color: $morandi-text-muted;
  282. }
  283. .lrd-intro {
  284. font-size: 28rpx;
  285. line-height: 1.65;
  286. color: $morandi-text-secondary;
  287. word-break: break-word;
  288. overflow-wrap: anywhere;
  289. white-space: pre-wrap;
  290. }
  291. .lrd-cover-wrap {
  292. width: 100%;
  293. max-width: 480rpx;
  294. height: 280rpx;
  295. margin: 0 auto;
  296. border-radius: 12rpx;
  297. overflow: hidden;
  298. background: $morandi-bg-card-inner;
  299. border: 1rpx solid $morandi-border-soft;
  300. display: flex;
  301. align-items: center;
  302. justify-content: center;
  303. }
  304. .lrd-cover {
  305. display: block;
  306. width: 100%;
  307. height: 100%;
  308. }
  309. .lrd-video-wrap {
  310. width: 100%;
  311. min-width: 0;
  312. border-radius: 12rpx;
  313. overflow: hidden;
  314. background: #1a1a1a;
  315. border: 1rpx solid $morandi-border-soft;
  316. }
  317. .lrd-video {
  318. display: block;
  319. width: 100%;
  320. height: 360rpx;
  321. background: #1a1a1a;
  322. }
  323. .lrd-kv {
  324. display: flex;
  325. flex-direction: row;
  326. align-items: flex-start;
  327. gap: 16rpx;
  328. min-width: 0;
  329. padding: 14rpx 0;
  330. border-top: 1rpx solid $morandi-border-soft;
  331. &:first-of-type {
  332. border-top: none;
  333. padding-top: 0;
  334. }
  335. &:last-of-type {
  336. padding-bottom: 0;
  337. }
  338. }
  339. .lrd-kv__label {
  340. flex-shrink: 0;
  341. width: 168rpx;
  342. font-size: 24rpx;
  343. line-height: 1.55;
  344. color: $morandi-text-soft;
  345. }
  346. .lrd-kv__value {
  347. flex: 1;
  348. min-width: 0;
  349. font-size: 26rpx;
  350. line-height: 1.55;
  351. color: $morandi-text;
  352. word-break: break-word;
  353. overflow-wrap: anywhere;
  354. }
  355. .lrd-page.lang-bo {
  356. .lrd-title {
  357. font-size: 34rpx;
  358. line-height: 1.75;
  359. letter-spacing: 2rpx;
  360. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  361. }
  362. .lrd-meta__tag,
  363. .lrd-meta__date,
  364. .lrd-intro,
  365. .lrd-muted,
  366. .lrd-kv__label,
  367. .lrd-kv__value {
  368. font-size: 22rpx;
  369. line-height: 1.75;
  370. letter-spacing: 2rpx;
  371. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  372. }
  373. .lrd-section__label {
  374. font-size: 28rpx;
  375. line-height: 1.75;
  376. letter-spacing: 2rpx;
  377. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  378. }
  379. }
  380. </style>