snatch.vue 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <script setup>
  2. import { onMounted, ref } from 'vue';
  3. import { onShow } from '@dcloudio/uni-app';
  4. import { useUserStore } from '@/stores/user';
  5. import { request, parseResponseJson } from '@/utils/request';
  6. import PhoneDialInline from '@/components/phone-dial-inline.vue';
  7. import ClientSkeleton from '@/components/client/client-skeleton.vue';
  8. import { IMG_EMPTY, IMG_LOGO_WATERMARK } from '@/utils/staticAsset';
  9. const userStore = useUserStore();
  10. const list = ref([]);
  11. const loading = ref(false);
  12. const snatchingId = ref('');
  13. async function fetchList() {
  14. loading.value = true;
  15. try {
  16. const res = await request({
  17. url: '/mini/deliverer/snatch/pending?page=1&pageSize=20',
  18. method: 'GET'
  19. });
  20. const body = parseResponseJson(res);
  21. if (body.code === 200) {
  22. list.value = body.data?.list ?? [];
  23. }
  24. }
  25. finally {
  26. loading.value = false;
  27. }
  28. }
  29. async function snatchTask(taskId) {
  30. snatchingId.value = taskId;
  31. try {
  32. const res = await request({
  33. url: `/mini/deliverer/snatch/${taskId}`,
  34. method: 'POST'
  35. });
  36. const body = parseResponseJson(res);
  37. if (body.code === 200) {
  38. uni.showToast({ title: '抢单成功', icon: 'success' });
  39. fetchList();
  40. }
  41. else {
  42. uni.showToast({ title: body.message || '抢单失败', icon: 'none' });
  43. }
  44. }
  45. finally {
  46. snatchingId.value = '';
  47. }
  48. }
  49. onMounted(() => {
  50. if (!userStore.isLoggedIn) {
  51. uni.reLaunch({ url: '/pages/login/login' });
  52. return;
  53. }
  54. if (!userStore.isDeliverer) {
  55. uni.showToast({ title: '仅配送员可访问', icon: 'none' });
  56. setTimeout(() => userStore.navigateToHome(), 1200);
  57. return;
  58. }
  59. fetchList();
  60. });
  61. onShow(() => {
  62. if (!userStore.isLoggedIn || !userStore.isDeliverer)
  63. return;
  64. void fetchList();
  65. });
  66. </script>
  67. <template>
  68. <view class="snatch-page">
  69. <view class="header">
  70. <view class="header-inner">
  71. <text class="title">抢单大厅</text>
  72. <text class="sub">待抢任务</text>
  73. </view>
  74. <image class="header-watermark" :src="staticAssetUrl('images/logo11.png')" mode="aspectFit" />
  75. </view>
  76. <view v-if="loading" class="tip">
  77. <ClientSkeleton variant="list" :rows="4" />
  78. </view>
  79. <scroll-view v-else scroll-y class="scroll">
  80. <view v-for="task in list" :key="task.id" class="card">
  81. <view class="row">
  82. <text class="no">{{ task.taskNo }}</text>
  83. <text class="sub"
  84. >{{ task.clientCount ?? 0 }} 客户 / {{ task.itemCount ?? 0 }} 明细</text
  85. >
  86. </view>
  87. <text class="name">{{ task.supplierName }}</text>
  88. <text v-if="task.supplierAddress" class="addr">{{ task.supplierAddress }}</text>
  89. <PhoneDialInline
  90. v-if="task.supplierPhone"
  91. class="snatch-phone"
  92. :phone="task.supplierPhone"
  93. color="#189b65"
  94. expand
  95. />
  96. <text v-if="task.remainingMinutes != null" class="time">剩余约 {{ task.remainingMinutes }} 分钟</text>
  97. <button
  98. class="btn"
  99. :disabled="snatchingId === task.id"
  100. @click="snatchTask(task.id)"
  101. >
  102. {{ snatchingId === task.id ? '抢单中…' : '抢单' }}
  103. </button>
  104. </view>
  105. <view v-if="list.length === 0" class="empty">
  106. <image class="empty-image" :src="IMG_EMPTY" mode="aspectFit" />
  107. <text class="empty-text">暂无可抢任务</text>
  108. </view>
  109. </scroll-view>
  110. </view>
  111. </template>
  112. <style lang="scss" scoped>
  113. .snatch-page {
  114. min-height: 100vh;
  115. background: #f3f7f5;
  116. }
  117. .header {
  118. position: relative;
  119. margin: 16rpx 16rpx 8rpx;
  120. border-radius: 18rpx;
  121. padding: 18rpx 16rpx;
  122. background: linear-gradient(145deg, #087f57, #0a9f6d);
  123. color: #fff;
  124. overflow: hidden;
  125. }
  126. .header-inner {
  127. position: relative;
  128. z-index: 2;
  129. display: flex;
  130. align-items: baseline;
  131. justify-content: space-between;
  132. gap: 12rpx;
  133. }
  134. .title {
  135. font-size: 32rpx;
  136. font-weight: 700;
  137. letter-spacing: 0.02em;
  138. }
  139. .sub {
  140. font-size: 22rpx;
  141. opacity: 0.92;
  142. }
  143. .header-watermark {
  144. position: absolute;
  145. right: -10rpx;
  146. top: 50%;
  147. transform: translateY(-50%);
  148. width: 220rpx;
  149. height: 220rpx;
  150. opacity: 0.12;
  151. z-index: 1;
  152. }
  153. .tip {
  154. text-align: center;
  155. padding: 40rpx;
  156. color: #6f7e77;
  157. }
  158. .scroll {
  159. height: 100vh;
  160. padding: 24rpx;
  161. }
  162. .card {
  163. background: #fff;
  164. border-radius: 18rpx;
  165. padding: 18rpx 16rpx;
  166. margin-bottom: 12rpx;
  167. border: 1rpx solid #d9e9e1;
  168. box-shadow: 0 4rpx 12rpx rgba(10, 159, 109, 0.06);
  169. .row {
  170. display: flex;
  171. justify-content: space-between;
  172. align-items: center;
  173. margin-bottom: 12rpx;
  174. }
  175. .no {
  176. font-size: 24rpx;
  177. font-weight: 700;
  178. color: #1f2a26;
  179. }
  180. .sub {
  181. font-size: 22rpx;
  182. color: #6f7e77;
  183. }
  184. .name {
  185. font-size: 28rpx;
  186. font-weight: 700;
  187. color: #1f2a26;
  188. display: block;
  189. }
  190. .addr {
  191. font-size: 24rpx;
  192. color: #6f7e77;
  193. margin-top: 8rpx;
  194. display: block;
  195. line-height: 1.4;
  196. }
  197. .snatch-phone {
  198. margin-top: 10rpx;
  199. }
  200. .snatch-phone :deep(.phone-dial-inline__text) {
  201. font-size: 26rpx;
  202. color: #1f2a26;
  203. }
  204. .time {
  205. font-size: 22rpx;
  206. color: #189b65;
  207. margin-top: 8rpx;
  208. display: block;
  209. }
  210. .btn {
  211. margin-top: 20rpx;
  212. width: 100%;
  213. height: 72rpx;
  214. line-height: 72rpx;
  215. background: #10b981;
  216. color: #fff;
  217. font-size: 28rpx;
  218. border-radius: 14rpx;
  219. border: none;
  220. &::after {
  221. border: none;
  222. }
  223. &[disabled] {
  224. opacity: 0.68;
  225. }
  226. }
  227. }
  228. .empty {
  229. display: flex;
  230. flex-direction: column;
  231. align-items: center;
  232. padding: 88rpx 0;
  233. .empty-image {
  234. width: 160rpx;
  235. height: 160rpx;
  236. margin-bottom: 12rpx;
  237. }
  238. .empty-text {
  239. font-size: 24rpx;
  240. color: #6f7e77;
  241. text-align: center;
  242. }
  243. }
  244. </style>