index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. <template>
  2. <view class="order-list-page">
  3. <nav-bar title="订单列表" :showBack="false"></nav-bar>
  4. <!-- 顶部状态栏 -->
  5. <view class="sticky-header">
  6. <scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
  7. <view class="tabs-row">
  8. <view v-for="tab in tabList" :key="tab.value"
  9. :class="['tab-item', { active: activeStatus === tab.value }]" @click="onTabClick(tab.value)">
  10. <text>{{ tab.title }}</text>
  11. </view>
  12. </view>
  13. </scroll-view>
  14. <!-- 搜索和过滤 -->
  15. <view class="filter-row">
  16. <picker :range="typeOptions" range-key="text" @change="onTypeChange">
  17. <view class="dropdown-btn">
  18. <text>{{ currentTypeName }}</text>
  19. <uni-icons type="bottom" size="12" color="#333"></uni-icons>
  20. </view>
  21. </picker>
  22. <view class="search-wrap">
  23. <uni-icons type="search" size="14" color="#999"></uni-icons>
  24. <input class="search-input" v-model="searchValue" placeholder="订单号/商户/宠主/手机号"
  25. placeholder-class="placeholder-style" @confirm="onSearch" />
  26. </view>
  27. </view>
  28. </view>
  29. <!-- 订单列表内容 -->
  30. <view class="list-container">
  31. <view class="order-card" v-for="order in orders" :key="order.id" @click="goToDetail(order)">
  32. <!-- 头部:订单号与状态 -->
  33. <view class="order-head">
  34. <text class="order-no">{{ order.id }}</text>
  35. <text :class="['status-text', order.statusClass]">{{ order.statusText }}</text>
  36. </view>
  37. <!-- 主体信息 -->
  38. <view class="order-body">
  39. <view class="service-row">
  40. <text class="service-name">{{ order.serviceType }}</text>
  41. <text class="service-tag tag-orange" v-if="order.serviceTags[0]">{{ order.serviceTags[0]
  42. }}</text>
  43. <text class="service-tag tag-blue" v-if="order.serviceTags[1] === '接'">接</text>
  44. <text class="service-tag tag-green" v-if="order.serviceTags[1] === '送'">送</text>
  45. </view>
  46. <view class="pet-row">
  47. <view class="pet-avatar-text">
  48. <text>{{ order.petName.substring(0, 1).toUpperCase() }}</text>
  49. </view>
  50. <view class="pet-desc">
  51. <text class="bold">{{ order.petName }}</text>
  52. <text class="sub">{{ order.petBreed }}</text>
  53. </view>
  54. <text class="user-desc">{{ order.userName }}</text>
  55. </view>
  56. <view class="info-list">
  57. <view class="info-item">
  58. <uni-icons type="location" size="14" color="#999"></uni-icons>
  59. <text>{{ order.address }}</text>
  60. </view>
  61. <view class="info-item">
  62. <uni-icons type="shop" size="14" color="#999"></uni-icons>
  63. <text>{{ order.shopName }} {{ order.userPhone }}</text>
  64. </view>
  65. <view class="info-item">
  66. <uni-icons type="calendar" size="14" color="#999"></uni-icons>
  67. <text>预约: {{ order.bookTime }}</text>
  68. </view>
  69. </view>
  70. </view>
  71. <!-- 履约与操作栏 -->
  72. <view class="order-foot">
  73. <view class="foot-left">
  74. <text class="create-time">下单: {{ order.createTime }}</text>
  75. <view class="assign-info">
  76. <text class="assign-label">履约信息:</text>
  77. <text class="assign-none" v-if="order.statusText === '待派单'">暂未派单</text>
  78. <text class="assign-none" v-else-if="order.statusText === '已取消'">订单已关闭</text>
  79. <text class="assign-name" v-else>{{ order.assigneeName }}</text>
  80. </view>
  81. <text class="cancel-time" v-if="order.statusText === '已取消' && order.cancelTime">取消: {{
  82. order.cancelTime }}</text>
  83. </view>
  84. <view class="actions">
  85. <template v-if="order.statusText === '待派单' || order.statusText === '待接单'">
  86. <button size="mini" class="action-btn btn-cancel"
  87. @click.stop="onCancelOrder(order)">取消</button>
  88. <button size="mini" class="action-btn btn-primary"
  89. @click.stop="goToDetail(order)">详情</button>
  90. </template>
  91. <template v-else-if="['服务中', '待商家确认', '已完成'].includes(order.statusText)">
  92. <button v-if="['服务中', '已完成'].includes(order.statusText)" size="mini"
  93. class="action-btn btn-cancel" @click.stop="onComplaint(order)">投诉</button>
  94. <button size="mini" class="action-btn btn-primary"
  95. @click.stop="goToDetail(order)">详情</button>
  96. </template>
  97. <template v-else>
  98. <button size="mini" class="action-btn btn-primary"
  99. @click.stop="goToDetail(order)">详情</button>
  100. </template>
  101. </view>
  102. </view>
  103. </view>
  104. <!-- 空状态 -->
  105. <view class="empty-state" v-if="!loading && orders.length === 0">
  106. <text class="empty-text">暂无相关订单</text>
  107. </view>
  108. <!-- 加载状态 -->
  109. <view class="loading-state" v-if="loading">
  110. <text class="loading-text">加载中...</text>
  111. </view>
  112. </view>
  113. <custom-tabbar></custom-tabbar>
  114. </view>
  115. </template>
  116. <script setup>
  117. import { ref, computed, onMounted } from 'vue'
  118. import { onLoad } from '@dcloudio/uni-app'
  119. import navBar from '@/components/nav-bar/index.vue'
  120. import customTabbar from '@/components/custom-tabbar/index.vue'
  121. import orderStatusData from '@/json/orderStatus.json'
  122. import { listAll } from '@/api/service/list'
  123. import { listSubOrder, cancelSubOrder } from '@/api/order/subOrder'
  124. import { listAreaStation } from '@/api/system/areaStation'
  125. // 加载状态
  126. const loading = ref(false)
  127. // 筛选与搜索
  128. const activeStatus = ref(-1) // -1 表示全部,其他值为枚举值
  129. const filterType = ref(0)
  130. const searchValue = ref('')
  131. // 分页参数
  132. const pagination = ref({
  133. current: 1,
  134. size: 10,
  135. total: 0
  136. })
  137. // 从 orderStatus.json 生成 tabList
  138. const tabList = ref([
  139. { title: '全部订单', value: -1 },
  140. ...orderStatusData.map(item => ({
  141. title: item.label,
  142. value: item.value,
  143. color: item.color
  144. }))
  145. ])
  146. const typeOptions = ref([{ text: '全部类型', value: 0 }])
  147. const serviceList = ref([])
  148. const areaStationList = ref([])
  149. const areaStationMap = ref({})
  150. // 加载服务类型列表
  151. const loadServiceTypes = async () => {
  152. try {
  153. const services = await listAll()
  154. if (services && services.length > 0) {
  155. serviceList.value = services
  156. const serviceTypes = services.map((service, index) => ({
  157. text: service.name,
  158. value: index + 1,
  159. id: service.id
  160. }))
  161. typeOptions.value = [
  162. { text: '全部类型', value: 0 },
  163. ...serviceTypes
  164. ]
  165. }
  166. } catch (error) {
  167. console.error('加载服务类型失败:', error)
  168. }
  169. }
  170. // 加载区域站点列表
  171. const loadAreaStations = async () => {
  172. try {
  173. const res = await listAreaStation()
  174. if (res && res.data) {
  175. areaStationList.value = res.data
  176. const map = {}
  177. for (const item of res.data) {
  178. if (item && item.id !== undefined && item.id !== null) {
  179. map[item.id] = item
  180. }
  181. }
  182. areaStationMap.value = map
  183. }
  184. } catch (error) {
  185. console.error('加载区域站点失败:', error)
  186. }
  187. }
  188. onMounted(() => {
  189. loadServiceTypes()
  190. loadAreaStations()
  191. loadOrders()
  192. })
  193. const currentTypeName = computed(() => {
  194. const option = typeOptions.value.find(opt => opt.value === filterType.value)
  195. return option ? option.text : '全部类型'
  196. })
  197. // 根据枚举值获取状态信息
  198. const getStatusInfo = (value) => {
  199. return orderStatusData.find(item => item.value === value)
  200. }
  201. // 根据服务ID获取服务名称
  202. const getServiceName = (serviceId) => {
  203. const service = serviceList.value.find(s => s.id === serviceId)
  204. return service ? service.name : '未知服务'
  205. }
  206. // 获取城市/区域文本
  207. const getCityDistrictText = (row) => {
  208. if (!row || !row.site) return ''
  209. const station = areaStationMap.value[row.site]
  210. if (!station) return ''
  211. const parent = station.parentId ? areaStationMap.value[station.parentId] : undefined
  212. if (!parent) return station.name || ''
  213. if (parent.type === 0) return parent.name || ''
  214. if (parent.type === 1) {
  215. const city = parent.parentId ? areaStationMap.value[parent.parentId] : undefined
  216. return `${city?.name || ''}/${parent.name || ''}`
  217. }
  218. return parent.name || ''
  219. }
  220. // 获取服务模式标签
  221. const getServiceModeTag = (row) => {
  222. const t = row?.type
  223. if (t === 0 || t === '0' || t === 1 || t === '1') return '往返'
  224. return ''
  225. }
  226. // 获取服务订单类型标签
  227. const getServiceOrderTypeTag = (row) => {
  228. const t = row?.type
  229. if (t === 0 || t === '0') return { label: '接', type: 'blue' }
  230. if (t === 1 || t === '1') return { label: '送', type: 'green' }
  231. if (t === 2 || t === '2') return { label: '单程接', type: 'blue' }
  232. if (t === 3 || t === '3') return { label: '单程送', type: 'green' }
  233. return null
  234. }
  235. const onTabClick = (value) => {
  236. activeStatus.value = value
  237. pagination.value.current = 1
  238. loadOrders()
  239. }
  240. const onTypeChange = (e) => {
  241. const index = Number(e.detail.value)
  242. filterType.value = index
  243. pagination.value.current = 1
  244. loadOrders()
  245. }
  246. // 订单列表数据
  247. const orders = ref([])
  248. // 加载订单列表
  249. const loadOrders = async () => {
  250. loading.value = true
  251. try {
  252. const selectedType = typeOptions.value.find(opt => opt.value === filterType.value)
  253. const params = {
  254. pageNum: pagination.value.current,
  255. pageSize: pagination.value.size,
  256. status: activeStatus.value !== -1 ? activeStatus.value : undefined,
  257. service: selectedType && selectedType.id ? selectedType.id : undefined,
  258. content: searchValue.value || undefined
  259. }
  260. const res = await listSubOrder(params)
  261. console.log('后端返回数据:', res)
  262. if (res) {
  263. const rows = res.rows || []
  264. console.log('rows:', rows)
  265. orders.value = rows.map(row => transformOrder(row))
  266. console.log('转换后的orders:', orders.value)
  267. pagination.value.total = res.total || 0
  268. }
  269. } catch (error) {
  270. console.error('加载订单列表失败:', error)
  271. } finally {
  272. loading.value = false
  273. }
  274. }
  275. // 转换订单数据格式
  276. const transformOrder = (row) => {
  277. const statusInfo = getStatusInfo(row.status)
  278. const serviceName = getServiceName(row.service)
  279. const modeTag = getServiceModeTag(row)
  280. const typeTag = getServiceOrderTypeTag(row)
  281. const serviceTags = []
  282. if (modeTag) serviceTags.push(modeTag)
  283. if (typeTag) serviceTags.push(typeTag.label)
  284. return {
  285. // 先展开原始字段,后面的手动赋值具有更高优先级
  286. ...row,
  287. // 显示用的单号(优先用业务编号 code,否则用数据库 ID)
  288. id: row.code || row.id,
  289. rawId: row.id,
  290. serviceType: serviceName,
  291. serviceTags: serviceTags,
  292. petName: row.petName || '未知',
  293. petBreed: row.petBreed || '未知',
  294. userName: row.customerName || '未知',
  295. address: row.toAddress || row.fromAddress || getCityDistrictText(row),
  296. shopName: row.storeName || '未知',
  297. userPhone: row.contactPhoneNumber || '',
  298. bookTime: row.serviceTime || '',
  299. createTime: row.createTime || '',
  300. statusText: statusInfo ? statusInfo.label : '未知',
  301. statusClass: statusInfo ? `text-${statusInfo.color.replace('#', '')}` : 'text-gray',
  302. assigneeName: row.fulfillerName || '',
  303. cancelTime: row.cancelTime || ''
  304. }
  305. }
  306. // 搜索订单
  307. const onSearch = () => {
  308. pagination.value.current = 1
  309. loadOrders()
  310. }
  311. // 取消订单
  312. const onCancelOrder = (order) => {
  313. uni.showModal({
  314. title: '提示',
  315. content: `确定要取消订单 [${order.id}] 吗?`,
  316. success: async (res) => {
  317. if (res.confirm) {
  318. try {
  319. await cancelSubOrder({ orderId: order.rawId })
  320. uni.showToast({ title: '订单已取消', icon: 'success' })
  321. loadOrders()
  322. } catch (error) {
  323. console.error('取消订单失败:', error)
  324. uni.showToast({ title: '取消失败', icon: 'none' })
  325. }
  326. }
  327. }
  328. })
  329. }
  330. // 跳转到订单详情
  331. const goToDetail = (order) => {
  332. uni.navigateTo({
  333. url: `/pages/order/detail/index?id=${order.rawId}`
  334. })
  335. }
  336. // 投诉
  337. const onComplaint = (order) => {
  338. uni.navigateTo({
  339. url: `/pages/my/complaint-submit/index?orderId=${order.id}`
  340. })
  341. }
  342. </script>
  343. <style lang="scss" scoped>
  344. .order-list-page {
  345. background-color: #f2f2f2;
  346. min-height: 100vh;
  347. padding-bottom: 120rpx;
  348. }
  349. .sticky-header {
  350. position: sticky;
  351. top: calc(44px + var(--status-bar-height, 44px));
  352. z-index: 99;
  353. background-color: #fff;
  354. }
  355. .tabs-scroll {
  356. white-space: nowrap;
  357. border-bottom: 1rpx solid #f5f5f5;
  358. }
  359. .tabs-row {
  360. display: flex;
  361. padding: 0 16rpx;
  362. }
  363. .tab-item {
  364. padding: 24rpx 24rpx;
  365. font-size: 28rpx;
  366. color: #666;
  367. position: relative;
  368. flex-shrink: 0;
  369. }
  370. .tab-item.active {
  371. color: #333;
  372. font-weight: bold;
  373. }
  374. .tab-item.active::after {
  375. content: '';
  376. position: absolute;
  377. bottom: 0;
  378. left: 50%;
  379. transform: translateX(-50%);
  380. width: 48rpx;
  381. height: 6rpx;
  382. background-color: #f7ca3e;
  383. border-radius: 6rpx;
  384. }
  385. .filter-row {
  386. display: flex;
  387. align-items: center;
  388. padding: 12rpx 16rpx;
  389. background-color: #fff;
  390. border-top: 1rpx solid #f9f9f9;
  391. gap: 16rpx;
  392. }
  393. .dropdown-btn {
  394. display: flex;
  395. align-items: center;
  396. gap: 8rpx;
  397. background: #f5f5f5;
  398. border-radius: 34rpx;
  399. padding: 12rpx 24rpx;
  400. font-size: 26rpx;
  401. color: #333;
  402. }
  403. .search-wrap {
  404. flex: 1;
  405. display: flex;
  406. align-items: center;
  407. background: #f5f5f5;
  408. border-radius: 34rpx;
  409. padding: 12rpx 20rpx;
  410. gap: 12rpx;
  411. }
  412. .search-input {
  413. flex: 1;
  414. font-size: 24rpx;
  415. }
  416. .placeholder-style {
  417. color: #999;
  418. font-size: 24rpx;
  419. }
  420. .list-container {
  421. padding: 24rpx;
  422. }
  423. .order-card {
  424. padding: 28rpx;
  425. margin-bottom: 24rpx;
  426. background: #fff;
  427. border-radius: 24rpx;
  428. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  429. }
  430. .order-head {
  431. display: flex;
  432. justify-content: space-between;
  433. align-items: center;
  434. border-bottom: 1rpx solid #f5f5f5;
  435. padding-bottom: 20rpx;
  436. margin-bottom: 20rpx;
  437. }
  438. .order-no {
  439. font-size: 26rpx;
  440. color: #666;
  441. }
  442. .status-text {
  443. font-size: 26rpx;
  444. font-weight: bold;
  445. }
  446. .text-red {
  447. color: #f44336;
  448. }
  449. .text-orange {
  450. color: #ff9800;
  451. }
  452. .text-blue {
  453. color: #2196f3;
  454. }
  455. .text-green {
  456. color: #4caf50;
  457. }
  458. .text-gray {
  459. color: #999;
  460. }
  461. .service-row {
  462. display: flex;
  463. align-items: center;
  464. margin-bottom: 20rpx;
  465. gap: 12rpx;
  466. }
  467. .service-name {
  468. font-size: 30rpx;
  469. font-weight: bold;
  470. color: #333;
  471. }
  472. .service-tag {
  473. font-size: 20rpx;
  474. padding: 2rpx 8rpx;
  475. border-radius: 8rpx;
  476. border: 1rpx solid;
  477. }
  478. .tag-orange {
  479. color: #ff9800;
  480. border-color: #ff9800;
  481. background: #fff3e0;
  482. }
  483. .tag-blue {
  484. color: #2196f3;
  485. border-color: #2196f3;
  486. background: #e3f2fd;
  487. }
  488. .tag-green {
  489. color: #4caf50;
  490. border-color: #4caf50;
  491. background: #e8f5e9;
  492. }
  493. .pet-row {
  494. display: flex;
  495. align-items: center;
  496. margin-bottom: 20rpx;
  497. background: #f7f8fa;
  498. padding: 16rpx 20rpx;
  499. border-radius: 16rpx;
  500. }
  501. .pet-avatar-text {
  502. width: 64rpx;
  503. height: 64rpx;
  504. border-radius: 50%;
  505. background-color: #e3f2fd;
  506. color: #2196f3;
  507. display: flex;
  508. align-items: center;
  509. justify-content: center;
  510. font-weight: bold;
  511. font-size: 32rpx;
  512. margin-right: 20rpx;
  513. }
  514. .pet-desc {
  515. display: flex;
  516. align-items: baseline;
  517. gap: 12rpx;
  518. flex: 1;
  519. }
  520. .pet-desc .bold {
  521. font-size: 28rpx;
  522. font-weight: bold;
  523. color: #333;
  524. }
  525. .pet-desc .sub {
  526. font-size: 24rpx;
  527. color: #666;
  528. }
  529. .user-desc {
  530. font-size: 26rpx;
  531. color: #333;
  532. }
  533. .info-list {
  534. display: flex;
  535. flex-direction: column;
  536. gap: 12rpx;
  537. }
  538. .info-item {
  539. display: flex;
  540. align-items: center;
  541. font-size: 24rpx;
  542. color: #666;
  543. gap: 12rpx;
  544. }
  545. .order-foot {
  546. display: flex;
  547. justify-content: space-between;
  548. align-items: flex-end;
  549. margin-top: 24rpx;
  550. padding-top: 20rpx;
  551. border-top: 1rpx solid #f5f5f5;
  552. }
  553. .foot-left {
  554. display: flex;
  555. flex-direction: column;
  556. gap: 12rpx;
  557. }
  558. .create-time,
  559. .cancel-time {
  560. font-size: 22rpx;
  561. color: #999;
  562. }
  563. .assign-info {
  564. display: flex;
  565. align-items: center;
  566. font-size: 24rpx;
  567. gap: 12rpx;
  568. }
  569. .assign-label {
  570. color: #999;
  571. }
  572. .assign-none {
  573. color: #ccc;
  574. }
  575. .assign-name {
  576. color: #333;
  577. font-weight: bold;
  578. }
  579. .actions {
  580. display: flex;
  581. gap: 16rpx;
  582. }
  583. .action-btn {
  584. height: 56rpx;
  585. line-height: 56rpx;
  586. min-width: 120rpx;
  587. font-size: 24rpx;
  588. font-weight: 600;
  589. padding: 0 28rpx;
  590. border-radius: 28rpx;
  591. display: inline-flex;
  592. align-items: center;
  593. justify-content: center;
  594. }
  595. .btn-cancel {
  596. border: 1rpx solid #ddd;
  597. color: #666;
  598. background: transparent;
  599. }
  600. .btn-primary {
  601. background: linear-gradient(90deg, #ffd53f, #ff9500);
  602. border: none;
  603. color: #fff;
  604. box-shadow: 0 6rpx 16rpx rgba(255, 149, 0, 0.3);
  605. }
  606. .empty-state {
  607. text-align: center;
  608. padding: 100rpx 0;
  609. }
  610. .empty-text {
  611. font-size: 28rpx;
  612. color: #999;
  613. }
  614. .loading-state {
  615. text-align: center;
  616. padding: 100rpx 0;
  617. }
  618. .loading-text {
  619. font-size: 28rpx;
  620. color: #999;
  621. }
  622. </style>