list.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <template>
  2. <view class="art-list-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">{{ copy.title }}</text>
  9. <view class="art-nav-placeholder" />
  10. </view>
  11. </view>
  12. <scroll-view
  13. scroll-y
  14. class="art-scroll"
  15. :style="{ height: scrollHeight + 'px' }"
  16. :lower-threshold="120"
  17. @scrolltolower="onScrollToLower"
  18. >
  19. <view class="art-list-body" :style="{ paddingBottom: safeBottom + 'px' }">
  20. <view v-if="loading && !rows.length" class="art-state">
  21. <text class="art-state-txt">{{ copy.loading }}</text>
  22. </view>
  23. <empty
  24. v-else-if="loaded && !rows.length"
  25. :text="copy.empty"
  26. />
  27. <view v-else class="art-list-stack">
  28. <view
  29. v-for="item in rows"
  30. :key="item.id"
  31. class="art-list-card"
  32. hover-class="art-list-card--press"
  33. @tap="openDetail(item)"
  34. >
  35. <view
  36. v-if="item.images && item.images.length > 2"
  37. class="art-list-media art-list-media--multi"
  38. >
  39. <image
  40. v-for="(img, key) in item.images.slice(0, 3)"
  41. :key="key"
  42. class="art-list-multi-img"
  43. :src="thumb(img)"
  44. mode="aspectFill"
  45. />
  46. <view class="art-list-img-tag">
  47. <text class="art-list-img-tag-txt">{{ imageCountLabel(item.images.length) }}</text>
  48. </view>
  49. </view>
  50. <view
  51. v-else-if="coverImage(item)"
  52. class="art-list-row"
  53. >
  54. <image
  55. class="art-list-thumb"
  56. :src="thumb(coverImage(item))"
  57. mode="aspectFill"
  58. />
  59. <view class="art-list-row-body">
  60. <text class="art-list-title">{{ item.title }}</text>
  61. <view class="art-list-foot">
  62. <text class="art-list-time">{{ item.createtime_text }}</text>
  63. <view class="art-list-foot-right">
  64. <text class="art-list-views">{{ viewsLabel(item.views) }}</text>
  65. <text
  66. v-if="flagClass(item)"
  67. class="art-list-flag"
  68. :class="'art-list-flag--' + flagClass(item)"
  69. >{{ item.flag_text }}</text>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. <view v-else class="art-list-text-only">
  75. <text class="art-list-title">{{ item.title }}</text>
  76. <view class="art-list-foot">
  77. <text class="art-list-time">{{ item.createtime_text }}</text>
  78. <view class="art-list-foot-right">
  79. <text class="art-list-views">{{ viewsLabel(item.views) }}</text>
  80. <text
  81. v-if="flagClass(item)"
  82. class="art-list-flag"
  83. :class="'art-list-flag--' + flagClass(item)"
  84. >{{ item.flag_text }}</text>
  85. </view>
  86. </view>
  87. </view>
  88. <template v-if="item.images && item.images.length > 2">
  89. <text class="art-list-title art-list-title--below">{{ item.title }}</text>
  90. <view class="art-list-foot">
  91. <text class="art-list-time">{{ item.createtime_text }}</text>
  92. <view class="art-list-foot-right">
  93. <text class="art-list-views">{{ viewsLabel(item.views) }}</text>
  94. <text
  95. v-if="flagClass(item)"
  96. class="art-list-flag"
  97. :class="'art-list-flag--' + flagClass(item)"
  98. >{{ item.flag_text }}</text>
  99. </view>
  100. </view>
  101. </template>
  102. </view>
  103. </view>
  104. <uni-load-more
  105. v-if="rows.length"
  106. :status="loadStatus"
  107. :content-text="loadMoreText"
  108. />
  109. </view>
  110. </scroll-view>
  111. </view>
  112. </template>
  113. <script>
  114. import { fetchArticleListPage } from '@/features/article/article-gateway'
  115. export default {
  116. data() {
  117. return {
  118. statusBar: 0,
  119. scrollHeight: 600,
  120. safeBottom: 0,
  121. rows: [],
  122. page: 1,
  123. lastPage: 1,
  124. loading: false,
  125. loaded: false,
  126. loadStatus: 'more'
  127. }
  128. },
  129. computed: {
  130. copy() {
  131. const t = this.$t.bind(this)
  132. return {
  133. title: t('article.headlines'),
  134. loading: t('article.loading'),
  135. empty: t('article.empty'),
  136. views: t('article.views'),
  137. network: t('user.realnameNetworkError')
  138. }
  139. },
  140. loadMoreText() {
  141. return {
  142. contentdown: ' ',
  143. contentrefresh: this.copy.loading,
  144. contentnomore: ''
  145. }
  146. }
  147. },
  148. onLoad() {
  149. const sys = uni.getSystemInfoSync()
  150. this.statusBar = sys.statusBarHeight || 0
  151. this.safeBottom = sys.safeAreaInsets ? sys.safeAreaInsets.bottom : 0
  152. const navH = this.statusBar + uni.upx2px(88)
  153. this.scrollHeight = (sys.windowHeight || 667) - navH
  154. this.reloadList()
  155. },
  156. onPullDownRefresh() {
  157. this.reloadList()
  158. },
  159. methods: {
  160. goBack() {
  161. if (getCurrentPages().length > 1) {
  162. uni.navigateBack({ delta: 1 })
  163. } else {
  164. uni.switchTab({
  165. url: '/pages/index/index',
  166. fail: () => uni.reLaunch({ url: '/pages/index/index' })
  167. })
  168. }
  169. },
  170. thumb(url) {
  171. return this.$appKit && this.$appKit.oss ? this.$appKit.oss(url, 248, 0) : url
  172. },
  173. coverImage(item) {
  174. if (!item) return ''
  175. if (item.image) return item.image
  176. if (item.images && item.images.length) return item.images[0]
  177. return ''
  178. },
  179. imageCountLabel(n) {
  180. return this.$t('article.imageCount').replace('{n}', String(n))
  181. },
  182. viewsLabel(views) {
  183. return `${views || 0} ${this.copy.views}`
  184. },
  185. flagClass(item) {
  186. const flag = item && item.flag
  187. if (flag === 'hot' || flag === 'index' || flag === 'recommend') return flag
  188. return ''
  189. },
  190. openDetail(item) {
  191. if (!item || !item.id) return
  192. const title = encodeURIComponent(item.title || '')
  193. uni.navigateTo({
  194. url: `/pages/article/details?id=${item.id}&title=${title}`
  195. })
  196. },
  197. reloadList() {
  198. this.page = 1
  199. this.lastPage = 1
  200. this.rows = []
  201. this.loaded = false
  202. this.loadStatus = 'more'
  203. this.loadPage(true)
  204. },
  205. onScrollToLower() {
  206. if (this.loading || this.page >= this.lastPage) {
  207. this.loadStatus = 'noMore'
  208. return
  209. }
  210. this.page += 1
  211. this.loadStatus = 'loading'
  212. this.loadPage(false)
  213. },
  214. async loadPage(replace) {
  215. if (this.loading) return
  216. this.loading = true
  217. try {
  218. const res = await fetchArticleListPage(this.page, 'new')
  219. this.rows = replace ? res.rows : this.rows.concat(res.rows)
  220. this.page = res.currentPage
  221. this.lastPage = res.lastPage
  222. this.loadStatus = res.total === 0 || this.page >= this.lastPage ? 'noMore' : 'more'
  223. } catch (err) {
  224. this.$appKit.msg((err && err.message) || this.copy.network)
  225. this.loadStatus = 'more'
  226. } finally {
  227. this.loading = false
  228. this.loaded = true
  229. uni.stopPullDownRefresh()
  230. }
  231. }
  232. }
  233. }
  234. </script>
  235. <style scoped>
  236. @import './article-shared.css';
  237. .art-list-page {
  238. min-height: 100vh;
  239. background-color: #f4f6fb;
  240. }
  241. .art-list-body {
  242. padding: 24rpx;
  243. box-sizing: border-box;
  244. }
  245. .art-list-stack {
  246. display: flex;
  247. flex-direction: column;
  248. gap: 20rpx;
  249. }
  250. .art-list-card {
  251. background-color: #ffffff;
  252. border-radius: 24rpx;
  253. padding: 24rpx;
  254. box-shadow: 0 8rpx 32rpx rgba(26, 29, 38, 0.04);
  255. }
  256. .art-list-card--press {
  257. opacity: 0.92;
  258. }
  259. .art-list-media {
  260. position: relative;
  261. border-radius: 16rpx;
  262. overflow: hidden;
  263. margin-bottom: 16rpx;
  264. }
  265. .art-list-media--multi {
  266. display: flex;
  267. flex-direction: row;
  268. justify-content: space-between;
  269. height: 180rpx;
  270. }
  271. .art-list-multi-img {
  272. width: 32%;
  273. height: 100%;
  274. border-radius: 12rpx;
  275. background-color: #f0f3f8;
  276. }
  277. .art-list-img-tag {
  278. position: absolute;
  279. right: 0;
  280. bottom: 0;
  281. padding: 4rpx 14rpx;
  282. background: rgba(26, 29, 38, 0.65);
  283. border-top-left-radius: 12rpx;
  284. }
  285. .art-list-img-tag-txt {
  286. font-size: 22rpx;
  287. color: #ffffff;
  288. }
  289. .art-list-row {
  290. display: flex;
  291. flex-direction: row;
  292. align-items: flex-start;
  293. }
  294. .art-list-thumb {
  295. width: 220rpx;
  296. height: 160rpx;
  297. border-radius: 16rpx;
  298. background-color: #f0f3f8;
  299. flex-shrink: 0;
  300. }
  301. .art-list-row-body {
  302. flex: 1;
  303. min-width: 0;
  304. margin-left: 20rpx;
  305. min-height: 160rpx;
  306. display: flex;
  307. flex-direction: column;
  308. justify-content: space-between;
  309. }
  310. .art-list-text-only {
  311. display: flex;
  312. flex-direction: column;
  313. }
  314. .art-list-title {
  315. display: block;
  316. font-size: 30rpx;
  317. font-weight: 600;
  318. color: #1a1d26;
  319. line-height: 1.45;
  320. overflow: hidden;
  321. text-overflow: ellipsis;
  322. display: -webkit-box;
  323. -webkit-line-clamp: 2;
  324. -webkit-box-orient: vertical;
  325. }
  326. .art-list-title--below {
  327. margin-top: 4rpx;
  328. }
  329. .art-list-foot {
  330. display: flex;
  331. flex-direction: row;
  332. align-items: center;
  333. justify-content: space-between;
  334. margin-top: 16rpx;
  335. }
  336. .art-list-time {
  337. font-size: 24rpx;
  338. color: #8a8d9e;
  339. }
  340. .art-list-foot-right {
  341. display: flex;
  342. flex-direction: row;
  343. align-items: center;
  344. }
  345. .art-list-views {
  346. font-size: 24rpx;
  347. color: #8a8d9e;
  348. }
  349. .art-list-flag {
  350. margin-left: 12rpx;
  351. padding: 2rpx 12rpx;
  352. font-size: 20rpx;
  353. border-radius: 8rpx;
  354. }
  355. .art-list-flag--hot {
  356. color: #ea580c;
  357. background: rgba(234, 88, 12, 0.12);
  358. }
  359. .art-list-flag--index {
  360. color: #4a8cff;
  361. background: rgba(74, 140, 255, 0.12);
  362. }
  363. .art-list-flag--recommend {
  364. color: #e11d48;
  365. background: rgba(225, 29, 72, 0.1);
  366. }
  367. </style>