爱简收旧衣按件回收前端代码仓库
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.

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