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

716 lines
19 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. <template>
  2. <view class="order-manage-container">
  3. <!-- 顶部导航栏 -->
  4. <view
  5. class="nav-bar"
  6. :style="{
  7. paddingTop: statusBarHeight + 'px',
  8. height: (statusBarHeight + navBarContentHeight) + 'px'
  9. }"
  10. >
  11. <uni-icons type="left" @tap="goBack" size="24" color="#222" />
  12. <text class="nav-title">{{ historyOrderMode ? '历史订单' : '订单管理' }}</text>
  13. </view>
  14. <!-- Tab栏 -->
  15. <scroll-view
  16. v-if="!historyOrderMode"
  17. class="order-tabs-scroll"
  18. scroll-x
  19. :style="{
  20. top: navBarRealHeight + 'px',
  21. height: tabBarHeight + 'px',
  22. position: 'fixed',
  23. left: 0,
  24. width: '100%',
  25. zIndex: 99
  26. }"
  27. >
  28. <view
  29. class="order-tabs"
  30. :style="{width: (tabs.length * 20) + '%'}"
  31. >
  32. <view
  33. v-for="(tab, idx) in tabs"
  34. :key="tab.value"
  35. :class="['tab-item', {active: currentTab === idx}]"
  36. @tap="onTabChange(idx)"
  37. >
  38. {{ tab.label }}
  39. </view>
  40. </view>
  41. </scroll-view>
  42. <!-- 搜索与筛选 -->
  43. <view
  44. v-if="!historyOrderMode"
  45. class="search-bar"
  46. :style="{
  47. position: 'fixed',
  48. zIndex: 10,
  49. top: (navBarRealHeight + tabBarHeight) + 'px',
  50. left: 0,
  51. width: '100vw',
  52. height: '40px'
  53. }"
  54. >
  55. <view class="search-bar-inner">
  56. <template v-if="!searchMode">
  57. <uni-icons class="search-icon" type="search" size="22" color="#999" @tap="onSearchIconClick" />
  58. <uni-icons class="scan-icon" type="scan" size="22" color="#999" @tap="scanCode" />
  59. </template>
  60. <template v-else>
  61. <view class="search-input-wrap">
  62. <uni-icons type="search" size="22" color="#999" />
  63. <input
  64. ref="searchInput"
  65. class="search-input"
  66. v-model="searchText"
  67. placeholder="请输入要查询的内容"
  68. placeholder-style="color:#ccc"
  69. />
  70. <uni-icons v-if="searchText" type="close" size="22" color="#ccc" @tap="onClearSearch" />
  71. </view>
  72. <text class="search-cancel" @tap="onCancelSearch">取消</text>
  73. </template>
  74. </view>
  75. </view>
  76. <!-- 订单卡片列表 -->
  77. <view class="order-list"
  78. :style="{paddingTop: (navBarRealHeight + (historyOrderMode ? 0 : (tabBarHeight + 16 + 40))) + 'px'}"
  79. >
  80. <view class="order-card" v-for="order in filteredOrders" :key="order.id" @tap="goToOrderDetail(order)">
  81. <view class="order-card-header">
  82. <text class="order-id">{{ order.orderNo }}</text>
  83. <view v-if="order.statusText === '不包邮'" class="order-status-tag red">{{ order.statusText }}</view>
  84. </view>
  85. <view class="order-info-wrapper">
  86. <view class="order-info">
  87. <view>
  88. <text class="info-label">用户名</text>
  89. <text class="info-value">{{ order.userName }}</text>
  90. </view>
  91. <view>
  92. <text class="info-label">电话</text>
  93. <text class="info-value">{{ order.phone }}</text>
  94. </view>
  95. <view v-if="order.appointTime">
  96. <text class="info-label">预约时间</text>
  97. <text class="info-value">{{ order.appointTime }}</text>
  98. </view>
  99. <view v-if="order.cancelTime">
  100. <text class="info-label">取消时间</text>
  101. <text class="info-value">{{ order.cancelTime }}</text>
  102. </view>
  103. <view v-if="order.qualityTime">
  104. <text class="info-label">质检时间</text>
  105. <text class="info-value">{{ order.qualityTime }}</text>
  106. </view>
  107. </view>
  108. <view class="order-status-label-bar order-info-status" :class="order.statusClass">{{ order.statusLabel }}</view>
  109. </view>
  110. <view class="order-card-footer" v-if="order.actions && order.actions.length && !historyOrderMode">
  111. <view class="order-actions-bar">
  112. <view class="action-btn-bar" v-for="action in order.actions" :key="action.text" @tap="action.text === '审批' ? goToOrderDetail(order) : null">
  113. <uni-icons :type="action.icon" size="28" color="#666" />
  114. <text>{{ action.text }}</text>
  115. </view>
  116. </view>
  117. </view>
  118. </view>
  119. <!-- 加载状态 -->
  120. <view v-if="isLoading && orderList.length === 0" class="loading-container">
  121. <uni-icons type="spinner-cycle" size="24" color="#999" class="loading-icon" />
  122. <text class="loading-text">加载中...</text>
  123. </view>
  124. <!-- 加载更多状态 -->
  125. <view v-if="loadingMore" class="load-more-container">
  126. <uni-icons type="spinner-cycle" size="20" color="#999" class="loading-icon" />
  127. <text class="load-more-text">加载更多...</text>
  128. </view>
  129. <!-- 到底提示 -->
  130. <view v-if="!hasMore && orderList.length > 0" class="no-more-container">
  131. <text class="no-more-text">已加载全部订单</text>
  132. </view>
  133. <!-- 空状态 -->
  134. <view v-if="!isLoading && orderList.length === 0" class="empty-container">
  135. <text class="empty-text">暂无订单数据</text>
  136. </view>
  137. </view>
  138. </view>
  139. </template>
  140. <script>
  141. import pullRefreshMixin from '../mixins/pullRefreshMixin.js'
  142. export default {
  143. mixins: [pullRefreshMixin],
  144. data() {
  145. return {
  146. statusBarHeight: 0,
  147. navBarContentHeight: 44,
  148. tabBarHeight: 48,
  149. navBarHeight: 44,
  150. navBarRealHeight: 44,
  151. tabs: [
  152. { label: '全部', value: -1 },
  153. { label: '已预约', value: 0 },
  154. { label: '待质检', value: 1 },
  155. { label: '已结款', value: 2 },
  156. { label: '已驳回', value: 3 }
  157. ],
  158. currentTab: 0,
  159. orderList: [],
  160. searchMode: false,
  161. searchText: '',
  162. historyOrderMode: false,
  163. pageNo: 1,
  164. pageSize: 10,
  165. hasMore: true,
  166. isLoading: false,
  167. loadingMore: false,
  168. userId: '',
  169. reachBottomTimer: null
  170. }
  171. },
  172. onLoad(options) {
  173. const sys = uni.getSystemInfoSync();
  174. this.statusBarHeight = sys.statusBarHeight;
  175. this.$nextTick(() => {
  176. uni.createSelectorQuery().select('.nav-bar').boundingClientRect(rect => {
  177. if (rect) {
  178. this.navBarRealHeight = rect.height;
  179. }
  180. }).exec();
  181. });
  182. if (options && options.historyOrder) {
  183. this.historyOrderMode = true;
  184. }
  185. if (options && options.userId) {
  186. this.userId = options.userId;
  187. }
  188. this.fetchOrderList()
  189. },
  190. computed: {
  191. filteredOrders() {
  192. if (this.searchText) {
  193. const text = this.searchText.toLowerCase();
  194. return this.orderList.filter(order =>
  195. (order.orderNo && order.orderNo.toLowerCase().includes(text)) ||
  196. (order.userName && order.userName.toLowerCase().includes(text)) ||
  197. (order.phone && order.phone.toLowerCase().includes(text))
  198. );
  199. }
  200. // 现在通过接口参数获取对应Tab的数据,不需要客户端过滤
  201. return this.orderList;
  202. }
  203. },
  204. methods: {
  205. goBack() {
  206. uni.navigateBack()
  207. },
  208. onTabChange(idx) {
  209. this.currentTab = idx
  210. this.pageNo = 1
  211. this.hasMore = true
  212. this.orderList = []
  213. this.isLoading = false
  214. this.loadingMore = false
  215. this.fetchOrderList()
  216. },
  217. onSearchIconClick() {
  218. this.searchMode = true;
  219. this.$nextTick(() => {
  220. this.$refs.searchInput && this.$refs.searchInput.focus();
  221. });
  222. },
  223. onClearSearch() {
  224. this.searchText = '';
  225. },
  226. onCancelSearch() {
  227. this.searchText = '';
  228. this.searchMode = false;
  229. },
  230. goToOrderDetail(order) {
  231. uni.navigateTo({
  232. url: '/pages/manager/order-detail?id=' + order.id
  233. })
  234. },
  235. refreshData() {
  236. // TODO: 实现订单列表刷新逻辑,如重新请求接口
  237. },
  238. async onRefresh() {
  239. await this.refreshData && this.refreshData()
  240. },
  241. fetchOrderList(isLoadMore) {
  242. if (this.isLoading) return
  243. if (isLoadMore && !this.hasMore) return
  244. console.log(isLoadMore,'isLoadMore')
  245. if (isLoadMore) {
  246. this.loadingMore = true
  247. } else {
  248. this.isLoading = true
  249. }
  250. // 根据当前Tab获取对应的status参数
  251. const tabValue = this.tabs[this.currentTab].value
  252. let statusParam = 0 // 默认值
  253. if (tabValue === -1) {
  254. // 全部:status = 0
  255. statusParam = 0
  256. } else if (tabValue === 0) {
  257. // 已预约:status = 0
  258. statusParam = 1
  259. } else if (tabValue === 1) {
  260. // 待质检:status = 1
  261. statusParam = 2
  262. } else if (tabValue === 2) {
  263. // 已结款:status = 2
  264. statusParam = 3
  265. } else if (tabValue === 3) {
  266. // 已驳回:status = 3
  267. statusParam = 4
  268. }
  269. const params = {
  270. pageNo: isLoadMore ? this.pageNo + 1 : 1,
  271. pageSize: this.pageSize,
  272. status: statusParam
  273. }
  274. console.log(params,'params')
  275. if (this.userId) {
  276. params.userId = this.userId;
  277. }
  278. this.$api && this.$api('getOrderList', params, res => {
  279. if (res && res.code === 200 && res.result && res.result.records) {
  280. console.log(res.result,'res.result.records')
  281. // 过滤掉 state为3 或者 status=3但是state为1 的订单
  282. const filteredRecords = res.result.records.filter(order => {
  283. // 过滤掉 state为3 的订单
  284. if (order.state === 3) return false
  285. // 过滤掉 status=3但是state为1 的订单
  286. // if (order.status === 3 && order.state === 1) return false
  287. return true
  288. })
  289. const newOrders = filteredRecords.map(order => {
  290. const statusInfo = this.getOrderStatusInfo(order.status, order.state)
  291. return {
  292. id: order.id,
  293. orderNo: order.ordeNo,
  294. userName: order.name,
  295. phone: order.phone,
  296. appointTime: order.goTime,
  297. cancelTime: order.state === 3 ? order.updateTime : '',
  298. qualityTime: order.status === 2 && order.state === 1 ? order.updateTime : '',
  299. statusText: order.isBy === 'Y' ? statusInfo.label : '不包邮',
  300. statusClass: statusInfo.class,
  301. statusLabel: statusInfo.label,
  302. actions: this.getOrderActions(order.status, order.state),
  303. status: this.getOrderStatus(order.status, order.state)
  304. }
  305. })
  306. if (isLoadMore) {
  307. // 数据去重处理
  308. const existingIds = new Set(this.orderList.map(order => order.id))
  309. const uniqueNewOrders = newOrders.filter(order => !existingIds.has(order.id))
  310. this.orderList = [...this.orderList, ...uniqueNewOrders]
  311. this.pageNo = params.pageNo +1
  312. } else {
  313. this.orderList = newOrders
  314. this.pageNo = 1
  315. }
  316. // 判断是否还有更多数据
  317. this.hasMore = newOrders.length === this.pageSize
  318. } else {
  319. this.hasMore = false
  320. }
  321. this.isLoading = false
  322. this.loadingMore = false
  323. }, err => {
  324. console.error('获取订单列表失败:', err)
  325. this.isLoading = false
  326. this.loadingMore = false
  327. this.hasMore = false
  328. uni.showToast({
  329. title: '加载失败,请重试',
  330. icon: 'none'
  331. })
  332. })
  333. },
  334. getOrderStatusInfo(status, state) {
  335. // if (state === 3) {
  336. // return { label: '已取消', class: 'gray' }
  337. // }
  338. if ((status === 1||status === 0) && state != 3 && state != 4) {
  339. return { label: '已预约', class: 'green' }
  340. } else if (state === 1) {
  341. return { label: '待质检', class: 'orange' }
  342. } else if (status === 3) {
  343. return { label: '已结款', class: 'blue' }
  344. } else if (state === 4) {
  345. return { label: '已驳回', class: 'red' }
  346. }
  347. return { label: '未知状态', class: 'gray' }
  348. },
  349. getOrderStatus(status, state) {
  350. // // 已取消状态
  351. // if (state === 3) return 4
  352. // 已预约状态 - 快递上门
  353. if (status === 1 ) return 1
  354. // 待质检状态 - 已取件
  355. if (state === 1) return 2
  356. // 已结款状态 - 现金打款
  357. if (status === 3 ) return 3
  358. // 已驳回状态 - 快递上门终止
  359. if (state ===4) return 4
  360. return -1
  361. },
  362. getOrderActions(status, state) {
  363. const actions = []
  364. // 只有待质检状态显示操作按钮
  365. if (status === 2 && state === 1) {
  366. actions.push({ icon: 'undo', text: '驳回' })
  367. actions.push({ icon: 'person', text: '审批' })
  368. }
  369. return actions
  370. },
  371. onLoadMore() {
  372. if (this.hasMore && !this.isLoading && !this.loadingMore) {
  373. this.fetchOrderList(true)
  374. }
  375. },
  376. scanCode() {
  377. uni.scanCode({
  378. scanType: ['qrCode'],
  379. success: (res) => {
  380. console.log('扫码结果:', res);
  381. // 这里可以根据扫码结果进行相应处理
  382. // 比如跳转到订单详情页
  383. if(res.result) {
  384. uni.navigateTo({
  385. url: '/pages/manager/order-detail?id=' + res.result
  386. })
  387. }
  388. },
  389. fail: (err) => {
  390. console.error('扫码失败:', err);
  391. uni.showToast({
  392. title: '扫码失败',
  393. icon: 'none'
  394. })
  395. }
  396. })
  397. },
  398. },
  399. onPullDownRefresh() {
  400. this.pageNo = 1;
  401. this.hasMore = true;
  402. this.orderList = [];
  403. this.isLoading = false;
  404. this.loadingMore = false;
  405. this.fetchOrderList();
  406. uni.stopPullDownRefresh();
  407. },
  408. onReachBottom() {
  409. // 防抖处理,避免频繁触发
  410. if (this.reachBottomTimer) {
  411. clearTimeout(this.reachBottomTimer)
  412. }
  413. this.reachBottomTimer = setTimeout(() => {
  414. this.onLoadMore()
  415. }, 100)
  416. }
  417. }
  418. </script>
  419. <style lang="scss" scoped>
  420. .order-manage-container {
  421. background: #f8f8f8;
  422. min-height: 100vh;
  423. padding-bottom: 24px;
  424. }
  425. .nav-bar {
  426. display: flex;
  427. align-items: center;
  428. justify-content: space-between;
  429. position: fixed;
  430. top: 0;
  431. left: 0;
  432. right: 0;
  433. z-index: 100;
  434. background: #fff;
  435. padding: 0 32rpx;
  436. box-sizing: border-box;
  437. .nav-title {
  438. flex: 1;
  439. text-align: center;
  440. font-size: 36rpx;
  441. font-weight: bold;
  442. color: #222;
  443. }
  444. .nav-icons {
  445. display: flex;
  446. align-items: center;
  447. gap: 32rpx;
  448. }
  449. }
  450. .order-tabs-scroll {
  451. position: fixed;
  452. left: 0;
  453. width: 100%;
  454. z-index: 99;
  455. background: #fff;
  456. border-bottom: 1px solid #f0f0f0;
  457. height: 96rpx;
  458. overflow-x: auto;
  459. white-space: nowrap;
  460. -webkit-overflow-scrolling: touch;
  461. &::-webkit-scrollbar {
  462. display: none;
  463. }
  464. }
  465. .order-tabs {
  466. display: flex;
  467. width: 100%;
  468. }
  469. .tab-item {
  470. flex: 1 0 0%;
  471. text-align: center;
  472. font-size: 34rpx;
  473. color: #bfbfbf;
  474. height: 96rpx;
  475. line-height: 96rpx;
  476. position: relative;
  477. font-weight: 500;
  478. transition: color 0.2s;
  479. letter-spacing: 0.5px;
  480. }
  481. .tab-item.active {
  482. color: #ffb400;
  483. font-weight: bold;
  484. }
  485. .tab-item.active::after {
  486. content: '';
  487. display: block;
  488. margin: 0 auto;
  489. margin-top: 2px;
  490. width: 22px;
  491. height: 3px;
  492. border-radius: 2px;
  493. background: #ffb400;
  494. }
  495. .search-bar {
  496. width: 100vw;
  497. height: 40px;
  498. position: fixed;
  499. z-index: 10;
  500. left: 0;
  501. top: 0;
  502. display: flex;
  503. align-items: center;
  504. }
  505. .search-bar-inner {
  506. margin: 0 16px;
  507. background: #fff;
  508. border-radius: 20px;
  509. height: 40px;
  510. flex: 1;
  511. display: flex;
  512. align-items: center;
  513. box-shadow: 0 2px 8px rgba(0,0,0,0.02);
  514. padding: 0 12px;
  515. justify-content: space-around;
  516. .search-icon {
  517. margin-right: 8px;
  518. }
  519. .scan-icon {
  520. margin-left: 8px;
  521. }
  522. }
  523. .search-input-wrap {
  524. display: flex;
  525. align-items: center;
  526. flex: 1;
  527. background: #f5f5f5;
  528. border-radius: 20px;
  529. height: 32px;
  530. margin: 0 0;
  531. padding: 0 8px;
  532. .search-input {
  533. flex: 1;
  534. border: none;
  535. outline: none;
  536. background: transparent;
  537. font-size: 15px;
  538. color: #222;
  539. margin-left: 8px;
  540. }
  541. }
  542. .search-cancel {
  543. margin-left: 8px;
  544. color: #999;
  545. font-size: 15px;
  546. line-height: 40px;
  547. }
  548. .order-list {
  549. margin: 0;
  550. padding-top: calc(var(--status-bar-height, 0px) + 44px + 44px + 16px);
  551. }
  552. .order-card {
  553. background: #fff;
  554. border-radius: 20px;
  555. margin: 0 16px 16px 16px;
  556. padding: 20px;
  557. box-shadow: 0 2px 8px rgba(0,0,0,0.04);
  558. }
  559. .order-card-header {
  560. display: flex;
  561. justify-content: space-between;
  562. align-items: flex-start;
  563. margin-bottom: 12px;
  564. .order-id {
  565. font-size: 16px;
  566. font-weight: bold;
  567. color: #222;
  568. }
  569. .order-status-tag {
  570. font-size: 14px;
  571. border-radius: 12px;
  572. padding: 2px 12px;
  573. &.green { background: #e6f9e6; color: #1ecb1e; }
  574. &.red { background: #ffeaea; color: #ff4d4f; }
  575. &.orange { background: #fff7e6; color: #ffb400; }
  576. &.blue { background: #e6f0ff; color: #409eff; }
  577. &.gray { background: #f5f5f5; color: #999; }
  578. }
  579. }
  580. .order-info-wrapper {
  581. position: relative;
  582. .order-info-status {
  583. position: absolute;
  584. right: 0;
  585. bottom: 0;
  586. font-size: 14px;
  587. border-radius: 12px;
  588. padding: 2px 12px;
  589. &.green { background: #e6f9e6; color: #1ecb1e; }
  590. &.red { background: #ffeaea; color: #ff4d4f; }
  591. &.orange { background: #fff7e6; color: #ffb400; }
  592. &.blue { background: #e6f0ff; color: #409eff; }
  593. &.gray { background: #f5f5f5; color: #999; }
  594. }
  595. }
  596. .order-info {
  597. font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  598. font-weight: 400;
  599. font-size: 14px;
  600. line-height: 1.4;
  601. letter-spacing: 0;
  602. vertical-align: middle;
  603. color: #666;
  604. margin-bottom: 12px;
  605. view {
  606. margin-bottom: 4px;
  607. display: flex;
  608. align-items: center;
  609. }
  610. .info-label {
  611. color: #999;
  612. font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  613. font-weight: 400;
  614. font-size: 14px;
  615. line-height: 1.4;
  616. margin-right: 4px;
  617. }
  618. .info-value {
  619. color: #222;
  620. font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  621. font-weight: 400;
  622. font-size: 14px;
  623. line-height: 1.4;
  624. }
  625. }
  626. .order-card-footer {
  627. display: flex;
  628. align-items: center;
  629. justify-content: center;
  630. margin: 0 -20px -20px -20px;
  631. padding: 0 20px;
  632. border-bottom-left-radius: 20px;
  633. border-bottom-right-radius: 20px;
  634. background: #fafbfc;
  635. min-height: 60px;
  636. position: relative;
  637. .order-actions-bar {
  638. display: flex;
  639. flex: 1;
  640. justify-content: center;
  641. align-items: center;
  642. gap: 48px;
  643. .action-btn-bar {
  644. display: flex;
  645. flex-direction: column;
  646. align-items: center;
  647. font-size: 14px;
  648. color: #666;
  649. margin-top: 8px;
  650. margin-bottom: 8px;
  651. uni-icons {
  652. margin-bottom: 2px;
  653. }
  654. }
  655. }
  656. }
  657. /* 加载状态样式 */
  658. .loading-container, .load-more-container, .no-more-container, .empty-container {
  659. display: flex;
  660. flex-direction: column;
  661. align-items: center;
  662. justify-content: center;
  663. padding: 40rpx 0;
  664. color: #999;
  665. }
  666. .loading-icon {
  667. animation: spin 1s linear infinite;
  668. margin-bottom: 16rpx;
  669. }
  670. @keyframes spin {
  671. 0% { transform: rotate(0deg); }
  672. 100% { transform: rotate(360deg); }
  673. }
  674. .loading-text, .load-more-text {
  675. font-size: 28rpx;
  676. color: #999;
  677. }
  678. .no-more-text {
  679. font-size: 26rpx;
  680. color: #ccc;
  681. }
  682. .empty-container {
  683. padding: 120rpx 0;
  684. }
  685. .empty-text {
  686. font-size: 30rpx;
  687. color: #ccc;
  688. }
  689. </style>