logisticsDetail.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <el-dialog v-model="visible" title="物流信息" width="650px" @close="handleClose">
  3. <div class="logistics-container">
  4. <div class="section-title">单号查询</div>
  5. <el-select v-model="selectedLogistics" placeholder="请选择物流单号" @change="handleLogisticNoChange" style="width: 100%">
  6. <el-option
  7. v-for="item in logisticsList"
  8. :key="item.id"
  9. :label="`${item.logisticNo || item.deliverCode},${getDictLabel(deliver_method, item.deliverMethod)}`"
  10. :value="item.logisticNo || item.deliverCode"
  11. />
  12. </el-select>
  13. <div class="section-title" style="margin-top: 20px">物流信息</div>
  14. <div class="timeline-container">
  15. <el-timeline>
  16. <el-timeline-item v-for="(item, index) in logisticsTrack" :key="index" :timestamp="item.time" placement="top" color="#409EFF">
  17. <div class="track-number">{{ item.trackingNo }}</div>
  18. <div class="track-content">{{ item.content }}</div>
  19. <div v-if="item.imagesUrl" class="timeline-images">
  20. <div class="image-label">签收图片:</div>
  21. <div class="image-list">
  22. <el-image
  23. v-for="(url, imgIndex) in item.imagesUrl.split(',')"
  24. :key="imgIndex"
  25. :src="url"
  26. :preview-src-list="item.imagesUrl.split(',')"
  27. :initial-index="imgIndex"
  28. fit="cover"
  29. class="sign-image"
  30. >
  31. <template #error>
  32. <div class="image-error">
  33. <el-icon><Picture /></el-icon>
  34. </div>
  35. </template>
  36. </el-image>
  37. </div>
  38. </div>
  39. </el-timeline-item>
  40. </el-timeline>
  41. </div>
  42. </div>
  43. </el-dialog>
  44. </template>
  45. <script setup lang="ts">
  46. import { ref, watch } from 'vue';
  47. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  48. const { deliver_method } = toRefs<any>(proxy?.useDict('deliver_method'));
  49. import { selectOrderDeliverByOrderId, queryTrack, listOrderStatusLog } from '@/api/pc/enterprise/order';
  50. import { Picture } from '@element-plus/icons-vue';
  51. interface TrackItem {
  52. time: string;
  53. trackingNo: string;
  54. content: string;
  55. imagesUrl: string;
  56. }
  57. const props = defineProps<{
  58. modelValue: boolean;
  59. orderId?: string | number;
  60. }>();
  61. const emit = defineEmits(['update:modelValue']);
  62. const visible = ref(false);
  63. const selectedLogistics = ref('');
  64. const logisticsList = ref<any[]>([]);
  65. const logisticsTrack = ref<TrackItem[]>([]);
  66. const getDictLabel = (dictOptions: any[], value: string) => {
  67. if (!dictOptions || !value) return value;
  68. const dict = dictOptions.find((item) => item.value === value);
  69. return dict ? dict.label : value;
  70. };
  71. watch(
  72. () => props.modelValue,
  73. (val) => {
  74. visible.value = val;
  75. if (val && props.orderId) {
  76. loadLogisticsData();
  77. }
  78. }
  79. );
  80. watch(visible, (val) => {
  81. emit('update:modelValue', val);
  82. });
  83. // watch(selectedLogistics, (val) => {
  84. // if (val) {
  85. // loadTrackData(val);
  86. // }
  87. // });
  88. const loadLogisticsData = async () => {
  89. if (!props.orderId) return;
  90. try {
  91. const res = await selectOrderDeliverByOrderId({ orderId: props.orderId });
  92. logisticsList.value = res.rows || [];
  93. if (logisticsList.value.length > 0) {
  94. selectedLogistics.value = logisticsList.value[0].logisticNo || logisticsList.value[0].deliverCode;
  95. handleLogisticNoChange(selectedLogistics.value);
  96. }
  97. } catch (error) {
  98. console.error('获取物流单号失败:', error);
  99. }
  100. };
  101. const handleLogisticNoChange = async (logisticNo: string) => {
  102. const selected = logisticsList.value.find((item) => item.logisticNo === logisticNo);
  103. try {
  104. if (selected) {
  105. const res = await queryTrack({ logisticNo: logisticNo, id: selected.id });
  106. // 1. 兼容处理:有些接口返回在 res.data,有些可能直接是 res
  107. const dataList = res.data || [];
  108. if (Array.isArray(dataList) && dataList.length > 0) {
  109. logisticsTrack.value = dataList.map((item: any) => {
  110. // 2. 核心修复:精准匹配时间字段
  111. // 顺丰用 'time',韵达用 'ftime'。
  112. // 优先取 ftime (韵达标准),如果没有则取 time (顺丰标准)
  113. const displayTime = item.ftime || item.time || item.acceptTime || '';
  114. return {
  115. time: displayTime,
  116. // 3. 建议:保留原始状态字段,方便后续筛选(如"已签收")
  117. content: item.context || item.content || '',
  118. // 4. 建议:如果有地址字段,也可以映射进来,没有则保持订单号
  119. trackingNo: item.location || (selected.orderCode ? `${selected.orderCode}` : ''),
  120. imagesUrl: ''
  121. };
  122. });
  123. }
  124. } else {
  125. await listOrderStatusLog({
  126. orderId: props.orderId,
  127. logisticNos: selectedLogistics.value,
  128. pageNum: 1,
  129. pageSize: 100
  130. }).then((res) => {
  131. if (res && res.code == 200) {
  132. logisticsTrack.value = res.rows.map((item: any) => {
  133. return {
  134. time: item.createTime,
  135. trackingNo: item.orderCode ? `${item.orderCode}` : '',
  136. content: item.statusName,
  137. imagesUrl: item.imagesUrl
  138. };
  139. });
  140. } else {
  141. logisticsTrack.value = [
  142. {
  143. time: (selected as any).createTime || '',
  144. trackingNo: selected.orderCode ? `${selected.orderCode}` : '',
  145. content: '已下单',
  146. imagesUrl: ''
  147. }
  148. ];
  149. }
  150. });
  151. }
  152. } catch (error) {
  153. console.error('查询物流轨迹失败:', error);
  154. logisticsTrack.value = [];
  155. }
  156. };
  157. // const loadTrackData = async (logisticsId: string | number) => {
  158. // const selectedItem = logisticsList.value.find((item) => item.id === logisticsId);
  159. // try {
  160. // if (selectedItem) {
  161. // const res = await queryTrack({ logisticNo: selectedItem.logisticNo });
  162. // // 1. 兼容处理:有些接口返回在 res.data,有些可能直接是 res
  163. // const dataList = res.data || [];
  164. // if (Array.isArray(dataList) && dataList.length > 0) {
  165. // logisticsTrack.value = dataList.map((item: any) => {
  166. // // 2. 核心修复:精准匹配时间字段
  167. // // 顺丰用 'time',韵达用 'ftime'。
  168. // // 优先取 ftime (韵达标准),如果没有则取 time (顺丰标准)
  169. // const displayTime = item.ftime || item.time || item.acceptTime || '';
  170. // return {
  171. // time: displayTime,
  172. // // 3. 建议:保留原始状态字段,方便后续筛选(如“已签收”)
  173. // content: item.context || item.content || '',
  174. // // 4. 建议:如果有地址字段,也可以映射进来,没有则保持订单号
  175. // trackingNo: selectedItem.orderCode ? `${selectedItem.orderCode}` : ''
  176. // };
  177. // });
  178. // }
  179. // } else {
  180. // await listOrderStatusLog({
  181. // orderId: props.orderId,
  182. // logisticNos: selectedLogistics.value,
  183. // pageNum: 1,
  184. // pageSize: 100
  185. // }).then((res) => {
  186. // if (res && res.code == 200) {
  187. // logisticsTrack.value = res.rows.map((item: any) => {
  188. // return {
  189. // time: item.createTime,
  190. // trackingNo: item.orderCode ? `${item.orderCode}` : '',
  191. // content: item.statusName
  192. // };
  193. // });
  194. // } else {
  195. // logisticsTrack.value = [
  196. // {
  197. // time: (selectedItem as any).createTime || '',
  198. // trackingNo: selectedItem.orderCode ? `${selectedItem.orderCode}` : '',
  199. // content: '已下单'
  200. // }
  201. // ];
  202. // }
  203. // });
  204. // }
  205. // } catch (error) {
  206. // console.error('查询物流轨迹失败:', error);
  207. // logisticsTrack.value = [];
  208. // }
  209. // };
  210. const handleClose = () => {
  211. visible.value = false;
  212. };
  213. </script>
  214. <style scoped lang="scss">
  215. .logistics-container {
  216. .section-title {
  217. font-size: 14px;
  218. font-weight: 500;
  219. color: #333;
  220. margin-bottom: 12px;
  221. }
  222. .timeline-container {
  223. max-height: 500px;
  224. overflow-y: auto;
  225. padding-right: 10px;
  226. :deep(.el-timeline) {
  227. padding-left: 0;
  228. }
  229. :deep(.el-timeline-item__timestamp) {
  230. font-size: 12px;
  231. color: #999;
  232. margin-bottom: 4px;
  233. }
  234. .track-number {
  235. font-size: 12px;
  236. color: #666;
  237. margin-bottom: 4px;
  238. }
  239. .track-content {
  240. font-size: 13px;
  241. color: #333;
  242. line-height: 1.6;
  243. }
  244. .timeline-images {
  245. margin-top: 8px;
  246. padding-top: 8px;
  247. border-top: 1px dashed #dcdfe6;
  248. .image-label {
  249. font-size: 13px;
  250. color: #909399;
  251. margin-bottom: 6px;
  252. }
  253. .image-list {
  254. display: flex;
  255. flex-wrap: wrap;
  256. gap: 8px;
  257. .sign-image {
  258. width: 60px;
  259. height: 60px;
  260. border-radius: 4px;
  261. border: 1px solid #dcdfe6;
  262. cursor: pointer;
  263. .image-error {
  264. width: 100%;
  265. height: 100%;
  266. display: flex;
  267. align-items: center;
  268. justify-content: center;
  269. background-color: #f5f7fa;
  270. color: #c0c4cc;
  271. }
  272. }
  273. }
  274. }
  275. }
  276. }
  277. </style>