DispatchOrderPanel.vue 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844
  1. <script setup lang="ts">
  2. import { ref, computed, watch, reactive, onMounted } from 'vue';
  3. import { message } from 'ant-design-vue';
  4. import dayjs, { type Dayjs } from 'dayjs';
  5. import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
  6. import {
  7. todayOrderMethod, pendingOrderMethod, allPieOrderMethod, todayPhysicalOrderMethod, pendingPhysicalOrderMethod, allPhysicalOrderMethod,
  8. getOrderLiaisonListMethod, confirmPieOrderMethod, confirmPhysicalOrderMethod, getPieOrderCountMethod
  9. } from '@/request/api/order.api';
  10. import type { OrderModel, OrderQuery, OrderLiaisonListModel } from '@/model/order.model';
  11. import DateTimePicker from './DateTimePicker.vue';
  12. defineOptions({
  13. name: 'DispatchOrderPanel',
  14. });
  15. interface Institution extends OrderLiaisonListModel {
  16. address: string;
  17. }
  18. const props = defineProps<{
  19. orderType: 'offline' | 'physical'; // 线下服务 | 实体商品
  20. }>();
  21. // 当前选中的子tab
  22. const activeSubTab = ref<'pending' | 'today' | 'all'>('pending');
  23. // 订单列表数据
  24. const orderList = ref<OrderModel[]>([]);
  25. // 可派单机构列表
  26. const institutionList = ref<Institution[]>([]);
  27. // 当前选中的订单
  28. const selectedOrderId = ref<number | null>(null);
  29. // 订单计数
  30. const orderCounts = ref({
  31. offlineCount: 0, // 线下服务订单总和
  32. allPieOfflineOrderCount: 0, // 线下服务——全部指派订单数量
  33. pendPieOfflineOrderCount: 0, // 线下服务——待指派订单数量
  34. todayPieOfflineOrderCount: 0, // 线下服务——今日指派订单数量
  35. onlineCount: 0, // 实体商品数量订单总和
  36. allPieOnlineOrderCount: 0, // 实体商品——全部指派订单数量
  37. pendPieOnlineOrderCount: 0, // 实体商品——待指派订单数量
  38. todayPieOnlineOrderCount: 0, // 实体商品——今日指派订单数量
  39. });
  40. // 当前订单的分配机构输入值
  41. const assignedInstitutionMap = ref<Record<number, string>>({});
  42. // 记录是否从右边列表选择指派(用于控制按钮显示)
  43. const assignedFromRightList = ref<Record<number, boolean>>({});
  44. // 日期时间选择弹窗相关
  45. const dateTimePickerVisible = ref(false);
  46. const currentPickerOrderId = ref<number | null>(null);
  47. const pickerType = ref<'date' | 'time'>('date'); // 选择器类型:日期或时间
  48. const isFromTimeButton = ref(false); // 是否从时间按钮进入
  49. // 加载状态
  50. const loading = ref(false);
  51. const institutionLoading = ref(false);
  52. // 订单列表分页配置
  53. const orderPageConfig = reactive({
  54. currentPage: 1,
  55. pageSize: 10,
  56. total: 0,
  57. });
  58. // 机构列表分页配置
  59. const institutionPageConfig = reactive({
  60. currentPage: 1,
  61. pageSize: 10,
  62. total: 0,
  63. });
  64. // 搜索条件
  65. const searchForm = reactive({
  66. customerName: '',
  67. contactPhone: '',
  68. orderTypes: [] as string[], // 线下服务: 'booked' | 'verified' | 实体商品: 'pendingShip' | 'shipped'
  69. });
  70. // 获取可派单机构名称列表
  71. const availableInstitutionNames = computed(() => {
  72. return allInstitutionData.value.map(inst => inst.name);
  73. });
  74. // 获取订单的完整地址
  75. function getOrderAddress(order: OrderModel): string {
  76. const addressParts = [
  77. order.provinceName,
  78. order.cityName,
  79. order.areaName,
  80. order.detailAddress,
  81. ].filter(Boolean);
  82. return addressParts.join('');
  83. }
  84. function formatCustomerInfo(order: OrderModel): string {
  85. const parts: string[] = [];
  86. if (order.liaison) {
  87. parts.push(order.liaison);
  88. }
  89. if (order.phone) {
  90. parts.push(order.phone);
  91. }
  92. const address = getOrderAddress(order);
  93. if (address) {
  94. parts.push(address);
  95. }
  96. return parts.join(',');
  97. }
  98. // 计算结束时间
  99. function calculateEndTime(order: OrderModel): string | null {
  100. if (!order.arrangeTime) return null;
  101. // 如果没有服务时长,无法计算结束时间
  102. if (!order.offlineDuration) return null;
  103. // 根据开始时间和服务时长计算结束时间
  104. const durationMinutes = parseDurationToMinutes(order.offlineDuration);
  105. if (durationMinutes > 0) {
  106. const start = dayjs(order.arrangeTime, 'HH:mm');
  107. const end = start.add(durationMinutes, 'minute');
  108. return end.format('HH:mm');
  109. }
  110. return null;
  111. }
  112. // 判断订单是否已核销(线下服务)
  113. function isOrderVerified(order: OrderModel): boolean {
  114. return props.orderType === 'offline' && order.type === 3;
  115. }
  116. // 判断订单是否取消预约派单(线下服务)
  117. function isOrderCancelPie(order: OrderModel): boolean {
  118. return props.orderType === 'offline' && order.type === 4;
  119. }
  120. // 判断订单是否已发货(实体商品)
  121. function isOrderShipped(order: OrderModel): boolean {
  122. return props.orderType === 'physical' && order.type === 3;
  123. }
  124. // 右侧机构表格配置
  125. const gridRef = ref<VxeGridInstance<Institution>>();
  126. const gridOptions = reactive<VxeGridProps<Institution>>({
  127. border: true,
  128. autoResize: true,
  129. height: '100%',
  130. scrollY: { enabled: true },
  131. columns: [
  132. {
  133. field: 'action',
  134. title: '操作',
  135. width: 150,
  136. align: 'center',
  137. showOverflow: false,
  138. cellRender: {
  139. name: 'VxeButton',
  140. props: {
  141. type: 'primary',
  142. size: 'large',
  143. content: '指派',
  144. },
  145. events: {
  146. click({ row }: { row: Institution }) {
  147. handleAssignInstitution(row.name);
  148. },
  149. },
  150. },
  151. },
  152. { field: 'name', title: '机构名称' },
  153. { field: 'detailAddress', title: '地址' },
  154. { field: 'phone', title: '联系电话' },
  155. { field: 'todayOrderQuantity', title: '当日订单数' },
  156. { field: 'evaluateScore', title: '机构评分' },
  157. ],
  158. data: [],
  159. pagerConfig: {
  160. enabled: false,
  161. },
  162. });
  163. // 初始化
  164. onMounted(async () => {
  165. await loadOrderCounts();
  166. await loadOrderList();
  167. if (orderList.value.length > 0) {
  168. selectedOrderId.value = getOrderId(orderList.value[0]);
  169. if (activeSubTab.value !== 'pending') {
  170. orderList.value.forEach(order => {
  171. if (order.conditioningProgramSupplierName) {
  172. const orderId = getOrderId(order);
  173. assignedInstitutionMap.value[orderId] = order.conditioningProgramSupplierName;
  174. }
  175. });
  176. }
  177. // 选中订单后再加载机构列表
  178. await loadInstitutionList();
  179. }
  180. });
  181. // 监听子tab切换
  182. watch(activeSubTab, async () => {
  183. orderPageConfig.currentPage = 1;
  184. allInstitutionData.value = [];
  185. institutionList.value = [];
  186. gridRef.value?.loadData([]);
  187. // 切换tab时,清空所有按钮显示标记,这样按钮就不会显示
  188. assignedFromRightList.value = {};
  189. await loadOrderCounts();
  190. await loadOrderList();
  191. if (orderList.value.length > 0) {
  192. selectedOrderId.value = getOrderId(orderList.value[0]);
  193. if (activeSubTab.value !== 'pending') {
  194. orderList.value.forEach(order => {
  195. if (order.conditioningProgramSupplierName) {
  196. const orderId = getOrderId(order);
  197. assignedInstitutionMap.value[orderId] = order.conditioningProgramSupplierName;
  198. }
  199. });
  200. } else {
  201. orderList.value.forEach(order => {
  202. const orderId = getOrderId(order);
  203. assignedInstitutionMap.value[orderId] = '';
  204. assignedFromRightList.value[orderId] = false;
  205. });
  206. }
  207. await loadInstitutionList();
  208. } else {
  209. selectedOrderId.value = null;
  210. allInstitutionData.value = [];
  211. institutionList.value = [];
  212. gridRef.value?.loadData([]);
  213. }
  214. });
  215. // 监听订单类型切换
  216. watch(() => props.orderType, async () => {
  217. activeSubTab.value = 'pending';
  218. selectedOrderId.value = null;
  219. assignedInstitutionMap.value = {};
  220. // 切换订单类型时,清空所有按钮显示标记
  221. assignedFromRightList.value = {};
  222. orderPageConfig.currentPage = 1;
  223. searchForm.customerName = '';
  224. searchForm.contactPhone = '';
  225. searchForm.orderTypes = [];
  226. allInstitutionData.value = [];
  227. institutionList.value = [];
  228. gridRef.value?.loadData([]);
  229. await loadOrderCounts();
  230. await loadOrderList();
  231. if (orderList.value.length > 0) {
  232. const firstOrder = orderList.value[0];
  233. const orderId = firstOrder.id ?? firstOrder.patientConditioningProgramId;
  234. selectedOrderId.value = orderId;
  235. const verifyOrder = orderList.value.find(order => {
  236. const orderId = order.id ?? order.patientConditioningProgramId;
  237. return orderId === selectedOrderId.value;
  238. });
  239. if (!verifyOrder) {
  240. console.error('错误:选中的订单ID不在订单列表中!', {
  241. selectedOrderId: selectedOrderId.value,
  242. orderListIds: orderList.value.map(o => o.id ?? o.patientConditioningProgramId),
  243. });
  244. }
  245. if (activeSubTab.value !== 'pending') {
  246. orderList.value.forEach(order => {
  247. if (order.conditioningProgramSupplierName) {
  248. const orderId = getOrderId(order);
  249. assignedInstitutionMap.value[orderId] = order.conditioningProgramSupplierName;
  250. }
  251. });
  252. } else {
  253. // 待指派订单tab:清空所有已分配的机构
  254. orderList.value.forEach(order => {
  255. const orderId = getOrderId(order);
  256. assignedInstitutionMap.value[orderId] = '';
  257. assignedFromRightList.value[orderId] = false;
  258. });
  259. }
  260. // 选中订单后必须加载机构列表(切换订单类型时)
  261. await loadInstitutionList();
  262. } else {
  263. // 如果没有订单,清空机构列表
  264. allInstitutionData.value = [];
  265. institutionPageConfig.total = 0;
  266. institutionList.value = [];
  267. gridRef.value?.loadData([]);
  268. }
  269. }, { immediate: false });
  270. function buildOrderQuery(): OrderQuery | undefined {
  271. if (activeSubTab.value !== 'all') {
  272. return undefined;
  273. }
  274. const query: Partial<OrderQuery> = {};
  275. // 联系人(客户姓名)
  276. if (searchForm.customerName) {
  277. query.liaison = searchForm.customerName;
  278. }
  279. // 电话
  280. if (searchForm.contactPhone) {
  281. query.phone = searchForm.contactPhone;
  282. }
  283. // 订单类型转换
  284. if (searchForm.orderTypes && searchForm.orderTypes.length > 0) {
  285. const types: string[] = [];
  286. if (props.orderType === 'offline') {
  287. // 线下服务
  288. if (searchForm.orderTypes.includes('booked')) {
  289. types.push('2'); // 已派单但未核销
  290. }
  291. if (searchForm.orderTypes.includes('verified')) {
  292. types.push('3'); // 已核销
  293. }
  294. } else if (props.orderType === 'physical') {
  295. // 实体商品
  296. if (searchForm.orderTypes.includes('pendingShip')) {
  297. types.push('2'); // 已派单但未核销(待发货)
  298. }
  299. if (searchForm.orderTypes.includes('shipped')) {
  300. types.push('3'); // 已核销(已发货)
  301. }
  302. }
  303. if (types.length > 0) {
  304. query.types = types;
  305. }
  306. }
  307. return Object.keys(query).length > 0 ? (query as OrderQuery) : undefined;
  308. }
  309. // 加载订单列表
  310. async function loadOrderList() {
  311. loading.value = true;
  312. try {
  313. let response;
  314. if (props.orderType === 'offline') {
  315. // 线下服务接口
  316. if (activeSubTab.value === 'pending') {
  317. // 待指派订单
  318. response = await pendingOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
  319. } else if (activeSubTab.value === 'today') {
  320. // 今日指派订单
  321. response = await todayOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
  322. } else if (activeSubTab.value === 'all') {
  323. // 全部指派订单
  324. const query = buildOrderQuery();
  325. response = await allPieOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize, query);
  326. } else {
  327. return;
  328. }
  329. } else {
  330. // 实体商品接口
  331. if (activeSubTab.value === 'pending') {
  332. // 待指派订单
  333. response = await pendingPhysicalOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
  334. } else if (activeSubTab.value === 'today') {
  335. // 今日指派订单
  336. response = await todayPhysicalOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize);
  337. } else if (activeSubTab.value === 'all') {
  338. // 全部指派订单
  339. const query = buildOrderQuery();
  340. response = await allPhysicalOrderMethod(orderPageConfig.currentPage, orderPageConfig.pageSize, query);
  341. } else {
  342. return;
  343. }
  344. }
  345. orderList.value = response.data;
  346. orderPageConfig.total = response.total;
  347. } catch (error) {
  348. orderList.value = [];
  349. orderPageConfig.total = 0;
  350. } finally {
  351. loading.value = false;
  352. }
  353. }
  354. const allInstitutionData = ref<Institution[]>([]);
  355. function getInstitutionAddress(institution: OrderLiaisonListModel): string {
  356. const addressParts = [
  357. institution.cityName,
  358. institution.areaName,
  359. institution.detailAddress,
  360. ].filter(Boolean);
  361. return addressParts.join('');
  362. }
  363. function transformToInstitution(item: OrderLiaisonListModel): Institution {
  364. return {
  365. ...item,
  366. address: getInstitutionAddress(item),
  367. };
  368. }
  369. // 加载可派单机构列表
  370. async function loadInstitutionList() {
  371. institutionLoading.value = true;
  372. allInstitutionData.value = [];
  373. institutionList.value = [];
  374. gridRef.value?.loadData([]);
  375. try {
  376. const selectedOrder = selectedOrderId.value
  377. ? orderList.value.find(order => {
  378. const orderId = order.id ?? order.patientConditioningProgramId;
  379. return orderId === selectedOrderId.value;
  380. })
  381. : null;
  382. if (!selectedOrder) {
  383. allInstitutionData.value = [];
  384. institutionPageConfig.total = 0;
  385. institutionList.value = [];
  386. gridRef.value?.loadData([]);
  387. return;
  388. }
  389. if (selectedOrder.institutionId == null || selectedOrder.conditioningProgramType == null) {
  390. allInstitutionData.value = [];
  391. institutionPageConfig.total = 0;
  392. institutionList.value = [];
  393. gridRef.value?.loadData([]);
  394. return;
  395. }
  396. if (props.orderType === 'offline') {
  397. if (!selectedOrder.arrangeDate || !selectedOrder.arrangeTime) {
  398. allInstitutionData.value = [];
  399. institutionPageConfig.total = 0;
  400. institutionList.value = [];
  401. gridRef.value?.loadData([]);
  402. // 提示用户需要先选择日期和时间(仅线下服务)
  403. if (!selectedOrder.arrangeDate && !selectedOrder.arrangeTime) {
  404. message.error('请先选择日期和时间');
  405. } else if (!selectedOrder.arrangeDate) {
  406. message.error('请先选择日期');
  407. } else if (!selectedOrder.arrangeTime) {
  408. message.error('请先选择时间');
  409. }
  410. return;
  411. }
  412. }
  413. const data: Partial<any> = {
  414. institutionId: selectedOrder.institutionId,
  415. conditioningProgramType: selectedOrder.conditioningProgramType,
  416. ...(props.orderType === 'offline' && selectedOrder.arrangeDate && selectedOrder.arrangeTime
  417. ? { timeStart: `${selectedOrder.arrangeDate} ${selectedOrder.arrangeTime}` }
  418. : {}),
  419. // 线下服务 type=2,实体商品 type=1
  420. type: props.orderType === 'offline' ? 2 : 1
  421. };
  422. // 获取可派单机构列表
  423. const response = await getOrderLiaisonListMethod(data);
  424. const responseData = Array.isArray(response) ? response : (response?.data || []);
  425. const transformedData = responseData.map(transformToInstitution);
  426. allInstitutionData.value = transformedData;
  427. institutionList.value = transformedData;
  428. // 更新表格数据
  429. gridRef.value?.loadData(institutionList.value);
  430. } catch (error) {
  431. allInstitutionData.value = [];
  432. institutionPageConfig.total = 0;
  433. institutionList.value = [];
  434. } finally {
  435. institutionLoading.value = false;
  436. }
  437. }
  438. // 处理日期按钮点击
  439. function handleDateBtnClick(orderId: number) {
  440. const order = orderList.value.find(o => getOrderId(o) === orderId);
  441. if (!order) return;
  442. if (selectedOrderId.value !== orderId) {
  443. selectedOrderId.value = orderId;
  444. loadInstitutionList();
  445. }
  446. currentPickerOrderId.value = orderId;
  447. pickerType.value = 'date';
  448. isFromTimeButton.value = false;
  449. dateTimePickerVisible.value = true;
  450. }
  451. // 处理时间按钮点击
  452. function handleTimeBtnClick(orderId: number) {
  453. const order = orderList.value.find(o => getOrderId(o) === orderId);
  454. if (!order) return;
  455. if (selectedOrderId.value !== orderId) {
  456. selectedOrderId.value = orderId;
  457. loadInstitutionList();
  458. }
  459. currentPickerOrderId.value = orderId;
  460. isFromTimeButton.value = true;
  461. if (order.arrangeDate) {
  462. pickerType.value = 'time';
  463. } else {
  464. pickerType.value = 'date'; // 先选择日期
  465. }
  466. dateTimePickerVisible.value = true;
  467. }
  468. // 处理日期时间选择器的事件
  469. function handleDateTimeDateSelect(date: Dayjs) {
  470. if (currentPickerOrderId.value !== null) {
  471. handleDateChange(currentPickerOrderId.value, date);
  472. if (isFromTimeButton.value) {
  473. pickerType.value = 'time';
  474. }
  475. }
  476. }
  477. function handleDateTimeTimeSelect(time: string, date: Dayjs) {
  478. if (currentPickerOrderId.value !== null) {
  479. const order = orderList.value.find(o => getOrderId(o) === currentPickerOrderId.value);
  480. if (order) {
  481. if (!order.arrangeDate) {
  482. order.arrangeDate = date.format('YYYY-MM-DD');
  483. }
  484. // 更新时间
  485. handleTimeChange(currentPickerOrderId.value, dayjs(time, 'HH:mm'), order);
  486. }
  487. }
  488. }
  489. function handleDateTimeDateConfirm(date: Dayjs) {
  490. handleDateTimeDateSelect(date);
  491. }
  492. function handleDateTimeClose() {
  493. dateTimePickerVisible.value = false;
  494. currentPickerOrderId.value = null;
  495. isFromTimeButton.value = false;
  496. }
  497. // 处理日期修改
  498. function handleDateChange(orderId: number, date: Dayjs | null) {
  499. if (!date) return;
  500. // 更新订单的日期
  501. const orderIndex = orderList.value.findIndex(o => getOrderId(o) === orderId);
  502. if (orderIndex !== -1) {
  503. const order = orderList.value[orderIndex];
  504. const newDateStr = date.format('YYYY-MM-DD');
  505. // 如果日期相同,不需要更新
  506. if (order.arrangeDate !== newDateStr) {
  507. order.arrangeDate = newDateStr;
  508. orderList.value = [...orderList.value];
  509. }
  510. }
  511. // 清空分配机构
  512. assignedInstitutionMap.value[orderId] = '';
  513. assignedFromRightList.value[orderId] = false;
  514. loadInstitutionList();
  515. }
  516. function handleTimeChange(orderId: number, startTime: any, order: OrderModel) {
  517. if (!startTime) return;
  518. // 更新订单的开始时间
  519. const targetOrder = orderList.value.find(o => getOrderId(o) === orderId);
  520. if (!targetOrder) return;
  521. const durationSource = targetOrder.offlineDuration || order.offlineDuration;
  522. const start = dayjs.isDayjs(startTime) ? startTime : dayjs(startTime);
  523. targetOrder.arrangeTime = start.format('HH:mm');
  524. if (durationSource) {
  525. const durationMinutes = parseDurationToMinutes(durationSource);
  526. if (durationMinutes > 0) {
  527. const end = start.add(durationMinutes, 'minute');
  528. targetOrder.arrangeEndTime = end.format('HH:mm');
  529. }
  530. }
  531. // 清空分配机构
  532. assignedInstitutionMap.value[orderId] = '';
  533. assignedFromRightList.value[orderId] = false;
  534. // 刷新可派单机构列表
  535. loadInstitutionList();
  536. }
  537. // 解析服务时长为分钟数
  538. function parseDurationToMinutes(duration: string): number {
  539. if (!duration) return 0;
  540. const cleanDuration = duration.trim();
  541. const numericValue = parseFloat(cleanDuration);
  542. if (!isNaN(numericValue) && cleanDuration === numericValue.toString()) {
  543. return numericValue;
  544. }
  545. const minutesMatch = cleanDuration.match(/([\d.]+)\s*分钟/);
  546. if (minutesMatch) {
  547. return Math.round(parseFloat(minutesMatch[1]));
  548. }
  549. const hoursMatch = cleanDuration.match(/([\d.]+)\s*小时/);
  550. if (hoursMatch) {
  551. return Math.round(parseFloat(hoursMatch[1]) * 60);
  552. }
  553. const hoursMinutesMatch = cleanDuration.match(/([\d.]+)\s*小时\s*([\d.]+)\s*分钟/);
  554. if (hoursMinutesMatch) {
  555. const hours = parseFloat(hoursMinutesMatch[1]);
  556. const minutes = parseFloat(hoursMinutesMatch[2]);
  557. return Math.round(hours * 60 + minutes);
  558. }
  559. return 0;
  560. }
  561. function handleLastInstitutionClick(orderId: number, institutionName: string) {
  562. if (!institutionName) return;
  563. if (selectedOrderId.value !== orderId) {
  564. selectedOrderId.value = orderId;
  565. loadInstitutionList();
  566. }
  567. if (!availableInstitutionNames.value.includes(institutionName)) {
  568. message.warning('供应商不可选择,且不填充此机构');
  569. return;
  570. }
  571. assignedInstitutionMap.value[orderId] = institutionName;
  572. // 标记为已填充(可以显示按钮)
  573. assignedFromRightList.value[orderId] = true;
  574. }
  575. function handleSupplierClick(orderId: number, supplierName: string) {
  576. if (!supplierName) return;
  577. if (selectedOrderId.value !== orderId) {
  578. selectedOrderId.value = orderId;
  579. loadInstitutionList();
  580. }
  581. if (!availableInstitutionNames.value.includes(supplierName)) {
  582. message.warning('供应商不可选择,且不填充此机构');
  583. return;
  584. }
  585. assignedInstitutionMap.value[orderId] = supplierName;
  586. // 标记为已填充(可以显示按钮)
  587. assignedFromRightList.value[orderId] = true;
  588. }
  589. function handleAssignInstitution(institutionName: string) {
  590. if (selectedOrderId.value !== null) {
  591. const selectedOrder = orderList.value.find(order => {
  592. const orderId = order.id ?? order.patientConditioningProgramId;
  593. return orderId === selectedOrderId.value;
  594. });
  595. if (selectedOrder && (isOrderVerified(selectedOrder))) {
  596. message.warning('已核销的订单不能指派');
  597. return;
  598. }
  599. if (selectedOrder && (isOrderShipped(selectedOrder))) {
  600. message.warning('已发货的订单不能指派');
  601. return;
  602. }
  603. assignedInstitutionMap.value[selectedOrderId.value] = institutionName;
  604. // 标记为从右边列表选择
  605. assignedFromRightList.value[selectedOrderId.value] = true;
  606. }
  607. }
  608. function validateAndGetInstitution(orderId: number) {
  609. const institutionName = assignedInstitutionMap.value[orderId];
  610. if (!institutionName) {
  611. message.warning('请先选择分配机构');
  612. return null;
  613. }
  614. const institution = allInstitutionData.value.find(inst => inst.name === institutionName);
  615. if (!institution) {
  616. message.error('未找到对应的供应商信息');
  617. return null;
  618. }
  619. return { institutionName, conditioningProgramSupplierId: institution.id };
  620. }
  621. function validateAndGetOrder(orderId: number) {
  622. const orderIndex = orderList.value.findIndex(order => getOrderId(order) === orderId);
  623. if (orderIndex === -1) {
  624. message.error('未找到对应的订单');
  625. return null;
  626. }
  627. return { orderIndex, order: orderList.value[orderIndex] };
  628. }
  629. // 验证线下服务订单的必要信息
  630. function validateOfflineOrder(order: OrderModel) {
  631. if (!order.id) {
  632. message.error('订单缺少必要信息(id)');
  633. return null;
  634. }
  635. if (!order.arrangeDate || !order.arrangeTime) {
  636. message.error('请先选择日期和时间');
  637. return null;
  638. }
  639. const endTime = order.arrangeEndTime || (order.offlineDuration ? calculateEndTime(order) : null);
  640. if (!endTime) {
  641. message.error('无法计算服务结束时间,请检查服务时长设置');
  642. return null;
  643. }
  644. return {
  645. id: order.id,
  646. pieTimeStart: `${order.arrangeDate} ${order.arrangeTime}`,
  647. pieTimeEnd: `${order.arrangeDate} ${endTime}`,
  648. };
  649. }
  650. // 验证实体商品订单的必要信息
  651. function validatePhysicalOrder(order: OrderModel) {
  652. if (!order.patientConditioningProgramId) {
  653. message.error('订单缺少必要信息(patientConditioningProgramId)');
  654. return null;
  655. }
  656. return {
  657. patientConditioningProgramId: order.patientConditioningProgramId,
  658. };
  659. }
  660. async function callConfirmOrderAPI(order: OrderModel, conditioningProgramSupplierId: number) {
  661. if (props.orderType === 'offline') {
  662. const offlineData = validateOfflineOrder(order);
  663. if (!offlineData) return false;
  664. await confirmPieOrderMethod({
  665. id: offlineData.id,
  666. conditioningProgramSupplierId,
  667. pieTimeStart: offlineData.pieTimeStart,
  668. pieTimeEnd: offlineData.pieTimeEnd,
  669. });
  670. } else {
  671. const physicalData = validatePhysicalOrder(order);
  672. if (!physicalData) return false;
  673. await confirmPhysicalOrderMethod({
  674. patientConditioningProgramId: physicalData.patientConditioningProgramId,
  675. conditioningProgramSupplierId,
  676. });
  677. }
  678. return true;
  679. }
  680. // 处理订单列表更新
  681. async function handleOrderListAfterAssign(orderId: number, orderIndex: number) {
  682. if (activeSubTab.value !== 'pending') {
  683. // 如果当前在"今日指派订单"或"全部指派订单"tab,订单保留但显示已指派状态
  684. return;
  685. }
  686. // 如果当前在"待指派订单"tab,指派成功后从列表中移除该订单
  687. orderList.value.splice(orderIndex, 1);
  688. // 如果移除后列表为空,取消选中
  689. if (orderList.value.length === 0) {
  690. selectedOrderId.value = null;
  691. return;
  692. }
  693. // 如果移除的是当前选中的订单,选中第一个订单
  694. if (selectedOrderId.value === orderId) {
  695. selectedOrderId.value = orderList.value[0] ? getOrderId(orderList.value[0]) : null;
  696. await loadInstitutionList();
  697. }
  698. }
  699. // 处理确认指派
  700. async function handleConfirmAssign(orderId: number) {
  701. // 如果订单未选中,自动选中该订单并加载机构列表
  702. if (selectedOrderId.value !== orderId) {
  703. selectedOrderId.value = orderId;
  704. await loadInstitutionList();
  705. }
  706. // 验证并获取机构信息
  707. const institutionInfo = validateAndGetInstitution(orderId);
  708. if (!institutionInfo) return;
  709. // 验证并获取订单信息
  710. const orderInfo = validateAndGetOrder(orderId);
  711. if (!orderInfo) return;
  712. const { orderIndex, order } = orderInfo;
  713. const originalOrder = { ...order };
  714. // 先更新本地数据
  715. order.conditioningProgramSupplierName = institutionInfo.institutionName;
  716. order.conditioningProgramSupplierId = institutionInfo.conditioningProgramSupplierId;
  717. try {
  718. const success = await callConfirmOrderAPI(order, institutionInfo.conditioningProgramSupplierId);
  719. if (!success) {
  720. // 验证失败
  721. orderList.value[orderIndex] = originalOrder;
  722. return;
  723. }
  724. // 处理订单列表更新
  725. await handleOrderListAfterAssign(orderId, orderIndex);
  726. // 清空分配机构输入框
  727. assignedInstitutionMap.value[orderId] = '';
  728. assignedFromRightList.value[orderId] = false;
  729. // 静默更新订单计数(不显示loading,避免闪烁)
  730. loadOrderCounts().catch(err => {
  731. console.error('更新订单计数失败:', err);
  732. });
  733. message.success('派单成功');
  734. } catch (error) {
  735. //API调用失败,恢复原始数据
  736. orderList.value[orderIndex] = originalOrder;
  737. }
  738. }
  739. // 处理取消
  740. function handleCancel(orderId: number) {
  741. // 只清空分配机构,保持按钮显示(不清空 assignedFromRightList)
  742. // 使用 null 表示用户主动清空,这样在非 pending tab 中也不会显示原有的供应商名称
  743. assignedInstitutionMap.value[orderId] = null as any;
  744. }
  745. function getOrderId(order: OrderModel): number {
  746. return order.id ?? order.patientConditioningProgramId;
  747. }
  748. // 判断订单是否被选中
  749. function isOrderSelected(order: OrderModel): boolean {
  750. const orderId = getOrderId(order);
  751. const result = selectedOrderId.value === orderId;
  752. return result;
  753. }
  754. // 获取分配机构显示值
  755. function getAssignedInstitutionDisplayValue(order: OrderModel): string {
  756. const orderId = getOrderId(order);
  757. // 检查 key 是否存在(使用 hasOwnProperty 或 in 操作符)
  758. const hasKey = orderId in assignedInstitutionMap.value;
  759. const assignedValue = assignedInstitutionMap.value[orderId];
  760. if (hasKey) {
  761. if (assignedValue === null) {
  762. return '';
  763. }
  764. return assignedValue || '';
  765. }
  766. if (activeSubTab.value !== 'pending') {
  767. return order.conditioningProgramSupplierName || '';
  768. }
  769. return '';
  770. }
  771. // 判断是否应该显示确认指派按钮
  772. function shouldShowConfirmAssignButton(order: OrderModel): boolean {
  773. // 如果订单已核销或已发货,不显示
  774. if (isOrderVerified(order) || isOrderShipped(order)) {
  775. return false;
  776. }
  777. const orderId = getOrderId(order);
  778. // 检查是否已标记为可显示按钮(从右边列表选择、上次供应商或下面供应商填充)
  779. // 只要标记为可显示,就显示按钮(即使后来点击取消清空了输入框)
  780. const canShowButton = assignedFromRightList.value[orderId];
  781. return canShowButton === true;
  782. }
  783. // 处理订单选中
  784. function handleOrderSelect(order: OrderModel) {
  785. const orderId = getOrderId(order);
  786. if (selectedOrderId.value === orderId) {
  787. return;
  788. }
  789. selectedOrderId.value = orderId;
  790. loadInstitutionList();
  791. }
  792. // 处理查询
  793. async function handleSearch() {
  794. // 重置到第一页
  795. orderPageConfig.currentPage = 1;
  796. await loadOrderList();
  797. }
  798. // 处理重置
  799. function handleReset() {
  800. searchForm.customerName = '';
  801. searchForm.contactPhone = '';
  802. searchForm.orderTypes = [];
  803. orderPageConfig.currentPage = 1;
  804. loadOrderList();
  805. }
  806. // 处理订单列表分页变化
  807. function handleOrderPageChange({ page, pageSize }: { page?: number; pageSize?: number }) {
  808. if (page !== undefined) {
  809. orderPageConfig.currentPage = page;
  810. }
  811. if (pageSize !== undefined) {
  812. orderPageConfig.pageSize = pageSize;
  813. }
  814. loadOrderList();
  815. }
  816. // 获取订单数量
  817. const pendingCount = computed(() => {
  818. return props.orderType === 'offline'
  819. ? orderCounts.value.pendPieOfflineOrderCount
  820. : orderCounts.value.pendPieOnlineOrderCount;
  821. });
  822. const todayCount = computed(() => {
  823. return props.orderType === 'offline'
  824. ? orderCounts.value.todayPieOfflineOrderCount
  825. : orderCounts.value.todayPieOnlineOrderCount;
  826. });
  827. const allCount = computed(() => {
  828. return props.orderType === 'offline'
  829. ? orderCounts.value.allPieOfflineOrderCount
  830. : orderCounts.value.allPieOnlineOrderCount;
  831. });
  832. // 加载订单计数
  833. async function loadOrderCounts() {
  834. try {
  835. const response: any = await getPieOrderCountMethod();
  836. const data = response?.data ?? response;
  837. orderCounts.value = {
  838. // offlineCount: Number(data?.offlineCount) || 0, // 线下服务订单总和
  839. offlineCount: Number(data?.pendPieOfflineOrderCount) || 0,
  840. allPieOfflineOrderCount: Number(data?.allPieOfflineOrderCount) || 0, // 线下服务——全部指派订单数量
  841. pendPieOfflineOrderCount: Number(data?.pendPieOfflineOrderCount) || 0, // 线下服务——待指派订单数量
  842. todayPieOfflineOrderCount: Number(data?.todayPieOfflineOrderCount) || 0, // 线下服务——今日指派订单数量
  843. // onlineCount: Number(data?.onlineCount) || 0, // 实体商品数量订单总和
  844. onlineCount: Number(data?.pendPieOnlineOrderCount) || 0, // 实体商品数量订单总和
  845. allPieOnlineOrderCount: Number(data?.allPieOnlineOrderCount) || 0, // 实体商品——全部指派订单数量
  846. pendPieOnlineOrderCount: Number(data?.pendPieOnlineOrderCount) || 0, // 实体商品——待指派订单数量
  847. todayPieOnlineOrderCount: Number(data?.todayPieOnlineOrderCount) || 0, // 实体商品——今日指派订单数量
  848. };
  849. } catch (error) {
  850. // 失败时使用默认值
  851. orderCounts.value = {
  852. offlineCount: 0, // 线下服务数量
  853. allPieOfflineOrderCount: 0, // 线下服务——全部指派订单数量
  854. pendPieOfflineOrderCount: 0, // 线下服务——待指派订单数量
  855. todayPieOfflineOrderCount: 0, // 线下服务——今日指派订单数量
  856. onlineCount: 0, // 实体商品数量
  857. allPieOnlineOrderCount: 0, // 实体商品——全部指派订单数量
  858. pendPieOnlineOrderCount: 0, // 实体商品——待指派订单数量
  859. todayPieOnlineOrderCount: 0, // 实体商品——今日指派订单数量
  860. };
  861. }
  862. }
  863. // 计算总订单数(直接使用后端返回的总和)
  864. const offlineCount = computed(() => {
  865. return orderCounts.value.offlineCount;
  866. });
  867. const onlineCount = computed(() => {
  868. return orderCounts.value.onlineCount;
  869. });
  870. // 暴露方法给父组件
  871. defineExpose({
  872. offlineCount,
  873. onlineCount,
  874. refresh: () => {
  875. loadOrderList();
  876. loadInstitutionList();
  877. },
  878. });
  879. </script>
  880. <template>
  881. <div class="dispatch-order-panel">
  882. <!-- 主要内容区域 -->
  883. <div class="main-content">
  884. <!-- 左侧区域 -->
  885. <div class="left-panel">
  886. <!-- 子tab -->
  887. <div class="sub-tabs">
  888. <div class="sub-tab" :class="{ active: activeSubTab === 'pending' }" @click="activeSubTab = 'pending'">
  889. 待指派订单 ({{ pendingCount }})
  890. </div>
  891. <div class="sub-tab" :class="{ active: activeSubTab === 'today' }" @click="activeSubTab = 'today'">
  892. 今日指派订单 ({{ todayCount }})
  893. </div>
  894. <div class="sub-tab" :class="{ active: activeSubTab === 'all' }" @click="activeSubTab = 'all'">
  895. 全部指派订单 ({{ allCount }})
  896. </div>
  897. </div>
  898. <!-- 订单列表 -->
  899. <div class="order-list-panel">
  900. <!-- 固定头部区域 -->
  901. <div class="order-list-header">
  902. <!-- 警告 - 今日指派和全部指派都显示 -->
  903. <div v-if="(activeSubTab === 'today' || activeSubTab === 'all')" class="warning-banner">
  904. <span class="warning-text">
  905. {{ props.orderType === 'physical' ? '已发货不能修改' : '距离开始时间1小时内,修改请先联系商家和客户' }}
  906. </span>
  907. </div>
  908. <!-- 搜索 - 仅全部指派时显示 -->
  909. <div v-if="activeSubTab === 'all'" class="search-section">
  910. <div class="search-form">
  911. <div class="search-item">
  912. <span class="search-label">客户姓名:</span>
  913. <a-input v-model:value="searchForm.customerName" placeholder="请输入" class="search-input" />
  914. </div>
  915. <div class="search-item">
  916. <span class="search-label">联系电话:</span>
  917. <a-input v-model:value="searchForm.contactPhone" placeholder="请输入" class="search-input" />
  918. </div>
  919. <div class="search-item">
  920. <span class="search-label">订单类型:</span>
  921. <a-checkbox-group v-model:value="searchForm.orderTypes" class="checkbox-group">
  922. <!-- 线下服务显示:已预约、已核销 -->
  923. <template v-if="props.orderType === 'offline'">
  924. <a-checkbox value="booked">已预约</a-checkbox>
  925. <a-checkbox value="verified">已核销</a-checkbox>
  926. </template>
  927. <!-- 实体商品显示:待发货、已发货 -->
  928. <template v-else-if="props.orderType === 'physical'">
  929. <a-checkbox value="pendingShip">待发货</a-checkbox>
  930. <a-checkbox value="shipped">已发货</a-checkbox>
  931. </template>
  932. </a-checkbox-group>
  933. </div>
  934. <div class="search-actions">
  935. <a-button type="primary" @click="handleSearch">查询</a-button>
  936. <a-button @click="handleReset">重置</a-button>
  937. </div>
  938. </div>
  939. </div>
  940. </div>
  941. <!-- 可滚动的订单列表内容 -->
  942. <div class="order-list-content">
  943. <div v-if="loading" class="loading">加载中...</div>
  944. <div v-else-if="orderList.length === 0" class="empty">暂无订单</div>
  945. <template v-else>
  946. <div v-for="(order, index) in orderList" :key="getOrderId(order)" class="order-card"
  947. :class="{ active: isOrderSelected(order) }" @click="handleOrderSelect(order)">
  948. <!-- 已核销/已发货印章 -->
  949. <!-- 线下服务:已核销状态显示已核销图标 -->
  950. <!-- 实体商品:已发货状态显示已发货图标 -->
  951. <div v-if="isOrderVerified(order) || isOrderShipped(order) || isOrderCancelPie(order)" class="verified-badge">
  952. <img v-if="isOrderVerified(order)" src="@/assets/images/verify.png" alt="已核销"
  953. style="width: 100px; height: 100px;" />
  954. <img v-else-if="isOrderCancelPie(order)" src="@/assets/images/cancel.png" alt="已取消派单"
  955. style="width: 100px; height: 100px;" />
  956. <img v-else-if="isOrderShipped(order)" src="@/assets/images/shipment.png" alt="已发货"
  957. style="width: 100px; height: 100px;" />
  958. </div>
  959. <!-- 左侧:图钉图标和数字 -->
  960. <div class="order-header-left">
  961. <div class="pin-icon-wrapper">
  962. <div class="pin-icon-inner">
  963. <span class="pin-icon-number">{{ index + 1 }}</span>
  964. </div>
  965. </div>
  966. <span class="service-name">
  967. {{ order.conditioningProgramName }}
  968. <span v-if="order.conditioningProgramType">({{ order.conditioningProgramType }})</span>
  969. </span>
  970. </div>
  971. <!-- 右侧:日期和时间按钮(仅线下服务显示) -->
  972. <div v-if="props.orderType === 'offline'" class="order-header-right">
  973. <!-- 日期按钮 -->
  974. <div v-if="isOrderVerified(order) || isOrderShipped(order)"
  975. class="date-display-btn date-display-btn-disabled">
  976. {{ order.arrangeDate || '--' }}
  977. </div>
  978. <template v-else>
  979. <div class="date-display-btn" @click.stop="handleDateBtnClick(getOrderId(order))">
  980. {{ order.arrangeDate || '选择日期' }}
  981. </div>
  982. </template>
  983. <!-- 分隔符 -->
  984. <span class="separator-dash">-</span>
  985. <!-- 时间范围按钮 -->
  986. <div v-if="isOrderVerified(order) || isOrderShipped(order)"
  987. class="time-range-display-btn time-range-display-btn-disabled">
  988. <template v-if="order.arrangeTime">
  989. {{ order.arrangeTime }}
  990. <template v-if="calculateEndTime(order)">-{{ calculateEndTime(order) }}</template>
  991. </template>
  992. <template v-else>--</template>
  993. </div>
  994. <div v-else class="time-range-display-btn" @click.stop="handleTimeBtnClick(getOrderId(order))">
  995. <template v-if="order.arrangeTime">
  996. {{ order.arrangeTime }}
  997. <template v-if="calculateEndTime(order)">-{{ calculateEndTime(order) }}</template>
  998. </template>
  999. <template v-else>选择时间</template>
  1000. </div>
  1001. </div>
  1002. <!-- 客户信息 -->
  1003. <div class="customer-info">
  1004. {{ formatCustomerInfo(order) }}
  1005. </div>
  1006. <!-- 分配机构 -->
  1007. <div class="assign-section">
  1008. <span class="assign-label">分配机构:</span>
  1009. <a-input
  1010. :value="getAssignedInstitutionDisplayValue(order)"
  1011. placeholder="请点击右侧指派按钮或点击上次/供应商选择分配机构" class="assign-input" readonly
  1012. :disabled="isOrderVerified(order) || isOrderShipped(order)" />
  1013. <a-button
  1014. v-if="shouldShowConfirmAssignButton(order)"
  1015. type="default"
  1016. size="small"
  1017. @click.stop="handleCancel(getOrderId(order))">
  1018. 取消
  1019. </a-button>
  1020. <a-button v-if="shouldShowConfirmAssignButton(order)" type="primary" size="small"
  1021. @click.stop="handleConfirmAssign(getOrderId(order))">
  1022. 确认指派
  1023. </a-button>
  1024. </div>
  1025. <!-- 上次机构和供应商 -->
  1026. <div class="institution-options">
  1027. <div v-if="order.preConditioningProgramSupplierName" class="institution-option"
  1028. :class="{ disabled: isOrderVerified(order) || isOrderShipped(order) }"
  1029. @click.stop="!(isOrderVerified(order) || isOrderShipped(order)) && handleLastInstitutionClick(getOrderId(order), order.preConditioningProgramSupplierName)">
  1030. 上次:{{ order.preConditioningProgramSupplierName }}
  1031. </div>
  1032. <div v-if="order.conditioningProgramSupplierName" class="institution-option"
  1033. :class="{ disabled: isOrderVerified(order) || isOrderShipped(order) }"
  1034. @click.stop="!(isOrderVerified(order) || isOrderShipped(order)) && handleSupplierClick(getOrderId(order), order.conditioningProgramSupplierName)">
  1035. 供应商:{{ order.conditioningProgramSupplierName }}
  1036. </div>
  1037. </div>
  1038. <!-- 订单信息 -->
  1039. <div class="order-info">
  1040. <div class="order-info-item" v-if="order.orderNo">订单编号: {{ order.orderNo }}</div>
  1041. <div class="order-info-item" v-if="order.applyTime">用户操作时间: {{ order.applyTime }}</div>
  1042. <div class="order-info-item" v-if="activeSubTab !== 'pending' && order.pieBy">派单员: {{ order.pieBy }}
  1043. </div>
  1044. <div class="order-info-item" v-if="order.offlineDuration">服务时长: {{ order.offlineDuration }}分钟</div>
  1045. </div>
  1046. </div>
  1047. </template>
  1048. </div>
  1049. <!-- 订单列表分页器 -->
  1050. <div class="order-pager-wrapper">
  1051. <vxe-pager v-model:current-page="orderPageConfig.currentPage" v-model:page-size="orderPageConfig.pageSize"
  1052. :total="orderPageConfig.total"
  1053. :layouts="['PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'Sizes', 'FullJump', 'Total']"
  1054. @page-change="handleOrderPageChange" />
  1055. </div>
  1056. </div>
  1057. </div>
  1058. <!-- 右侧可派单机构列表 -->
  1059. <div class="institution-panel">
  1060. <div class="panel-title">可派单机构</div>
  1061. <div class="institution-grid-wrapper">
  1062. <vxe-grid ref="gridRef" v-bind="gridOptions" :loading="institutionLoading" />
  1063. </div>
  1064. </div>
  1065. </div>
  1066. <!-- 日期时间选择弹窗 -->
  1067. <DateTimePicker v-model:visible="dateTimePickerVisible" :picker-type="pickerType"
  1068. :initial-date="currentPickerOrderId ? (orderList.find(o => getOrderId(o) === currentPickerOrderId)?.arrangeDate || null) : null"
  1069. :initial-time="currentPickerOrderId ? (orderList.find(o => getOrderId(o) === currentPickerOrderId)?.arrangeTime || null) : null"
  1070. :is-from-time-button="isFromTimeButton" @date-select="handleDateTimeDateSelect"
  1071. @time-select="handleDateTimeTimeSelect" @date-confirm="handleDateTimeDateConfirm" @close="handleDateTimeClose">
  1072. </DateTimePicker>
  1073. </div>
  1074. </template>
  1075. <style scoped lang="scss">
  1076. .dispatch-order-panel {
  1077. display: flex;
  1078. flex-direction: column;
  1079. height: 100%;
  1080. min-height: 0;
  1081. .main-content {
  1082. display: flex;
  1083. gap: 16px;
  1084. flex: 1;
  1085. min-height: 0;
  1086. overflow: hidden;
  1087. align-items: stretch;
  1088. .left-panel {
  1089. flex: 1;
  1090. display: flex;
  1091. flex-direction: column;
  1092. min-height: 0;
  1093. overflow: hidden;
  1094. border: 1px solid #8bc34a;
  1095. .sub-tabs {
  1096. display: flex;
  1097. gap: 0;
  1098. margin-bottom: 0;
  1099. background: #8bc34a;
  1100. padding: 0;
  1101. align-items: flex-end;
  1102. box-sizing: border-box;
  1103. .sub-tab {
  1104. padding: 12px 20px;
  1105. cursor: pointer;
  1106. color: #fff;
  1107. background: #8bc34a;
  1108. border: none;
  1109. border-bottom: 3px solid transparent;
  1110. transition: all 0.3s;
  1111. font-size: 14px;
  1112. position: relative;
  1113. white-space: nowrap;
  1114. &.active {
  1115. background: #fff;
  1116. color: #52c41a;
  1117. border-bottom-color: #52c41a;
  1118. border-bottom-width: 3px;
  1119. font-weight: 500;
  1120. }
  1121. &:hover:not(.active) {
  1122. background: #a5d6a7;
  1123. }
  1124. }
  1125. }
  1126. .order-list-panel {
  1127. flex: 1;
  1128. min-height: 0;
  1129. display: flex;
  1130. flex-direction: column;
  1131. overflow: hidden;
  1132. .order-list-header {
  1133. flex-shrink: 0;
  1134. padding: 16px;
  1135. padding-bottom: 0;
  1136. background: #fff;
  1137. .warning-banner {
  1138. margin-bottom: 16px;
  1139. border-radius: 4px;
  1140. .warning-text {
  1141. color: #ff4d4f;
  1142. font-size: 14px;
  1143. font-weight: 500;
  1144. }
  1145. }
  1146. .search-section {
  1147. margin-bottom: 16px;
  1148. padding: 16px;
  1149. background: #fff;
  1150. border: 1px solid #e8e8e8;
  1151. border-radius: 4px;
  1152. .search-form {
  1153. display: flex;
  1154. align-items: center;
  1155. gap: 16px;
  1156. flex-wrap: wrap;
  1157. .search-item {
  1158. display: flex;
  1159. align-items: center;
  1160. gap: 8px;
  1161. .search-label {
  1162. white-space: nowrap;
  1163. font-size: 14px;
  1164. color: #333;
  1165. }
  1166. .search-input {
  1167. width: 180px;
  1168. }
  1169. }
  1170. .checkbox-group {
  1171. display: flex;
  1172. gap: 8px;
  1173. }
  1174. .search-actions {
  1175. display: flex;
  1176. gap: 8px;
  1177. :deep(.ant-btn) {
  1178. height: 32px;
  1179. padding: 4px 15px;
  1180. font-size: 14px;
  1181. }
  1182. }
  1183. }
  1184. }
  1185. }
  1186. // 可滚动的订单列表内容
  1187. .order-list-content {
  1188. flex: 1;
  1189. min-height: 0;
  1190. overflow-y: auto;
  1191. overflow-x: hidden;
  1192. padding: 16px;
  1193. padding-top: 0;
  1194. padding-right: 24px;
  1195. .loading,
  1196. .empty {
  1197. text-align: center;
  1198. padding: 40px;
  1199. color: #999;
  1200. }
  1201. .order-card {
  1202. padding: 16px;
  1203. margin-bottom: 16px;
  1204. border: 1px solid #e8e8e8;
  1205. border-radius: 4px;
  1206. background: #fff;
  1207. cursor: pointer;
  1208. transition: all 0.3s;
  1209. position: relative;
  1210. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
  1211. overflow: visible;
  1212. &.active {
  1213. background: rgba(24, 144, 255, 0.08);
  1214. box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
  1215. }
  1216. .verified-badge {
  1217. position: absolute;
  1218. bottom: 35px;
  1219. right: 16px;
  1220. width: 100px;
  1221. height: 100px;
  1222. display: flex;
  1223. align-items: center;
  1224. justify-content: center;
  1225. pointer-events: none;
  1226. z-index: 1;
  1227. &::before {
  1228. content: '';
  1229. position: absolute;
  1230. top: 0;
  1231. left: 0;
  1232. width: 100%;
  1233. height: 100%;
  1234. }
  1235. &::after {
  1236. content: '';
  1237. position: absolute;
  1238. top: 12px;
  1239. left: 12px;
  1240. width: calc(100% - 24px);
  1241. height: calc(100% - 24px);
  1242. }
  1243. span {
  1244. position: relative;
  1245. z-index: 1;
  1246. display: inline-block;
  1247. &::before {
  1248. content: '';
  1249. position: absolute;
  1250. top: 50%;
  1251. left: 50%;
  1252. transform: translate(-50%, -50%);
  1253. width: 50px;
  1254. height: 50px;
  1255. border: 1px solid #5a9fe3;
  1256. border-radius: 50%;
  1257. opacity: 0.6;
  1258. }
  1259. }
  1260. }
  1261. .order-header-left {
  1262. display: flex;
  1263. align-items: center;
  1264. gap: 12px;
  1265. margin-bottom: 12px;
  1266. .pin-icon-wrapper {
  1267. width: 24px;
  1268. height: 32px;
  1269. position: relative;
  1270. .pin-icon-inner {
  1271. position: absolute;
  1272. top: 0;
  1273. left: 0;
  1274. width: 25px;
  1275. height: 25px;
  1276. transform: rotate(-45deg);
  1277. transform-origin: 15px 14px;
  1278. &::before {
  1279. content: '';
  1280. position: absolute;
  1281. width: 25px;
  1282. height: 25px;
  1283. background: #5a9fe3;
  1284. border-radius: 50% 50% 50% 0;
  1285. top: 0;
  1286. left: 0;
  1287. }
  1288. .pin-icon-number {
  1289. position: absolute;
  1290. top: 50%;
  1291. left: 50%;
  1292. transform: translate(-50%, -50%) rotate(45deg);
  1293. z-index: 1;
  1294. color: #fff;
  1295. font-size: 12px;
  1296. font-weight: 600;
  1297. line-height: 1;
  1298. display: flex;
  1299. align-items: center;
  1300. justify-content: center;
  1301. width: 16px;
  1302. height: 16px;
  1303. }
  1304. }
  1305. &::after {
  1306. content: '';
  1307. position: absolute;
  1308. width: 0;
  1309. height: 0;
  1310. border-left: 4px solid transparent;
  1311. border-right: 4px solid transparent;
  1312. border-top: 6px solid #5a9fe3;
  1313. bottom: 0;
  1314. left: 50%;
  1315. transform: translateX(-50%);
  1316. z-index: 0;
  1317. }
  1318. }
  1319. .service-name {
  1320. font-size: 16px;
  1321. font-weight: 500;
  1322. color: #333;
  1323. }
  1324. }
  1325. .order-header-right {
  1326. position: absolute;
  1327. top: 16px;
  1328. right: 16px;
  1329. display: flex;
  1330. gap: 8px;
  1331. align-items: center;
  1332. z-index: 10;
  1333. visibility: visible;
  1334. opacity: 1;
  1335. .date-display-btn {
  1336. background: #8bc34a;
  1337. color: #fff;
  1338. padding: 4px 11px;
  1339. border-radius: 4px;
  1340. font-size: 14px;
  1341. line-height: 1.5715;
  1342. cursor: pointer;
  1343. user-select: none;
  1344. white-space: nowrap;
  1345. min-width: 110px;
  1346. text-align: center;
  1347. transition: background-color 0.3s;
  1348. &:hover {
  1349. background: #7cb342;
  1350. }
  1351. &.date-display-btn-disabled {
  1352. cursor: default;
  1353. opacity: 0.8;
  1354. }
  1355. }
  1356. .separator-dash {
  1357. color: #999;
  1358. font-size: 14px;
  1359. margin: 0 4px;
  1360. }
  1361. .time-range-display-btn {
  1362. background: #8bc34a;
  1363. color: #fff;
  1364. padding: 4px 11px;
  1365. border-radius: 4px;
  1366. font-size: 14px;
  1367. line-height: 1.5715;
  1368. cursor: pointer;
  1369. user-select: none;
  1370. white-space: nowrap;
  1371. min-width: 100px;
  1372. text-align: center;
  1373. transition: background-color 0.3s;
  1374. &:hover {
  1375. background: #7cb342;
  1376. }
  1377. &.time-range-display-btn-disabled {
  1378. cursor: default;
  1379. opacity: 0.8;
  1380. }
  1381. }
  1382. .hidden-date-picker,
  1383. .hidden-time-picker {
  1384. position: absolute;
  1385. opacity: 0;
  1386. pointer-events: none;
  1387. width: 0;
  1388. height: 0;
  1389. overflow: hidden;
  1390. }
  1391. }
  1392. .customer-info {
  1393. font-size: 14px;
  1394. color: #5da1f9;
  1395. margin-bottom: 12px;
  1396. line-height: 1.6;
  1397. }
  1398. .assign-section {
  1399. display: flex;
  1400. align-items: center;
  1401. gap: 8px;
  1402. margin-bottom: 8px;
  1403. flex-wrap: wrap;
  1404. .assign-label {
  1405. white-space: nowrap;
  1406. color: #333;
  1407. font-size: 14px;
  1408. margin-right: 4px;
  1409. width: 72px;
  1410. flex-shrink: 0;
  1411. }
  1412. .assign-input {
  1413. flex: 1;
  1414. min-width: 200px;
  1415. max-width: 300px;
  1416. :deep(.ant-input) {
  1417. border-color: #52c41a;
  1418. cursor: default;
  1419. &[readonly] {
  1420. background-color: #fafafa;
  1421. cursor: not-allowed;
  1422. }
  1423. &:disabled {
  1424. background-color: #f5f5f5;
  1425. cursor: not-allowed;
  1426. }
  1427. }
  1428. }
  1429. :deep(.ant-btn) {
  1430. height: 32px;
  1431. padding: 4px 15px;
  1432. font-size: 14px;
  1433. }
  1434. }
  1435. .assigned-institution {
  1436. font-size: 14px;
  1437. color: #666;
  1438. margin-bottom: 12px;
  1439. padding-left: 80px;
  1440. }
  1441. .institution-options {
  1442. display: flex;
  1443. gap: 12px;
  1444. margin-bottom: 12px;
  1445. flex-wrap: wrap;
  1446. margin-left: 85px;
  1447. .institution-option {
  1448. padding: 4px 12px;
  1449. border-radius: 4px;
  1450. font-size: 13px;
  1451. color: #5da1f9;
  1452. cursor: pointer;
  1453. transition: all 0.3s;
  1454. &.disabled {
  1455. opacity: 0.5;
  1456. cursor: not-allowed;
  1457. color: #999;
  1458. }
  1459. }
  1460. }
  1461. .order-info {
  1462. font-size: 13px;
  1463. color: black;
  1464. line-height: 1.8;
  1465. display: flex;
  1466. .order-info-item {
  1467. margin-right: 30px;
  1468. }
  1469. div {
  1470. margin-bottom: 4px;
  1471. }
  1472. }
  1473. }
  1474. .order-pager-wrapper {
  1475. flex-shrink: 0;
  1476. border-top: 1px solid #e8e8e8;
  1477. background: #fff;
  1478. padding: 8px 16px;
  1479. margin-top: 16px;
  1480. }
  1481. }
  1482. }
  1483. }
  1484. .institution-panel {
  1485. flex: 1;
  1486. min-height: 0;
  1487. border: 1px solid #e8e8e8;
  1488. border-radius: 4px;
  1489. background: #fff;
  1490. display: flex;
  1491. flex-direction: column;
  1492. overflow: hidden;
  1493. .panel-title {
  1494. padding: 12px 16px;
  1495. font-size: 16px;
  1496. font-weight: 500;
  1497. border-bottom: 1px solid #e8e8e8;
  1498. background: #5a9fe3;
  1499. color: white;
  1500. height: 48px;
  1501. display: flex;
  1502. align-items: center;
  1503. box-sizing: border-box;
  1504. flex-shrink: 0;
  1505. }
  1506. .institution-grid-wrapper {
  1507. flex: 1;
  1508. min-height: 0;
  1509. overflow: hidden;
  1510. display: flex;
  1511. flex-direction: column;
  1512. }
  1513. .institution-pager-wrapper {
  1514. flex-shrink: 0;
  1515. border-top: 1px solid #e8e8e8;
  1516. background: #fff;
  1517. padding: 8px 16px;
  1518. }
  1519. :deep(.vxe-grid) {
  1520. flex: 1;
  1521. overflow: hidden;
  1522. display: flex;
  1523. flex-direction: column;
  1524. min-height: 0;
  1525. height: 100%;
  1526. // 表格主体区域
  1527. .vxe-table--wrapper {
  1528. flex: 1;
  1529. overflow: hidden;
  1530. min-height: 0;
  1531. height: 100%;
  1532. display: flex;
  1533. flex-direction: column;
  1534. }
  1535. .vxe-table {
  1536. height: 100%;
  1537. display: flex;
  1538. flex-direction: column;
  1539. }
  1540. .vxe-table--header-wrapper {
  1541. flex-shrink: 0;
  1542. position: relative;
  1543. z-index: 10;
  1544. }
  1545. .vxe-table--header {
  1546. background: #fafafa;
  1547. .vxe-header--column {
  1548. font-weight: 500;
  1549. color: #333;
  1550. }
  1551. }
  1552. .vxe-table--body-wrapper {
  1553. flex: 1;
  1554. min-height: 0;
  1555. position: relative;
  1556. }
  1557. :deep(.vxe-table--body-wrapper),
  1558. :deep(.vxe-table--body),
  1559. :deep(.vxe-table--body-wrapper > *),
  1560. :deep([class*="scroll"]) {
  1561. &::-webkit-scrollbar {
  1562. width: 8px;
  1563. }
  1564. &::-webkit-scrollbar-thumb {
  1565. background: #d9d9d9;
  1566. border-radius: 4px;
  1567. }
  1568. &::-webkit-scrollbar-thumb:hover {
  1569. background: #bfbfbf;
  1570. }
  1571. }
  1572. .vxe-table--body {
  1573. .vxe-body--row {
  1574. &:hover {
  1575. background: #f5f5f5;
  1576. }
  1577. .vxe-body--column {
  1578. padding: 12px 8px;
  1579. }
  1580. }
  1581. }
  1582. .vxe-button {
  1583. padding: 4px 15px;
  1584. height: 35px;
  1585. font-size: 14px;
  1586. width: 100px;
  1587. background: #1890ff;
  1588. color: white;
  1589. }
  1590. }
  1591. }
  1592. }
  1593. }
  1594. </style>