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

10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 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: 0 // 竖屏方向,0代表竖屏,1代表横屏,-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>