DispatchOrderPanel.vue 54 KB

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