task.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <script setup>
  2. import { computed, 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. const userStore = useUserStore();
  8. const activeTab = ref('pending');
  9. const keyword = ref('');
  10. const tasks = ref([]);
  11. const loading = ref(false);
  12. const tabToStatus = {
  13. pending: 'pending',
  14. delivering: 'delivering',
  15. completed: 'completed'
  16. };
  17. const statusMap = {
  18. pending_pickup: { text: '待取货', color: '#07c160' },
  19. pending: { text: '待分配', color: '#07c160' },
  20. assigned: { text: '待取货', color: '#07c160' },
  21. picking: { text: '取货中', color: '#05a050' },
  22. delivering: { text: '配送中', color: '#05a050' },
  23. delivered: { text: '已完成', color: '#07c160' },
  24. completed: { text: '已完成', color: '#07c160' },
  25. cancelled: { text: '已取消', color: '#909399' }
  26. };
  27. function rowStatusBadge(row) {
  28. const ds = String(row.delivererStatus || '').toLowerCase();
  29. if (ds && statusMap[ds])
  30. return statusMap[ds];
  31. const st = String(row.status || '').toLowerCase();
  32. return statusMap[st] || { text: row.status || '-', color: '#909399' };
  33. }
  34. function rowStatusClass(row) {
  35. const ds = String(row.delivererStatus || row.status || '').toLowerCase();
  36. if (['pending_pickup', 'pending', 'assigned', 'picked'].includes(ds))
  37. return 'pending';
  38. if (['delivering', 'picking'].includes(ds))
  39. return 'delivering';
  40. if (['delivered', 'completed'].includes(ds))
  41. return 'completed';
  42. if (['cancelled', 'canceled'].includes(ds))
  43. return 'cancelled';
  44. return 'default';
  45. }
  46. const tabs = [
  47. { key: 'pending', label: '待取货' },
  48. { key: 'delivering', label: '配送中' },
  49. { key: 'completed', label: '已完成' }
  50. ];
  51. function normalizeText(v) {
  52. return String(v || '').trim().toLowerCase();
  53. }
  54. const filteredTasks = computed(() => {
  55. const kw = normalizeText(keyword.value);
  56. if (!kw)
  57. return tasks.value;
  58. return tasks.value.filter((t) => {
  59. const taskNo = normalizeText(t.taskNo);
  60. const supplierName = normalizeText(t.supplierName);
  61. return [taskNo, supplierName].some((x) => x.includes(kw));
  62. });
  63. });
  64. async function fetchTasks() {
  65. loading.value = true;
  66. try {
  67. const st = tabToStatus[activeTab.value] || activeTab.value;
  68. const res = await request({
  69. url: `/mini/deliverer/tasks?status=${encodeURIComponent(st)}&page=1&pageSize=50`,
  70. method: 'GET'
  71. });
  72. const body = parseResponseJson(res);
  73. if (Number(body.code) === 200) {
  74. tasks.value = body.data?.list || [];
  75. }
  76. }
  77. finally {
  78. loading.value = false;
  79. }
  80. }
  81. function switchTab(key) {
  82. activeTab.value = key;
  83. fetchTasks();
  84. }
  85. function goDetail(id) {
  86. uni.navigateTo({ url: `/pages/deliverer/task/detail/detail?id=${id}` });
  87. }
  88. function onSearchClick() {
  89. uni.hideKeyboard && uni.hideKeyboard();
  90. }
  91. onMounted(() => {
  92. if (!userStore.isLoggedIn) {
  93. uni.reLaunch({ url: '/pages/login/login' });
  94. return;
  95. }
  96. if (!userStore.isDeliverer) {
  97. uni.showToast({ title: '仅配送员可访问', icon: 'none' });
  98. setTimeout(() => userStore.navigateToHome(), 1200);
  99. return;
  100. }
  101. fetchTasks();
  102. });
  103. onShow(() => {
  104. if (!userStore.isLoggedIn || !userStore.isDeliverer)
  105. return;
  106. fetchTasks();
  107. });
  108. </script>
  109. <template>
  110. <view v-if="pageLoading" class="page-loading-wrap">
  111. <ClientSkeleton variant="hero-list" :rows="3" />
  112. </view>
  113. <view v-else class="task-page">
  114. <view class="panel">
  115. <view class="search-row">
  116. <view class="search-bar">
  117. <input
  118. v-model.trim="keyword"
  119. class="search-input"
  120. type="text"
  121. placeholder="搜索任务号 / 供应商"
  122. confirm-type="search"
  123. @confirm="onSearchClick"
  124. />
  125. <text class="search-action" @click="onSearchClick">搜索</text>
  126. </view>
  127. </view>
  128. <view class="tabs">
  129. <view
  130. v-for="t in tabs"
  131. :key="t.key"
  132. class="tab"
  133. :class="{ active: activeTab === t.key }"
  134. @click="switchTab(t.key)"
  135. >
  136. <text>{{ t.label }}</text>
  137. </view>
  138. </view>
  139. </view>
  140. <ClientSkeleton v-if="loading" variant="list" :rows="3" />
  141. <scroll-view v-else scroll-y class="list">
  142. <view
  143. v-for="task in filteredTasks"
  144. :key="task.id"
  145. class="task-card"
  146. @click="goDetail(task.id)"
  147. >
  148. <view class="line1">
  149. <text class="no">任务号:{{ task.taskNo || '-' }}</text>
  150. <text class="status" :class="`status--${rowStatusClass(task)}`">{{ rowStatusBadge(task).text }}</text>
  151. </view>
  152. <text class="supplier">{{ task.supplierName || '—' }}</text>
  153. <view class="foot" @click.stop>
  154. <text class="qty">共 {{ task.totalQuantity != null ? task.totalQuantity : 0 }} 件</text>
  155. <PhoneDialInline
  156. v-if="task.supplierPhone"
  157. class="dial"
  158. :phone="task.supplierPhone"
  159. icon-only
  160. color="#189b65"
  161. />
  162. <text v-else class="muted">暂无电话</text>
  163. </view>
  164. </view>
  165. <view v-if="filteredTasks.length === 0" class="empty">
  166. <text>暂无任务</text>
  167. </view>
  168. </scroll-view>
  169. </view>
  170. </template>
  171. <style lang="scss">
  172. page {
  173. height: 100%;
  174. overflow: hidden;
  175. }
  176. </style>
  177. <style lang="scss" scoped>
  178. .page-loading-wrap {
  179. min-height: 100vh;
  180. }
  181. .task-page {
  182. height: 100vh;
  183. display: flex;
  184. flex-direction: column;
  185. overflow: hidden;
  186. background: #f3f7f5;
  187. }
  188. .panel {
  189. margin: 12rpx 16rpx 10rpx;
  190. padding: 12rpx;
  191. border-radius: 16rpx;
  192. background: #ffffff;
  193. border: 1rpx solid #d9e9e1;
  194. box-sizing: border-box;
  195. }
  196. .search-row {
  197. margin-bottom: 10rpx;
  198. }
  199. .search-bar {
  200. display: flex;
  201. align-items: center;
  202. width: 100%;
  203. height: 72rpx;
  204. background: #ffffff;
  205. border: 1rpx solid #d9e9e1;
  206. border-radius: 36rpx;
  207. padding: 0 8rpx 0 24rpx;
  208. box-sizing: border-box;
  209. }
  210. .search-input {
  211. flex: 1;
  212. min-width: 0;
  213. height: 72rpx;
  214. background: transparent;
  215. border: none;
  216. padding: 0;
  217. font-size: 28rpx;
  218. color: #303133;
  219. box-sizing: border-box;
  220. }
  221. .search-input::placeholder {
  222. color: #909399;
  223. }
  224. .search-action {
  225. flex-shrink: 0;
  226. padding: 0 16rpx 0 8rpx;
  227. font-size: 24rpx;
  228. color: #0a9f6d;
  229. font-weight: 600;
  230. }
  231. .tabs {
  232. display: flex;
  233. background: #fff;
  234. border-radius: 14rpx;
  235. border: 1rpx solid #d9e9e1;
  236. padding: 4rpx;
  237. .tab {
  238. flex: 1;
  239. text-align: center;
  240. min-height: 64rpx;
  241. display: flex;
  242. align-items: center;
  243. justify-content: center;
  244. font-size: 24rpx;
  245. color: #6f7e77;
  246. border-radius: 10rpx;
  247. &.active {
  248. background: #e9f9f2;
  249. color: #189b65;
  250. font-weight: 600;
  251. }
  252. }
  253. }
  254. .hint {
  255. text-align: center;
  256. padding: 40rpx;
  257. color: #6f7e77;
  258. }
  259. .list {
  260. flex: 1;
  261. min-height: 0;
  262. padding: 0 16rpx calc(16rpx + env(safe-area-inset-bottom, 0px));
  263. }
  264. .task-card {
  265. background: #ffffff;
  266. border: 1rpx solid #d9e9e1;
  267. border-radius: 18rpx;
  268. padding: 16rpx;
  269. margin-top: 12rpx;
  270. box-shadow: 0 4rpx 12rpx rgba(10, 159, 109, 0.06);
  271. }
  272. .line1 {
  273. display: flex;
  274. align-items: center;
  275. justify-content: space-between;
  276. gap: 12rpx;
  277. margin-bottom: 10rpx;
  278. }
  279. .no {
  280. flex: 1;
  281. min-width: 0;
  282. font-size: 24rpx;
  283. font-weight: 700;
  284. color: #1f2a26;
  285. word-break: break-all;
  286. }
  287. .status {
  288. flex-shrink: 0;
  289. min-height: 44rpx;
  290. padding: 0 16rpx;
  291. border-radius: 999rpx;
  292. font-size: 20rpx;
  293. display: inline-flex;
  294. align-items: center;
  295. }
  296. .status--pending {
  297. background: #fff4dd;
  298. color: #cf8a19;
  299. }
  300. .status--delivering {
  301. background: #e9f9f2;
  302. color: #189b65;
  303. }
  304. .status--completed {
  305. background: #d1fae5;
  306. color: #087f57;
  307. }
  308. .status--cancelled {
  309. background: #f1f5f9;
  310. color: #64748b;
  311. }
  312. .status--default {
  313. background: #eef3f1;
  314. color: #6f7e77;
  315. }
  316. .supplier {
  317. display: block;
  318. font-size: 26rpx;
  319. color: #1f2a26;
  320. margin-bottom: 10rpx;
  321. }
  322. .foot {
  323. display: flex;
  324. align-items: center;
  325. justify-content: space-between;
  326. gap: 12rpx;
  327. }
  328. .qty {
  329. font-size: 22rpx;
  330. color: #6f7e77;
  331. }
  332. .dial :deep(.phone-dial-inline__btn) {
  333. width: 44rpx;
  334. height: 44rpx;
  335. background: transparent;
  336. border-radius: 0;
  337. }
  338. .muted {
  339. font-size: 22rpx;
  340. color: #9aa9a2;
  341. }
  342. .empty {
  343. text-align: center;
  344. padding: 120rpx;
  345. color: #6f7e77;
  346. font-size: 24rpx;
  347. }
  348. </style>