scenic-list.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <template>
  2. <view class="sl-page">
  3. <view class="sl-hd" :style="{ paddingTop: statusBar + 'px' }">
  4. <view class="sl-nav-row">
  5. <view class="sl-back" @tap="goBack">
  6. <text class="sl-back-icon">←</text>
  7. </view>
  8. <text class="sl-nav-title">{{ copy.listTitle }}</text>
  9. <view class="sl-nav-placeholder" />
  10. </view>
  11. <scroll-view scroll-x class="sl-tabs-scroll" :show-scrollbar="false">
  12. <view class="sl-tabs">
  13. <view
  14. v-for="cat in categories"
  15. :key="cat.id"
  16. class="sl-tab"
  17. :class="{ 'sl-tab--on': String(activeCateId) === String(cat.id) }"
  18. @tap="onCateTap(cat)"
  19. >
  20. <text class="sl-tab-text">{{ cat.name }}</text>
  21. </view>
  22. </view>
  23. </scroll-view>
  24. </view>
  25. <scroll-view
  26. scroll-y
  27. class="sl-scroll"
  28. :style="{ height: scrollHeight + 'px' }"
  29. :lower-threshold="120"
  30. :refresher-enabled="true"
  31. :refresher-triggered="refreshing"
  32. @refresherrefresh="onRefresh"
  33. @refresherrestore="onRefreshEnd"
  34. @refresherabort="onRefreshEnd"
  35. @scrolltolower="onLoadMore"
  36. >
  37. <view class="sl-body" :style="{ paddingBottom: safeBottom + 'px' }">
  38. <view v-if="loading && !items.length" class="sl-state">
  39. <text class="sl-state-txt">{{ copy.loading }}</text>
  40. </view>
  41. <empty
  42. v-else-if="loaded && !items.length"
  43. src="find_default3x"
  44. :text="copy.empty"
  45. />
  46. <view v-else class="sl-list">
  47. <view
  48. v-for="item in items"
  49. :key="item.id"
  50. class="sl-card"
  51. @tap="onItemTap(item)"
  52. >
  53. <view class="sl-card-cover-wrap">
  54. <image
  55. v-if="item.image"
  56. class="sl-card-cover"
  57. :src="item.image"
  58. mode="aspectFill"
  59. />
  60. <view v-else class="sl-card-cover sl-card-cover--ph" />
  61. </view>
  62. <view class="sl-card-body">
  63. <text class="sl-card-title">{{ item.title }}</text>
  64. <text v-if="item.address" class="sl-card-addr">{{ item.address }}</text>
  65. <text class="sl-card-views">{{ copy.views }} {{ item.views }}</text>
  66. </view>
  67. </view>
  68. </view>
  69. <uni-load-more
  70. v-if="items.length"
  71. :status="loadStatus"
  72. :content-text="loadMoreText"
  73. />
  74. </view>
  75. </scroll-view>
  76. </view>
  77. </template>
  78. <script>
  79. import { pullScenicCategories, pullScenicAreaPage } from '@/features/scenic/scenic-gateway'
  80. export default {
  81. data() {
  82. return {
  83. statusBar: 0,
  84. scrollHeight: 600,
  85. safeBottom: 0,
  86. categories: [],
  87. activeCateId: '',
  88. items: [],
  89. page: 1,
  90. lastPage: 1,
  91. loading: false,
  92. loaded: false,
  93. refreshing: false,
  94. loadStatus: 'more',
  95. /** 来自路由 query:cate_id / cateId / tab */
  96. initialCateId: ''
  97. }
  98. },
  99. computed: {
  100. copy() {
  101. const t = this.$t.bind(this)
  102. return {
  103. listTitle: t('scenic.listTitle'),
  104. loading: t('home.loading'),
  105. loadFailed: t('home.loadFailed'),
  106. empty: t('scenic.empty'),
  107. views: t('scenic.views')
  108. }
  109. },
  110. loadMoreText() {
  111. const t = this.$t.bind(this)
  112. return {
  113. contentdown: t('home.loadMore'),
  114. contentrefresh: t('home.loading'),
  115. contentnomore: t('home.noMore')
  116. }
  117. }
  118. },
  119. onLoad(query) {
  120. this.initialCateId = (query && (query.cate_id || query.cateId || query.tab)) || ''
  121. this.initLayout()
  122. this.bootstrap()
  123. },
  124. methods: {
  125. initLayout() {
  126. try {
  127. const info = uni.getSystemInfoSync()
  128. this.statusBar = info.statusBarHeight || 0
  129. this.safeBottom = (info.safeAreaInsets && info.safeAreaInsets.bottom) || 0
  130. const navH = this.statusBar + uni.upx2px(88) + uni.upx2px(72)
  131. this.scrollHeight = (info.windowHeight || info.screenHeight || 600) - navH
  132. } catch (_) {}
  133. },
  134. goBack() {
  135. uni.navigateBack({
  136. fail: () => uni.switchTab({ url: '/pages/index/index' })
  137. })
  138. },
  139. async bootstrap() {
  140. try {
  141. const cats = await pullScenicCategories()
  142. this.categories = cats
  143. if (cats.length) {
  144. this.activeCateId = this.resolveInitialCateId(cats)
  145. await this.reload()
  146. } else {
  147. this.loaded = true
  148. }
  149. } catch (err) {
  150. this.loaded = true
  151. this.$appKit && this.$appKit.msg((err && err.message) || this.copy.loadFailed)
  152. }
  153. },
  154. resolveInitialCateId(cats) {
  155. const preset = String(this.initialCateId || '').trim()
  156. if (!preset) return cats[0].id
  157. const byId = cats.find((c) => String(c.id) === preset)
  158. if (byId) return byId.id
  159. const decoded = decodeURIComponent(preset)
  160. const byName = cats.find((c) => c.name === decoded || c.name === preset)
  161. if (byName) return byName.id
  162. return cats[0].id
  163. },
  164. onCateTap(cat) {
  165. if (!cat || cat.id == null) return
  166. if (String(this.activeCateId) === String(cat.id)) return
  167. this.activeCateId = cat.id
  168. this.reload()
  169. },
  170. async reload() {
  171. this.page = 1
  172. this.lastPage = 1
  173. this.loadStatus = 'more'
  174. this.loaded = false
  175. this.items = []
  176. await this.loadPage(true)
  177. },
  178. onRefresh() {
  179. this.refreshing = true
  180. this.reload().finally(() => this.onRefreshEnd())
  181. },
  182. onRefreshEnd() {
  183. this.refreshing = false
  184. },
  185. onLoadMore() {
  186. if (this.loadStatus === 'loading' || this.loadStatus === 'noMore') return
  187. this.loadPage(false)
  188. },
  189. async loadPage(reset) {
  190. if (this.loading) return
  191. if (!this.activeCateId && this.activeCateId !== 0) return
  192. if (!reset && this.page >= this.lastPage) {
  193. this.loadStatus = 'noMore'
  194. return
  195. }
  196. const nextPage = reset ? 1 : this.page + 1
  197. this.loading = true
  198. this.loadStatus = 'loading'
  199. try {
  200. const pack = await pullScenicAreaPage({
  201. cateId: this.activeCateId,
  202. page: nextPage
  203. })
  204. this.page = pack.currentPage
  205. this.lastPage = pack.lastPage
  206. if (reset) {
  207. this.items = pack.rows
  208. } else {
  209. this.items = this.items.concat(pack.rows)
  210. }
  211. this.loadStatus =
  212. pack.currentPage >= pack.lastPage || !pack.rows.length ? 'noMore' : 'more'
  213. this.loaded = true
  214. } catch (err) {
  215. this.loadStatus = 'more'
  216. if (reset) this.items = []
  217. this.$appKit && this.$appKit.msg((err && err.message) || this.copy.loadFailed)
  218. this.loaded = true
  219. } finally {
  220. this.loading = false
  221. }
  222. },
  223. onItemTap(item) {
  224. if (!item || item.id == null) return
  225. const title = encodeURIComponent(item.title || '')
  226. uni.navigateTo({
  227. url: `/pages/index/scenic-detail?id=${encodeURIComponent(item.id)}&title=${title}`
  228. })
  229. }
  230. }
  231. }
  232. </script>
  233. <style scoped>
  234. .sl-page {
  235. min-height: 100vh;
  236. background-color: #f7f8fc;
  237. }
  238. .sl-hd {
  239. background-color: #ffffff;
  240. border-bottom: 1rpx solid #eef1f6;
  241. }
  242. .sl-nav-row {
  243. display: flex;
  244. flex-direction: row;
  245. align-items: center;
  246. height: 88rpx;
  247. padding: 0 24rpx;
  248. box-sizing: border-box;
  249. }
  250. .sl-back {
  251. width: 64rpx;
  252. height: 64rpx;
  253. display: flex;
  254. align-items: center;
  255. justify-content: center;
  256. }
  257. .sl-back-icon {
  258. font-size: 40rpx;
  259. color: #1a1d26;
  260. line-height: 1;
  261. }
  262. .sl-nav-title {
  263. flex: 1;
  264. text-align: center;
  265. font-size: 32rpx;
  266. font-weight: 600;
  267. color: #1a1d26;
  268. }
  269. .sl-nav-placeholder {
  270. width: 64rpx;
  271. }
  272. .sl-tabs-scroll {
  273. width: 100%;
  274. white-space: nowrap;
  275. }
  276. .sl-tabs {
  277. display: flex;
  278. flex-direction: row;
  279. padding: 0 16rpx 16rpx;
  280. box-sizing: border-box;
  281. }
  282. .sl-tab {
  283. flex-shrink: 0;
  284. padding: 12rpx 28rpx;
  285. margin: 0 8rpx;
  286. border-radius: 999rpx;
  287. background-color: #f2f4f8;
  288. }
  289. .sl-tab--on {
  290. background: linear-gradient(135deg, #4a8cff 0%, #6ba0ff 100%);
  291. }
  292. .sl-tab-text {
  293. font-size: 26rpx;
  294. color: #4a4d5e;
  295. line-height: 1.2;
  296. }
  297. .sl-tab--on .sl-tab-text {
  298. color: #ffffff;
  299. font-weight: 600;
  300. }
  301. .sl-body {
  302. padding: 24rpx;
  303. box-sizing: border-box;
  304. }
  305. .sl-state {
  306. padding: 120rpx 0;
  307. text-align: center;
  308. }
  309. .sl-state-txt {
  310. font-size: 28rpx;
  311. color: #8a8d9e;
  312. }
  313. .sl-list {
  314. display: flex;
  315. flex-direction: column;
  316. }
  317. .sl-card {
  318. display: flex;
  319. flex-direction: row;
  320. align-items: stretch;
  321. background-color: #ffffff;
  322. border-radius: 18rpx;
  323. overflow: hidden;
  324. margin-bottom: 20rpx;
  325. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  326. }
  327. .sl-card-cover-wrap {
  328. width: 220rpx;
  329. min-height: 160rpx;
  330. flex-shrink: 0;
  331. overflow: hidden;
  332. background-color: #e6e8ef;
  333. }
  334. .sl-card-cover {
  335. width: 100%;
  336. height: 100%;
  337. display: block;
  338. }
  339. .sl-card-cover--ph {
  340. background: linear-gradient(135deg, #c1c8d6 0%, #8e96a6 100%);
  341. }
  342. .sl-card-body {
  343. flex: 1;
  344. padding: 20rpx 24rpx;
  345. display: flex;
  346. flex-direction: column;
  347. justify-content: center;
  348. min-width: 0;
  349. }
  350. .sl-card-title {
  351. font-size: 28rpx;
  352. font-weight: 600;
  353. color: #1a1d26;
  354. line-height: 40rpx;
  355. overflow: hidden;
  356. text-overflow: ellipsis;
  357. display: -webkit-box;
  358. -webkit-line-clamp: 2;
  359. -webkit-box-orient: vertical;
  360. }
  361. .sl-card-addr {
  362. margin-top: 10rpx;
  363. font-size: 22rpx;
  364. color: #8a8d9e;
  365. line-height: 32rpx;
  366. overflow: hidden;
  367. text-overflow: ellipsis;
  368. display: -webkit-box;
  369. -webkit-line-clamp: 2;
  370. -webkit-box-orient: vertical;
  371. }
  372. .sl-card-views {
  373. margin-top: 12rpx;
  374. font-size: 22rpx;
  375. color: #4a8cff;
  376. }
  377. </style>