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

599 lines
16 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. <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. </view>
  120. </view>
  121. </template>
  122. <script>
  123. import pullRefreshMixin from '../mixins/pullRefreshMixin.js'
  124. export default {
  125. mixins: [pullRefreshMixin],
  126. data() {
  127. return {
  128. statusBarHeight: 0,
  129. navBarContentHeight: 44,
  130. tabBarHeight: 48,
  131. navBarHeight: 44,
  132. navBarRealHeight: 44,
  133. tabs: [
  134. { label: '全部', value: -1 },
  135. { label: '已预约', value: 0 },
  136. { label: '待质检', value: 1 },
  137. { label: '已结款', value: 2 },
  138. { label: '已驳回', value: 3 }
  139. ],
  140. currentTab: 0,
  141. orderList: [],
  142. searchMode: false,
  143. searchText: '',
  144. historyOrderMode: false,
  145. pageNum: 1,
  146. pageSize: 10,
  147. hasMore: true,
  148. isLoading: false,
  149. userId: ''
  150. }
  151. },
  152. onLoad(options) {
  153. const sys = uni.getSystemInfoSync();
  154. this.statusBarHeight = sys.statusBarHeight;
  155. this.$nextTick(() => {
  156. uni.createSelectorQuery().select('.nav-bar').boundingClientRect(rect => {
  157. if (rect) {
  158. this.navBarRealHeight = rect.height;
  159. }
  160. }).exec();
  161. });
  162. if (options && options.historyOrder) {
  163. this.historyOrderMode = true;
  164. }
  165. if (options && options.userId) {
  166. this.userId = options.userId;
  167. }
  168. this.fetchOrderList()
  169. },
  170. computed: {
  171. filteredOrders() {
  172. if (this.searchText) {
  173. const text = this.searchText.toLowerCase();
  174. return this.orderList.filter(order =>
  175. (order.orderNo && order.orderNo.toLowerCase().includes(text)) ||
  176. (order.userName && order.userName.toLowerCase().includes(text)) ||
  177. (order.phone && order.phone.toLowerCase().includes(text))
  178. );
  179. }
  180. const tabValue = this.tabs[this.currentTab].value;
  181. // console.log('当前tab:', tabValue, 'orderList:', this.orderList);
  182. if (tabValue === -1) return this.orderList;
  183. if (tabValue === 0) {
  184. // 已预约:status == 1
  185. // console.log(this.orderList.filter(order => order.status == 1),'this.orderList')
  186. return this.orderList.filter(order => order.status == 1);
  187. } else if (tabValue === 1) {
  188. // 待质检:state == 1
  189. return this.orderList.filter(order => order.status == 2);
  190. } else if (tabValue === 2) {
  191. // 已结款:status == 3
  192. return this.orderList.filter(order => order.status == 3);
  193. } else if (tabValue === 3) {
  194. // 已驳回:status == 1 && state == 3
  195. return this.orderList.filter(order => order.status == 4);
  196. }
  197. return this.orderList;
  198. }
  199. },
  200. methods: {
  201. goBack() {
  202. uni.navigateBack()
  203. },
  204. onTabChange(idx) {
  205. this.currentTab = idx
  206. this.pageNum = 1
  207. this.hasMore = true
  208. this.orderList = []
  209. this.fetchOrderList()
  210. },
  211. onSearchIconClick() {
  212. this.searchMode = true;
  213. this.$nextTick(() => {
  214. this.$refs.searchInput && this.$refs.searchInput.focus();
  215. });
  216. },
  217. onClearSearch() {
  218. this.searchText = '';
  219. },
  220. onCancelSearch() {
  221. this.searchText = '';
  222. this.searchMode = false;
  223. },
  224. goToOrderDetail(order) {
  225. uni.navigateTo({
  226. url: '/pages/manager/order-detail?id=' + order.id
  227. })
  228. },
  229. refreshData() {
  230. // TODO: 实现订单列表刷新逻辑,如重新请求接口
  231. },
  232. async onRefresh() {
  233. await this.refreshData && this.refreshData()
  234. },
  235. fetchOrderList(isLoadMore = false) {
  236. if (this.isLoading || (!isLoadMore && !this.hasMore)) return
  237. this.isLoading = true
  238. const params = {
  239. pageNum: this.pageNum,
  240. pageSize: this.pageSize
  241. }
  242. if (this.userId) {
  243. params.userId = this.userId;
  244. }
  245. this.$api && this.$api('getOrderList', params, res => {
  246. if (res && res.code === 200 && res.result && res.result.records) {
  247. const newOrders = res.result.records.map(order => {
  248. const statusInfo = this.getOrderStatusInfo(order.status, order.state)
  249. return {
  250. id: order.id,
  251. orderNo: order.ordeNo,
  252. userName: order.name,
  253. phone: order.phone,
  254. appointTime: order.goTime,
  255. cancelTime: order.state === 3 ? order.updateTime : '',
  256. qualityTime: order.status === 2 && order.state === 1 ? order.updateTime : '',
  257. statusText: order.isBy === 'Y' ? statusInfo.label : '不包邮',
  258. statusClass: statusInfo.class,
  259. statusLabel: statusInfo.label,
  260. actions: this.getOrderActions(order.status, order.state),
  261. status: this.getOrderStatus(order.status, order.state)
  262. }
  263. })
  264. if (isLoadMore) {
  265. this.orderList = [...this.orderList, ...newOrders]
  266. } else {
  267. this.orderList = newOrders
  268. }
  269. this.hasMore = newOrders.length === this.pageSize
  270. this.pageNum = isLoadMore ? this.pageNum + 1 : 1
  271. }
  272. this.isLoading = false
  273. })
  274. },
  275. getOrderStatusInfo(status, state) {
  276. // if (state === 3) {
  277. // return { label: '已取消', class: 'gray' }
  278. // }
  279. if (status === 1 ) {
  280. return { label: '已预约', class: 'green' }
  281. } else if (state === 1) {
  282. return { label: '待质检', class: 'orange' }
  283. } else if (status === 3) {
  284. return { label: '已结款', class: 'blue' }
  285. } else if (status === 1 && state === 3) {
  286. return { label: '已驳回', class: 'red' }
  287. }
  288. return { label: '未知状态', class: 'gray' }
  289. },
  290. getOrderStatus(status, state) {
  291. // // 已取消状态
  292. // if (state === 3) return 4
  293. // 已预约状态 - 快递上门
  294. if (status === 1 ) return 1
  295. // 待质检状态 - 已取件
  296. if (state === 1) return 2
  297. // 已结款状态 - 现金打款
  298. if (status === 3 ) return 3
  299. // 已驳回状态 - 快递上门终止
  300. if (status === 1 && state === 3) return 4
  301. return -1
  302. },
  303. getOrderActions(status, state) {
  304. const actions = []
  305. // 只有待质检状态显示操作按钮
  306. if (status === 2 && state === 1) {
  307. actions.push({ icon: 'undo', text: '驳回' })
  308. actions.push({ icon: 'person', text: '审批' })
  309. }
  310. return actions
  311. },
  312. onLoadMore() {
  313. if (this.hasMore && !this.isLoading) {
  314. this.fetchOrderList(true)
  315. }
  316. },
  317. scanCode() {
  318. uni.scanCode({
  319. scanType: ['qrCode'],
  320. success: (res) => {
  321. console.log('扫码结果:', res);
  322. // 这里可以根据扫码结果进行相应处理
  323. // 比如跳转到订单详情页
  324. if(res.result) {
  325. uni.navigateTo({
  326. url: '/pages/manager/order-detail?id=' + res.result
  327. })
  328. }
  329. },
  330. fail: (err) => {
  331. console.error('扫码失败:', err);
  332. uni.showToast({
  333. title: '扫码失败',
  334. icon: 'none'
  335. })
  336. }
  337. })
  338. },
  339. },
  340. onPullDownRefresh() {
  341. this.pageNum = 1;
  342. this.hasMore = true;
  343. this.orderList = [];
  344. this.fetchOrderList();
  345. uni.stopPullDownRefresh();
  346. },
  347. onReachBottom() {
  348. this.onLoadMore()
  349. }
  350. }
  351. </script>
  352. <style lang="scss" scoped>
  353. .order-manage-container {
  354. background: #f8f8f8;
  355. min-height: 100vh;
  356. padding-bottom: 24px;
  357. }
  358. .nav-bar {
  359. display: flex;
  360. align-items: center;
  361. justify-content: space-between;
  362. position: fixed;
  363. top: 0;
  364. left: 0;
  365. right: 0;
  366. z-index: 100;
  367. background: #fff;
  368. padding: 0 32rpx;
  369. box-sizing: border-box;
  370. .nav-title {
  371. flex: 1;
  372. text-align: center;
  373. font-size: 36rpx;
  374. font-weight: bold;
  375. color: #222;
  376. }
  377. .nav-icons {
  378. display: flex;
  379. align-items: center;
  380. gap: 32rpx;
  381. }
  382. }
  383. .order-tabs-scroll {
  384. position: fixed;
  385. left: 0;
  386. width: 100%;
  387. z-index: 99;
  388. background: #fff;
  389. border-bottom: 1px solid #f0f0f0;
  390. height: 96rpx;
  391. overflow-x: auto;
  392. white-space: nowrap;
  393. -webkit-overflow-scrolling: touch;
  394. &::-webkit-scrollbar {
  395. display: none;
  396. }
  397. }
  398. .order-tabs {
  399. display: flex;
  400. width: 100%;
  401. }
  402. .tab-item {
  403. flex: 1 0 0%;
  404. text-align: center;
  405. font-size: 34rpx;
  406. color: #bfbfbf;
  407. height: 96rpx;
  408. line-height: 96rpx;
  409. position: relative;
  410. font-weight: 500;
  411. transition: color 0.2s;
  412. letter-spacing: 0.5px;
  413. }
  414. .tab-item.active {
  415. color: #ffb400;
  416. font-weight: bold;
  417. }
  418. .tab-item.active::after {
  419. content: '';
  420. display: block;
  421. margin: 0 auto;
  422. margin-top: 2px;
  423. width: 22px;
  424. height: 3px;
  425. border-radius: 2px;
  426. background: #ffb400;
  427. }
  428. .search-bar {
  429. width: 100vw;
  430. height: 40px;
  431. position: fixed;
  432. z-index: 10;
  433. left: 0;
  434. top: 0;
  435. display: flex;
  436. align-items: center;
  437. }
  438. .search-bar-inner {
  439. margin: 0 16px;
  440. background: #fff;
  441. border-radius: 20px;
  442. height: 40px;
  443. flex: 1;
  444. display: flex;
  445. align-items: center;
  446. box-shadow: 0 2px 8px rgba(0,0,0,0.02);
  447. padding: 0 12px;
  448. justify-content: space-around;
  449. .search-icon {
  450. margin-right: 8px;
  451. }
  452. .scan-icon {
  453. margin-left: 8px;
  454. }
  455. }
  456. .search-input-wrap {
  457. display: flex;
  458. align-items: center;
  459. flex: 1;
  460. background: #f5f5f5;
  461. border-radius: 20px;
  462. height: 32px;
  463. margin: 0 0;
  464. padding: 0 8px;
  465. .search-input {
  466. flex: 1;
  467. border: none;
  468. outline: none;
  469. background: transparent;
  470. font-size: 15px;
  471. color: #222;
  472. margin-left: 8px;
  473. }
  474. }
  475. .search-cancel {
  476. margin-left: 8px;
  477. color: #999;
  478. font-size: 15px;
  479. line-height: 40px;
  480. }
  481. .order-list {
  482. margin: 0;
  483. padding-top: calc(var(--status-bar-height, 0px) + 44px + 44px + 16px);
  484. }
  485. .order-card {
  486. background: #fff;
  487. border-radius: 20px;
  488. margin: 0 16px 16px 16px;
  489. padding: 20px;
  490. box-shadow: 0 2px 8px rgba(0,0,0,0.04);
  491. }
  492. .order-card-header {
  493. display: flex;
  494. justify-content: space-between;
  495. align-items: flex-start;
  496. margin-bottom: 12px;
  497. .order-id {
  498. font-size: 16px;
  499. font-weight: bold;
  500. color: #222;
  501. }
  502. .order-status-tag {
  503. font-size: 14px;
  504. border-radius: 12px;
  505. padding: 2px 12px;
  506. &.green { background: #e6f9e6; color: #1ecb1e; }
  507. &.red { background: #ffeaea; color: #ff4d4f; }
  508. &.orange { background: #fff7e6; color: #ffb400; }
  509. &.blue { background: #e6f0ff; color: #409eff; }
  510. &.gray { background: #f5f5f5; color: #999; }
  511. }
  512. }
  513. .order-info-wrapper {
  514. position: relative;
  515. .order-info-status {
  516. position: absolute;
  517. right: 0;
  518. bottom: 0;
  519. font-size: 14px;
  520. border-radius: 12px;
  521. padding: 2px 12px;
  522. &.green { background: #e6f9e6; color: #1ecb1e; }
  523. &.red { background: #ffeaea; color: #ff4d4f; }
  524. &.orange { background: #fff7e6; color: #ffb400; }
  525. &.blue { background: #e6f0ff; color: #409eff; }
  526. &.gray { background: #f5f5f5; color: #999; }
  527. }
  528. }
  529. .order-info {
  530. font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  531. font-weight: 400;
  532. font-size: 14px;
  533. line-height: 1.4;
  534. letter-spacing: 0;
  535. vertical-align: middle;
  536. color: #666;
  537. margin-bottom: 12px;
  538. view {
  539. margin-bottom: 4px;
  540. display: flex;
  541. align-items: center;
  542. }
  543. .info-label {
  544. color: #999;
  545. font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  546. font-weight: 400;
  547. font-size: 14px;
  548. line-height: 1.4;
  549. margin-right: 4px;
  550. }
  551. .info-value {
  552. color: #222;
  553. font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
  554. font-weight: 400;
  555. font-size: 14px;
  556. line-height: 1.4;
  557. }
  558. }
  559. .order-card-footer {
  560. display: flex;
  561. align-items: center;
  562. justify-content: center;
  563. margin: 0 -20px -20px -20px;
  564. padding: 0 20px;
  565. border-bottom-left-radius: 20px;
  566. border-bottom-right-radius: 20px;
  567. background: #fafbfc;
  568. min-height: 60px;
  569. position: relative;
  570. .order-actions-bar {
  571. display: flex;
  572. flex: 1;
  573. justify-content: center;
  574. align-items: center;
  575. gap: 48px;
  576. .action-btn-bar {
  577. display: flex;
  578. flex-direction: column;
  579. align-items: center;
  580. font-size: 14px;
  581. color: #666;
  582. margin-top: 8px;
  583. margin-bottom: 8px;
  584. uni-icons {
  585. margin-bottom: 2px;
  586. }
  587. }
  588. }
  589. }
  590. </style>