income.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <script lang="ts" setup>
  2. import { usePagination, useRequest } from 'alova/client'
  3. import { storeToRefs } from 'pinia'
  4. import { computed, ref, watch } from 'vue'
  5. import { getAccountCount } from '@/api/home'
  6. import { getCouponIssuerAccountByPageMap } from '@/api/income'
  7. import IncomeItem from '@/components/IncomeItem.vue'
  8. import MescrollUni from '@/components/mescroll.vue'
  9. import { useShare } from '@/hooks/useShare'
  10. import { useUserStore } from '@/store'
  11. import { useTokenStore } from '@/store/token'
  12. import { changtime, menuButtonInfo, rpxToPx, safeAreaInsets, systemInfo } from '@/utils'
  13. import { getImageUrl } from '@/utils/imageUtil'
  14. definePage({
  15. style: {
  16. navigationBarTitleText: '收益',
  17. navigationStyle: 'custom',
  18. },
  19. })
  20. const userStore = useUserStore()
  21. const tokenStore = useTokenStore()
  22. // 使用storeToRefs解构userInfo
  23. const { userInfo } = storeToRefs(userStore)
  24. const { hasLogin } = storeToRefs(tokenStore)
  25. // 发券人账户表-通过userId查询账户信息
  26. const { send: getAccountCountRequest, data: accountCountData } = useRequest(getAccountCount, {
  27. immediate: false,
  28. })
  29. // 新增:刷新状态
  30. const refreshing = ref(false)
  31. const activeIndex = ref(0)
  32. const showLoading = ref(false)
  33. const pickerDate = ref(Date.now())
  34. const pickerShow = ref(false)
  35. const totalAmount = ref(0)
  36. let mescroll = null
  37. // 新增:滚动距离和透明度
  38. const scrollTop = ref(0)
  39. const headerOverlayOpacity = computed(() => {
  40. // 当滚动距离超过150px时,完全显示纯色背景
  41. const headerHeightForRPX = 526
  42. const headerHeightForPx = rpxToPx(headerHeightForRPX)
  43. const maxScroll = menuButtonInfo.height + safeAreaInsets.top + 64 + (headerHeightForPx - (menuButtonInfo.height + safeAreaInsets.top + 64)) / 2
  44. return Math.min(scrollTop.value / maxScroll, 1)
  45. })
  46. // 新增:监听页面滚动
  47. onPageScroll((e) => {
  48. scrollTop.value = e.scrollTop
  49. })
  50. const { send, data: incomeData, pageCount, loading, page, pageSize, isLastPage, refresh, reload, onSuccess, onError } = usePagination((page, pageSize) => getCouponIssuerAccountByPageMap({
  51. pageNo: page,
  52. pageSize,
  53. type: 1,
  54. status: activeIndex.value,
  55. year: changtime(pickerDate.value, 'YYYY'),
  56. month: changtime(pickerDate.value, 'MM'),
  57. }), {
  58. initialPage: 1,
  59. initialPageSize: 10,
  60. append: true,
  61. preloadNextPage: false,
  62. preloadPreviousPage: false,
  63. total: response => response?.total || 0,
  64. data: response => response?.detailList || [],
  65. immediate: false,
  66. })
  67. watch(() => incomeData.value, (newVal) => {
  68. if (newVal.length === 0 && !loading.value) {
  69. mescroll.showEmpty()
  70. }
  71. else {
  72. mescroll.removeEmpty()
  73. }
  74. })
  75. // ============ mescroll 配置 ============
  76. const downOption = reactive({
  77. use: true,
  78. auto: false,
  79. bgColor: '#ffffff',
  80. textColor: '#666',
  81. textInOffset: '下拉刷新',
  82. textOutOffset: '释放刷新',
  83. textLoading: '刷新中...',
  84. offset: 80,
  85. })
  86. const upOption = reactive({
  87. use: true,
  88. auto: false,
  89. noMoreSize: 7,
  90. isBounce: true,
  91. page: {
  92. num: page.value,
  93. size: pageSize.value,
  94. },
  95. empty: {
  96. use: true,
  97. tip: '暂无数据',
  98. btnText: '刷新试试',
  99. icon: '/static/images/mescroll-empty.png',
  100. fixed: false,
  101. top: '200rpx',
  102. zIndex: 1,
  103. },
  104. textNoMore: '--- 已经到底了 ---',
  105. toTop: {
  106. src: '/static/images/mescroll-totop.png',
  107. offset: 1000,
  108. bottom: 120,
  109. },
  110. // 开启虚拟列表优化(大数据量时)
  111. virtual: {
  112. use: false,
  113. rowHeight: 300,
  114. }
  115. })
  116. function mescrollInit(ref) {
  117. mescroll = ref
  118. }
  119. async function downCallback() {
  120. console.log('下拉刷新触发')
  121. // await refresh()
  122. await getAccountCountRequest()
  123. await refresh()
  124. await userStore.fetchUserInfo()
  125. }
  126. async function onRefresh() {
  127. refreshing.value = true
  128. await getAccountCountRequest()
  129. await refresh()
  130. await userStore.fetchUserInfo()
  131. refreshing.value = false
  132. }
  133. async function upCallback() {
  134. console.log('上拉加载触发')
  135. if (isLastPage.value) {
  136. console.log(mescroll)
  137. mescroll.endSuccess(0, false)
  138. return
  139. }
  140. page.value++
  141. }
  142. onSuccess((response) => {
  143. let isNextPage = isLastPage.value
  144. // 此处处理alova isLastPage默认值为true,首次请求出现的无法加载下一页问题
  145. if (pageCount.value > page.value && isLastPage.value) {
  146. isNextPage = false
  147. }
  148. totalAmount.value = response.data?.totalAmount || 0
  149. // mescroll.setPageNum(page.value - 1)
  150. mescroll.endSuccess(incomeData.value.length, !isNextPage)
  151. })
  152. onError(() => {
  153. console.log('请求失败')
  154. mescroll.endErr()
  155. })
  156. onShow(async () => {
  157. await send()
  158. })
  159. async function tabChange(index) {
  160. // 切换tab时,重置上拉加载
  161. // mescroll.resetUpScroll(true)
  162. // mescroll.showDownScroll()
  163. activeIndex.value = index
  164. mescroll.hideUpScroll()
  165. mescroll.scrollTo(0, 0)
  166. showLoading.value = true
  167. await reload()
  168. showLoading.value = false
  169. }
  170. async function pickerConfirm() {
  171. pickerShow.value = false
  172. mescroll.scrollTo(0, 0)
  173. mescroll.hideUpScroll()
  174. showLoading.value = true
  175. await reload()
  176. showLoading.value = false
  177. }
  178. onShow(async () => {
  179. // 登录后查询收益数据
  180. if (hasLogin) {
  181. await getAccountCountRequest()
  182. await send()
  183. // 数据会在useScroll的onMounted中自动加载,这里不需要额外调用
  184. }
  185. })
  186. // 创建分享hook实例
  187. const { getShareConfig, getTimelineShareConfig } = useShare()
  188. // #ifdef MP-WEIXIN
  189. // 分享给好友生命周期函数
  190. onShareAppMessage(async (options) => {
  191. return await getShareConfig()
  192. })
  193. // 分享到朋友圈生命周期函数
  194. onShareTimeline(async () => {
  195. return await getTimelineShareConfig()
  196. })
  197. // #endif
  198. const isUnlock = computed(() => accountCountData && accountCountData?.status === 0)
  199. function goPage(page: string) {
  200. uni.navigateTo({
  201. url: `/pages-A/${page}/index`,
  202. })
  203. }
  204. </script>
  205. <template>
  206. <view class="profile-container" :style="{ minHeight: `calc(100vh - 100rpx)` }">
  207. <!-- <up-pull-refresh :refreshing="refreshing" :threshold="60" @refresh="onRefresh"> -->
  208. <!-- 顶部区域 -->
  209. <view class="income-header"
  210. :style="{ background: `url(${getImageUrl('@img/income/income-bg.png')})`, backgroundSize: '100% 110%', backgroundPosition: 'left bottom', backgroundRepeat: 'no-repeat' }">
  211. <view class="income-header-avatar-info"
  212. :style="{ paddingTop: `calc(${safeAreaInsets.top}px + ${menuButtonInfo.height}px)` }">
  213. <view class="income-header-balance">
  214. 当前账户余额(元)
  215. </view>
  216. <view class="income-header-balance-num">
  217. <view class="income-header-balance-num-amount">
  218. {{ accountCountData?.balance || 0 }}
  219. </view>
  220. <view class="income-header-balance-num-btns">
  221. <view v-if="isUnlock" class="income-header-balance-num-btn js" @click="goPage('unlockRewards')">
  222. 解锁
  223. </view>
  224. </view>
  225. </view>
  226. </view>
  227. <view class="income-header-tips">
  228. <view class="income-header-tips-item">
  229. <view class="income-header-tips-item-num">
  230. {{ accountCountData?.withdrawableAmount || 0 }}
  231. </view>
  232. <view class="income-header-tips-item-des">
  233. 可提现金额
  234. </view>
  235. </view>
  236. <view class="income-header-tips-item">
  237. <view class="income-header-tips-item-num">
  238. {{ accountCountData?.unsettledAmount || 0 }}
  239. </view>
  240. <view class="income-header-tips-item-des">
  241. 未结算金额
  242. </view>
  243. </view>
  244. <view class="income-header-tips-item">
  245. <view class="income-header-tips-item-num">
  246. {{ accountCountData?.totalCommission || 0 }}
  247. </view>
  248. <view class="income-header-tips-item-des">
  249. 累计收益
  250. </view>
  251. </view>
  252. </view>
  253. </view>
  254. <!-- 公告 -->
  255. <view v-if="isUnlock" class="income-header-notice">
  256. <view class="income-header-notice-icon">
  257. <image :src="getImageUrl('@img/income/notice.png')" mode="aspectFit" />
  258. </view>
  259. <view class="income-header-notice-content">
  260. 在考核周期内未达标已锁定收益,<text>前往查看~</text>
  261. </view>
  262. </view>
  263. <!-- 菜单 -->
  264. <view class="income-header-menu" :style="{ marginTop: isUnlock ? '24rpx' : '-50rpx' }">
  265. <view class="income-header-overlay"
  266. :style="{ opacity: headerOverlayOpacity, height: `calc(${menuButtonInfo.bottom + 4}px + 50rpx)` }" />
  267. <!-- 结算筛选 -->
  268. <view class="income-header-menu-filter" :style="{ top: `${menuButtonInfo.bottom + 4}px` }">
  269. <view class="income-header-menu-filter-item" :class="[activeIndex === 0 ? 'active' : '']"
  270. @click="tabChange(0)">
  271. 待结算
  272. </view>
  273. <view class="income-header-menu-filter-item" :class="[activeIndex === 1 ? 'active' : '']"
  274. @click="tabChange(1)">
  275. 已结算
  276. </view>
  277. </view>
  278. <!-- 时间筛选 -->
  279. <view class="income-menu-time-filter" :style="{ top: `calc(${menuButtonInfo.bottom + 4}px + 90rpx)` }">
  280. <view class="income-menu-time-filter-text" @click="pickerShow = true">
  281. <text>{{ changtime(pickerDate) }}</text>
  282. <up-icon name="arrow-down" color="#666666" size="14" />
  283. <up-datetime-picker v-model="pickerDate" :show="pickerShow" mode="year-month" close-on-click-overlay
  284. @confirm="pickerConfirm" @cancel="pickerShow = false" />
  285. </view>
  286. <view>
  287. 合计:¥{{ totalAmount }}
  288. </view>
  289. </view>
  290. <view class="income-menu-list">
  291. <mescroll-uni id="mescrollContainer" :down="downOption" :up="upOption" :fixed="false"
  292. :is-show-empty="true" @init="mescrollInit" @down="downCallback" @up="upCallback"
  293. @emptyclick="reload">
  294. <view v-for="item in incomeData" :key="item.userId" class="income-content-item">
  295. <income-item :data="item" :type="activeIndex" />
  296. </view>
  297. <!-- 滚动区域内的loading遮罩 -->
  298. <view v-if="showLoading && loading" class="loading-mask">
  299. <view class="loading-spinner" />
  300. <text class="loading-text">加载中...</text>
  301. </view>
  302. </mescroll-uni>
  303. </view>
  304. </view>
  305. <!-- </up-pull-refresh> -->
  306. </view>
  307. </template>
  308. <style lang="scss" scoped>
  309. .profile-container {
  310. background-color: #f5f5f5;
  311. line-height: 1;
  312. display: flex;
  313. flex-direction: column;
  314. /* 添加这行,设置容器高度为视口高度 */
  315. .income-header {
  316. height: 526rpx;
  317. .income-header-avatar-info {
  318. display: flex;
  319. flex-direction: column;
  320. gap: 30rpx;
  321. padding: 0 24rpx;
  322. .income-header-balance {
  323. font-weight: 400;
  324. font-size: 26rpx;
  325. color: #ffffff;
  326. }
  327. .income-header-balance-num {
  328. display: flex;
  329. justify-content: space-between;
  330. align-items: center;
  331. .income-header-balance-num-amount {
  332. font-weight: 500;
  333. font-size: 65rpx;
  334. color: #ffffff;
  335. }
  336. .income-header-balance-num-btns {
  337. display: flex;
  338. gap: 15rpx;
  339. .income-header-balance-num-btn {
  340. padding: 20rpx 40rpx;
  341. border-radius: 33rpx;
  342. font-weight: 400;
  343. font-size: 26rpx;
  344. &.js {
  345. background: #da4c47;
  346. color: #ffffff;
  347. }
  348. &.tx {
  349. background: #bfbfbf;
  350. color: #747474;
  351. }
  352. }
  353. }
  354. }
  355. }
  356. .income-header-tips {
  357. height: 124rpx;
  358. display: flex;
  359. background: linear-gradient(114deg, #f67873, #fb847f, #f67873);
  360. border-radius: 10rpx;
  361. margin: 47rpx 24rpx 0 24rpx;
  362. .income-header-tips-item {
  363. flex: 1;
  364. display: flex;
  365. flex-direction: column;
  366. justify-content: center;
  367. align-items: center;
  368. gap: 15rpx;
  369. font-weight: 500;
  370. font-size: 34rpx;
  371. color: #ffffff;
  372. line-height: 1;
  373. position: relative;
  374. &:not(:last-child):after {
  375. content: '';
  376. position: absolute;
  377. top: 50%;
  378. right: 0;
  379. transform: translateY(-50%);
  380. width: 2px;
  381. height: 40rpx;
  382. background: #ffffff;
  383. }
  384. .income-header-tips-item-num {
  385. font-weight: bold;
  386. }
  387. .income-header-tips-item-des {
  388. font-weight: 400;
  389. font-size: 24rpx;
  390. color: #ffffff;
  391. }
  392. }
  393. }
  394. }
  395. .income-header-notice {
  396. margin: -50rpx 24rpx 24rpx 24rpx;
  397. background: #ffffff;
  398. border-radius: 10rpx;
  399. display: flex;
  400. align-items: center;
  401. gap: 20rpx;
  402. padding: 28rpx 20rpx;
  403. .income-header-notice-icon {
  404. width: 52rpx;
  405. height: 35rpx;
  406. image {
  407. width: 100%;
  408. height: 100%;
  409. }
  410. }
  411. .income-header-notice-content {
  412. font-weight: 400;
  413. font-size: 24rpx;
  414. color: #333333;
  415. text {
  416. color: #c52d27;
  417. }
  418. }
  419. }
  420. .income-header-menu {
  421. // background: #ffffff;
  422. border-radius: 10rpx 10rpx 0rpx 0rpx;
  423. margin-left: 24rpx;
  424. margin-right: 23rpx;
  425. margin-bottom: 20rpx;
  426. flex: 1;
  427. display: flex;
  428. flex-direction: column;
  429. min-height: 0;
  430. // 新增:吸顶区域上方的覆盖元素
  431. .income-header-overlay {
  432. position: fixed;
  433. top: 0;
  434. left: 0;
  435. right: 0;
  436. background-color: #ed6b66;
  437. border-radius: 10rpx 10rpx 0rpx 0rpx;
  438. z-index: 99;
  439. pointer-events: none; // 确保不影响点击事件
  440. }
  441. .income-header-menu-filter {
  442. display: flex;
  443. justify-content: space-between;
  444. align-items: center;
  445. font-weight: 500;
  446. font-size: 32rpx;
  447. color: #333333;
  448. height: 90rpx;
  449. background: #ffffff;
  450. box-shadow: 0rpx 3rpx 7rpx 0rpx rgba(213, 213, 213, 0.29);
  451. border-radius: 10rpx 10rpx 0rpx 0rpx;
  452. position: sticky;
  453. top: 0;
  454. z-index: 100;
  455. .income-header-menu-filter-item {
  456. flex: 1;
  457. display: flex;
  458. justify-content: center;
  459. align-items: center;
  460. position: relative;
  461. height: 100%;
  462. &.active {
  463. color: #ed6b66;
  464. &:after {
  465. content: '';
  466. position: absolute;
  467. bottom: 0;
  468. left: 50%;
  469. transform: translateX(-50%);
  470. width: 67rpx;
  471. height: 6rpx;
  472. background: #ed6b66;
  473. border-radius: 3rpx;
  474. }
  475. }
  476. }
  477. }
  478. .income-menu-time-filter {
  479. display: flex;
  480. justify-content: space-between;
  481. align-items: center;
  482. font-weight: 400;
  483. font-size: 24rpx;
  484. color: #333333;
  485. padding: 30rpx 20rpx 14rpx;
  486. background: #ffffff;
  487. margin-top: 4rpx;
  488. position: sticky;
  489. z-index: 100;
  490. .income-menu-time-filter-text {
  491. font-weight: 400;
  492. font-size: 26rpx;
  493. color: #000000;
  494. display: flex;
  495. align-items: center;
  496. gap: 12rpx;
  497. }
  498. }
  499. .income-menu-list {
  500. background: #ffffff;
  501. // flex: 1;
  502. display: flex;
  503. flex-direction: column;
  504. position: relative;
  505. #mescrollContainer {
  506. height: 100%;
  507. flex: 1;
  508. }
  509. .income-content-item {
  510. margin-top: 20rpx;
  511. }
  512. // loading遮罩样式
  513. .loading-mask {
  514. position: absolute;
  515. top: 0;
  516. left: 0;
  517. right: 0;
  518. bottom: 0;
  519. background-color: rgba(255, 255, 255, 0.8);
  520. display: flex;
  521. flex-direction: column;
  522. justify-content: center;
  523. align-items: center;
  524. z-index: 999;
  525. }
  526. .loading-spinner {
  527. width: 40rpx;
  528. height: 40rpx;
  529. border: 4rpx solid #e0e0e0;
  530. border-top-color: #ed6b66;
  531. border-radius: 50%;
  532. animation: spin 1s linear infinite;
  533. }
  534. .loading-text {
  535. margin-top: 16rpx;
  536. font-size: 24rpx;
  537. color: #666;
  538. }
  539. @keyframes spin {
  540. to {
  541. transform: rotate(360deg);
  542. }
  543. }
  544. }
  545. }
  546. }
  547. </style>