list.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <template>
  2. <view class="bx-page pl-driver-plan-page">
  3. <view class="topbar" :style="safeAreaStyle">
  4. <view class="topbar-left">
  5. <text class="topbar-back" @click="goBack">←</text>
  6. <text class="topbar-title">司机运输计划单</text>
  7. </view>
  8. <view class="topbar-right" />
  9. </view>
  10. <view class="filters">
  11. <uni-calendar
  12. :insert="true"
  13. :date="selectedDate"
  14. :selected="calendarSelected"
  15. :showMonth="false"
  16. @change="onCalendarChange"
  17. @monthSwitch="onMonthSwitch"
  18. />
  19. </view>
  20. <view class="list-body">
  21. <view v-if="loading" class="page-loading">加载中...</view>
  22. <view v-else-if="!records.length" class="empty-hint">暂无运输记录</view>
  23. <view
  24. v-for="(row, index) in records"
  25. :key="row.planId || index"
  26. class="record-card"
  27. @click="openRecord(row)"
  28. >
  29. <text class="index-tag">{{ index + 1 }}</text>
  30. <view class="record-top">
  31. <text class="card-title">{{ cardTitle(row) }}</text>
  32. <view class="record-top-actions" @click.stop>
  33. <view class="pound-qr-btn" @click="openPoundQr(row)">磅单二维码</view>
  34. <text class="tag" :class="tagClass(row)">{{ row.statusLabel || '进行中' }}</text>
  35. </view>
  36. </view>
  37. <view class="meta">
  38. 车牌:<text class="plate-hl">{{ row.licensePlate || '—' }}</text>
  39. </view>
  40. <template v-if="(row.stops || []).length">
  41. <view
  42. v-for="(stop, sIndex) in row.stops"
  43. :key="`${stop.stopType}-${stop.seq ?? sIndex}`"
  44. class="stop-block"
  45. >
  46. <view class="stop-block__title"
  47. >{{ stop.label }} · {{ stop.customerName || '—' }}</view
  48. >
  49. <view class="meta">时间:{{ formatDay(stop.stopTime) }}</view>
  50. <view class="meta">地址:{{ stop.address || '—' }}</view>
  51. <template v-if="!(stop.products || []).length">
  52. <view class="meta">产品:—</view>
  53. </template>
  54. <template v-for="(prod, pIndex) in stop.products || []" :key="pIndex">
  55. <view class="meta">产品:{{ prod.productName || '—' }}</view>
  56. <view class="meta">规格型号:{{ prod.model || '—' }}</view>
  57. <view class="meta">包装规格:{{ prod.packageSpec || '—' }}</view>
  58. <view class="meta"
  59. >数量:{{ formatQty(prod.quantity) }} · 单位:{{ prod.unit || '—' }}</view
  60. >
  61. </template>
  62. </view>
  63. </template>
  64. <view class="meta">运输计划号:{{ row.planNo || '—' }}</view>
  65. <view class="card-actions" @click.stop>
  66. <button class="btn btn-sm btn-ghost" @click="handleCopy(row)">复制发群</button>
  67. </view>
  68. </view>
  69. </view>
  70. <db-pound-qrcode-popup
  71. :show="poundQrOpen"
  72. :inventory-id="poundQrId"
  73. :snapshot="poundQrSnapshot"
  74. @close="poundQrOpen = false"
  75. @update:show="(v) => (poundQrOpen = v)"
  76. />
  77. </view>
  78. </template>
  79. <script setup>
  80. import { ref } from 'vue'
  81. import { onShow } from '@dcloudio/uni-app'
  82. import { PAGE_PATHS } from '@/config/api'
  83. import { driverPlanApi } from '@/api/pl'
  84. import { getSafeAreaStyle } from '@/utils/safeArea'
  85. import { copyDriverPlanToClipboard } from '@/utils/plDriverPlanCopy'
  86. import DbPoundQrcodePopup from '@/components/db-ui/db-pound-qrcode-popup.vue'
  87. const safeAreaStyle = getSafeAreaStyle()
  88. const poundQrOpen = ref(false)
  89. const poundQrId = ref('')
  90. const poundQrSnapshot = ref(null)
  91. function todayStr() {
  92. const d = new Date()
  93. const m = `${d.getMonth() + 1}`.padStart(2, '0')
  94. const day = `${d.getDate()}`.padStart(2, '0')
  95. return `${d.getFullYear()}-${m}-${day}`
  96. }
  97. const selectedDate = ref(todayStr())
  98. const records = ref([])
  99. const loading = ref(false)
  100. const calendarSelected = ref([])
  101. let calendarMonth = selectedDate.value.slice(0, 7)
  102. function formatDay(value) {
  103. if (!value) return '—'
  104. const normalized = String(value).replace('T', ' ')
  105. return normalized.length >= 10 ? normalized.slice(0, 10) : normalized
  106. }
  107. function formatQty(value) {
  108. if (value == null || value === '') return '—'
  109. return String(value)
  110. }
  111. function cardTitleDate(row) {
  112. const stopTimes = (row.stops || [])
  113. .map((stop) => formatDay(stop.stopTime))
  114. .filter((value) => value !== '—')
  115. if (stopTimes.length) return stopTimes[0]
  116. const fromPickup = formatDay(row.pickupTime)
  117. if (fromPickup !== '—') return fromPickup
  118. const fromDelivery = formatDay(row.deliveryTime)
  119. if (fromDelivery !== '—') return fromDelivery
  120. return selectedDate.value
  121. }
  122. function cardTitle(row) {
  123. return cardTitleDate(row)
  124. }
  125. function tagClass(row) {
  126. if (row.planVoided || row.statusLabel === '已作废') return 'tag--danger'
  127. if (row.planArchived || row.statusLabel === '已归档') return 'tag--muted'
  128. return 'tag--success'
  129. }
  130. async function loadList() {
  131. loading.value = true
  132. try {
  133. const res = await driverPlanApi.history({ date: selectedDate.value })
  134. records.value = res.records || []
  135. } catch (e) {
  136. records.value = []
  137. } finally {
  138. loading.value = false
  139. }
  140. }
  141. async function loadCalendarCounts(month) {
  142. const m = String(month || '').slice(0, 7)
  143. if (!/^\d{4}-\d{2}$/.test(m)) return
  144. calendarMonth = m
  145. const today = todayStr()
  146. try {
  147. const counts = await driverPlanApi.calendarCounts(m)
  148. calendarSelected.value = Object.keys(counts || {})
  149. .filter((date) => Number(counts[date]) > 0 && date >= today)
  150. .map((date) => ({ date, info: String(counts[date]) }))
  151. } catch (e) {
  152. calendarSelected.value = []
  153. }
  154. }
  155. function onCalendarChange(e) {
  156. const v = e?.fulldate || e?.date || (typeof e === 'string' ? e : '')
  157. if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}/.test(v)) {
  158. selectedDate.value = v.slice(0, 10)
  159. const month = selectedDate.value.slice(0, 7)
  160. if (month !== calendarMonth) {
  161. loadCalendarCounts(month)
  162. }
  163. loadList()
  164. }
  165. }
  166. function onMonthSwitch(e) {
  167. const year = e?.year
  168. const month = e?.month
  169. if (year == null || month == null) return
  170. const m = `${year}-${`${month}`.padStart(2, '0')}`
  171. loadCalendarCounts(m)
  172. }
  173. function openRecord(row) {
  174. const id = row.planId || row.plan_id
  175. if (!id) {
  176. uni.showToast({ title: '计划无效', icon: 'none' })
  177. return
  178. }
  179. uni.navigateTo({ url: `${PAGE_PATHS.plDriverPlanDetail}?planId=${id}` })
  180. }
  181. function poundItemsOf(row) {
  182. const items = row?.poundQrcodeItems || row?.pound_qrcode_items || []
  183. return Array.isArray(items) ? items : []
  184. }
  185. function openPoundForInventoryId(inventoryId) {
  186. const id = String(inventoryId || '').trim()
  187. if (!id) {
  188. uni.showToast({ title: '缺少出入库单', icon: 'none' })
  189. return
  190. }
  191. poundQrId.value = id
  192. poundQrSnapshot.value = null
  193. poundQrOpen.value = true
  194. }
  195. function openPoundQr(row) {
  196. const items = poundItemsOf(row)
  197. if (!items.length) {
  198. uni.showToast({ title: '暂无磅单二维码', icon: 'none' })
  199. return
  200. }
  201. if (items.length === 1) {
  202. openPoundForInventoryId(items[0].inventoryId || items[0].inventory_id)
  203. return
  204. }
  205. uni.showActionSheet({
  206. itemList: items.map((item) => item.label || item.partnerCompany || '磅单'),
  207. success: (res) => {
  208. const picked = items[res.tapIndex]
  209. if (!picked) return
  210. openPoundForInventoryId(picked.inventoryId || picked.inventory_id)
  211. }
  212. })
  213. }
  214. async function handleCopy(row) {
  215. const id = row?.planId || row?.plan_id
  216. if (!id) {
  217. uni.showToast({ title: '计划无效', icon: 'none' })
  218. return
  219. }
  220. uni.showLoading({ title: '加载中...', mask: true })
  221. try {
  222. const page = (await driverPlanApi.get(id)) || {}
  223. const ensured = await driverPlanApi.ensureCopySeq(id)
  224. const copySeqNo = ensured?.copySeqNo ?? ensured?.copy_seq_no
  225. if (copySeqNo == null || copySeqNo === '') {
  226. throw new Error('no seq')
  227. }
  228. uni.hideLoading()
  229. await copyDriverPlanToClipboard({ ...page, copySeqNo })
  230. } catch (e) {
  231. uni.hideLoading()
  232. uni.showToast({ title: '复制失败', icon: 'none' })
  233. }
  234. }
  235. function goBack() {
  236. uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
  237. }
  238. onShow(() => {
  239. loadCalendarCounts(calendarMonth || selectedDate.value.slice(0, 7))
  240. loadList()
  241. })
  242. </script>
  243. <!-- 微信端:scss 须在 style 中 @import;script 里 import 只会生成空 js,H5 有样式而小程序无样式 -->
  244. <style lang="scss">
  245. @import '@/styles/bx-theme.scss';
  246. @import '@/styles/pl-driver-plan.scss';
  247. </style>
  248. <style lang="scss" scoped>
  249. :deep(.uni-calendar__backtoday) {
  250. display: none;
  251. }
  252. :deep(.uni-calendar),
  253. :deep(.uni-calendar__content),
  254. :deep(.uni-calendar__box) {
  255. box-shadow: none !important;
  256. border: none !important;
  257. background: transparent !important;
  258. }
  259. :deep(.uni-calendar-item--checked),
  260. :deep(.uni-calendar-item--isDay),
  261. :deep(.uni-calendar-item--multiple) {
  262. background-color: #0a9f6d !important;
  263. color: #fff !important;
  264. }
  265. :deep(.uni-calendar-item--isDay-text) {
  266. color: #0a9f6d !important;
  267. }
  268. :deep(.uni-calendar-item--checked.uni-calendar-item--isDay-text),
  269. :deep(.uni-calendar-item--isDay.uni-calendar-item--isDay-text) {
  270. color: #fff !important;
  271. }
  272. </style>