爱简收旧衣按件回收前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

759 lines
24 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar" :style="{paddingTop: statusBarHeight + 'px'}">
  5. <view class="back" @tap="goBack">
  6. <uni-icons type="left" size="20" color="#fff"></uni-icons>
  7. </view>
  8. <text class="nav-title">钱包流水</text>
  9. <view class="nav-icons">
  10. <!-- 占位元素保持布局对称 -->
  11. </view>
  12. </view>
  13. <!-- banner -->
  14. <view class="banner"
  15. :style="{ marginTop: 'calc(150rpx)', height: (bannerBaseHeight + statusBarHeight + 88) + 'rpx' }">
  16. <swiper :indicator-dots="false" :autoplay="true" :interval="3000" :duration="500" circular
  17. style="width: 100%; height: 100%;">
  18. <swiper-item v-for="(item, index) in bannerList" :key="item.id || index">
  19. <view v-if="item.type == 1" class="video-container">
  20. <!-- 预览状态显示封面图 -->
  21. <image
  22. v-if="!videoPlayingStates[index]"
  23. :src="item.image || ''"
  24. mode="aspectFill"
  25. style="width: 100%; height: 100%;"
  26. class="video-poster"
  27. @click="playVideoFullscreen(item, index)"
  28. />
  29. <!-- 播放状态显示视频 -->
  30. <video
  31. v-else
  32. :id="`wallet-video-${index}`"
  33. :src="item.voUrl"
  34. :autoplay="true"
  35. :muted="false"
  36. :loop="false"
  37. :controls="true"
  38. :show-play-btn="true"
  39. :show-center-play-btn="false"
  40. :show-fullscreen-btn="true"
  41. :show-progress="true"
  42. :show-mute-btn="true"
  43. :enable-progress-gesture="true"
  44. :enable-play-gesture="true"
  45. object-fit="cover"
  46. style="width: 100%; height: 100%;"
  47. @fullscreenchange="onFullscreenChange"
  48. @play="onVideoPlay(index)"
  49. @pause="onVideoPause(index)"
  50. @ended="onVideoEnded(index)"
  51. ></video>
  52. <!-- 播放按钮覆盖层 -->
  53. <view v-if="!videoPlayingStates[index]" class="video-overlay" @click="playVideoFullscreen(item, index)">
  54. <view class="play-button-large">
  55. <view class="play-triangle"></view>
  56. </view>
  57. </view>
  58. </view>
  59. <image v-else :src="item.image" mode="aspectFill" style="width: 100%; height: 100%;" />
  60. </swiper-item>
  61. </swiper>
  62. </view>
  63. <!-- 账户余额 -->
  64. <view class="balance-card">
  65. <view class="balance-info">
  66. <text class="label">账户</text>
  67. <view class="amount">
  68. <text class="symbol">¥</text>
  69. <text class="value">{{ userInfo.money || '0.00' }}</text>
  70. </view>
  71. </view>
  72. <button class="withdraw-btn" @tap="withdraw">提现</button>
  73. </view>
  74. <!-- 记录切换标签 -->
  75. <view class="record-tabs">
  76. <view class="tab-item" :class="{ active: currentTab === 'settlement' }" @tap="switchTab('settlement')">
  77. 结算日志
  78. </view>
  79. <view class="tab-item" :class="{ active: currentTab === 'withdrawal' }" @tap="switchTab('withdrawal')">
  80. 提现记录
  81. </view>
  82. </view>
  83. <!-- 记录列表 -->
  84. <view class="record-item" v-for="(item, index) in recordList" :key="index">
  85. <view class="record-info">
  86. <view class="record-left">
  87. <text class="type">{{ item.title || (currentTab === 'settlement' ? '结算记录' : '提现记录') }}</text>
  88. <!-- 结算记录显示状态 -->
  89. <view class="status" v-if="currentTab === 'settlement' && item.state === 0">
  90. <text class="status-text">24小时后结算</text>
  91. </view>
  92. <!-- 提现记录显示时间 -->
  93. <text v-if="currentTab === 'withdrawal'" class="date">{{ formatDate(item.createTime) }}</text>
  94. </view>
  95. <view class="record-right">
  96. <text class="amount">¥{{ item.money || '0.00' }}</text>
  97. <!-- 结算记录显示时间 -->
  98. <text v-if="currentTab === 'settlement'" class="date">{{ formatDate(item.createTime) }}</text>
  99. <!-- 提现记录显示操作按钮 -->
  100. <template v-if="currentTab === 'withdrawal'">
  101. <button v-if="item.state === 0" class="withdraw-btn" @tap="receiveWithdrawal(item, index)">
  102. 提现
  103. </button>
  104. <text v-else class="received-text">已到账</text>
  105. </template>
  106. </view>
  107. </view>
  108. </view>
  109. <!-- 初始加载状态 -->
  110. <view v-if="loading && !isInitialized" class="loading-more">
  111. <text>数据加载中...</text>
  112. </view>
  113. <!-- 空状态 -->
  114. <view v-if="recordList.length === 0 && !loading && isInitialized" class="empty-state">
  115. <text>{{ currentTab === 'settlement' ? '暂无结算记录' : '暂无提现记录' }}</text>
  116. </view>
  117. <!-- 调试信息临时启用查看状态 -->
  118. <!-- <view class="debug-info" style="padding: 20rpx; background: #f0f0f0; margin: 20rpx; font-size: 24rpx; border-radius: 10rpx;">
  119. <text>recordList.length: {{ recordList.length }}</text><br/>
  120. <text>loading: {{ loading }}</text><br/>
  121. <text>isInitialized: {{ isInitialized }}</text><br/>
  122. <text>currentTab: {{ currentTab }}</text><br/>
  123. <text>loadingMore: {{ loadingMore }}</text><br/>
  124. <text>noMore: {{ noMore }}</text>
  125. </view> -->
  126. <!-- 加载更多状态 -->
  127. <view v-if="loadingMore" class="loading-more">
  128. <text>加载更多中...</text>
  129. </view>
  130. <!-- 没有更多 -->
  131. <view v-if="noMore && recordList.length > 0" class="no-more">
  132. <text>没有更多了</text>
  133. </view>
  134. </view>
  135. </template>
  136. <script>
  137. import pullRefreshMixin from '@/pages/mixins/pullRefreshMixin.js'
  138. import api from '@/api/api.js'
  139. export default {
  140. mixins: [pullRefreshMixin],
  141. data() {
  142. return {
  143. statusBarHeight: 0,
  144. bannerBaseHeight: 300,
  145. currentTab: 'settlement',
  146. userInfo: {
  147. money: '0.00'
  148. },
  149. recordList: [],
  150. pageNum: 1,
  151. pageSize: 20,
  152. loading: false,
  153. loadingMore: false,
  154. noMore: false,
  155. refreshing: false,
  156. isInitialized: false,
  157. bannerList: [],
  158. videoPlayingStates: {} // 记录每个视频的播放状态
  159. }
  160. },
  161. computed: {
  162. },
  163. onLoad() {
  164. this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight
  165. this.initData()
  166. },
  167. onPullDownRefresh() {
  168. this.onRefresh()
  169. },
  170. onReachBottom(){
  171. this.loadMore()
  172. },
  173. onUnload() {
  174. uni.$off('bannerListUpdated')
  175. },
  176. methods: {
  177. async initData() {
  178. this.getBannerList()
  179. await this.getUserInfo()
  180. await this.loadRecords()
  181. },
  182. getBannerList() {
  183. // 从全局数据获取轮播图
  184. this.bannerList = getApp().globalData.bannerList || []
  185. // 监听轮播图数据更新
  186. uni.$on('bannerListUpdated', () => {
  187. this.bannerList = getApp().globalData.bannerList || []
  188. })
  189. },
  190. async getUserInfo() {
  191. try {
  192. const res = await api('getUserByToken')
  193. if (res.code === 200 && res.data) {
  194. this.userInfo = res.data
  195. }
  196. } catch (error) {
  197. console.error('获取用户信息失败:', error)
  198. }
  199. },
  200. async loadRecords(isRefresh = false) {
  201. if (this.loading) return
  202. this.loading = true
  203. // 如果是刷新,重置分页状态
  204. if (isRefresh) {
  205. this.pageNum = 1
  206. this.noMore = false
  207. this.recordList = []
  208. }
  209. try {
  210. // 获取记录
  211. const res = await api('getMyMoneyLogPage', {
  212. status: this.currentTab === 'settlement' ? 0 : 1,
  213. pageNum: this.pageNum,
  214. pageSize: this.pageSize
  215. })
  216. if (res.code === 200 && res.data) {
  217. const records = res.data.records || []
  218. if (isRefresh) {
  219. this.recordList = records
  220. } else {
  221. this.recordList = [...this.recordList, ...records]
  222. }
  223. if (records.length < this.pageSize) {
  224. this.noMore = true
  225. }
  226. } else {
  227. this.recordList = []
  228. }
  229. } catch (error) {
  230. console.error('获取流水记录失败:', error)
  231. uni.showToast({
  232. title: '获取数据失败',
  233. icon: 'none'
  234. })
  235. } finally {
  236. this.loading = false
  237. this.isInitialized = true
  238. }
  239. },
  240. formatDate(timestamp) {
  241. if (!timestamp) return ''
  242. const date = new Date(timestamp)
  243. const month = String(date.getMonth() + 1).padStart(2, '0')
  244. const day = String(date.getDate()).padStart(2, '0')
  245. return `${month}-${day}`
  246. },
  247. async onRefresh() {
  248. this.refreshing = true
  249. try {
  250. await this.getUserInfo()
  251. await this.loadRecords(true)
  252. } finally {
  253. this.refreshing = false
  254. uni.stopPullRefresh()
  255. }
  256. },
  257. goBack() {
  258. uni.navigateBack()
  259. },
  260. async switchTab(tab) {
  261. this.currentTab = tab
  262. // 切换标签时重新加载数据
  263. this.pageNum = 1
  264. this.noMore = false
  265. this.recordList = []
  266. this.isInitialized = false
  267. await this.loadRecords()
  268. },
  269. withdraw() {
  270. uni.navigateTo({
  271. url: '/pages/subcomponent/withdraw'
  272. })
  273. },
  274. async receiveWithdrawal(item, index) {
  275. try {
  276. // 验证提现金额
  277. if (!item.money || parseFloat(item.money) <= 0) {
  278. uni.showToast({
  279. title: '提现金额无效',
  280. icon: 'none'
  281. })
  282. return
  283. }
  284. // 获取用户名
  285. const userName = this.userInfo.name || this.userInfo.nickName || this.userInfo.userName
  286. if (!userName) {
  287. uni.showToast({
  288. title: '请先完善个人信息',
  289. icon: 'none'
  290. })
  291. return
  292. }
  293. // 显示确认弹窗
  294. const [error, res] = await uni.showModal({
  295. title: '确认提现',
  296. content: `确认提现金额 ¥${item.money} 到您的账户?`,
  297. confirmText: '确认提现',
  298. cancelText: '取消'
  299. })
  300. if (error || !res.confirm) {
  301. return
  302. }
  303. // 显示加载状态
  304. uni.showLoading({
  305. title: '正在提现...'
  306. })
  307. // 调用提现接口
  308. const result = await api('withdraw', {
  309. userName: userName,
  310. money: parseFloat(item.money)
  311. })
  312. if (result.code === 200) {
  313. // 更新本地状态
  314. this.recordList[index].state = 1
  315. uni.hideLoading()
  316. uni.showToast({
  317. title: '提现成功',
  318. icon: 'success'
  319. })
  320. // 重新获取用户信息更新余额
  321. await this.getUserInfo()
  322. } else {
  323. throw new Error(result.message || result.msg || '提现失败')
  324. }
  325. } catch (error) {
  326. uni.hideLoading()
  327. console.error('提现失败:', error)
  328. uni.showToast({
  329. title: error.message || '提现失败,请重试',
  330. icon: 'none'
  331. })
  332. }
  333. },
  334. async loadMore() {
  335. if (this.noMore || this.loadingMore) return
  336. this.loadingMore = true
  337. this.pageNum++
  338. try {
  339. const res = await api('getMyMoneyLogPage', {
  340. status: this.currentTab === 'settlement' ? 0 : 1,
  341. pageNum: this.pageNum,
  342. pageSize: this.pageSize
  343. })
  344. if (res.code === 200 && res.data) {
  345. const records = res.data.records || []
  346. this.recordList = [...this.recordList, ...records]
  347. if (records.length < this.pageSize) {
  348. this.noMore = true
  349. }
  350. }
  351. } catch (error) {
  352. console.error('加载更多记录失败:', error)
  353. this.pageNum-- // 失败时回退页码
  354. uni.showToast({
  355. title: '加载更多数据失败',
  356. icon: 'none'
  357. })
  358. } finally {
  359. this.loadingMore = false
  360. }
  361. },
  362. // 视频相关方法
  363. playVideoFullscreen(item, index) {
  364. if (!this.videoPlayingStates[index]) {
  365. // 第一次点击:显示视频并开始播放
  366. this.$set(this.videoPlayingStates, index, true);
  367. // 等待视频元素渲染后再进行操作
  368. this.$nextTick(() => {
  369. setTimeout(() => {
  370. const videoContext = uni.createVideoContext(`wallet-video-${index}`, this);
  371. if (videoContext) {
  372. // 直接进入全屏播放
  373. videoContext.requestFullScreen({
  374. direction: -1 // 自动选择方向
  375. });
  376. }
  377. }, 200);
  378. });
  379. }
  380. },
  381. onVideoPlay(index) {
  382. this.$set(this.videoPlayingStates, index, true);
  383. },
  384. onVideoPause(index) {
  385. this.$set(this.videoPlayingStates, index, false);
  386. },
  387. onVideoEnded(index) {
  388. // 视频播放结束,回到预览状态
  389. this.$set(this.videoPlayingStates, index, false);
  390. },
  391. onFullscreenChange(e) {
  392. console.log('全屏状态改变:', e.detail);
  393. const videoIndex = e.target.id.replace('wallet-video-', '');
  394. if (e.detail.fullScreen) {
  395. // 进入全屏时,不做任何操作
  396. console.log('进入全屏模式,方向:', e.detail.direction);
  397. } else {
  398. // 退出全屏时,回到预览状态
  399. console.log('退出全屏模式,回到预览状态');
  400. this.$set(this.videoPlayingStates, videoIndex, false);
  401. }
  402. }
  403. }
  404. }
  405. </script>
  406. <style lang="scss" scoped>
  407. .container {
  408. min-height: 100vh;
  409. background: #fff;
  410. }
  411. .nav-bar {
  412. display: flex;
  413. align-items: center;
  414. height: calc(150rpx + var(--status-bar-height));
  415. padding: 0 32rpx;
  416. background: linear-gradient(to right, #f68240 0%, #fc8940 10%);
  417. position: fixed;
  418. top: 0;
  419. left: 0;
  420. right: 0;
  421. z-index: 999;
  422. box-sizing: border-box;
  423. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
  424. }
  425. .back {
  426. padding: 20rpx;
  427. margin-left: -20rpx;
  428. }
  429. .nav-title {
  430. flex: 1;
  431. text-align: center;
  432. font-size: 32rpx;
  433. font-weight: 500;
  434. color: #fff;
  435. }
  436. .nav-icons {
  437. display: flex;
  438. align-items: center;
  439. gap: 12px;
  440. }
  441. .banner {
  442. position: relative;
  443. background: linear-gradient(to right, #f78b49, #fc8940);
  444. overflow: hidden;
  445. border-radius: 0 0 30rpx 30rpx;
  446. margin-top: 0;
  447. .video-container {
  448. position: relative;
  449. width: 100%;
  450. height: 100%;
  451. .video-poster {
  452. cursor: pointer;
  453. transition: transform 0.2s ease;
  454. &:active {
  455. transform: scale(0.98);
  456. }
  457. }
  458. .video-overlay {
  459. position: absolute;
  460. top: 0;
  461. left: 0;
  462. right: 0;
  463. bottom: 0;
  464. background: rgba(0, 0, 0, 0.4);
  465. z-index: 10;
  466. cursor: pointer;
  467. display: flex;
  468. justify-content: center;
  469. align-items: center;
  470. .play-button-large {
  471. width: 120rpx;
  472. height: 120rpx;
  473. background: transparent;
  474. border-radius: 50%;
  475. display: flex;
  476. justify-content: center;
  477. align-items: center;
  478. backdrop-filter: blur(10rpx);
  479. transition: all 0.3s ease;
  480. box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.3);
  481. &:active {
  482. transform: scale(0.9);
  483. background: rgba(255, 255, 255, 0.1);
  484. }
  485. .play-triangle {
  486. width: 0;
  487. height: 0;
  488. border-left: 24rpx solid #fff;
  489. border-top: 18rpx solid transparent;
  490. border-bottom: 18rpx solid transparent;
  491. margin-left: 8rpx;
  492. transition: all 0.3s ease;
  493. filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3));
  494. }
  495. }
  496. &:hover .play-button-large {
  497. transform: scale(1.1);
  498. box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.4);
  499. .play-triangle {
  500. border-left-color: #ff8917;
  501. }
  502. }
  503. }
  504. }
  505. }
  506. /* .banner-content 样式已移除,现在使用轮播图 */
  507. .balance-card {
  508. margin: -60rpx 30rpx 0;
  509. padding: 30rpx;
  510. background: #fff;
  511. border-radius: 20rpx;
  512. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  513. display: flex;
  514. justify-content: space-between;
  515. align-items: center;
  516. position: relative;
  517. z-index: 2;
  518. .balance-info {
  519. .label {
  520. font-size: 28rpx;
  521. color: #666;
  522. }
  523. .amount {
  524. margin-top: 8rpx;
  525. display: flex;
  526. align-items: baseline;
  527. .symbol {
  528. font-size: 32rpx;
  529. color: #333;
  530. }
  531. .value {
  532. font-size: 48rpx;
  533. font-weight: bold;
  534. color: #333;
  535. margin-left: 4rpx;
  536. }
  537. }
  538. }
  539. .withdraw-btn {
  540. width: 160rpx;
  541. height: 70rpx;
  542. background: #FFB74D;
  543. color: #fff;
  544. font-size: 28rpx;
  545. border-radius: 35rpx;
  546. display: flex;
  547. align-items: center;
  548. justify-content: center;
  549. border: none;
  550. &::after {
  551. border: none;
  552. }
  553. }
  554. }
  555. .record-tabs {
  556. display: flex;
  557. padding: 20rpx 0;
  558. border-bottom: 1rpx solid #f5f5f5;
  559. .tab-item {
  560. position: relative;
  561. padding: 16rpx 0;
  562. font-size: 28rpx;
  563. color: #666;
  564. flex: 1;
  565. text-align: center;
  566. &.active {
  567. color: #333;
  568. font-weight: 500;
  569. &::after {
  570. content: '';
  571. position: absolute;
  572. left: 50%;
  573. transform: translateX(-50%);
  574. bottom: -21rpx;
  575. width: 48rpx;
  576. height: 2rpx;
  577. background: #FFB74D;
  578. }
  579. }
  580. }
  581. }
  582. .record-item {
  583. padding: 30rpx;
  584. border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
  585. .record-info {
  586. display: flex;
  587. justify-content: space-between;
  588. align-items: flex-start;
  589. .record-left {
  590. flex: 1;
  591. .type {
  592. font-size: 28rpx;
  593. color: #333;
  594. margin-bottom: 8rpx;
  595. }
  596. .status {
  597. display: inline-block;
  598. padding: 4rpx 8rpx;
  599. background: #FFB74D;
  600. border-radius: 8rpx;
  601. margin-top: 8rpx;
  602. .status-text {
  603. font-size: 22rpx;
  604. color: #fff;
  605. }
  606. }
  607. .date {
  608. font-size: 24rpx;
  609. color: #999;
  610. margin-top: 8rpx;
  611. }
  612. }
  613. .record-right {
  614. display: flex;
  615. flex-direction: column;
  616. align-items: flex-end;
  617. .amount {
  618. font-size: 28rpx;
  619. color: #333;
  620. font-weight: 500;
  621. margin-bottom: 8rpx;
  622. }
  623. .date {
  624. font-size: 24rpx;
  625. color: #999;
  626. }
  627. .withdraw-btn {
  628. width: 100rpx;
  629. height: 50rpx;
  630. background: #FFB74D;
  631. color: #fff;
  632. font-size: 24rpx;
  633. border-radius: 25rpx;
  634. display: flex;
  635. align-items: center;
  636. justify-content: center;
  637. border: none;
  638. margin-top: 8rpx;
  639. &::after {
  640. border: none;
  641. }
  642. }
  643. .received-text {
  644. font-size: 22rpx;
  645. color: #52C41A;
  646. margin-top: 8rpx;
  647. }
  648. }
  649. }
  650. }
  651. .empty-state {
  652. width: 100%;
  653. text-align: center;
  654. padding: 100rpx 0;
  655. color: #999;
  656. font-size: 28rpx;
  657. }
  658. .loading-more {
  659. text-align: center;
  660. padding: 20rpx 0;
  661. color: #999;
  662. font-size: 28rpx;
  663. }
  664. .no-more {
  665. text-align: center;
  666. padding: 20rpx 0;
  667. color: #999;
  668. font-size: 28rpx;
  669. }
  670. </style>