index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. <template>
  2. <view class="order-list-page">
  3. <!-- 顶部状态栏 -->
  4. <view class="sticky-header">
  5. <scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
  6. <view class="tabs-row">
  7. <view v-for="tab in tabList" :key="tab.name"
  8. :class="['tab-item', { active: activeStatus === tab.name }]" @click="onTabClick(tab.name)">
  9. <text>{{ tab.title }}</text>
  10. </view>
  11. </view>
  12. </scroll-view>
  13. <!-- 搜索和过滤 -->
  14. <view class="filter-row">
  15. <picker :range="typeOptions" range-key="text" @change="onTypeChange">
  16. <view class="dropdown-btn">
  17. <text>{{ currentTypeName }}</text>
  18. <uni-icons type="bottom" size="12" color="#333"></uni-icons>
  19. </view>
  20. </picker>
  21. <view class="search-wrap">
  22. <uni-icons type="search" size="14" color="#999"></uni-icons>
  23. <input class="search-input" v-model="searchValue" placeholder="订单号/商户/宠主/手机号"
  24. placeholder-class="placeholder-style" />
  25. </view>
  26. </view>
  27. </view>
  28. <!-- 订单列表内容 -->
  29. <view class="list-container">
  30. <view class="order-card" v-for="order in filteredOrders" :key="order.id" @click="goToDetail(order)">
  31. <!-- 头部:订单号与状态 -->
  32. <view class="order-head">
  33. <text class="order-no">{{ order.id }}</text>
  34. <text :class="['status-text', order.statusClass]">{{ order.statusText }}</text>
  35. </view>
  36. <!-- 主体信息 -->
  37. <view class="order-body">
  38. <view class="service-row">
  39. <text class="service-name">{{ order.serviceType }}</text>
  40. <text class="service-tag tag-orange" v-if="order.serviceTags[0]">{{ order.serviceTags[0]
  41. }}</text>
  42. <text class="service-tag tag-blue" v-if="order.serviceTags[1] === '接'">接</text>
  43. <text class="service-tag tag-green" v-if="order.serviceTags[1] === '送'">送</text>
  44. </view>
  45. <view class="pet-row">
  46. <view class="pet-avatar-text">
  47. <text>{{ order.petName.substring(0, 1).toUpperCase() }}</text>
  48. </view>
  49. <view class="pet-desc">
  50. <text class="bold">{{ order.petName }}</text>
  51. <text class="sub">{{ order.petBreed }}</text>
  52. </view>
  53. <text class="user-desc">{{ order.userName }}</text>
  54. </view>
  55. <view class="info-list">
  56. <view class="info-item">
  57. <uni-icons type="location" size="14" color="#999"></uni-icons>
  58. <text>{{ order.address }}</text>
  59. </view>
  60. <view class="info-item">
  61. <uni-icons type="shop" size="14" color="#999"></uni-icons>
  62. <text>{{ order.shopName }} {{ order.userPhone }}</text>
  63. </view>
  64. <view class="info-item">
  65. <uni-icons type="calendar" size="14" color="#999"></uni-icons>
  66. <text>预约: {{ order.bookTime }}</text>
  67. </view>
  68. </view>
  69. </view>
  70. <!-- 履约与操作栏 -->
  71. <view class="order-foot">
  72. <view class="foot-left">
  73. <text class="create-time">下单: {{ order.createTime }}</text>
  74. <view class="assign-info">
  75. <text class="assign-label">履约信息:</text>
  76. <text class="assign-none" v-if="order.statusText === '待派单'">暂未派单</text>
  77. <text class="assign-none" v-else-if="order.statusText === '已取消'">订单已关闭</text>
  78. <text class="assign-name" v-else>{{ order.assigneeName }}</text>
  79. </view>
  80. <text class="cancel-time" v-if="order.statusText === '已取消' && order.cancelTime">取消: {{
  81. order.cancelTime }}</text>
  82. </view>
  83. <view class="actions">
  84. <template v-if="order.statusText === '待派单' || order.statusText === '待接单'">
  85. <button size="mini" class="action-btn btn-cancel"
  86. @click.stop="onCancelOrder(order)">取消</button>
  87. <button size="mini" class="action-btn btn-primary"
  88. @click.stop="goToDetail(order)">详情</button>
  89. </template>
  90. <template v-else-if="['服务中', '待商家确认', '已完成'].includes(order.statusText)">
  91. <button v-if="['服务中', '已完成'].includes(order.statusText)" size="mini"
  92. class="action-btn btn-cancel" @click.stop="onComplaint(order)">投诉</button>
  93. <button size="mini" class="action-btn btn-primary"
  94. @click.stop="goToDetail(order)">详情</button>
  95. </template>
  96. <template v-else>
  97. <button size="mini" class="action-btn btn-primary"
  98. @click.stop="goToDetail(order)">详情</button>
  99. </template>
  100. </view>
  101. </view>
  102. </view>
  103. <!-- 空状态 -->
  104. <view class="empty-state" v-if="filteredOrders.length === 0">
  105. <text class="empty-text">暂无相关订单</text>
  106. </view>
  107. </view>
  108. <custom-tabbar></custom-tabbar>
  109. </view>
  110. </template>
  111. <script setup>
  112. import { ref, computed } from 'vue'
  113. import customTabbar from '@/components/custom-tabbar/index.vue'
  114. import orderMockData from '@/mock/order.json'
  115. // 筛选与搜索
  116. const activeStatus = ref('all')
  117. const filterType = ref(0)
  118. const searchValue = ref('')
  119. const tabList = [
  120. { title: '全部订单', name: 'all' },
  121. { title: '待派单', name: 'wait_dispatch' },
  122. { title: '待接单', name: 'wait_accept' },
  123. { title: '服务中', name: 'serving' },
  124. { title: '已完成', name: 'done' },
  125. { title: '已取消', name: 'cancel' }
  126. ]
  127. const typeOptions = [
  128. { text: '全部类型', value: 0 },
  129. { text: '宠物接送', value: 1 },
  130. { text: '上门喂遛', value: 2 },
  131. { text: '上门洗护', value: 3 }
  132. ]
  133. const currentTypeName = computed(() => {
  134. return typeOptions[filterType.value].text
  135. })
  136. const statusMap = {
  137. all: '全部',
  138. wait_dispatch: '待派单',
  139. wait_accept: '待接单',
  140. serving: '服务中',
  141. done: '已完成',
  142. cancel: '已取消'
  143. }
  144. const statusKeyMap = {
  145. '待派单': 'wait_dispatch',
  146. '待接单': 'wait_accept',
  147. '服务中': 'serving',
  148. '已完成': 'done',
  149. '已取消': 'cancel'
  150. }
  151. const onTabClick = (name) => {
  152. activeStatus.value = name
  153. }
  154. const onTypeChange = (e) => {
  155. filterType.value = Number(e.detail.value)
  156. }
  157. // 搜索与过滤后的订单列表
  158. const filteredOrders = computed(() => {
  159. return orders.value.filter(order => {
  160. const statusMatch = activeStatus.value === 'all' || order.statusText === statusMap[activeStatus.value]
  161. const typeMap = { 1: '宠物接送', 2: '上门喂遛', 3: '上门洗护' }
  162. const typeMatch = filterType.value === 0 || order.serviceType === typeMap[filterType.value]
  163. const searchLower = searchValue.value.toLowerCase()
  164. const searchMatch = !searchValue.value ||
  165. order.id.toLowerCase().includes(searchLower) ||
  166. order.userName.toLowerCase().includes(searchLower) ||
  167. order.petName.toLowerCase().includes(searchLower) ||
  168. order.userPhone.includes(searchLower)
  169. return statusMatch && typeMatch && searchMatch
  170. })
  171. })
  172. const orders = ref(orderMockData)
  173. const onCancelOrder = (order) => {
  174. uni.showModal({
  175. title: '提示',
  176. content: `确定要取消订单 [${order.id}] 吗?`,
  177. success: (res) => {
  178. if (res.confirm) {
  179. uni.showToast({ title: '订单已取消', icon: 'success' })
  180. order.statusText = '已取消'
  181. order.statusClass = 'text-gray'
  182. activeStatus.value = 'cancel'
  183. }
  184. }
  185. })
  186. }
  187. const goToDetail = (order) => {
  188. const serviceKey = order.serviceType === '宠物接送' ? 'transport' : (order.serviceType === '上门喂遛' ? 'feed' : 'wash')
  189. const statusKey = statusKeyMap[order.statusText] || 'serving'
  190. const cancelTime = order.cancelTime ? encodeURIComponent(order.cancelTime) : ''
  191. uni.navigateTo({
  192. url: `/pages/order/detail/index?service=${serviceKey}&status=${statusKey}${cancelTime ? '&cancelTime=' + cancelTime : ''}`
  193. })
  194. }
  195. const onComplaint = (order) => {
  196. uni.navigateTo({
  197. url: `/pages/my/complaint-submit/index?orderId=${order.id}`
  198. })
  199. }
  200. </script>
  201. <style lang="scss" scoped>
  202. .order-list-page {
  203. background-color: #f2f2f2;
  204. min-height: 100vh;
  205. padding-bottom: 120rpx;
  206. }
  207. .sticky-header {
  208. position: sticky;
  209. top: 0;
  210. z-index: 99;
  211. background-color: #fff;
  212. }
  213. .tabs-scroll {
  214. white-space: nowrap;
  215. border-bottom: 1rpx solid #f5f5f5;
  216. }
  217. .tabs-row {
  218. display: flex;
  219. padding: 0 16rpx;
  220. }
  221. .tab-item {
  222. padding: 24rpx 24rpx;
  223. font-size: 28rpx;
  224. color: #666;
  225. position: relative;
  226. flex-shrink: 0;
  227. }
  228. .tab-item.active {
  229. color: #333;
  230. font-weight: bold;
  231. }
  232. .tab-item.active::after {
  233. content: '';
  234. position: absolute;
  235. bottom: 0;
  236. left: 50%;
  237. transform: translateX(-50%);
  238. width: 48rpx;
  239. height: 6rpx;
  240. background-color: #f7ca3e;
  241. border-radius: 6rpx;
  242. }
  243. .filter-row {
  244. display: flex;
  245. align-items: center;
  246. padding: 12rpx 16rpx;
  247. background-color: #fff;
  248. border-top: 1rpx solid #f9f9f9;
  249. gap: 16rpx;
  250. }
  251. .dropdown-btn {
  252. display: flex;
  253. align-items: center;
  254. gap: 8rpx;
  255. background: #f5f5f5;
  256. border-radius: 34rpx;
  257. padding: 12rpx 24rpx;
  258. font-size: 26rpx;
  259. color: #333;
  260. }
  261. .search-wrap {
  262. flex: 1;
  263. display: flex;
  264. align-items: center;
  265. background: #f5f5f5;
  266. border-radius: 34rpx;
  267. padding: 12rpx 20rpx;
  268. gap: 12rpx;
  269. }
  270. .search-input {
  271. flex: 1;
  272. font-size: 24rpx;
  273. }
  274. .placeholder-style {
  275. color: #999;
  276. font-size: 24rpx;
  277. }
  278. .list-container {
  279. padding: 24rpx;
  280. }
  281. .order-card {
  282. padding: 28rpx;
  283. margin-bottom: 24rpx;
  284. background: #fff;
  285. border-radius: 24rpx;
  286. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  287. }
  288. .order-head {
  289. display: flex;
  290. justify-content: space-between;
  291. align-items: center;
  292. border-bottom: 1rpx solid #f5f5f5;
  293. padding-bottom: 20rpx;
  294. margin-bottom: 20rpx;
  295. }
  296. .order-no {
  297. font-size: 26rpx;
  298. color: #666;
  299. }
  300. .status-text {
  301. font-size: 26rpx;
  302. font-weight: bold;
  303. }
  304. .text-red {
  305. color: #f44336;
  306. }
  307. .text-orange {
  308. color: #ff9800;
  309. }
  310. .text-blue {
  311. color: #2196f3;
  312. }
  313. .text-green {
  314. color: #4caf50;
  315. }
  316. .text-gray {
  317. color: #999;
  318. }
  319. .service-row {
  320. display: flex;
  321. align-items: center;
  322. margin-bottom: 20rpx;
  323. gap: 12rpx;
  324. }
  325. .service-name {
  326. font-size: 30rpx;
  327. font-weight: bold;
  328. color: #333;
  329. }
  330. .service-tag {
  331. font-size: 20rpx;
  332. padding: 2rpx 8rpx;
  333. border-radius: 8rpx;
  334. border: 1rpx solid;
  335. }
  336. .tag-orange {
  337. color: #ff9800;
  338. border-color: #ff9800;
  339. background: #fff3e0;
  340. }
  341. .tag-blue {
  342. color: #2196f3;
  343. border-color: #2196f3;
  344. background: #e3f2fd;
  345. }
  346. .tag-green {
  347. color: #4caf50;
  348. border-color: #4caf50;
  349. background: #e8f5e9;
  350. }
  351. .pet-row {
  352. display: flex;
  353. align-items: center;
  354. margin-bottom: 20rpx;
  355. background: #f7f8fa;
  356. padding: 16rpx 20rpx;
  357. border-radius: 16rpx;
  358. }
  359. .pet-avatar-text {
  360. width: 64rpx;
  361. height: 64rpx;
  362. border-radius: 50%;
  363. background-color: #e3f2fd;
  364. color: #2196f3;
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. font-weight: bold;
  369. font-size: 32rpx;
  370. margin-right: 20rpx;
  371. }
  372. .pet-desc {
  373. display: flex;
  374. align-items: baseline;
  375. gap: 12rpx;
  376. flex: 1;
  377. }
  378. .pet-desc .bold {
  379. font-size: 28rpx;
  380. font-weight: bold;
  381. color: #333;
  382. }
  383. .pet-desc .sub {
  384. font-size: 24rpx;
  385. color: #666;
  386. }
  387. .user-desc {
  388. font-size: 26rpx;
  389. color: #333;
  390. }
  391. .info-list {
  392. display: flex;
  393. flex-direction: column;
  394. gap: 12rpx;
  395. }
  396. .info-item {
  397. display: flex;
  398. align-items: center;
  399. font-size: 24rpx;
  400. color: #666;
  401. gap: 12rpx;
  402. }
  403. .order-foot {
  404. display: flex;
  405. justify-content: space-between;
  406. align-items: flex-end;
  407. margin-top: 24rpx;
  408. padding-top: 20rpx;
  409. border-top: 1rpx solid #f5f5f5;
  410. }
  411. .foot-left {
  412. display: flex;
  413. flex-direction: column;
  414. gap: 12rpx;
  415. }
  416. .create-time,
  417. .cancel-time {
  418. font-size: 22rpx;
  419. color: #999;
  420. }
  421. .assign-info {
  422. display: flex;
  423. align-items: center;
  424. font-size: 24rpx;
  425. gap: 12rpx;
  426. }
  427. .assign-label {
  428. color: #999;
  429. }
  430. .assign-none {
  431. color: #ccc;
  432. }
  433. .assign-name {
  434. color: #333;
  435. font-weight: bold;
  436. }
  437. .actions {
  438. display: flex;
  439. gap: 16rpx;
  440. }
  441. .action-btn {
  442. height: 56rpx;
  443. line-height: 56rpx;
  444. min-width: 120rpx;
  445. font-size: 24rpx;
  446. font-weight: 600;
  447. padding: 0 28rpx;
  448. border-radius: 28rpx;
  449. display: inline-flex;
  450. align-items: center;
  451. justify-content: center;
  452. }
  453. .btn-cancel {
  454. border: 1rpx solid #ddd;
  455. color: #666;
  456. background: transparent;
  457. }
  458. .btn-primary {
  459. background: linear-gradient(90deg, #ffd53f, #ff9500);
  460. border: none;
  461. color: #fff;
  462. box-shadow: 0 6rpx 16rpx rgba(255, 149, 0, 0.3);
  463. }
  464. .empty-state {
  465. text-align: center;
  466. padding: 100rpx 0;
  467. }
  468. .empty-text {
  469. font-size: 28rpx;
  470. color: #999;
  471. }
  472. </style>