crowdFunding.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. <template>
  2. <view class="crowd-funding-page">
  3. <!-- 头部导航 -->
  4. <view class="navbar" ref="header">
  5. <view style="display: flex; align-items: center">
  6. <view
  7. class="nav-left"
  8. @click="goBack"
  9. >
  10. <text class="fa fa-angle-left back-icon"></text>
  11. </view>
  12. <view class="nav-title">众筹</view>
  13. </view>
  14. <!-- 搜索框 -->
  15. <view
  16. class="search-bar-container"
  17. @click="goPages('/pages/crowdFunding/Search')"
  18. >
  19. <view class="search-input-wrapper">
  20. <image
  21. src="/static/crowdFunding/search.png"
  22. class="search-icon"
  23. ></image>
  24. <input
  25. type="text"
  26. placeholder="搜索你感兴趣的内容"
  27. class="search-input"
  28. disabled
  29. />
  30. </view>
  31. </view>
  32. <view class="nav-right" @click="goPages('/pages/crowdFunding/favorites')">
  33. <image
  34. src="/static/crowdFunding/collect-active1.png"
  35. class="action-icon"
  36. ></image>
  37. </view>
  38. </view>
  39. <!-- Tab导航 -->
  40. <view class="tab-box" ref="tabbar">
  41. <scroll-view
  42. scroll-x
  43. class="tabs-scroll-view"
  44. :show-scrollbar="false"
  45. :scroll-into-view="'tab-' + (currentTab - 1)"
  46. scroll-with-animation
  47. >
  48. <view class="tabs-wrapper">
  49. <view
  50. v-for="(tab, index) in tabs"
  51. :key="index"
  52. :id="'tab-' + index"
  53. :class="['tab-item', currentTab === index ? 'active' : '']"
  54. @click="switchTab(index)"
  55. >
  56. <text>{{ tab.name }}</text>
  57. </view>
  58. <!-- 右侧占位空白 -->
  59. <view class="tab-placeholder"></view>
  60. </view>
  61. </scroll-view>
  62. <view class="mask"></view>
  63. </view>
  64. <!-- 内容区域:swiper实现左右滑动切换tab,每个tab一个scroll-view,支持下拉刷新 -->
  65. <swiper
  66. class="tab-swiper"
  67. :current="currentTab"
  68. @change="onSwiperChange"
  69. :style="{ height: swiperHeight + 'px', width: '100vw' }"
  70. >
  71. <swiper-item
  72. v-for="(tab, tabIndex) in tabs"
  73. :key="tabIndex"
  74. >
  75. <scroll-view
  76. scroll-y
  77. class="content-scroll"
  78. :style="{ height: swiperHeight + 'px' }"
  79. :refresher-enabled="true"
  80. :refresher-triggered="isRefreshing[tabIndex]"
  81. refresher-background="#f2f6f2"
  82. @refresherrefresh="onRefresh(tabIndex)"
  83. @scroll="(e) => onScroll(e, tabIndex)"
  84. :scroll-top="shouldRestoreScroll[tabIndex] ? (scrollTop[tabIndex] || 0) : undefined"
  85. @scrolltolower="onScrollToLower(tabIndex)"
  86. >
  87. <view class="items-grid">
  88. <CrowdFundingItem
  89. v-for="item in tabData[tabIndex]"
  90. :key="item.id"
  91. :item="item"
  92. @click="goToDetail(item.id)"
  93. />
  94. </view>
  95. <view v-if="loadingMore[tabIndex]" class="loading-more">加载中...</view>
  96. <view v-else-if="!hasMore[tabIndex]" class="no-more">没有更多了</view>
  97. </scroll-view>
  98. </swiper-item>
  99. </swiper>
  100. </view>
  101. </template>
  102. <script>
  103. import CrowdFundingItem from "./components/CrowdFundingItem/CrowdFundingItem.vue";
  104. export default {
  105. components: { CrowdFundingItem },
  106. data() {
  107. return {
  108. tabs: [
  109. { name: "全部", icon: "" },
  110. { name: "🌟创意", icon: "/static/icon/creative-star.png" },
  111. { name: "🧸潮玩", icon: "/static/icon/toy-bear.png" },
  112. { name: "📖出版", icon: "" },
  113. { name: "🃏桌游", icon: "" },
  114. { name: "🃏桌游", icon: "" },
  115. ],
  116. currentTab: 0,
  117. tabData: [[], [], [], [], [], []],
  118. isRefreshing: [false, false, false, false, false, false],
  119. scrollTop: {},
  120. shouldRestoreScroll: [true, true, true, true, true, true],
  121. swiperHeight: 600,
  122. page: [1, 1, 1, 1, 1, 1],
  123. hasMore: [true, true, true, true, true, true],
  124. loadingMore: [false, false, false, false, false, false],
  125. };
  126. },
  127. mounted() {
  128. // 动态获取头部和tab栏高度
  129. const sys = uni.getSystemInfoSync();
  130. const windowHeight = sys.windowHeight;
  131. this.$nextTick(() => {
  132. uni.createSelectorQuery()
  133. .in(this)
  134. .select('.navbar')
  135. .boundingClientRect(rect1 => {
  136. uni.createSelectorQuery()
  137. .in(this)
  138. .select('.tab-box')
  139. .boundingClientRect(rect2 => {
  140. const headerHeight = rect1 ? rect1.height : 0;
  141. const tabbarHeight = rect2 ? rect2.height : 0;
  142. this.swiperHeight = windowHeight - headerHeight - tabbarHeight;
  143. })
  144. .exec();
  145. })
  146. .exec();
  147. });
  148. // 初始化加载第一个tab数据
  149. this.fetchData(0);
  150. },
  151. methods: {
  152. goBack() {
  153. uni.navigateBack();
  154. },
  155. goPages(url) {
  156. uni.navigateTo({
  157. url,
  158. });
  159. },
  160. switchTab(index) {
  161. this.currentTab = index;
  162. this.$set(this.shouldRestoreScroll, index, true);
  163. if (!this.tabData[index] || this.tabData[index].length === 0) {
  164. this.fetchData(index);
  165. }
  166. },
  167. onSwiperChange(e) {
  168. this.switchTab(e.detail.current);
  169. },
  170. async onRefresh(tabIndex) {
  171. this.$set(this.isRefreshing, tabIndex, true);
  172. await this.fetchData(tabIndex, true);
  173. this.$set(this.isRefreshing, tabIndex, false);
  174. },
  175. onScroll(e, tabIndex) {
  176. this.$set(this.scrollTop, tabIndex, e.detail.scrollTop);
  177. if (this.shouldRestoreScroll[tabIndex]) {
  178. this.$set(this.shouldRestoreScroll, tabIndex, false);
  179. }
  180. },
  181. async onScrollToLower(tabIndex) {
  182. if (!this.hasMore[tabIndex] || this.loadingMore[tabIndex]) return;
  183. this.$set(this.loadingMore, tabIndex, true);
  184. await this.fetchData(tabIndex, false, true);
  185. this.$set(this.loadingMore, tabIndex, false);
  186. },
  187. async fetchData(tabIndex, isRefresh = false, isLoadMore = false) {
  188. if (isRefresh) {
  189. this.page[tabIndex] = 1;
  190. this.hasMore[tabIndex] = true;
  191. }
  192. if (isLoadMore) {
  193. this.page[tabIndex]++;
  194. } else {
  195. this.page[tabIndex] = 1;
  196. }
  197. return new Promise((resolve) => {
  198. setTimeout(() => {
  199. const now = Date.now();
  200. const page = this.page[tabIndex];
  201. let demo = [];
  202. if (page <= 3) {
  203. for (let i = 0; i < 4; i++) {
  204. demo.push({
  205. id: now + page * 100 + i,
  206. imageUrl: `/static/crowdFunding/top-img.png`,
  207. title: `糖指数100%12分BJD可动人偶盲盒_${page}_${i}`,
  208. raisedAmount: (769.8 + Math.floor(Math.random() * 100)).toString(),
  209. supporters: 3788 + Math.floor(Math.random() * 100),
  210. });
  211. }
  212. }
  213. if (isLoadMore) {
  214. if (demo.length) {
  215. this.$set(this.tabData, tabIndex, [...this.tabData[tabIndex], ...demo]);
  216. }
  217. } else {
  218. this.$set(this.tabData, tabIndex, demo);
  219. }
  220. this.$set(this.hasMore, tabIndex, page < 3);
  221. resolve();
  222. }, 1000);
  223. });
  224. },
  225. goToDetail(id) {
  226. // 跳转详情页
  227. uni.navigateTo({ url: '/pages/crowdFunding/crowdfundingDetails?id=' + id });
  228. },
  229. },
  230. };
  231. </script>
  232. <style lang="scss" scoped>
  233. .crowd-funding-page {
  234. display: flex;
  235. flex-direction: column;
  236. background: #f2f6f2;
  237. height: 100vh; // 移除,避免撑死内容
  238. }
  239. .navbar {
  240. display: flex;
  241. align-items: center;
  242. justify-content: space-between;
  243. // height: 96rpx;
  244. background: #fff;
  245. padding: 46rpx 20rpx 26rpx 30rpx;
  246. padding-top: calc(var(--status-bar-height) + 46rpx);
  247. box-sizing: content-box;
  248. .nav-left {
  249. width: 36rpx;
  250. display: flex;
  251. align-items: center;
  252. .back-icon {
  253. font-size: 50rpx;
  254. }
  255. }
  256. .nav-title {
  257. font-size: 36rpx;
  258. font-weight: bold;
  259. color: #222;
  260. letter-spacing: 2rpx;
  261. }
  262. .search-bar-container {
  263. padding: 0 24rpx;
  264. display: flex;
  265. align-items: center;
  266. width: 450rpx;
  267. height: 64rpx;
  268. background: #f2f6f2;
  269. border-radius: 36rpx;
  270. ::v-deep.input-placeholder {
  271. color: #999;
  272. }
  273. .search-input-wrapper {
  274. display: flex;
  275. align-items: center;
  276. width: 100%;
  277. .search-icon {
  278. width: 32rpx;
  279. height: 32rpx;
  280. margin-right: 10rpx;
  281. }
  282. .search-input {
  283. font-size: 28rpx;
  284. color: #999;
  285. line-height: 64rpx;
  286. background: transparent;
  287. border: none;
  288. outline: none;
  289. width: 100%;
  290. }
  291. }
  292. }
  293. .nav-right {
  294. .action-icon {
  295. width: 64rpx;
  296. height: 64rpx;
  297. border-radius: 50%;
  298. }
  299. }
  300. }
  301. .tab-box {
  302. width: 100vw;
  303. height: auto;
  304. position: relative;
  305. left: 0;
  306. top: 0;
  307. border-bottom: solid 5rpx #F2F6F2;
  308. .mask {
  309. position: absolute;
  310. right: 0;
  311. top: 0;
  312. width: 200rpx;
  313. height: 100%;
  314. background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #ffffff 100%);
  315. pointer-events: none;
  316. }
  317. }
  318. .tabs-scroll-view {
  319. width: 100%;
  320. background: #fff;
  321. padding-bottom: 22rpx;
  322. .tabs-wrapper {
  323. display: flex;
  324. align-items: center;
  325. padding-left: 12rpx;
  326. padding-right: 200rpx;
  327. box-sizing: content-box;
  328. }
  329. }
  330. .tab-item {
  331. display: flex;
  332. align-items: center;
  333. font-size: 28rpx;
  334. color: #666;
  335. padding: 8rpx 28rpx;
  336. margin-right: 8rpx;
  337. border-radius: 32rpx;
  338. font-weight: 500;
  339. background: transparent;
  340. transition: background 0.2s, color 0.2s;
  341. text {
  342. white-space: nowrap;
  343. }
  344. text-wrap: nowrap;
  345. .tab-icon {
  346. width: 32rpx;
  347. height: 32rpx;
  348. margin-right: 8rpx;
  349. }
  350. &.active {
  351. font-weight: bold;
  352. color: #fff;
  353. background: #222;
  354. box-shadow: 0 2rpx 8rpx rgba(34, 34, 34, 0.08);
  355. .tab-icon {
  356. filter: brightness(1.5) saturate(2);
  357. }
  358. }
  359. }
  360. .tab-swiper {
  361. width: 100vw;
  362. background: #f2f6f2;
  363. }
  364. .swiper-item {
  365. background: #f2f6f2;
  366. }
  367. .content-scroll {
  368. overflow-y: auto;
  369. padding-bottom: 24rpx;
  370. background: #f2f6f2;
  371. }
  372. .items-grid {
  373. display: grid;
  374. grid-template-columns: repeat(2, 1fr);
  375. gap: 24rpx 12rpx;
  376. padding: 16rpx 12rpx 0 12rpx;
  377. background: #f2f6f2;
  378. }
  379. .tab-placeholder {
  380. width: 200rpx;
  381. flex-shrink: 0;
  382. height: 1px; // 不影响高度
  383. }
  384. .loading-more { text-align: center; color: #999; font-size: 26rpx; padding: 24rpx 0; }
  385. .no-more { text-align: center; color: #ccc; font-size: 24rpx; padding: 20rpx 0; }
  386. </style>