newFactoryProductionScheduling.vue 121 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583
  1. <template>
  2. <ele-modal
  3. :key="dialogRenderKey"
  4. width="100%"
  5. :visible.sync="visible"
  6. :close-on-click-modal="false"
  7. custom-class="ele-dialog-form scheduling-dialog"
  8. :title="schedulingTitle"
  9. :maxable="true"
  10. >
  11. <projectGantt
  12. :style="{ height: ganttHeight }"
  13. :ganttTasks="tableData"
  14. :planList="planDataList"
  15. :calendarSourceData="calendarSourceData"
  16. :defaultTab="defaultGanttTab"
  17. :resource-tabs-config="resourceTabsConfig"
  18. :resource-label-mode="schedulingMode"
  19. :dateRange="dateRange"
  20. @tab-change="handleTabChange"
  21. >
  22. <template v-slot:toolbar>
  23. <el-date-picker
  24. v-model="dateRange"
  25. type="daterange"
  26. size="small"
  27. value-format="yyyy-MM-dd"
  28. range-separator="至"
  29. start-placeholder="计划开始时间"
  30. end-placeholder="计划结束时间"
  31. @input="handleDateInput"
  32. @change="handleDateChange"
  33. :append-to-body="false"
  34. ></el-date-picker>
  35. </template>
  36. </projectGantt>
  37. <ele-split-layout
  38. class="scheduling-content"
  39. space="0px"
  40. :width="splitPaneWidth"
  41. :resizable="true"
  42. :min-size="splitPaneMinSize"
  43. :max-size="splitPaneMaxSize"
  44. :left-style="{
  45. overflow: 'visible',
  46. width: '100%'
  47. }"
  48. :right-style="{
  49. overflow: 'hidden'
  50. }"
  51. :responsive="isCompactSchedulingLayout"
  52. :style="{ height: schedulingContentHeight }"
  53. >
  54. <div
  55. :class="[
  56. 'form-wrapper',
  57. {
  58. 'is-order-scheduling': isOrderScheduling,
  59. 'is-plan-scheduling': !isOrderScheduling
  60. }
  61. ]"
  62. >
  63. <div class="plan-table-status-tabs">
  64. <el-tabs
  65. v-model="planTableStatusTab"
  66. @tab-click="handlePlanTableStatusTabClick"
  67. >
  68. <el-tab-pane
  69. v-for="item in planTableStatusTabs"
  70. :key="item.name"
  71. :name="item.name"
  72. :label="item.label"
  73. />
  74. </el-tabs>
  75. </div>
  76. <ele-pro-table
  77. :key="planTableCacheKey"
  78. width="100%"
  79. ref="planTable"
  80. :columns="columns"
  81. :cache-key="planTableCacheKey"
  82. :datasource="planDataList"
  83. row-key="id"
  84. :selection.sync="selection"
  85. :height="planTableHeight"
  86. :page-size="20"
  87. :pagination-class="planPaginationClass"
  88. @reload="handlePlanTableReload"
  89. @update:selection="handlePlanSelectionChange"
  90. >
  91. <template v-slot:action="{ row }">
  92. <el-link
  93. type="primary"
  94. :underline="false"
  95. @click="handlePublish(row)"
  96. >
  97. {{ getPlanActionText(row) }}
  98. </el-link>
  99. </template>
  100. <template v-slot:dotLineStatus="{ row }">
  101. <el-tag type="success" size="mini" v-if="row.dotLineStatus == 1">
  102. 已布点
  103. </el-tag>
  104. <el-tag type="danger" size="mini" v-if="row.dotLineStatus == 0">
  105. 未布点
  106. </el-tag>
  107. </template>
  108. </ele-pro-table>
  109. </div>
  110. <template v-slot:content>
  111. <div
  112. v-loading="dotLineLoading"
  113. :class="[
  114. 'plan-dot-line-side',
  115. {
  116. 'is-fullscreen': dotLineFullscreen,
  117. 'is-plan-scheduling': !isOrderScheduling
  118. }
  119. ]"
  120. >
  121. <div class="plan-dot-line-detail">
  122. <plan-route-panel
  123. v-if="!isOrderScheduling"
  124. :plan-start-display="planStartDisplay"
  125. :plan-end-display="planEndDisplay"
  126. :plan-num-display="planNumDisplay"
  127. :task-list="taskList"
  128. :route-steps-active="routeStepsActive"
  129. :route-des-index="routeDesIndex"
  130. :route-step-title="routeStepTitle"
  131. :compact="isOrderScheduling"
  132. />
  133. <div
  134. :class="[
  135. 'plan-dot-line-scrollable',
  136. {
  137. 'is-order-dispatch-scroll': isOrderScheduling,
  138. 'is-plan-config-scroll': !isOrderScheduling
  139. }
  140. ]"
  141. >
  142. <task-config-panel
  143. :key="taskConfigPanelRenderKey"
  144. :class="{ 'factory-task-config-panel': !isOrderScheduling }"
  145. :dot-line-fullscreen="dotLineFullscreen"
  146. :is-current-plan-readonly="isCurrentPlanReadonly"
  147. :order-detail-readonly="isOrderDetailReadonly"
  148. :current-plan="currentPlan"
  149. :dot-line-loading="dotLineLoading"
  150. :task-list="taskList"
  151. :execution-type-options="executionTypeOptions"
  152. :team-options="teamOptions"
  153. :get-homemade-options="getHomemadeOptions"
  154. :homemade-object-label="homemadeObjectLabel"
  155. :show-quantity-column="isOrderScheduling"
  156. :order-dispatch-style="isOrderScheduling"
  157. :show-approve-button="!isOrderScheduling"
  158. :approve-button-disabled="!canApproveCurrentPlan"
  159. :approve-loading="approvalLoading"
  160. :table-max-height="taskConfigTableMaxHeight"
  161. :fill-table-height="!isOrderScheduling"
  162. :factory-list="factoryList"
  163. :exec-type="EXEC_TYPE"
  164. :task-name="taskName"
  165. :exec-type-num="execTypeNum"
  166. :is-execution-end-over-delivery="isExecutionEndOverDelivery"
  167. :is-execution-end-over-plan-end="isExecutionEndOverPlanEnd"
  168. @toggle-fullscreen="toggleDotLineFullscreen"
  169. @submit="submitDotLineConfig"
  170. @execution-type-change="onExecutionTypeChange"
  171. @execution-object-change="onExecutionObjectChange"
  172. @parallelism-change="handleParallelismChange"
  173. @time-change="handleTimeChange"
  174. @add-task-row="addTaskRow"
  175. @remove-task-row="removeTaskRow"
  176. @add-dispatch-stations="addDispatchStations"
  177. @remove-dispatch-station="removeDispatchStation"
  178. @dispatch-action-success="handleDispatchActionSuccess"
  179. @row-submit="submitDotLineRow"
  180. @approve="handleApproveCurrentPlan"
  181. />
  182. </div>
  183. </div>
  184. </div>
  185. </template>
  186. </ele-split-layout>
  187. <div slot="footer">
  188. <!-- <el-button plain @click="cancel">取消</el-button> -->
  189. <el-button @click="cancel">关闭</el-button>
  190. </div>
  191. <plan-picker-dialog
  192. ref="planPickerTable"
  193. :title="planPickerTitle"
  194. :visible.sync="planPickerVisible"
  195. :where.sync="planPickerWhere"
  196. :status-options="planPickerStatusOptions"
  197. :columns="columns"
  198. :loading="planPickerLoading"
  199. :datasource="planPickerDatasource"
  200. :selection.sync="planPickerSelection"
  201. @search="handlePlanPickerSearch"
  202. @reset-search="resetPlanPickerSearch"
  203. @confirm="confirmPlanPicker"
  204. @update:selection="handlePlanPickerSelectionChange"
  205. />
  206. </ele-modal>
  207. </template>
  208. <script>
  209. import projectGantt from './gantt/project-gantt.vue';
  210. import PlanPickerDialog from './newFactoryProductionScheduling/PlanPickerDialog.vue';
  211. import PlanRoutePanel from './newFactoryProductionScheduling/PlanRoutePanel.vue';
  212. import TaskConfigPanel from './newFactoryProductionScheduling/TaskConfigPanel.vue';
  213. import {
  214. getList,
  215. teamSchedulingGanttChart,
  216. workstationSchedulingGanttChart,
  217. factorySchedulingGanttChart,
  218. getPlansByIds
  219. } from '@/api/productionPlan/index.js';
  220. import { getTaskListById } from '@/api/materialPlan';
  221. import {
  222. listByFirstTaskId,
  223. parameterGetByCode,
  224. teamPage
  225. } from '@/api/mainData';
  226. import { getFactoryarea } from '@/api/saleOrder';
  227. import { getUserPage } from '@/api/system/organization';
  228. import { release, releaseByApproval } from '@/api/productionPlan/order.js';
  229. import {
  230. getPlanDotLine,
  231. savePlanDotLine
  232. } from '@/api/productionPlan/planDotLine';
  233. import { deepClone } from '@/utils';
  234. import {
  235. EXEC_TYPE,
  236. EXEC_TYPE_OPTIONS,
  237. PLAN_PICKER_STATUS_OPTIONS,
  238. READONLY_LAYOUT_STATUSES
  239. } from './newFactoryProductionScheduling.constants';
  240. import {
  241. createSchedulingColumns,
  242. createWorkOrderSchedulingColumns
  243. } from './newFactoryProductionScheduling.columns';
  244. import {
  245. createPlanPickerWhere,
  246. formatDotLineDateTime,
  247. isEndAfterPlanEnd,
  248. isEndBeforeStart,
  249. toSafeNumber
  250. } from './newFactoryProductionScheduling.utils';
  251. export default {
  252. components: {
  253. projectGantt,
  254. PlanPickerDialog,
  255. PlanRoutePanel,
  256. TaskConfigPanel
  257. },
  258. data() {
  259. const now = new Date();
  260. const startDate = `${now.getFullYear()}-${String(
  261. now.getMonth() + 1
  262. ).padStart(2, '0')}-01`;
  263. const lastDay = new Date(
  264. now.getFullYear(),
  265. now.getMonth() + 1,
  266. 0
  267. ).getDate();
  268. const endDate = `${now.getFullYear()}-${String(
  269. now.getMonth() + 1
  270. ).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
  271. return {
  272. visible: false,
  273. entryReadonly: false,
  274. form: {},
  275. currentPlan: null,
  276. planRoutingPayload: null,
  277. taskList: [],
  278. tableData: [],
  279. calendarSourceData: [],
  280. planDataList: [],
  281. allPlanDataList: [],
  282. selection: [],
  283. activeTab: 'factory',
  284. dateRange: [startDate, endDate],
  285. planPickerVisible: false,
  286. planPickerTitle: '添加计划',
  287. planPickerLoading: false,
  288. planPickerTimeDimensionPlanType: '',
  289. planPickerWhere: createPlanPickerWhere(),
  290. planPickerSelection: [],
  291. planPickerSelectionAll: [],
  292. planPickerSelectionSyncing: false,
  293. planTableSelectionSyncing: false,
  294. planTableStatusTab: 'unpublished',
  295. dialogRenderKey: 0,
  296. dotLineLoading: false,
  297. dotLineFullscreen: false,
  298. approvalLoading: false,
  299. isReview: false,
  300. orderDetailReadonly: false,
  301. orderDetailActionMode: '',
  302. teamOptions: [],
  303. workstationOptionsMap: {},
  304. workstationOptionsLoadingMap: {},
  305. factoryList: [],
  306. executionTypeOptions: EXEC_TYPE_OPTIONS,
  307. planPickerStatusOptions: PLAN_PICKER_STATUS_OPTIONS,
  308. EXEC_TYPE,
  309. schedulingMode: 'plan',
  310. selectedWorkOrders: [],
  311. orderListLoader: null,
  312. orderDateReloadTimer: null,
  313. columns: createSchedulingColumns(),
  314. viewportWidth: typeof window === 'undefined' ? 1920 : window.innerWidth,
  315. viewportHeight: typeof window === 'undefined' ? 900 : window.innerHeight
  316. };
  317. },
  318. computed: {
  319. routeStepsActive() {
  320. const list = this.taskList;
  321. if (!list.length) return 0;
  322. for (let i = 0; i < list.length; i++) {
  323. if (!this.isTaskDone(list[i])) return i;
  324. }
  325. return list.length;
  326. },
  327. routeDesIndex() {
  328. const list = this.taskList;
  329. if (!list.length) return -1;
  330. const a = this.routeStepsActive;
  331. return a >= list.length ? list.length - 1 : a;
  332. },
  333. planStartDisplay() {
  334. const t = this.currentPlan?.startTime;
  335. return t ? formatDotLineDateTime(t) || String(t) : '—';
  336. },
  337. planEndDisplay() {
  338. const t = this.currentPlan?.endTime;
  339. return t ? formatDotLineDateTime(t) || String(t) : '—';
  340. },
  341. planNumDisplay() {
  342. const num = this.currentPlan?.productNum;
  343. const unit = this.currentPlan?.measuringUnit || this.currentPlan?.unit;
  344. const text = [num, unit]
  345. .filter((item) => item !== null && item !== undefined && item !== '')
  346. .join('');
  347. return text || '—';
  348. },
  349. isCurrentPlanReadonly() {
  350. return (
  351. (!this.isOrderScheduling && this.entryReadonly) ||
  352. this.isOrderDetailReadonly ||
  353. (!this.isOrderScheduling && this.isReadonlyPlan(this.currentPlan))
  354. );
  355. },
  356. isOrderDetailReadonly() {
  357. return (
  358. this.isOrderScheduling &&
  359. !!this.currentPlan &&
  360. this.orderDetailActionMode === 'view'
  361. );
  362. },
  363. // 当前弹窗同时承载两条业务链路:
  364. // 1. 工厂排程:从生产计划页面进入,不传 mode,保持默认 plan 模式。
  365. // 2. 班组排程:从工单页面进入,显式传入 mode = order。
  366. // 后续表格列、资源页签、执行对象和保存字段都以该判断隔离。
  367. isOrderScheduling() {
  368. return this.schedulingMode === 'order';
  369. },
  370. resourceTabsConfig() {
  371. if (this.isOrderScheduling) {
  372. return [
  373. { key: 'team', label: '工位' },
  374. { key: 'line', label: '设备' },
  375. { key: 'person', label: '人员' }
  376. ];
  377. }
  378. return undefined;
  379. },
  380. homemadeObjectLabel() {
  381. return this.isOrderScheduling ? '执行对象(人员/工位)' : '执行对象';
  382. },
  383. defaultGanttTab() {
  384. return this.isOrderScheduling ? 'team' : 'factory';
  385. },
  386. schedulingTitle() {
  387. return this.isOrderScheduling ? '班组排程' : '工厂排产';
  388. },
  389. taskConfigPanelRenderKey() {
  390. const planKey =
  391. this.currentPlan?.workOrderId || this.currentPlan?.id || 'empty';
  392. return [
  393. this.schedulingMode,
  394. this.planTableStatusTab,
  395. planKey,
  396. this.orderDetailActionMode || 'none',
  397. this.isCurrentPlanReadonly ? 'readonly' : 'edit'
  398. ].join('-');
  399. },
  400. planTableCacheKey() {
  401. return this.isOrderScheduling
  402. ? 'aps-team-scheduling-work-order-table'
  403. : 'aps-factory-scheduling-plan-table';
  404. },
  405. planTableStatusTabs() {
  406. if (this.isOrderScheduling) {
  407. return [
  408. {
  409. name: 'undispatched',
  410. label: '未派单'
  411. },
  412. {
  413. name: 'dispatched',
  414. label: '已派单'
  415. }
  416. ];
  417. }
  418. return [
  419. {
  420. name: 'unpublished',
  421. label: '未发布'
  422. },
  423. {
  424. name: 'published',
  425. label: '已发布'
  426. }
  427. ];
  428. },
  429. isCompactSchedulingLayout() {
  430. return this.viewportWidth <= 1180;
  431. },
  432. splitPaneWidth() {
  433. if (this.isOrderScheduling) {
  434. if (this.viewportWidth <= 1280) return '36%';
  435. if (this.viewportWidth <= 1440) return '39%';
  436. if (this.viewportWidth <= 1680) return '42%';
  437. return '44%';
  438. }
  439. if (this.viewportWidth <= 1280) return '44%';
  440. if (this.viewportWidth <= 1440) return '50%';
  441. return '58%';
  442. },
  443. splitPaneMaxSize() {
  444. if (this.isOrderScheduling) {
  445. if (this.viewportWidth <= 1280) return -620;
  446. if (this.viewportWidth <= 1440) return -680;
  447. return -720;
  448. }
  449. return this.viewportWidth <= 1440 ? -520 : -420;
  450. },
  451. splitPaneMinSize() {
  452. if (this.viewportWidth <= 1280) return 280;
  453. if (this.viewportWidth <= 1440) return 320;
  454. return 360;
  455. },
  456. schedulingLayoutAvailableHeightValue() {
  457. const headerHeight = this.viewportHeight <= 760 ? 38 : 42;
  458. const footerHeight = this.viewportHeight <= 760 ? 34 : 38;
  459. const bodyVerticalPadding = this.viewportHeight <= 760 ? 8 : 12;
  460. const contentGap = this.viewportHeight <= 760 ? 5 : 6;
  461. return (
  462. this.viewportHeight -
  463. 10 -
  464. headerHeight -
  465. footerHeight -
  466. bodyVerticalPadding -
  467. contentGap
  468. );
  469. },
  470. ganttPanelHeightValue() {
  471. const minGanttHeight =
  472. this.viewportHeight <= 720
  473. ? 230
  474. : this.viewportHeight <= 820
  475. ? 300
  476. : this.viewportHeight <= 960
  477. ? 340
  478. : 380;
  479. const maxGanttHeight =
  480. this.viewportHeight <= 820
  481. ? 390
  482. : this.viewportHeight <= 1080
  483. ? 500
  484. : 560;
  485. const balancedHeight = Math.floor(
  486. this.schedulingLayoutAvailableHeightValue / 2
  487. );
  488. return Math.max(
  489. 232,
  490. Math.min(maxGanttHeight, Math.max(minGanttHeight, balancedHeight))
  491. );
  492. },
  493. schedulingContentHeightValue() {
  494. const remainingHeight =
  495. this.schedulingLayoutAvailableHeightValue -
  496. this.ganttPanelHeightValue;
  497. const minHeight =
  498. this.viewportHeight <= 720
  499. ? 230
  500. : this.viewportHeight <= 820
  501. ? 300
  502. : this.viewportHeight <= 900
  503. ? 340
  504. : 380;
  505. return Math.max(minHeight, remainingHeight);
  506. },
  507. ganttHeight() {
  508. return `${this.ganttPanelHeightValue}px`;
  509. },
  510. schedulingContentHeight() {
  511. return `${this.schedulingContentHeightValue}px`;
  512. },
  513. planTableHeight() {
  514. const tabHeight = 42;
  515. const paginationHeight = 44;
  516. const tableHeight = Math.max(
  517. 220,
  518. this.schedulingContentHeightValue - tabHeight - paginationHeight
  519. );
  520. if (tableHeight >= 0) {
  521. return `${tableHeight}px`;
  522. }
  523. if (this.isOrderScheduling) {
  524. // ele-pro-table 的 height 只作用在表格主体,工具栏和分页器会继续占用额外高度。
  525. // 班组排产右侧是自定义 flex 分页区,左侧需要略收表格主体高度,保证两个分页器垂直位置一致。
  526. if (this.viewportHeight <= 760) return `${360 - tabHeight}px`;
  527. if (this.viewportHeight <= 900) return `${420 - tabHeight}px`;
  528. return `${470 - tabHeight}px`;
  529. }
  530. if (this.viewportHeight <= 760) return `${360 - tabHeight}px`;
  531. if (this.viewportHeight <= 900) return `${420 - tabHeight}px`;
  532. return `${455 - tabHeight}px`;
  533. },
  534. planPaginationClass() {
  535. return this.isOrderScheduling
  536. ? 'ele-pagination-circle order-plan-pagination'
  537. : 'ele-pagination-circle plan-scheduling-pagination';
  538. },
  539. taskConfigTableMaxHeight() {
  540. if (this.isOrderScheduling) {
  541. return Math.max(260, this.schedulingContentHeightValue - 132);
  542. }
  543. return Math.max(280, this.schedulingContentHeightValue - 132);
  544. },
  545. canApproveCurrentPlan() {
  546. const row = this.currentPlan;
  547. const hasPermission =
  548. typeof this.$hasPermission !== 'function' ||
  549. this.$hasPermission('aps:productionplan:release');
  550. return (
  551. !this.isOrderScheduling &&
  552. !!row?.id &&
  553. this.planTableStatusTab === 'unpublished' &&
  554. String(row.status) === '2' &&
  555. String(row.dotLineStatus) === '1' &&
  556. row.splitBatch != 1 &&
  557. row.approvalStatus != 1 &&
  558. row.approvalStatus != 2 &&
  559. hasPermission
  560. );
  561. }
  562. },
  563. created() {
  564. this.loadTeamOptions();
  565. this.loadFactoryOptions();
  566. this.loadApprovalReviewConfig();
  567. },
  568. mounted() {
  569. this.updateViewportSize();
  570. window.addEventListener('resize', this.updateViewportSize);
  571. },
  572. beforeDestroy() {
  573. window.removeEventListener('resize', this.updateViewportSize);
  574. },
  575. methods: {
  576. updateViewportSize() {
  577. this.viewportWidth = window.innerWidth;
  578. this.viewportHeight = window.innerHeight;
  579. },
  580. normalizeId(value) {
  581. return value === null || value === undefined ? '' : String(value);
  582. },
  583. normalizeBackendLongId(value) {
  584. if (value === null || value === undefined || value === '') {
  585. return '';
  586. }
  587. const text = String(value).trim();
  588. return /^\d+$/.test(text) ? text : '';
  589. },
  590. normalizeBackendTextValue(value) {
  591. if (value === null || value === undefined || value === '') {
  592. return '';
  593. }
  594. const text = String(value).trim();
  595. return ['undefined', 'null'].includes(text.toLowerCase()) ? '' : text;
  596. },
  597. pickBackendTextValue(...values) {
  598. for (const value of values) {
  599. const text = this.normalizeBackendTextValue(value);
  600. if (text) {
  601. return text;
  602. }
  603. }
  604. return '';
  605. },
  606. normalizeOrderQuantity(value) {
  607. if (value === null || value === undefined || value === '') {
  608. return '';
  609. }
  610. const text = String(value).trim();
  611. return /^0+(\.0+)?$/.test(text) ? '' : value;
  612. },
  613. optionMatchesId(optionId, targetId) {
  614. return this.normalizeId(optionId) === this.normalizeId(targetId);
  615. },
  616. // Plan list helpers
  617. getPlanRowKey(row) {
  618. return String(row?.id ?? row?.planId ?? '');
  619. },
  620. isReadonlyPlan(row) {
  621. return READONLY_LAYOUT_STATUSES.includes(String(row?.status ?? ''));
  622. },
  623. getOrderStatusCode(row) {
  624. const status = row?.status;
  625. if (status && typeof status === 'object') {
  626. return String(status.code ?? status.value ?? status.id ?? '');
  627. }
  628. return String(status ?? '');
  629. },
  630. isOrderSchedulingReadonlyRow(row) {
  631. if (!this.isOrderScheduling) {
  632. return false;
  633. }
  634. return (
  635. this.planTableStatusTab === 'dispatched' ||
  636. ['4', '5', '6', '7'].includes(this.getOrderStatusCode(row))
  637. );
  638. },
  639. getPlanActionText(row) {
  640. if (this.isOrderSchedulingReadonlyRow(row)) {
  641. return '查看';
  642. }
  643. return !this.isOrderScheduling && this.isReadonlyPlan(row)
  644. ? '查看'
  645. : '布点';
  646. },
  647. getDefaultPlanTableStatusTab(mode = this.schedulingMode) {
  648. return mode === 'order' ? 'undispatched' : 'unpublished';
  649. },
  650. getPlanTableStatusRequestWhere(tabName = this.planTableStatusTab) {
  651. if (this.isOrderScheduling) {
  652. return tabName === 'dispatched'
  653. ? { statusList: [4, 5, 6, 7] }
  654. : { statusList: [8] };
  655. }
  656. return tabName === 'published'
  657. ? { status: ['4'] }
  658. : { status: ['2', '3'] };
  659. },
  660. getPlanTableStatusRequestWhereList(tabName = this.planTableStatusTab) {
  661. return [this.getPlanTableStatusRequestWhere(tabName)];
  662. },
  663. async handlePlanTableStatusTabClick() {
  664. this.orderDetailReadonly = false;
  665. this.orderDetailActionMode = '';
  666. this.currentPlan = null;
  667. this.planRoutingPayload = null;
  668. this.taskList = [];
  669. this.selection = [];
  670. this.syncTableSelection('planTable', []);
  671. await this.reloadPlanTableByStatus();
  672. },
  673. async reloadPlanTableByStatus() {
  674. if (this.isOrderScheduling) {
  675. await this.reloadOrderSchedulingOrdersByDateRange(this.dateRange);
  676. } else {
  677. await this.reloadPlanSchedulingPlansByStatus();
  678. }
  679. },
  680. async reloadPlanSchedulingPlansByStatus(options = {}) {
  681. const preserveCurrent = !!options.preserveCurrent;
  682. const currentPlanKey = preserveCurrent
  683. ? this.getPlanRowKey(this.currentPlan)
  684. : '';
  685. this.orderDetailReadonly = false;
  686. this.orderDetailActionMode = '';
  687. const loading = this.$loading({
  688. lock: true,
  689. text: '加载计划中...'
  690. });
  691. let matchedCurrentPlan = null;
  692. try {
  693. const requestWhereList = this.getPlanTableStatusRequestWhereList();
  694. const resultList = await Promise.all(
  695. requestWhereList.map((where) =>
  696. getList({
  697. pageNum: 1,
  698. size: -1,
  699. ...this.getSchedulingDateRangeParams(),
  700. ...where
  701. })
  702. )
  703. );
  704. const rows = resultList.reduce(
  705. (list, data) => list.concat(data?.list || []),
  706. []
  707. );
  708. this.allPlanDataList = deepClone(rows);
  709. this.planDataList = this.buildPlanTableRows(rows, rows);
  710. if (preserveCurrent && currentPlanKey) {
  711. matchedCurrentPlan =
  712. this.planDataList.find(
  713. (item) => this.getPlanRowKey(item) === currentPlanKey
  714. ) || null;
  715. }
  716. } catch (e) {
  717. this.$message.error(e.message || '获取计划数据失败');
  718. } finally {
  719. loading.close();
  720. }
  721. if (preserveCurrent && matchedCurrentPlan) {
  722. this.currentPlan = {
  723. ...(this.currentPlan || {}),
  724. ...matchedCurrentPlan
  725. };
  726. this.selection = [matchedCurrentPlan];
  727. this.syncTableSelection('planTable', [matchedCurrentPlan]);
  728. return;
  729. }
  730. this.selection = [];
  731. this.currentPlan = null;
  732. this.planRoutingPayload = null;
  733. this.taskList = [];
  734. this.syncTableSelection('planTable', []);
  735. },
  736. async handlePlanTableReload() {
  737. if (this.isOrderScheduling) {
  738. await this.reloadOrderSchedulingOrdersByDateRange(this.dateRange);
  739. return;
  740. }
  741. await this.refreshSchedulingView();
  742. await this.reloadPlanSchedulingPlansByStatus({ preserveCurrent: true });
  743. if (this.currentPlan?.id) {
  744. await this.loadPlanDotLineDetail(this.currentPlan);
  745. }
  746. },
  747. getSchedulingDateRangeParams() {
  748. if (!Array.isArray(this.dateRange) || this.dateRange.length !== 2) {
  749. return {};
  750. }
  751. if (!this.dateRange[0] || !this.dateRange[1]) {
  752. return {};
  753. }
  754. return {
  755. startDate: this.dateRange[0],
  756. endDate: this.dateRange[1]
  757. };
  758. },
  759. async loadPlansByIds(ids = []) {
  760. const uniqueIds = Array.from(
  761. new Set(
  762. (ids || []).filter(
  763. (id) => id !== null && id !== undefined && id !== ''
  764. )
  765. )
  766. );
  767. if (!uniqueIds.length) {
  768. return [];
  769. }
  770. const data = await getList({
  771. pageNum: 1,
  772. size: uniqueIds.length,
  773. ids: uniqueIds
  774. });
  775. const orderMap = new Map(
  776. uniqueIds.map((id, index) => [String(id), index])
  777. );
  778. const list = Array.isArray(data?.list) ? data.list : [];
  779. return [...list].sort((a, b) => {
  780. const aIndex = orderMap.get(String(a?.id ?? ''));
  781. const bIndex = orderMap.get(String(b?.id ?? ''));
  782. return (
  783. (aIndex ?? Number.MAX_SAFE_INTEGER) -
  784. (bIndex ?? Number.MAX_SAFE_INTEGER)
  785. );
  786. });
  787. },
  788. buildPlanTableRows(selectedRows = [], sourceRows = []) {
  789. const sourceList =
  790. Array.isArray(sourceRows) && sourceRows.length
  791. ? sourceRows
  792. : selectedRows;
  793. const selectedKeySet = new Set(
  794. (selectedRows || [])
  795. .map((item) => this.getPlanRowKey(item))
  796. .filter((item) => item !== '')
  797. );
  798. return sourceList
  799. .filter((item) => selectedKeySet.has(this.getPlanRowKey(item)))
  800. .map((item, index) => ({
  801. ...deepClone(item),
  802. serialNumber: index + 1
  803. }));
  804. },
  805. buildWorkOrderTableRows(rows = []) {
  806. return (rows || []).map((item, index) => ({
  807. ...deepClone(item),
  808. id: item.id ?? item.code ?? `work-order-${index}`,
  809. serialNumber: index + 1
  810. }));
  811. },
  812. syncTableSelection(refName, rows) {
  813. this.$nextTick(() => {
  814. const table = this.$refs[refName];
  815. if (!table) {
  816. return;
  817. }
  818. if (refName === 'planPickerTable') {
  819. this.planPickerSelectionSyncing = true;
  820. }
  821. if (refName === 'planTable') {
  822. this.planTableSelectionSyncing = true;
  823. }
  824. setTimeout(() => {
  825. if (typeof table?.setSelectedRows === 'function') {
  826. table.setSelectedRows(rows || []);
  827. } else {
  828. table?.clearSelection?.();
  829. (rows || []).forEach((row) => {
  830. table?.toggleRowSelection?.(row, true);
  831. });
  832. }
  833. if (refName === 'planPickerTable') {
  834. this.$nextTick(() => {
  835. this.planPickerSelectionSyncing = false;
  836. });
  837. }
  838. if (refName === 'planTable') {
  839. this.$nextTick(() => {
  840. this.planTableSelectionSyncing = false;
  841. });
  842. }
  843. }, 0);
  844. });
  845. },
  846. // Plan picker flow
  847. async planPickerDatasource({ page, limit, where }) {
  848. this.planPickerLoading = true;
  849. try {
  850. const requestWhere = { ...(where || {}) };
  851. requestWhere.status = Array.isArray(requestWhere.status)
  852. ? requestWhere.status
  853. : requestWhere.status
  854. ? [requestWhere.status]
  855. : [];
  856. const data = await getList({
  857. ...requestWhere,
  858. pageNum: page,
  859. size: limit,
  860. timeDimensionPlanType: this.planPickerTimeDimensionPlanType
  861. });
  862. this.allPlanDataList = Array.isArray(data?.list) ? data.list : [];
  863. const selectedMap = new Map(
  864. (this.planPickerSelectionAll || []).map((item) => [
  865. this.getPlanRowKey(item),
  866. item
  867. ])
  868. );
  869. this.planPickerSelection = this.allPlanDataList.filter((item) =>
  870. selectedMap.has(this.getPlanRowKey(item))
  871. );
  872. this.syncTableSelection('planPickerTable', this.planPickerSelection);
  873. return data;
  874. } finally {
  875. this.planPickerLoading = false;
  876. }
  877. },
  878. async openPlanPicker(timeDimensionPlanType, title) {
  879. if (this.entryReadonly) {
  880. return;
  881. }
  882. this.planPickerTitle = title || '添加计划';
  883. this.planPickerTimeDimensionPlanType = timeDimensionPlanType;
  884. this.planPickerWhere = createPlanPickerWhere();
  885. this.planPickerSelectionAll = this.buildPlanTableRows(
  886. this.planDataList
  887. );
  888. this.planPickerSelection = [];
  889. this.allPlanDataList = [];
  890. this.planPickerVisible = true;
  891. this.$nextTick(() => {
  892. this.$refs.planPickerTable?.reload({ page: 1, where: {} });
  893. });
  894. },
  895. handlePlanPickerSearch() {
  896. this.$refs.planPickerTable?.reload({
  897. page: 1,
  898. where: { ...this.planPickerWhere }
  899. });
  900. },
  901. resetPlanPickerSearch() {
  902. this.planPickerWhere = createPlanPickerWhere();
  903. this.$refs.planPickerTable?.reload({ page: 1, where: {} });
  904. },
  905. handlePlanPickerSelectionChange(rows) {
  906. if (this.planPickerSelectionSyncing) {
  907. return;
  908. }
  909. const currentPageSelection = Array.isArray(rows) ? rows : [];
  910. const currentPageKeySet = new Set(
  911. (this.allPlanDataList || [])
  912. .map((item) => this.getPlanRowKey(item))
  913. .filter((item) => item !== '')
  914. );
  915. const selectedMap = new Map(
  916. (this.planPickerSelectionAll || [])
  917. .filter((item) => !currentPageKeySet.has(this.getPlanRowKey(item)))
  918. .map((item) => [this.getPlanRowKey(item), item])
  919. );
  920. currentPageSelection.forEach((item) => {
  921. selectedMap.set(this.getPlanRowKey(item), item);
  922. });
  923. this.planPickerSelection = currentPageSelection;
  924. this.planPickerSelectionAll = Array.from(selectedMap.values());
  925. },
  926. async confirmPlanPicker() {
  927. const nextPlanDataList = this.buildPlanTableRows(
  928. this.planPickerSelectionAll
  929. );
  930. if (!nextPlanDataList.length) {
  931. this.$message.warning('请至少选择一条计划');
  932. return;
  933. }
  934. const currentPlanKey = this.getPlanRowKey(this.currentPlan);
  935. const matchedCurrentPlan = nextPlanDataList.find(
  936. (item) => this.getPlanRowKey(item) === currentPlanKey
  937. );
  938. this.planPickerVisible = false;
  939. this.planDataList = nextPlanDataList;
  940. this.orderDetailReadonly = false;
  941. this.orderDetailActionMode = '';
  942. this.currentPlan = null;
  943. this.planRoutingPayload = null;
  944. this.taskList = [];
  945. this.workstationOptionsMap = {};
  946. this.workstationOptionsLoadingMap = {};
  947. this.selection = [];
  948. this.dotLineLoading = false;
  949. await this.refreshSchedulingView();
  950. this.selection = matchedCurrentPlan ? [matchedCurrentPlan] : [];
  951. this.syncTableSelection(
  952. 'planTable',
  953. matchedCurrentPlan ? [matchedCurrentPlan] : []
  954. );
  955. },
  956. // Selected plan detail
  957. handlePlanSelectionChange(rows) {
  958. if (this.planTableSelectionSyncing) {
  959. return;
  960. }
  961. const nextRows = Array.isArray(rows) ? rows : [];
  962. const row = nextRows[nextRows.length - 1];
  963. if (!row) {
  964. this.selection = [];
  965. this.syncTableSelection('planTable', []);
  966. return;
  967. }
  968. this.handlePublish(row);
  969. },
  970. resolveCurrentPlan(row) {
  971. if (!this.isOrderScheduling) {
  972. return row;
  973. }
  974. return {
  975. ...row,
  976. workOrderId: row.id,
  977. id: row.productionPlanId || row.id,
  978. code: row.productionPlanCode || row.code,
  979. productionOrderNumber: row.code,
  980. startTime: row.planStartTime || row.startTime,
  981. endTime: row.planCompleteTime || row.endTime,
  982. productNum: row.formingNum ?? row.productNum,
  983. unit: row.measuringUnit || row.unit
  984. };
  985. },
  986. handlePublish(row) {
  987. const skipBomCheck =
  988. this.isOrderScheduling && String(row?.workOrderType ?? '') == '3';
  989. if (!skipBomCheck && !row.bomCategoryId) {
  990. this.$message.warning('该计划没有配置BOM请先配置BOM');
  991. return;
  992. }
  993. const readonly = this.isOrderSchedulingReadonlyRow(row);
  994. this.orderDetailReadonly = readonly;
  995. this.orderDetailActionMode = readonly ? 'view' : 'edit';
  996. const currentPlan = this.resolveCurrentPlan(row);
  997. currentPlan._orderDetailReadonly = readonly;
  998. currentPlan.orderDetailReadonly = readonly;
  999. this.currentPlan = currentPlan;
  1000. this.selection = [row];
  1001. this.syncTableSelection('planTable', [row]);
  1002. this.loadPlanDotLineDetail(currentPlan);
  1003. },
  1004. async loadPlanDotLineDetail(currentPlan) {
  1005. this.dotLineLoading = true;
  1006. this.planRoutingPayload = null;
  1007. this.taskList = [];
  1008. try {
  1009. const planData = await getPlanDotLine({ planId: currentPlan.id });
  1010. this.planRoutingPayload = planData || null;
  1011. const details = planData?.detailList;
  1012. if (details?.length) {
  1013. this.taskList = [...details]
  1014. .sort((a, b) => (a.taskSort ?? 0) - (b.taskSort ?? 0))
  1015. .map((item, index) => this.normalizeDetailRow(item, index));
  1016. // 班组排程的“执行对象”支持人员/工位,工位必须按当前工序查询;
  1017. // 工厂排程仍然使用原来的班组执行对象,preload 内部会直接跳过。
  1018. await this.preloadTaskWorkstationOptions();
  1019. this.syncWorkOrderExecutionSelections();
  1020. return;
  1021. }
  1022. if (!currentPlan.produceRoutingId) {
  1023. this.$message.warning('暂无布点明细且缺少工艺路线,无法加载工序');
  1024. return;
  1025. }
  1026. const list = await getTaskListById(currentPlan.produceRoutingId);
  1027. this.taskList = (list || []).map((item, index) =>
  1028. this.normalizeRoutingTaskRow(item, index)
  1029. );
  1030. // 首次没有布点明细时会从工艺路线生成工序,班组排程仍需补齐
  1031. // 每道工序可派工位;工厂排程不加载这部分数据。
  1032. await this.preloadTaskWorkstationOptions();
  1033. this.syncWorkOrderExecutionSelections();
  1034. } catch (e) {
  1035. this.planRoutingPayload = null;
  1036. this.taskList = [];
  1037. this.$message.error(e.message || '获取工序布点信息失败');
  1038. } finally {
  1039. this.dotLineLoading = false;
  1040. }
  1041. },
  1042. normalizeDetailRow(item, index) {
  1043. const execType = toSafeNumber(item.executionType) ?? EXEC_TYPE.HOMEMADE;
  1044. const outsource = execType === EXEC_TYPE.OUTSOURCE;
  1045. const entrust = execType === EXEC_TYPE.ENTRUST;
  1046. const homemade = execType === EXEC_TYPE.HOMEMADE;
  1047. const orderMode = this.isOrderScheduling;
  1048. const savedObjectType =
  1049. orderMode && homemade
  1050. ? item.executionObjectType ||
  1051. item.executionTargetType ||
  1052. item.executionAssigneeType ||
  1053. (item.executionWorkstationId ? 'workstation' : 'person')
  1054. : '';
  1055. const objectId = orderMode
  1056. ? item.executionObjectId ||
  1057. item.executionAssigneeId ||
  1058. item.executionUserId ||
  1059. item.executionCrewId ||
  1060. item.executionWorkstationId ||
  1061. item.executionTeamId ||
  1062. ''
  1063. : item.executionTeamId ?? '';
  1064. const objectName = orderMode
  1065. ? item.executionObjectName ||
  1066. item.executionAssigneeName ||
  1067. item.executionUserName ||
  1068. item.executionCrewName ||
  1069. item.executionWorkstationName ||
  1070. item.executionTeamName ||
  1071. ''
  1072. : item.executionTeamName ?? '';
  1073. const dispatchTeamId =
  1074. item.dispatchTeamId ||
  1075. item.teamId ||
  1076. item.firstTaskTeamId ||
  1077. item.firstTeamId ||
  1078. item.executionTeamId ||
  1079. '';
  1080. const dispatchTeamName =
  1081. item.dispatchTeamName ||
  1082. item.teamName ||
  1083. item.firstTaskTeamName ||
  1084. item.firstTeamName ||
  1085. item.executionTeamName ||
  1086. '';
  1087. const dispatchTeamLeaderId =
  1088. item.dispatchTeamLeaderId ||
  1089. item.teamLeaderUserId ||
  1090. item.leaderUserId ||
  1091. item.executionTeamLeaderId ||
  1092. '';
  1093. const executionStartTime = formatDotLineDateTime(
  1094. item.executionStartTime
  1095. );
  1096. const executionEndTime = formatDotLineDateTime(item.executionEndTime);
  1097. return {
  1098. ...item,
  1099. _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
  1100. isParallelism: toSafeNumber(item.isParallelism) === 1 ? 1 : 0,
  1101. executionStartTime,
  1102. executionEndTime,
  1103. dispatchDefaultStartTime: executionStartTime,
  1104. dispatchDefaultEndTime: executionEndTime,
  1105. executionType: execType,
  1106. executionFactoryId: entrust
  1107. ? item.executionFactoryId ?? item.executionTeamId ?? ''
  1108. : '',
  1109. executionFactoryName: entrust
  1110. ? item.executionFactoryName ?? item.executionTeamName ?? ''
  1111. : '',
  1112. executionTeamId:
  1113. homemade && orderMode && objectId
  1114. ? `${savedObjectType}:${objectId}`
  1115. : homemade
  1116. ? item.executionTeamId ?? ''
  1117. : '',
  1118. rawExecutionTeamId: homemade && orderMode ? objectId : '',
  1119. executionObjectType: homemade && orderMode ? savedObjectType : '',
  1120. executionObjectName: homemade && orderMode ? objectName : '',
  1121. dispatchTeamId,
  1122. dispatchTeamName,
  1123. dispatchTeamLeaderId,
  1124. quantity: orderMode
  1125. ? this.normalizeOrderQuantity(
  1126. item.quantity ??
  1127. item.taskQuantity ??
  1128. item.requiredFormingNum ??
  1129. item.productNum ??
  1130. ''
  1131. )
  1132. : item.quantity,
  1133. executionTeamLeader:
  1134. outsource || entrust ? '' : item.executionTeamLeader ?? '',
  1135. executionTeamLeaderId:
  1136. outsource || entrust ? '' : item.executionTeamLeaderId ?? '',
  1137. executionTeamName: outsource || entrust ? '' : objectName
  1138. };
  1139. },
  1140. normalizeRoutingTaskRow(item, index) {
  1141. const plan = this.currentPlan || {};
  1142. const taskId = item.id;
  1143. const row = {
  1144. ...item,
  1145. _taskKey: item.id ?? `new-${taskId ?? index}-${index}`,
  1146. taskId: taskId ?? undefined,
  1147. taskName: item.name ?? item.taskName,
  1148. taskCode: item.code ?? item.taskCode,
  1149. taskSort: item.taskSort ?? index + 1,
  1150. type: item.type ?? undefined,
  1151. sourceTaskId: item.sourceTaskId,
  1152. planId: plan.id,
  1153. planCode: plan.code,
  1154. routingId: plan.produceRoutingId,
  1155. routingName: plan.produceRoutingName ?? plan.routingName,
  1156. isParallelism: 0,
  1157. executionType: EXEC_TYPE.HOMEMADE,
  1158. executionTeamId: '',
  1159. executionObjectType: '',
  1160. executionObjectName: '',
  1161. dispatchTeamId:
  1162. item.dispatchTeamId ||
  1163. item.teamId ||
  1164. item.firstTaskTeamId ||
  1165. item.firstTeamId ||
  1166. item.executionTeamId ||
  1167. '',
  1168. dispatchTeamName:
  1169. item.dispatchTeamName ||
  1170. item.teamName ||
  1171. item.firstTaskTeamName ||
  1172. item.firstTeamName ||
  1173. item.executionTeamName ||
  1174. '',
  1175. dispatchTeamLeaderId:
  1176. item.dispatchTeamLeaderId ||
  1177. item.teamLeaderUserId ||
  1178. item.leaderUserId ||
  1179. item.executionTeamLeaderId ||
  1180. '',
  1181. executionTeamLeader: '',
  1182. executionTeamLeaderId: '',
  1183. executionTeamName: '',
  1184. executionStartTime: '',
  1185. executionEndTime: '',
  1186. dispatchDefaultStartTime: '',
  1187. dispatchDefaultEndTime: '',
  1188. executionFactoryId: '',
  1189. executionFactoryName: ''
  1190. };
  1191. if (this.isOrderScheduling) {
  1192. row.quantity = '';
  1193. }
  1194. return row;
  1195. },
  1196. taskName(item, index) {
  1197. return item.taskName || item.name || `工艺${index + 1}`;
  1198. },
  1199. routeStepTitle(item, index) {
  1200. return (
  1201. item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
  1202. );
  1203. },
  1204. isTaskDone(item) {
  1205. return item.executionType != null && item.executionType !== '';
  1206. },
  1207. execTypeNum(row) {
  1208. return toSafeNumber(row.executionType);
  1209. },
  1210. clearExecutionTeamFields(row) {
  1211. this.$set(row, 'executionTeamId', '');
  1212. this.$set(row, 'executionTeamName', '');
  1213. this.$set(row, 'executionObjectType', '');
  1214. this.$set(row, 'executionObjectName', '');
  1215. this.$set(row, 'executionTeamLeader', '');
  1216. this.$set(row, 'executionTeamLeaderId', '');
  1217. this.$set(row, 'executionFactoryId', '');
  1218. this.$set(row, 'executionFactoryName', '');
  1219. },
  1220. onExecutionTypeChange(row) {
  1221. if (this.isCurrentPlanReadonly) {
  1222. return;
  1223. }
  1224. this.clearExecutionTeamFields(row);
  1225. },
  1226. onExecutionObjectChange(row) {
  1227. if (this.isCurrentPlanReadonly) {
  1228. return;
  1229. }
  1230. const t = this.execTypeNum(row);
  1231. if (t === EXEC_TYPE.HOMEMADE) {
  1232. const team = this.findHomemadeOption(row);
  1233. row.executionTeamName = team?.name ?? '';
  1234. row.executionObjectType = team?.objectType ?? '';
  1235. row.executionObjectName = team?.name ?? '';
  1236. row.rawExecutionTeamId = team?.rawId ?? row.executionTeamId;
  1237. row.executionTeamLeader = team?.leaderUserName ?? '';
  1238. row.executionTeamLeaderId = team?.leaderUserId ?? '';
  1239. row.executionFactoryId = '';
  1240. row.executionFactoryName = '';
  1241. } else if (t === EXEC_TYPE.ENTRUST) {
  1242. const f = this.factoryList.find(
  1243. (x) => x.value === row.executionFactoryId
  1244. );
  1245. row.executionFactoryName = f?.label ?? '';
  1246. row.executionTeamId = '';
  1247. row.executionTeamName = '';
  1248. row.executionTeamLeader = '';
  1249. row.executionTeamLeaderId = '';
  1250. }
  1251. },
  1252. async loadTeamOptions() {
  1253. try {
  1254. const factoryId = this.$store.state.user.info.factoryId;
  1255. const res = await teamPage({ pageNum: 1, size: -1, factoryId });
  1256. this.teamOptions = (res?.list || []).map(
  1257. ({ id, name, leaderUserName, leaderUserId }) => ({
  1258. id,
  1259. name,
  1260. objectType: 'team',
  1261. leaderUserName,
  1262. leaderUserId
  1263. })
  1264. );
  1265. } catch {
  1266. this.teamOptions = [];
  1267. }
  1268. },
  1269. async loadApprovalReviewConfig() {
  1270. try {
  1271. const res = await parameterGetByCode({
  1272. code: 'planned_release_require'
  1273. });
  1274. this.isReview = res?.value == '1';
  1275. } catch (e) {
  1276. this.isReview = false;
  1277. }
  1278. },
  1279. async loadWorkOrderExecutionOptions() {
  1280. // 班组排程自制执行对象分两类:
  1281. // - 人员:打开弹窗时一次性加载,所有工序共用。
  1282. // - 工位:依赖具体工序,不能取全厂工位,需要在工序加载后按 taskId 查询。
  1283. try {
  1284. const res = await getUserPage({ pageNum: 1, size: -1 });
  1285. const users = Array.isArray(res?.list) ? res.list : [];
  1286. this.teamOptions = users.map((item) => ({
  1287. id: `person:${item.id}`,
  1288. rawId: item.id,
  1289. objectType: 'person',
  1290. name: `人员:${
  1291. item.name ||
  1292. item.userName ||
  1293. item.loginName ||
  1294. item.jobNumber ||
  1295. item.id
  1296. }`,
  1297. leaderUserName: item.name || item.userName || '',
  1298. leaderUserId: item.id
  1299. }));
  1300. } catch {
  1301. this.teamOptions = [];
  1302. }
  1303. this.syncWorkOrderExecutionSelections();
  1304. },
  1305. getTaskWorkstationKey(row) {
  1306. const rowTaskId = row?.taskId;
  1307. const currentTaskId = this.currentPlan?.taskId;
  1308. const taskId =
  1309. row?.sourceTaskId ??
  1310. row?.firstTaskId ??
  1311. (rowTaskId && Number(rowTaskId) >= 0 ? rowTaskId : undefined) ??
  1312. (currentTaskId && Number(currentTaskId) >= 0
  1313. ? currentTaskId
  1314. : undefined) ??
  1315. this.currentPlan?.firstTaskId ??
  1316. '';
  1317. return taskId === null || taskId === undefined || taskId === ''
  1318. ? ''
  1319. : String(taskId);
  1320. },
  1321. formatWorkstationOptions(list = []) {
  1322. return (Array.isArray(list) ? list : []).map((item) => {
  1323. const substance =
  1324. item.workstationSubstanceList?.[0]?.substance ||
  1325. item.substance ||
  1326. item.asset ||
  1327. {};
  1328. const category =
  1329. substance.category?.category || substance.category || {};
  1330. const name =
  1331. item.name || item.workstationName || item.stationName || item.id;
  1332. const code = item.code || item.workstationCode || item.stationCode;
  1333. const displayName = code ? `${name}(${code})` : name;
  1334. const rawId =
  1335. item.id ??
  1336. item.workstationId ??
  1337. item.factoryWorkstationId ??
  1338. item.stationId ??
  1339. code ??
  1340. name;
  1341. return {
  1342. id: `workstation:${rawId}`,
  1343. rawId,
  1344. objectType: 'workstation',
  1345. name: `工位:${displayName}`,
  1346. code: item.code || item.workstationCode || item.stationCode || '',
  1347. workstationCode:
  1348. item.workstationCode || item.code || item.stationCode || '',
  1349. assetCode: this.pickBackendTextValue(
  1350. item.assetCode,
  1351. item.deviceCode,
  1352. item.extInfo?.assetCode,
  1353. item.extInfo?.deviceCode,
  1354. substance.code
  1355. ),
  1356. assetId:
  1357. this.normalizeBackendLongId(item.assetId) ||
  1358. this.normalizeBackendLongId(item.deviceId) ||
  1359. this.normalizeBackendLongId(item.extInfo?.assetId) ||
  1360. this.normalizeBackendLongId(substance.id) ||
  1361. '',
  1362. assetName: this.pickBackendTextValue(
  1363. item.assetName,
  1364. item.deviceName,
  1365. item.extInfo?.assetName,
  1366. item.extInfo?.deviceName,
  1367. substance.name
  1368. ),
  1369. assetModelType:
  1370. item.assetModelType ||
  1371. item.deviceModel ||
  1372. category.modelType ||
  1373. '',
  1374. assetCategoryLevelPath:
  1375. item.assetCategoryLevelPath ||
  1376. item.deviceType ||
  1377. category.categoryLevelPath?.split('-')?.[0] ||
  1378. '',
  1379. extInfo: item.extInfo || {},
  1380. isNew: item.isNew || '',
  1381. leaderUserName: item.leaderName || '',
  1382. leaderUserId: item.leaderId || ''
  1383. };
  1384. });
  1385. },
  1386. async loadWorkstationOptionsByTaskId(taskId) {
  1387. const key =
  1388. taskId === null || taskId === undefined || taskId === ''
  1389. ? ''
  1390. : String(taskId);
  1391. // 这里复用任务派单“工序派单”的工位来源:
  1392. // /main/factoryworkstation/listByFirstTaskId/{taskId}
  1393. // 这样每行工序只展示它能派发的工位。isOrderScheduling 用来保证
  1394. // 该接口不会在工厂排程中触发,避免改变现有工厂排程数据链路。
  1395. if (
  1396. !this.isOrderScheduling ||
  1397. !key ||
  1398. this.workstationOptionsMap[key] ||
  1399. this.workstationOptionsLoadingMap[key]
  1400. ) {
  1401. return;
  1402. }
  1403. this.$set(this.workstationOptionsLoadingMap, key, true);
  1404. try {
  1405. const res = await listByFirstTaskId(taskId);
  1406. this.$set(
  1407. this.workstationOptionsMap,
  1408. key,
  1409. this.formatWorkstationOptions(res)
  1410. );
  1411. this.syncWorkOrderExecutionSelections();
  1412. } catch {
  1413. this.$set(this.workstationOptionsMap, key, []);
  1414. } finally {
  1415. this.$delete(this.workstationOptionsLoadingMap, key);
  1416. }
  1417. },
  1418. preloadTaskWorkstationOptions(rows = this.taskList) {
  1419. if (!this.isOrderScheduling) {
  1420. return Promise.resolve();
  1421. }
  1422. // 多个工序可能引用同一个 taskId,去重后再请求,减少重复接口调用。
  1423. const taskIds = Array.from(
  1424. new Set(
  1425. (rows || [])
  1426. .map((row) => this.getTaskWorkstationKey(row))
  1427. .filter(Boolean)
  1428. )
  1429. );
  1430. return Promise.all(
  1431. taskIds.map((taskId) => this.loadWorkstationOptionsByTaskId(taskId))
  1432. );
  1433. },
  1434. getHomemadeOptions(row) {
  1435. if (!this.isOrderScheduling) {
  1436. return this.teamOptions;
  1437. }
  1438. // 班组排程的自制执行对象 = 人员 + 当前工序可派工位。
  1439. // 下拉框每一行调用一次该方法,因此这里按行拿 taskId,再合并该行的
  1440. // 工位缓存;若缓存还没回来,先显示人员,接口返回后 Vue 会刷新下拉。
  1441. const key = this.getTaskWorkstationKey(row);
  1442. if (key && !this.workstationOptionsMap[key]) {
  1443. this.loadWorkstationOptionsByTaskId(key);
  1444. }
  1445. return [
  1446. ...this.teamOptions,
  1447. ...(key ? this.workstationOptionsMap[key] || [] : [])
  1448. ];
  1449. },
  1450. findHomemadeOption(row) {
  1451. return this.getHomemadeOptions(row).find((option) =>
  1452. this.optionMatchesId(option.id, row.executionTeamId)
  1453. );
  1454. },
  1455. syncWorkOrderExecutionSelections() {
  1456. if (!this.isOrderScheduling) {
  1457. return;
  1458. }
  1459. // 前端下拉为避免人员和工位 id 冲突,会加 person:/workstation: 前缀,
  1460. // 所以工位选项加载完成后需要把原始 id 回填成下拉可识别的值。
  1461. this.taskList.forEach((row) => {
  1462. if (this.execTypeNum(row) !== EXEC_TYPE.HOMEMADE) return;
  1463. const objectType = row.executionObjectType || 'person';
  1464. const matched = this.getHomemadeOptions(row).find(
  1465. (option) =>
  1466. option.objectType === objectType &&
  1467. this.optionMatchesId(option.rawId, row.rawExecutionTeamId)
  1468. );
  1469. if (matched) {
  1470. this.$set(row, 'executionTeamId', matched.id);
  1471. this.$set(row, 'executionTeamName', matched.name);
  1472. this.$set(row, 'executionObjectName', matched.name);
  1473. this.$set(row, 'rawExecutionTeamId', matched.rawId);
  1474. }
  1475. });
  1476. },
  1477. async loadFactoryOptions() {
  1478. try {
  1479. const res = await getFactoryarea({ type: 1, size: 9999 });
  1480. if (res.list?.length) {
  1481. this.factoryList = res.list.map((el) => ({
  1482. value: el.id,
  1483. label: el.name
  1484. }));
  1485. } else {
  1486. this.factoryList = [];
  1487. }
  1488. } catch {
  1489. this.factoryList = [];
  1490. }
  1491. },
  1492. // Task config editing
  1493. handleTimeChange(row, changeKey) {
  1494. if (this.isCurrentPlanReadonly) {
  1495. return;
  1496. }
  1497. if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
  1498. this.$message.warning('执行结束时间不能小于执行开始时间');
  1499. this.$set(row, changeKey, '');
  1500. return;
  1501. }
  1502. if (
  1503. changeKey === 'executionStartTime' &&
  1504. this.isExecutionStartBeforePlanStart(row.executionStartTime)
  1505. ) {
  1506. this.$message.warning('执行开始时间不能小于计划开始时间');
  1507. this.$set(row, changeKey, '');
  1508. return;
  1509. }
  1510. if (!this.validateTaskSequence(row, { resetKey: changeKey })) {
  1511. return;
  1512. }
  1513. const deadline =
  1514. this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
  1515. if (
  1516. changeKey === 'executionStartTime' &&
  1517. deadline &&
  1518. row.executionStartTime
  1519. ) {
  1520. const d = new Date(deadline).getTime();
  1521. const v = new Date(row.executionStartTime).getTime();
  1522. if (!Number.isNaN(d) && !Number.isNaN(v) && v > d) {
  1523. this.$message.warning('时间不能大于计划交付时间');
  1524. this.$set(row, changeKey, '');
  1525. return;
  1526. }
  1527. }
  1528. if (changeKey === 'executionEndTime' && row.executionEndTime) {
  1529. const planEnd = this.currentPlan?.endTime;
  1530. const overDelivery =
  1531. !!deadline && isEndAfterPlanEnd(row.executionEndTime, deadline);
  1532. const overPlanEnd =
  1533. !!planEnd && isEndAfterPlanEnd(row.executionEndTime, planEnd);
  1534. if (overDelivery && overPlanEnd) {
  1535. this.$message.warning(
  1536. '执行结束时间晚于计划交付时间及计划结束时间,已保留填写内容,请留意核对'
  1537. );
  1538. } else if (overDelivery) {
  1539. this.$message.warning(
  1540. '执行结束时间晚于计划交付时间,已保留填写内容,请留意核对'
  1541. );
  1542. } else if (overPlanEnd) {
  1543. this.$message.warning(
  1544. '执行结束时间晚于计划结束时间,已保留填写内容,请留意核对'
  1545. );
  1546. }
  1547. }
  1548. },
  1549. handleParallelismChange(row) {
  1550. if (this.isCurrentPlanReadonly || !row?.executionStartTime) {
  1551. return;
  1552. }
  1553. if (this.isTaskParallel(row)) {
  1554. if (this.isExecutionStartBeforePlanStart(row.executionStartTime)) {
  1555. this.$message.warning('执行开始时间不能小于计划开始时间');
  1556. this.$set(row, 'executionStartTime', '');
  1557. }
  1558. return;
  1559. }
  1560. this.validateTaskSequence(row, { resetKey: 'executionStartTime' });
  1561. },
  1562. isExecutionEndOverDelivery(row) {
  1563. const deadline =
  1564. this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
  1565. return (
  1566. !!deadline &&
  1567. !!row?.executionEndTime &&
  1568. isEndAfterPlanEnd(row.executionEndTime, deadline)
  1569. );
  1570. },
  1571. isExecutionEndOverPlanEnd(row) {
  1572. const planEnd = this.currentPlan?.endTime;
  1573. return (
  1574. !!planEnd &&
  1575. !!row?.executionEndTime &&
  1576. isEndAfterPlanEnd(row.executionEndTime, planEnd)
  1577. );
  1578. },
  1579. toggleDotLineFullscreen() {
  1580. this.dotLineFullscreen = !this.dotLineFullscreen;
  1581. },
  1582. getTaskLabel(item, index) {
  1583. if (item) {
  1584. return (
  1585. item.taskName ||
  1586. item.name ||
  1587. (typeof index === 'number' ? `工艺${index + 1}` : '当前工艺')
  1588. );
  1589. }
  1590. return index !== undefined ? `工艺${index + 1}` : '当前工艺';
  1591. },
  1592. getTaskIndex(row) {
  1593. return this.taskList.findIndex((item) => item === row);
  1594. },
  1595. getTaskSequenceGroupKey(row) {
  1596. const key =
  1597. row?.sourceTaskId ??
  1598. row?.firstTaskId ??
  1599. row?.taskId ??
  1600. row?.id ??
  1601. row?._taskKey ??
  1602. '';
  1603. return key === null || key === undefined ? '' : String(key);
  1604. },
  1605. getTaskSequenceSortValue(row, index) {
  1606. const values = [
  1607. row?.taskSort,
  1608. row?.sort,
  1609. row?.sequence,
  1610. row?.taskSequence,
  1611. row?.orderNo,
  1612. row?.sortNo
  1613. ];
  1614. for (const value of values) {
  1615. if (value !== null && value !== undefined && value !== '') {
  1616. const num = Number(value);
  1617. if (!Number.isNaN(num)) {
  1618. return num;
  1619. }
  1620. }
  1621. }
  1622. return index;
  1623. },
  1624. getTaskSequenceBoundary(group, timeKey, pickLatest) {
  1625. if (!group?.items?.length) {
  1626. return null;
  1627. }
  1628. return group.items.reduce((best, item, index) => {
  1629. const value = item?.[timeKey];
  1630. if (!value) {
  1631. return best;
  1632. }
  1633. const time = new Date(value).getTime();
  1634. if (Number.isNaN(time)) {
  1635. return best;
  1636. }
  1637. if (!best || (pickLatest ? time > best.time : time < best.time)) {
  1638. return {
  1639. item,
  1640. time,
  1641. index
  1642. };
  1643. }
  1644. return best;
  1645. }, null);
  1646. },
  1647. getOrderTaskSequenceContext(row) {
  1648. const currentKey = this.getTaskSequenceGroupKey(row);
  1649. if (!currentKey) {
  1650. return null;
  1651. }
  1652. const groupMap = new Map();
  1653. (this.taskList || []).forEach((item, index) => {
  1654. const key = this.getTaskSequenceGroupKey(item);
  1655. if (!key) {
  1656. return;
  1657. }
  1658. if (!groupMap.has(key)) {
  1659. groupMap.set(key, {
  1660. key,
  1661. firstIndex: index,
  1662. sortValue: this.getTaskSequenceSortValue(item, index),
  1663. items: []
  1664. });
  1665. }
  1666. const group = groupMap.get(key);
  1667. group.sortValue = Math.min(
  1668. group.sortValue,
  1669. this.getTaskSequenceSortValue(item, index)
  1670. );
  1671. group.items.push(item);
  1672. });
  1673. const groups = Array.from(groupMap.values()).sort(
  1674. (a, b) => a.sortValue - b.sortValue || a.firstIndex - b.firstIndex
  1675. );
  1676. const groupIndex = groups.findIndex((item) => item.key === currentKey);
  1677. if (groupIndex < 0) {
  1678. return null;
  1679. }
  1680. const prevGroup = groups[groupIndex - 1];
  1681. const nextGroup = groups[groupIndex + 1];
  1682. const prevBoundary = this.getTaskSequenceBoundary(
  1683. prevGroup,
  1684. 'executionEndTime',
  1685. true
  1686. );
  1687. const nextBoundary = this.getTaskSequenceBoundary(
  1688. nextGroup,
  1689. 'executionStartTime',
  1690. false
  1691. );
  1692. return {
  1693. index: groupIndex,
  1694. prev: prevBoundary?.item || null,
  1695. prevIndex: groupIndex - 1,
  1696. next: nextBoundary?.item || null,
  1697. nextIndex: groupIndex + 1
  1698. };
  1699. },
  1700. getTaskSequenceContext(row) {
  1701. const index = this.getTaskIndex(row);
  1702. if (index < 0) {
  1703. return null;
  1704. }
  1705. if (!this.isOrderScheduling) {
  1706. return {
  1707. index,
  1708. prev: this.taskList[index - 1],
  1709. prevIndex: index - 1,
  1710. next: this.taskList[index + 1],
  1711. nextIndex: index + 1
  1712. };
  1713. }
  1714. return (
  1715. this.getOrderTaskSequenceContext(row) || {
  1716. index,
  1717. prev: this.taskList[index - 1],
  1718. prevIndex: index - 1,
  1719. next: this.taskList[index + 1],
  1720. nextIndex: index + 1
  1721. }
  1722. );
  1723. },
  1724. compareTaskTime(start, end) {
  1725. if (!start || !end) return false;
  1726. const startTime = new Date(start).getTime();
  1727. const endTime = new Date(end).getTime();
  1728. return (
  1729. !Number.isNaN(startTime) &&
  1730. !Number.isNaN(endTime) &&
  1731. startTime < endTime
  1732. );
  1733. },
  1734. getCurrentPlanStartTime() {
  1735. return (
  1736. this.currentPlan?.startTime ||
  1737. this.currentPlan?.planStartTime ||
  1738. this.currentPlan?.executionStartTime ||
  1739. ''
  1740. );
  1741. },
  1742. isExecutionStartBeforePlanStart(executionStartTime) {
  1743. if (this.isOrderScheduling || !executionStartTime) {
  1744. return false;
  1745. }
  1746. const planStartTime = this.getCurrentPlanStartTime();
  1747. if (!planStartTime) {
  1748. return false;
  1749. }
  1750. const executionStart = new Date(executionStartTime).getTime();
  1751. const planStart = new Date(planStartTime).getTime();
  1752. return (
  1753. !Number.isNaN(executionStart) &&
  1754. !Number.isNaN(planStart) &&
  1755. executionStart < planStart
  1756. );
  1757. },
  1758. isTaskParallel(row) {
  1759. return toSafeNumber(row?.isParallelism) === 1;
  1760. },
  1761. validateTaskSequence(row, options = {}) {
  1762. const sequence = this.getTaskSequenceContext(row);
  1763. if (!sequence) {
  1764. return true;
  1765. }
  1766. const { index, prev, prevIndex, next, nextIndex } = sequence;
  1767. const resetKey = options.resetKey;
  1768. const checkPrev =
  1769. (!resetKey || resetKey === 'executionStartTime') &&
  1770. !this.isTaskParallel(row);
  1771. const checkNext =
  1772. (!resetKey || resetKey === 'executionEndTime') &&
  1773. !this.isTaskParallel(next);
  1774. if (
  1775. checkPrev &&
  1776. prev?.executionEndTime &&
  1777. row.executionStartTime &&
  1778. this.compareTaskTime(row.executionStartTime, prev.executionEndTime)
  1779. ) {
  1780. this.$message.warning(
  1781. `${this.getTaskLabel(
  1782. row,
  1783. index
  1784. )}执行开始时间不能小于上道工序${this.getTaskLabel(
  1785. prev,
  1786. prevIndex
  1787. )}的执行结束时间`
  1788. );
  1789. if (resetKey) {
  1790. this.$set(row, resetKey, '');
  1791. }
  1792. return false;
  1793. }
  1794. if (
  1795. checkNext &&
  1796. next?.executionStartTime &&
  1797. row.executionEndTime &&
  1798. this.compareTaskTime(next.executionStartTime, row.executionEndTime)
  1799. ) {
  1800. this.$message.warning(
  1801. `下道工序${this.getTaskLabel(
  1802. next,
  1803. nextIndex
  1804. )}的执行开始时间不能小于上道工序${this.getTaskLabel(
  1805. row,
  1806. index
  1807. )}的执行结束时间`
  1808. );
  1809. if (resetKey) {
  1810. this.$set(row, resetKey, '');
  1811. }
  1812. return false;
  1813. }
  1814. return true;
  1815. },
  1816. validateTaskRow(item, options = {}) {
  1817. const label = (t) => this.getTaskLabel(t);
  1818. const checkSequence = options.checkSequence !== false;
  1819. const execType = toSafeNumber(item.executionType);
  1820. if (execType === undefined) {
  1821. this.$message.warning(`${label(item)}请选择执行模式`);
  1822. return false;
  1823. }
  1824. if (execType === EXEC_TYPE.HOMEMADE) {
  1825. if (
  1826. !item.executionTeamId ||
  1827. !item.executionStartTime ||
  1828. !item.executionEndTime
  1829. ) {
  1830. this.$message.warning(
  1831. `${label(
  1832. item
  1833. )}执行模式为自制时,需选择执行班组并填写执行开始和结束时间`
  1834. );
  1835. return false;
  1836. }
  1837. }
  1838. if (execType === EXEC_TYPE.ENTRUST) {
  1839. if (
  1840. !item.executionFactoryId ||
  1841. !item.executionStartTime ||
  1842. !item.executionEndTime
  1843. ) {
  1844. this.$message.warning(
  1845. `${label(
  1846. item
  1847. )}执行模式为请托时,需选择工厂并填写执行开始和结束时间`
  1848. );
  1849. return false;
  1850. }
  1851. }
  1852. if (execType === EXEC_TYPE.OUTSOURCE) {
  1853. if (!item.executionStartTime || !item.executionEndTime) {
  1854. this.$message.warning(
  1855. `${label(item)}执行模式为委外时,需填写执行开始和结束时间`
  1856. );
  1857. return false;
  1858. }
  1859. }
  1860. if (isEndBeforeStart(item.executionStartTime, item.executionEndTime)) {
  1861. this.$message.warning(
  1862. `${label(item)}执行结束时间不能小于执行开始时间`
  1863. );
  1864. return false;
  1865. }
  1866. if (this.isExecutionStartBeforePlanStart(item.executionStartTime)) {
  1867. this.$message.warning(
  1868. `${label(item)}执行开始时间不能小于计划开始时间`
  1869. );
  1870. return false;
  1871. }
  1872. const deadline =
  1873. this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
  1874. if (deadline) {
  1875. const d = new Date(deadline).getTime();
  1876. const checkStart = (val, name) => {
  1877. if (!val) return true;
  1878. const t = new Date(val).getTime();
  1879. return Number.isNaN(d) || Number.isNaN(t) || t <= d
  1880. ? true
  1881. : (this.$message.warning(
  1882. `${label(item)}${name}不能大于计划交付时间`
  1883. ),
  1884. false);
  1885. };
  1886. if (!checkStart(item.executionStartTime, '执行开始时间'))
  1887. return false;
  1888. }
  1889. return checkSequence ? this.validateTaskSequence(item) : true;
  1890. },
  1891. validateTaskList() {
  1892. for (const item of this.taskList) {
  1893. if (!this.validateTaskRow(item)) return false;
  1894. }
  1895. return true;
  1896. },
  1897. addTaskRow(row) {
  1898. if (row) {
  1899. this.taskList.push(row);
  1900. }
  1901. },
  1902. removeTaskRow(row) {
  1903. const index = this.taskList.findIndex((item) => item === row);
  1904. if (index >= 0) {
  1905. this.taskList.splice(index, 1);
  1906. }
  1907. },
  1908. addDispatchStations({ task, list }) {
  1909. if (!this.isOrderScheduling || !task) {
  1910. return;
  1911. }
  1912. const key = this.getTaskWorkstationKey(task);
  1913. if (!key) {
  1914. this.$message.warning('当前工序缺少任务信息,无法添加工位');
  1915. return;
  1916. }
  1917. const currentList = this.workstationOptionsMap[key] || [];
  1918. const nextList = [...currentList];
  1919. const existedIds = new Set(currentList.map((item) => String(item.id)));
  1920. this.formatWorkstationOptions(list).forEach((item) => {
  1921. if (!existedIds.has(String(item.id))) {
  1922. existedIds.add(String(item.id));
  1923. nextList.push(item);
  1924. }
  1925. });
  1926. this.$set(this.workstationOptionsMap, key, nextList);
  1927. },
  1928. removeDispatchStation({ task, row }) {
  1929. if (!this.isOrderScheduling || !task || !row) {
  1930. return;
  1931. }
  1932. const key = this.getTaskWorkstationKey(task);
  1933. if (!key) {
  1934. return;
  1935. }
  1936. const rowIds = new Set(
  1937. [row.id, row.rawId, row.workstationId, row.factoryWorkstationId]
  1938. .filter(
  1939. (item) => item !== null && item !== undefined && item !== ''
  1940. )
  1941. .map((item) => String(item))
  1942. );
  1943. const currentList = this.workstationOptionsMap[key] || [];
  1944. const nextList = currentList.filter((item) => {
  1945. const itemIds = [
  1946. item.id,
  1947. item.rawId,
  1948. item.workstationId,
  1949. item.factoryWorkstationId
  1950. ]
  1951. .filter(
  1952. (value) => value !== null && value !== undefined && value !== ''
  1953. )
  1954. .map((value) => String(value));
  1955. return !itemIds.some((id) => rowIds.has(id));
  1956. });
  1957. this.$set(this.workstationOptionsMap, key, nextList);
  1958. },
  1959. async handleDispatchActionSuccess(event = {}) {
  1960. if (!this.isOrderScheduling) {
  1961. return;
  1962. }
  1963. if (event.type === 'confirm') {
  1964. await this.reloadOrderSchedulingOrdersByDateRange(this.dateRange);
  1965. return;
  1966. }
  1967. await this.refreshSchedulingView();
  1968. },
  1969. buildSavePayload(isCache = 0) {
  1970. const plan = this.currentPlan;
  1971. const head = this.planRoutingPayload || {};
  1972. const detailList = this.taskList.map((row) => {
  1973. const execType = toSafeNumber(row.executionType) ?? 0;
  1974. // 只有班组排程需要把带前缀的下拉值还原为人员/工位原始 id;
  1975. // 工厂排程保持原 executionTeamId 保存方式。
  1976. const homemadeOption = this.isOrderScheduling
  1977. ? this.findHomemadeOption(row)
  1978. : null;
  1979. const detail = {
  1980. executionEndTime: row.executionEndTime || undefined,
  1981. executionStartTime: row.executionStartTime || undefined,
  1982. executionTeamId:
  1983. execType === EXEC_TYPE.HOMEMADE
  1984. ? (this.isOrderScheduling
  1985. ? homemadeOption?.rawId ||
  1986. row.rawExecutionTeamId ||
  1987. row.executionTeamId
  1988. : row.executionTeamId) || undefined
  1989. : undefined,
  1990. executionTeamLeader: row.executionTeamLeader || '',
  1991. executionTeamLeaderId: row.executionTeamLeaderId || '',
  1992. executionTeamName: row.executionTeamName || '',
  1993. executionType: execType,
  1994. isParallelism: this.isTaskParallel(row) ? 1 : 0,
  1995. planCode: row.planCode || plan?.code,
  1996. planId: row.planId ?? plan?.id,
  1997. routingId:
  1998. row.routingId ?? plan?.produceRoutingId ?? head.routingId,
  1999. routingName:
  2000. row.routingName || plan?.produceRoutingName || head.routingName,
  2001. sourceTaskId: row.sourceTaskId ?? row.taskId,
  2002. taskCode: row.taskCode,
  2003. taskId: row.taskId,
  2004. taskName: row.taskName,
  2005. taskSort: row.taskSort,
  2006. type: toSafeNumber(row.type)
  2007. };
  2008. // 数量输入框是班组排程工序配置新增字段,工厂排程不写入,
  2009. // 避免改变原工厂排程保存 payload。
  2010. if (this.isOrderScheduling) {
  2011. detail.quantity =
  2012. row.quantity !== '' &&
  2013. row.quantity !== null &&
  2014. row.quantity !== undefined
  2015. ? row.quantity
  2016. : undefined;
  2017. }
  2018. // 班组排程自制模式下,执行对象可能是人员也可能是工位;
  2019. // executionObject* 字段用于明确记录对象类型和值,委外/外协不写入。
  2020. if (this.isOrderScheduling && execType === EXEC_TYPE.HOMEMADE) {
  2021. detail.executionObjectId =
  2022. homemadeOption?.rawId ||
  2023. row.rawExecutionTeamId ||
  2024. row.executionTeamId ||
  2025. undefined;
  2026. detail.executionObjectType = row.executionObjectType || undefined;
  2027. detail.executionObjectName =
  2028. row.executionObjectName || row.executionTeamName || '';
  2029. }
  2030. if (execType === EXEC_TYPE.ENTRUST) {
  2031. detail.executionFactoryId = row.executionFactoryId || undefined;
  2032. detail.executionFactoryName = row.executionFactoryName || '';
  2033. }
  2034. if (row.id != null) detail.id = row.id;
  2035. return detail;
  2036. });
  2037. return {
  2038. detailList,
  2039. fileParam: head.fileParam ?? [],
  2040. id: head.id,
  2041. isCache,
  2042. planCode: plan?.code ?? head.planCode,
  2043. planId: plan?.id ?? head.planId,
  2044. produceVersionId: head.produceVersionId,
  2045. produceVersionName: head.produceVersionName,
  2046. releaseTime: head.releaseTime,
  2047. routingCode: head.routingCode,
  2048. routingId: head.routingId ?? plan?.produceRoutingId,
  2049. routingName: head.routingName ?? plan?.produceRoutingName,
  2050. routingVersion: head.routingVersion
  2051. };
  2052. },
  2053. // Scheduling data refresh
  2054. async handleTabChange(tabKey) {
  2055. this.activeTab = tabKey;
  2056. await this.refreshSchedulingView();
  2057. if (!this.isOrderScheduling) {
  2058. await this.reloadPlanSchedulingPlansByStatus();
  2059. }
  2060. },
  2061. async handleDateChange(val) {
  2062. this.dateRange = val;
  2063. if (this.isOrderScheduling) {
  2064. this.scheduleOrderSchedulingDateReload(val);
  2065. return;
  2066. }
  2067. await this.refreshSchedulingView();
  2068. await this.reloadPlanSchedulingPlansByStatus();
  2069. },
  2070. handleDateInput(val) {
  2071. if (!this.isOrderScheduling) {
  2072. return;
  2073. }
  2074. this.dateRange = val;
  2075. this.scheduleOrderSchedulingDateReload(val);
  2076. },
  2077. scheduleOrderSchedulingDateReload(dateRange) {
  2078. if (!Array.isArray(dateRange) || dateRange.length !== 2) {
  2079. return;
  2080. }
  2081. if (!dateRange[0] || !dateRange[1]) {
  2082. return;
  2083. }
  2084. if (this.orderDateReloadTimer) {
  2085. clearTimeout(this.orderDateReloadTimer);
  2086. }
  2087. this.orderDateReloadTimer = setTimeout(() => {
  2088. this.orderDateReloadTimer = null;
  2089. this.reloadOrderSchedulingOrdersByDateRange(dateRange);
  2090. }, 0);
  2091. },
  2092. async reloadOrderSchedulingOrdersByDateRange(dateRange) {
  2093. this.orderDetailReadonly = false;
  2094. this.orderDetailActionMode = '';
  2095. if (typeof this.orderListLoader !== 'function') {
  2096. await this.refreshSchedulingView();
  2097. return;
  2098. }
  2099. const loading = this.$loading({
  2100. lock: true,
  2101. text: '加载订单中...'
  2102. });
  2103. try {
  2104. const list = await this.orderListLoader(
  2105. dateRange,
  2106. this.getPlanTableStatusRequestWhere()
  2107. );
  2108. this.selectedWorkOrders = this.buildWorkOrderTableRows(list);
  2109. this.planDataList = this.selectedWorkOrders;
  2110. const currentWorkOrderId = this.currentPlan?.workOrderId;
  2111. const matchedCurrentOrder = currentWorkOrderId
  2112. ? this.selectedWorkOrders.find(
  2113. (item) => String(item.id) === String(currentWorkOrderId)
  2114. )
  2115. : null;
  2116. if (matchedCurrentOrder) {
  2117. const currentPlan = this.resolveCurrentPlan(matchedCurrentOrder);
  2118. this.orderDetailReadonly =
  2119. this.isOrderSchedulingReadonlyRow(matchedCurrentOrder);
  2120. this.orderDetailActionMode = this.orderDetailReadonly
  2121. ? 'view'
  2122. : 'edit';
  2123. currentPlan._orderDetailReadonly = this.orderDetailReadonly;
  2124. currentPlan.orderDetailReadonly = this.orderDetailReadonly;
  2125. this.currentPlan = currentPlan;
  2126. this.selection = [matchedCurrentOrder];
  2127. this.syncTableSelection('planTable', [matchedCurrentOrder]);
  2128. await this.loadPlanDotLineDetail(currentPlan);
  2129. } else {
  2130. this.orderDetailReadonly = false;
  2131. this.orderDetailActionMode = '';
  2132. this.currentPlan = null;
  2133. this.planRoutingPayload = null;
  2134. this.taskList = [];
  2135. this.selection = [];
  2136. this.syncTableSelection('planTable', []);
  2137. }
  2138. await this.refreshSchedulingView();
  2139. } catch (e) {
  2140. this.$message.error(e.message || '获取订单数据失败');
  2141. } finally {
  2142. loading.close();
  2143. }
  2144. },
  2145. extractPlanIdsFromGanttData(data = []) {
  2146. const planIds = new Set();
  2147. (Array.isArray(data) ? data : []).forEach((item) => {
  2148. (item.resourcePeriods || []).forEach((period) => {
  2149. if (period.planId) planIds.add(period.planId);
  2150. });
  2151. (item.resourceList || []).forEach((resource) => {
  2152. if (resource.planId) planIds.add(resource.planId);
  2153. });
  2154. });
  2155. return Array.from(planIds);
  2156. },
  2157. async syncPlanTableRowsByGanttData(data = []) {
  2158. if (this.isOrderScheduling) {
  2159. return;
  2160. }
  2161. const planIds = this.extractPlanIdsFromGanttData(data);
  2162. if (!planIds.length) {
  2163. return;
  2164. }
  2165. const plans = await this.loadPlansByIds(planIds);
  2166. this.planDataList = this.buildPlanTableRows(plans);
  2167. },
  2168. async refreshSchedulingView() {
  2169. let data;
  2170. const params = {};
  2171. if (this.dateRange && this.dateRange.length === 2) {
  2172. params.startDate = this.dateRange[0];
  2173. params.endDate = this.dateRange[1];
  2174. }
  2175. // 班组排程从工单页面进入,用户选择的是工单数据;
  2176. // 甘特接口仍按计划维度查询,所以这里提取工单关联的 productionPlanId
  2177. // 作为 planIds 过滤条件。工厂排程本身就是计划入口,不需要该过滤。
  2178. if (this.isOrderScheduling) {
  2179. const planIds = Array.from(
  2180. new Set(
  2181. (this.selectedWorkOrders || [])
  2182. .map((item) => item.productionPlanId)
  2183. .filter((id) => id !== null && id !== undefined && id !== '')
  2184. )
  2185. );
  2186. if (planIds.length) {
  2187. params.planIds = planIds;
  2188. } else {
  2189. this.calendarSourceData = [];
  2190. this.tableData = [];
  2191. return;
  2192. }
  2193. }
  2194. if (this.activeTab === 'factory') {
  2195. // 工厂维度甘特:
  2196. // - 工厂排程默认进入该页签,继续使用 factorySchedulingGanttChart。
  2197. data = await factorySchedulingGanttChart(params);
  2198. await this.syncPlanTableRowsByGanttData(data);
  2199. } else if (this.isOrderScheduling) {
  2200. // 班组排程甘特按工单关联计划过滤,再使用工位/设备/人员专用接口。
  2201. // 展示结构仍交给现有甘特组件处理,保持和工厂排产一致的树形时间轴形式。
  2202. data = await workstationSchedulingGanttChart(params);
  2203. data = this.filterOrderSchedulingGanttDataByTab(data || []);
  2204. } else {
  2205. // 班组/工位维度甘特:
  2206. // 工厂排程切到该页签时保持原班组甘特逻辑;
  2207. data = await teamSchedulingGanttChart(params);
  2208. await this.syncPlanTableRowsByGanttData(data);
  2209. }
  2210. this.calendarSourceData = this.normalizeOrderSchedulingGanttData(
  2211. data || []
  2212. );
  2213. this.tableData = this.initGanttFlatRows(this.calendarSourceData);
  2214. },
  2215. getOrderSchedulingGanttWorkstationType(tabKey = this.activeTab) {
  2216. const workstationTypeMap = {
  2217. line: 1,
  2218. person: 3
  2219. };
  2220. return workstationTypeMap[tabKey];
  2221. },
  2222. hasOrderSchedulingDeviceResource(item = {}) {
  2223. return !!(item.deviceId || item.deviceCode || item.deviceName);
  2224. },
  2225. getOrderSchedulingAssigneeUsers(item = {}) {
  2226. if (Array.isArray(item.assigneeUser)) {
  2227. return item.assigneeUser;
  2228. }
  2229. if (item.assigneeUser && typeof item.assigneeUser === 'object') {
  2230. return [item.assigneeUser];
  2231. }
  2232. return [];
  2233. },
  2234. buildOrderSchedulingDeviceResource(item = {}) {
  2235. const deviceName =
  2236. item.deviceName || item.equipmentName || item.assetName || '';
  2237. const deviceCode = item.deviceCode || item.assetCode || '';
  2238. const deviceId =
  2239. this.normalizeBackendLongId(item.deviceId) ||
  2240. this.normalizeBackendLongId(item.assetId) ||
  2241. this.normalizeBackendLongId(item.extInfo?.assetId);
  2242. const resourceId =
  2243. deviceId || deviceCode || deviceName || item.resourceId;
  2244. const workstationName = item.name || item.resourceName || '';
  2245. const workstationCode = item.workstationCode || item.code || '';
  2246. return {
  2247. ...item,
  2248. resourceId: `device:${resourceId || ''}`,
  2249. resourceName: deviceName || deviceCode || workstationName,
  2250. resourceCode: deviceCode,
  2251. deviceId,
  2252. deviceCode,
  2253. deviceName,
  2254. equipmentName: deviceName || deviceCode,
  2255. workstationName,
  2256. workstationCode,
  2257. originResourceName: item.resourceName || item.name || '',
  2258. originResourceCode: item.resourceCode || item.code || '',
  2259. orderResourceTabType: 'device'
  2260. };
  2261. },
  2262. buildOrderSchedulingPersonResources(item = {}) {
  2263. const workstationName = item.name || item.resourceName || '';
  2264. const workstationCode = item.workstationCode || item.code || '';
  2265. return this.getOrderSchedulingAssigneeUsers(item)
  2266. .map((user, index) => {
  2267. const userName =
  2268. user?.userName || user?.name || user?.nickName || '';
  2269. const userId = user?.userId || user?.id || userName;
  2270. const jobNumber = user?.jobNumber || user?.code || '';
  2271. if (!userName && !userId && !jobNumber) {
  2272. return null;
  2273. }
  2274. return {
  2275. ...item,
  2276. resourceId: `person:${userId || index}-${
  2277. item.resourceId || item.taskId || item.sourceTaskId || index
  2278. }`,
  2279. resourceName: userName || jobNumber || String(userId),
  2280. resourceCode: jobNumber || String(userId || ''),
  2281. assigneeUser: [user],
  2282. assigneeUserId: userId,
  2283. assigneeUserName: userName,
  2284. assigneeJobNumber: jobNumber,
  2285. executionObjectType: 'person',
  2286. executionObjectName: userName || jobNumber || String(userId),
  2287. workstationName,
  2288. workstationCode,
  2289. originResourceName: item.resourceName || item.name || '',
  2290. originResourceCode: item.resourceCode || item.code || '',
  2291. orderResourceTabType: 'person'
  2292. };
  2293. })
  2294. .filter(Boolean);
  2295. },
  2296. filterOrderSchedulingGanttDataByTab(data = []) {
  2297. const list = Array.isArray(data) ? data : [];
  2298. if (this.activeTab === 'line') {
  2299. return list
  2300. .filter((item) => this.hasOrderSchedulingDeviceResource(item))
  2301. .map((item) => this.buildOrderSchedulingDeviceResource(item));
  2302. }
  2303. if (this.activeTab === 'person') {
  2304. return list.flatMap((item) =>
  2305. this.buildOrderSchedulingPersonResources(item)
  2306. );
  2307. }
  2308. const targetWorkstationType =
  2309. this.getOrderSchedulingGanttWorkstationType();
  2310. if (!targetWorkstationType) {
  2311. return list;
  2312. }
  2313. return list.filter(
  2314. (item) => Number(item?.workstationType) === targetWorkstationType
  2315. );
  2316. },
  2317. getOrderSchedulingResourceInfo(item = {}) {
  2318. const resourceName = item.resourceName || item.name || '';
  2319. const resourceCode = item.resourceCode || item.code || '';
  2320. const resourceType = Number(item.type);
  2321. const workstationType = Number(item.workstationType);
  2322. const deviceName =
  2323. item.deviceName || item.equipmentName || item.assetName || '';
  2324. const deviceCode = item.deviceCode || item.assetCode || '';
  2325. const personName =
  2326. item.executionObjectName || item.assigneeUserName || '';
  2327. return {
  2328. resourceName,
  2329. resourceCode,
  2330. resourceType,
  2331. workstationType,
  2332. orderResourceTabType: item.orderResourceTabType || '',
  2333. deviceId:
  2334. this.normalizeBackendLongId(item.deviceId) ||
  2335. this.normalizeBackendLongId(item.assetId) ||
  2336. this.normalizeBackendLongId(item.extInfo?.assetId) ||
  2337. '',
  2338. deviceCode,
  2339. deviceName,
  2340. assigneeUser: item.assigneeUser || [],
  2341. assigneeUserId: item.assigneeUserId || '',
  2342. assigneeUserName: item.assigneeUserName || '',
  2343. assigneeJobNumber: item.assigneeJobNumber || '',
  2344. workstationName:
  2345. item.workstationName ||
  2346. item.stationName ||
  2347. (resourceType === 1 ? resourceName : ''),
  2348. equipmentName:
  2349. item.equipmentName ||
  2350. deviceName ||
  2351. item.assetName ||
  2352. (item.orderResourceTabType === 'device' ? resourceName : ''),
  2353. executionObjectType:
  2354. item.executionObjectType || (workstationType === 3 ? 'person' : ''),
  2355. executionObjectName:
  2356. item.executionObjectName ||
  2357. personName ||
  2358. (workstationType === 3 ? resourceName : '')
  2359. };
  2360. },
  2361. normalizeOrderSchedulingPlanRef(source = {}) {
  2362. const productionOrderNumber =
  2363. source.productionOrderNumber ||
  2364. source.apsWorkOrderCode ||
  2365. source.workOrderCode ||
  2366. source.orderCode ||
  2367. '';
  2368. return {
  2369. planId: source.planId ?? source.apsWorkOrderId ?? source.workOrderId,
  2370. planCode:
  2371. source.planCode ||
  2372. source.apsWorkOrderCode ||
  2373. source.workOrderCode ||
  2374. '',
  2375. productionOrderNumber
  2376. };
  2377. },
  2378. getOrderSchedulingDisplayOrderCode(...sources) {
  2379. for (const source of sources) {
  2380. if (!source) continue;
  2381. const code =
  2382. source.productionOrderNumber ||
  2383. source.apsWorkOrderCode ||
  2384. source.workOrderCode ||
  2385. source.orderCode ||
  2386. source.planCode ||
  2387. '';
  2388. if (code) return code;
  2389. }
  2390. return '';
  2391. },
  2392. buildGanttPlanLabel(planInfo = {}, fallbackInfo = {}) {
  2393. const displayCode = this.isOrderScheduling
  2394. ? this.getOrderSchedulingDisplayOrderCode(planInfo, fallbackInfo)
  2395. : planInfo.planCode || '';
  2396. return [displayCode, planInfo.productName || fallbackInfo.productName]
  2397. .filter(Boolean)
  2398. .join(' ');
  2399. },
  2400. normalizeOrderSchedulingGanttData(data = []) {
  2401. if (!this.isOrderScheduling) {
  2402. return data;
  2403. }
  2404. return (Array.isArray(data) ? data : []).map((item) => {
  2405. const resourceInfo = this.getOrderSchedulingResourceInfo(item);
  2406. const resourceList = Array.isArray(item.resourceList)
  2407. ? item.resourceList.map((resource) => {
  2408. const planRef = this.normalizeOrderSchedulingPlanRef(resource);
  2409. return {
  2410. ...resource,
  2411. ...planRef,
  2412. productName: resource.productName || '',
  2413. workstationName:
  2414. resource.workstationName ||
  2415. resource.stationName ||
  2416. (resourceInfo.resourceType === 1
  2417. ? resourceInfo.resourceName
  2418. : ''),
  2419. equipmentName:
  2420. resource.equipmentName ||
  2421. resource.deviceName ||
  2422. resource.assetName ||
  2423. (resourceInfo.orderResourceTabType === 'device'
  2424. ? resourceInfo.resourceName
  2425. : ''),
  2426. deviceId:
  2427. this.normalizeBackendLongId(resource.deviceId) ||
  2428. this.normalizeBackendLongId(resource.assetId) ||
  2429. this.normalizeBackendLongId(resource.extInfo?.assetId) ||
  2430. resourceInfo.deviceId ||
  2431. '',
  2432. deviceCode:
  2433. resource.deviceCode || resourceInfo.deviceCode || '',
  2434. deviceName:
  2435. resource.deviceName || resourceInfo.deviceName || '',
  2436. assigneeUser:
  2437. resource.assigneeUser || resourceInfo.assigneeUser || [],
  2438. assigneeUserId:
  2439. resource.assigneeUserId ||
  2440. resourceInfo.assigneeUserId ||
  2441. '',
  2442. assigneeUserName:
  2443. resource.assigneeUserName ||
  2444. resourceInfo.assigneeUserName ||
  2445. '',
  2446. assigneeJobNumber:
  2447. resource.assigneeJobNumber ||
  2448. resourceInfo.assigneeJobNumber ||
  2449. '',
  2450. executionObjectName:
  2451. resource.executionObjectName ||
  2452. (resourceInfo.orderResourceTabType === 'person'
  2453. ? resourceInfo.resourceName
  2454. : '')
  2455. };
  2456. })
  2457. : [];
  2458. const resourcePeriods = Array.isArray(item.resourcePeriods)
  2459. ? item.resourcePeriods.map((period) => ({
  2460. ...period,
  2461. ...this.normalizeOrderSchedulingPlanRef(period)
  2462. }))
  2463. : [];
  2464. return {
  2465. ...item,
  2466. resourceCode: resourceInfo.resourceCode,
  2467. resourceName: resourceInfo.resourceName,
  2468. resourceList,
  2469. resourcePeriods,
  2470. orderResourceTabType: resourceInfo.orderResourceTabType,
  2471. deviceId: resourceInfo.deviceId,
  2472. deviceCode: resourceInfo.deviceCode,
  2473. deviceName: resourceInfo.deviceName,
  2474. assigneeUser: resourceInfo.assigneeUser,
  2475. assigneeUserId: resourceInfo.assigneeUserId,
  2476. assigneeUserName: resourceInfo.assigneeUserName,
  2477. assigneeJobNumber: resourceInfo.assigneeJobNumber,
  2478. workstationName: resourceInfo.workstationName,
  2479. equipmentName: resourceInfo.equipmentName,
  2480. executionObjectType: resourceInfo.executionObjectType,
  2481. executionObjectName: resourceInfo.executionObjectName
  2482. };
  2483. });
  2484. },
  2485. buildGanttPeriodTaskId(parentId, matchedPlanInfo, period, periodIndex) {
  2486. const periodTaskId = [
  2487. matchedPlanInfo.planId || period.planId || matchedPlanInfo.planCode,
  2488. period.apsWorkOrderId || period.workOrderId || period.planCode,
  2489. period.startTime,
  2490. period.endTime,
  2491. periodIndex
  2492. ]
  2493. .filter(
  2494. (value) => value !== '' && value !== null && value !== undefined
  2495. )
  2496. .join('-');
  2497. return `${parentId}-${periodTaskId || periodIndex}`;
  2498. },
  2499. async open(row) {
  2500. const ids = Array.isArray(row?.ids) ? row.ids : [];
  2501. const readonly = !!row?.readonly;
  2502. const mode = row?.mode === 'order' ? 'order' : 'plan';
  2503. const selectedWorkOrders = Array.isArray(row?.selectedWorkOrders)
  2504. ? row.selectedWorkOrders
  2505. : [];
  2506. const orderListLoader =
  2507. typeof row?.orderListLoader === 'function'
  2508. ? row.orderListLoader
  2509. : null;
  2510. const loading = ids.length
  2511. ? this.$loading({
  2512. lock: true,
  2513. text: mode === 'order' ? '加载订单中...' : '加载计划中...'
  2514. })
  2515. : null;
  2516. let selectedPlans = Array.isArray(row)
  2517. ? row
  2518. : Array.isArray(row?.selectedPlans)
  2519. ? row.selectedPlans
  2520. : [];
  2521. let allPlans = Array.isArray(row?.allPlans)
  2522. ? row.allPlans
  2523. : selectedPlans;
  2524. try {
  2525. if (ids.length) {
  2526. const planList = await this.loadPlansByIds(ids);
  2527. selectedPlans = planList;
  2528. allPlans = planList;
  2529. }
  2530. } finally {
  2531. loading?.close();
  2532. }
  2533. this.dialogRenderKey += 1;
  2534. this.schedulingMode = mode;
  2535. this.planTableStatusTab = this.getDefaultPlanTableStatusTab(mode);
  2536. this.orderListLoader = orderListLoader;
  2537. // 弹窗复用同一个组件,但入口数据不同:
  2538. // - plan:生产计划行,使用工厂排程列 createSchedulingColumns。
  2539. // - order:工单行,使用班组排程列 createWorkOrderSchedulingColumns。
  2540. this.columns =
  2541. mode === 'order'
  2542. ? createWorkOrderSchedulingColumns()
  2543. : createSchedulingColumns();
  2544. this.selectedWorkOrders =
  2545. this.buildWorkOrderTableRows(selectedWorkOrders);
  2546. if (mode === 'order' && this.selectedWorkOrders.length) {
  2547. // 班组排程表格展示工单,但工序布点和甘特仍依赖生产计划 id;
  2548. // 因此把工单里的 productionPlan* 字段映射成 currentPlan 可识别的形态。
  2549. selectedPlans = this.selectedWorkOrders
  2550. .filter((item) => item.productionPlanId)
  2551. .map((item) => ({
  2552. ...item,
  2553. id: item.productionPlanId,
  2554. code: item.productionPlanCode,
  2555. startTime: item.planStartTime,
  2556. endTime: item.planCompleteTime,
  2557. productNum: item.formingNum,
  2558. unit: item.measuringUnit
  2559. }));
  2560. allPlans = selectedPlans;
  2561. }
  2562. this.entryReadonly = readonly;
  2563. this.orderDetailReadonly = false;
  2564. this.orderDetailActionMode = '';
  2565. this.allPlanDataList = deepClone(allPlans || []);
  2566. this.planDataList =
  2567. mode === 'order'
  2568. ? this.selectedWorkOrders
  2569. : this.buildPlanTableRows(selectedPlans, this.allPlanDataList);
  2570. this.currentPlan = null;
  2571. this.planRoutingPayload = null;
  2572. this.taskList = [];
  2573. // 每次打开弹窗都清理按工序缓存,避免上一次班组排程的工位
  2574. // 泄露到新的工单/计划中。
  2575. this.workstationOptionsMap = {};
  2576. this.workstationOptionsLoadingMap = {};
  2577. this.selection = [];
  2578. this.planPickerVisible = false;
  2579. this.planPickerSelection = [];
  2580. this.dotLineLoading = false;
  2581. this.dotLineFullscreen = false;
  2582. this.calendarSourceData = [];
  2583. this.tableData = [];
  2584. this.activeTab = mode === 'order' ? 'team' : 'factory';
  2585. const now = new Date();
  2586. this.dateRange = [
  2587. `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
  2588. 2,
  2589. '0'
  2590. )}-01`,
  2591. `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
  2592. 2,
  2593. '0'
  2594. )}-${String(
  2595. new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
  2596. ).padStart(2, '0')}`
  2597. ];
  2598. if (mode === 'order') {
  2599. // 班组排程只预加载人员选项;工位必须等工序列表出来后,
  2600. // 按每道工序 taskId 调任务派单同款接口补齐。
  2601. await this.loadWorkOrderExecutionOptions();
  2602. } else {
  2603. // 工厂排程仍使用班组主数据作为自制执行对象,不走人员/工位混合逻辑。
  2604. await this.loadTeamOptions();
  2605. }
  2606. if (mode === 'order') {
  2607. await this.reloadPlanTableByStatus();
  2608. } else {
  2609. await this.refreshSchedulingView();
  2610. await this.reloadPlanSchedulingPlansByStatus();
  2611. }
  2612. this.visible = true;
  2613. },
  2614. // Legacy operations kept for compatibility
  2615. swap(index1, index2) {
  2616. let temp1 = deepClone(this.form.scheduledPlans[index1]);
  2617. let temp2 = deepClone(this.form.scheduledPlans[index2]);
  2618. this.$set(this.form.scheduledPlans, index1, temp2);
  2619. this.$set(this.form.scheduledPlans, index2, temp1);
  2620. this.indexSort();
  2621. },
  2622. indexSort() {
  2623. this.form.scheduledPlans.forEach((item, index) => {
  2624. this.$set(this.form.scheduledPlans[index], 'serialNumber', index + 1);
  2625. });
  2626. },
  2627. remove() {
  2628. let codeS = this.selection.map((item) => item.code);
  2629. this.form.scheduledPlans = this.form.scheduledPlans.filter(
  2630. (item) => !codeS.includes(item.code)
  2631. );
  2632. this.indexSort();
  2633. },
  2634. moveUp() {
  2635. let index = this.selection[0].serialNumber - 1;
  2636. if (index > 0) {
  2637. this.swap(index, index - 1);
  2638. }
  2639. },
  2640. moveDown() {
  2641. let index = this.selection[0].serialNumber - 1;
  2642. if (index < this.form.scheduledPlans.length - 1) {
  2643. this.swap(index, index + 1);
  2644. }
  2645. },
  2646. exportList() {
  2647. const plans = this.form.scheduledPlans || [];
  2648. if (!plans.length) {
  2649. this.$message.warning('暂无可导出数据');
  2650. return;
  2651. }
  2652. const headers = [
  2653. '序号',
  2654. '计划状态',
  2655. '计划编号',
  2656. '生产订单号',
  2657. '产线名称',
  2658. '计划开始时间',
  2659. '计划结束时间',
  2660. '交期',
  2661. '计划数量',
  2662. '单位',
  2663. '优先级'
  2664. ];
  2665. const escapeCsv = (value) => {
  2666. const text =
  2667. value === null || value === undefined ? '' : String(value);
  2668. return `"${text.replace(/"/g, '""')}"`;
  2669. };
  2670. const rows = plans.map((item, index) =>
  2671. [
  2672. item.serialNumber || index + 1,
  2673. item.mesStatusName,
  2674. item.code,
  2675. item.productionOrderNumber,
  2676. item.productionLineName,
  2677. item.startTime,
  2678. item.endTime,
  2679. item.deliveryTime,
  2680. item.productNum,
  2681. item.unit,
  2682. item.priority
  2683. ]
  2684. .map((cell) => escapeCsv(cell))
  2685. .join(',')
  2686. );
  2687. const csvContent = [headers.join(','), ...rows].join('\n');
  2688. const blob = new Blob([`\uFEFF${csvContent}`], {
  2689. type: 'text/csv;charset=utf-8;'
  2690. });
  2691. const url = window.URL.createObjectURL(blob);
  2692. const link = document.createElement('a');
  2693. link.href = url;
  2694. link.download = `factory_production_scheduling_${Date.now()}.csv`;
  2695. document.body.appendChild(link);
  2696. link.click();
  2697. document.body.removeChild(link);
  2698. window.URL.revokeObjectURL(url);
  2699. },
  2700. initGanttFlatRows(data) {
  2701. if (!Array.isArray(data) || data.length === 0) {
  2702. return [];
  2703. }
  2704. const rows = [];
  2705. data.forEach((item, index) => {
  2706. const planList = Array.isArray(item.resourceList)
  2707. ? item.resourceList
  2708. : [];
  2709. const planInfo = planList[0] || {};
  2710. const taskLabel = item.taskName || item.name || '工序';
  2711. const planLabel = this.buildGanttPlanLabel(planInfo);
  2712. const resourceLabel =
  2713. planInfo.teamName ||
  2714. planInfo.productionLineName ||
  2715. item.name ||
  2716. item.taskName ||
  2717. item.resourceId ||
  2718. `资源${index + 1}`;
  2719. const teamName = planInfo.teamName || resourceLabel;
  2720. const productionLineName =
  2721. planInfo.productionLineName || resourceLabel;
  2722. const resourceName = this.isOrderScheduling
  2723. ? item.resourceName || item.name || ''
  2724. : '';
  2725. const resourceCode = this.isOrderScheduling
  2726. ? item.resourceCode || item.code || ''
  2727. : '';
  2728. const periods = Array.isArray(item.resourcePeriods)
  2729. ? item.resourcePeriods
  2730. : [];
  2731. const parentId = `gantt-${item.resourceId || 'resource'}-${
  2732. item.taskId || item.sourceTaskId || index
  2733. }`;
  2734. rows.push({
  2735. id: parentId,
  2736. code:
  2737. item.taskId || item.sourceTaskId || planInfo.planCode || parentId,
  2738. type: 2,
  2739. name: taskLabel,
  2740. text: taskLabel,
  2741. executor: planLabel || taskLabel,
  2742. laneLabel: resourceLabel,
  2743. resourceName,
  2744. resourceCode,
  2745. teamName,
  2746. productionOrderNumber: planInfo.productionOrderNumber || '',
  2747. productionLineName,
  2748. workstationName:
  2749. item.workstationName ||
  2750. item.stationName ||
  2751. planInfo.workstationName ||
  2752. planInfo.stationName ||
  2753. resourceLabel,
  2754. equipmentName:
  2755. item.equipmentName ||
  2756. item.deviceName ||
  2757. item.assetName ||
  2758. planInfo.equipmentName ||
  2759. planInfo.deviceName ||
  2760. planInfo.assetName ||
  2761. '',
  2762. deviceId:
  2763. this.normalizeBackendLongId(item.deviceId) ||
  2764. this.normalizeBackendLongId(item.assetId) ||
  2765. this.normalizeBackendLongId(planInfo.deviceId) ||
  2766. this.normalizeBackendLongId(planInfo.assetId) ||
  2767. '',
  2768. deviceCode: item.deviceCode || planInfo.deviceCode || '',
  2769. deviceName: item.deviceName || planInfo.deviceName || '',
  2770. assetName: item.assetName || planInfo.assetName || '',
  2771. assigneeUser: item.assigneeUser || planInfo.assigneeUser || [],
  2772. assigneeUserId:
  2773. item.assigneeUserId || planInfo.assigneeUserId || '',
  2774. assigneeUserName:
  2775. item.assigneeUserName || planInfo.assigneeUserName || '',
  2776. assigneeJobNumber:
  2777. item.assigneeJobNumber || planInfo.assigneeJobNumber || '',
  2778. executionObjectType: item.executionObjectType || '',
  2779. executionObjectName: item.executionObjectName || '',
  2780. render: periods.length ? 'split' : '',
  2781. open: true
  2782. });
  2783. periods.forEach((period, periodIndex) => {
  2784. const matchedPlanInfo =
  2785. planList.find(
  2786. (planItem) =>
  2787. String(planItem.planId || '') === String(period.planId || '')
  2788. ) ||
  2789. planList[periodIndex] ||
  2790. planInfo;
  2791. const childPlanLabel = this.buildGanttPlanLabel(
  2792. matchedPlanInfo,
  2793. period
  2794. );
  2795. const childDisplayOrderCode = this.isOrderScheduling
  2796. ? this.getOrderSchedulingDisplayOrderCode(matchedPlanInfo, period)
  2797. : matchedPlanInfo.planCode;
  2798. rows.push({
  2799. id: this.buildGanttPeriodTaskId(
  2800. parentId,
  2801. matchedPlanInfo,
  2802. period,
  2803. periodIndex
  2804. ),
  2805. parent: parentId,
  2806. code:
  2807. matchedPlanInfo.planCode ||
  2808. item.taskId ||
  2809. item.sourceTaskId ||
  2810. `${parentId}-${periodIndex}`,
  2811. planId: matchedPlanInfo.planId || period.planId || '',
  2812. planCode: matchedPlanInfo.planCode || '',
  2813. productionOrderNumber:
  2814. matchedPlanInfo.productionOrderNumber ||
  2815. period.productionOrderNumber ||
  2816. period.apsWorkOrderCode ||
  2817. period.workOrderCode ||
  2818. '',
  2819. productName: matchedPlanInfo.productName || '',
  2820. productCode: matchedPlanInfo.productCode || '',
  2821. routingName:
  2822. matchedPlanInfo.routingName ||
  2823. matchedPlanInfo.produceRoutingName ||
  2824. '',
  2825. batchNo: matchedPlanInfo.batchNo || '',
  2826. specification: matchedPlanInfo.specification || '',
  2827. modeType: matchedPlanInfo.modeType || '',
  2828. workstationName:
  2829. matchedPlanInfo.workstationName ||
  2830. matchedPlanInfo.stationName ||
  2831. item.workstationName ||
  2832. item.stationName ||
  2833. '',
  2834. equipmentName:
  2835. matchedPlanInfo.equipmentName ||
  2836. matchedPlanInfo.deviceName ||
  2837. matchedPlanInfo.assetName ||
  2838. item.equipmentName ||
  2839. item.deviceName ||
  2840. item.assetName ||
  2841. '',
  2842. deviceId:
  2843. this.normalizeBackendLongId(matchedPlanInfo.deviceId) ||
  2844. this.normalizeBackendLongId(matchedPlanInfo.assetId) ||
  2845. this.normalizeBackendLongId(item.deviceId) ||
  2846. this.normalizeBackendLongId(item.assetId) ||
  2847. '',
  2848. deviceCode: matchedPlanInfo.deviceCode || item.deviceCode || '',
  2849. deviceName: matchedPlanInfo.deviceName || item.deviceName || '',
  2850. assetName: matchedPlanInfo.assetName || item.assetName || '',
  2851. assigneeUser:
  2852. matchedPlanInfo.assigneeUser || item.assigneeUser || [],
  2853. assigneeUserId:
  2854. matchedPlanInfo.assigneeUserId || item.assigneeUserId || '',
  2855. assigneeUserName:
  2856. matchedPlanInfo.assigneeUserName || item.assigneeUserName || '',
  2857. assigneeJobNumber:
  2858. matchedPlanInfo.assigneeJobNumber ||
  2859. item.assigneeJobNumber ||
  2860. '',
  2861. executionObjectType: item.executionObjectType || '',
  2862. executionObjectName: item.executionObjectName || '',
  2863. type: 5,
  2864. name:
  2865. [taskLabel, childPlanLabel].filter(Boolean).join(' / ') ||
  2866. taskLabel,
  2867. text: [
  2868. taskLabel,
  2869. childDisplayOrderCode,
  2870. matchedPlanInfo.productName,
  2871. matchedPlanInfo.batchNo
  2872. ]
  2873. .filter(Boolean)
  2874. .join(' / '),
  2875. executor: childPlanLabel || taskLabel,
  2876. laneLabel: resourceLabel,
  2877. resourceName,
  2878. resourceCode,
  2879. teamName,
  2880. productionLineName,
  2881. start_date: formatDotLineDateTime(period.startTime),
  2882. end_date: formatDotLineDateTime(period.endTime)
  2883. });
  2884. });
  2885. });
  2886. return rows;
  2887. },
  2888. // 添加班组行合并
  2889. // addDuplicateCount(arr) {
  2890. // const elementCount = {}; // 用于存储元素及其出现次数
  2891. // arr.forEach((element, index) => {
  2892. // if (elementCount[element.shiftGroupCode] === undefined) {
  2893. // // 第一次出现,记录索引
  2894. // elementCount[element.shiftGroupCode] = { index, count: 1 };
  2895. // } else {
  2896. // // 不是第一次出现,增加计数
  2897. // elementCount[element.shiftGroupCode].count++;
  2898. // }
  2899. // });
  2900. // arr.forEach((item, index) => {
  2901. // if (elementCount[item.shiftGroupCode]?.index == index) {
  2902. // item.groupRowSpan = elementCount[item.shiftGroupCode].count;
  2903. // }
  2904. // });
  2905. // return arr;
  2906. // },
  2907. async handleApproveCurrentPlan() {
  2908. const row = this.currentPlan;
  2909. if (!row?.id) {
  2910. this.$message.warning('请先勾选一条计划后再审批');
  2911. return;
  2912. }
  2913. if (!row.produceRoutingName && !row.routingName) {
  2914. this.$message.error('请先选择工艺路线!');
  2915. return;
  2916. }
  2917. if (String(row.dotLineStatus) !== '1') {
  2918. this.$message.error('请先完成布点!');
  2919. return;
  2920. }
  2921. if (!this.canApproveCurrentPlan) {
  2922. this.$message.warning('当前计划不满足审批条件');
  2923. return;
  2924. }
  2925. const titleText = this.isReview
  2926. ? '是否发起流程'
  2927. : '审批工单后不可撤回,确定审批吗?';
  2928. const title = this.isReview ? '流程确认' : '审批确认';
  2929. const text = this.isReview ? '流程审批中...' : '工单审批中...';
  2930. try {
  2931. await this.$confirm(titleText, title);
  2932. } catch (e) {
  2933. return;
  2934. }
  2935. const loading = this.$loading({
  2936. lock: true,
  2937. fullscreen: true,
  2938. text
  2939. });
  2940. this.approvalLoading = true;
  2941. try {
  2942. const api = this.isReview ? releaseByApproval : release;
  2943. const data = await api([row.id]);
  2944. if (data || data === 0) {
  2945. this.$message.success('提交审核成功!');
  2946. } else {
  2947. this.$message.error('提交审核失败,请重新提交!');
  2948. }
  2949. await this.refreshSchedulingView();
  2950. await this.reloadPlanSchedulingPlansByStatus();
  2951. } catch (e) {
  2952. this.$message.error(e.message || '提交审核失败,请重新提交!');
  2953. } finally {
  2954. this.approvalLoading = false;
  2955. loading.close();
  2956. }
  2957. },
  2958. async submitDotLineConfig() {
  2959. if (!this.currentPlan?.id) {
  2960. this.$message.warning('请先勾选一条计划后再确定');
  2961. return;
  2962. }
  2963. if (!this.isOrderScheduling && this.isCurrentPlanReadonly) {
  2964. this.$message.warning('当前计划状态下布局数据仅支持查看');
  2965. return;
  2966. }
  2967. if (!this.taskList.length) {
  2968. this.$message.warning('当前没有可提交的工艺配置');
  2969. return;
  2970. }
  2971. if (!this.validateTaskList()) return;
  2972. const payload = this.buildSavePayload(0);
  2973. const loading = this.$loading({ lock: true, text: '保存中...' });
  2974. try {
  2975. await savePlanDotLine(payload);
  2976. await this.refreshSchedulingView();
  2977. if (!this.isOrderScheduling) {
  2978. await this.reloadPlanSchedulingPlansByStatus({
  2979. preserveCurrent: true
  2980. });
  2981. }
  2982. if (this.currentPlan?.id) {
  2983. await this.loadPlanDotLineDetail(this.currentPlan);
  2984. }
  2985. this.dotLineFullscreen = false;
  2986. this.$message.success('保存成功');
  2987. } catch (e) {
  2988. this.$message.error(e.message || '保存失败');
  2989. } finally {
  2990. loading.close();
  2991. }
  2992. },
  2993. async submitDotLineRow(row) {
  2994. if (!this.currentPlan?.id) {
  2995. this.$message.warning('请先勾选一条计划后再确定');
  2996. return;
  2997. }
  2998. if (!this.isOrderScheduling && this.isCurrentPlanReadonly) {
  2999. this.$message.warning('当前计划状态下布局数据仅支持查看');
  3000. return;
  3001. }
  3002. if (!row) {
  3003. this.$message.warning('请选择需要保存的工艺配置');
  3004. return;
  3005. }
  3006. if (!this.validateTaskRow(row, { checkSequence: false })) return;
  3007. const payload = this.buildSavePayload(this.isOrderScheduling ? 0 : 1);
  3008. const loadingText = this.isOrderScheduling ? '保存中...' : '缓存中...';
  3009. const successText = this.isOrderScheduling ? '保存成功' : '缓存成功';
  3010. const errorText = this.isOrderScheduling ? '保存失败' : '缓存失败';
  3011. const loading = this.$loading({ lock: true, text: loadingText });
  3012. try {
  3013. await savePlanDotLine(payload);
  3014. await this.refreshSchedulingView();
  3015. if (!this.isOrderScheduling) {
  3016. await this.reloadPlanSchedulingPlansByStatus({
  3017. preserveCurrent: true
  3018. });
  3019. }
  3020. if (this.currentPlan?.id) {
  3021. await this.loadPlanDotLineDetail(this.currentPlan);
  3022. }
  3023. this.$message.success(successText);
  3024. } catch (e) {
  3025. this.$message.error(e.message || errorText);
  3026. } finally {
  3027. loading.close();
  3028. }
  3029. },
  3030. // Dialog lifecycle
  3031. cancel() {
  3032. if (this.orderDateReloadTimer) {
  3033. clearTimeout(this.orderDateReloadTimer);
  3034. this.orderDateReloadTimer = null;
  3035. }
  3036. this.form = {};
  3037. this.entryReadonly = false;
  3038. this.orderDetailReadonly = false;
  3039. this.orderDetailActionMode = '';
  3040. this.currentPlan = null;
  3041. this.planRoutingPayload = null;
  3042. this.taskList = [];
  3043. this.selection = [];
  3044. this.planPickerSelection = [];
  3045. this.planPickerSelectionAll = [];
  3046. this.planPickerTimeDimensionPlanType = '';
  3047. this.planPickerWhere = createPlanPickerWhere();
  3048. this.schedulingMode = 'plan';
  3049. this.planTableStatusTab = this.getDefaultPlanTableStatusTab('plan');
  3050. this.selectedWorkOrders = [];
  3051. this.orderListLoader = null;
  3052. this.columns = createSchedulingColumns();
  3053. this.dotLineLoading = false;
  3054. this.dotLineFullscreen = false;
  3055. this.visible = false;
  3056. }
  3057. }
  3058. };
  3059. </script>
  3060. <style lang="scss" scoped>
  3061. .pane-box {
  3062. padding: 20px 0;
  3063. }
  3064. .btnList {
  3065. display: flex;
  3066. align-items: center;
  3067. gap: 8px;
  3068. margin: 10px 0 12px;
  3069. button {
  3070. margin: 0;
  3071. }
  3072. }
  3073. .scheduling-content {
  3074. position: relative;
  3075. margin-top: 6px;
  3076. width: 100%;
  3077. min-height: 0;
  3078. padding: 0;
  3079. box-sizing: border-box;
  3080. }
  3081. .form-wrapper {
  3082. position: relative;
  3083. display: flex;
  3084. flex-direction: column;
  3085. width: 100%;
  3086. height: 100%;
  3087. min-width: 0;
  3088. overflow: hidden;
  3089. border: 1px solid #d8e3ef;
  3090. border-radius: 10px;
  3091. background: linear-gradient(180deg, #ffffff 0%, #fafdff 100%);
  3092. box-shadow: 0 10px 26px rgba(26, 39, 55, 0.08);
  3093. }
  3094. .form-wrapper::before,
  3095. .plan-dot-line-detail::before {
  3096. position: absolute;
  3097. top: 0;
  3098. right: 0;
  3099. left: 0;
  3100. height: 2px;
  3101. background: linear-gradient(90deg, #2489e8 0%, #14b8a6 60%, #7aa7ff 100%);
  3102. content: '';
  3103. pointer-events: none;
  3104. }
  3105. .plan-table-status-tabs {
  3106. flex: 0 0 42px;
  3107. height: 42px;
  3108. padding: 3px 12px 0;
  3109. box-sizing: border-box;
  3110. border-bottom: 1px solid #e5edf6;
  3111. background: linear-gradient(180deg, #f9fcff 0%, #f3f7fb 100%);
  3112. }
  3113. .plan-table-status-tabs ::v-deep {
  3114. .el-tabs__header {
  3115. margin: 0;
  3116. }
  3117. .el-tabs__nav-wrap::after {
  3118. display: none;
  3119. }
  3120. .el-tabs__item {
  3121. display: inline-flex;
  3122. align-items: center;
  3123. justify-content: center;
  3124. height: 36px;
  3125. line-height: 1;
  3126. margin-right: 8px;
  3127. padding: 0 16px;
  3128. border-radius: 7px 7px 0 0;
  3129. color: #667085;
  3130. font-size: 14px;
  3131. font-weight: 600;
  3132. transition: all 0.18s ease;
  3133. }
  3134. .el-tabs__item:not(.is-active):hover {
  3135. color: #1683d8;
  3136. background: rgba(31, 140, 235, 0.06);
  3137. }
  3138. .el-tabs__item.is-active {
  3139. color: #0f75bd;
  3140. background: #fff;
  3141. box-shadow: 0 -1px 0 #dce8f4, 1px 0 0 #e5edf6, -1px 0 0 #e5edf6;
  3142. }
  3143. .el-tabs__active-bar {
  3144. height: 2px;
  3145. border-radius: 2px 2px 0 0;
  3146. background: linear-gradient(90deg, #1f8ceb 0%, #16b8a6 100%);
  3147. }
  3148. }
  3149. .form-wrapper ::v-deep .ele-pro-table {
  3150. flex: 1 1 auto;
  3151. height: calc(100% - 42px);
  3152. min-height: 0;
  3153. overflow: hidden;
  3154. background: transparent;
  3155. }
  3156. .form-wrapper.is-order-scheduling ::v-deep .ele-pro-table,
  3157. .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table {
  3158. display: flex;
  3159. flex-direction: column;
  3160. }
  3161. .form-wrapper.is-order-scheduling ::v-deep .ele-pro-table > .el-table {
  3162. flex: 1 1 auto;
  3163. height: calc(100% - 44px) !important;
  3164. min-height: 0;
  3165. }
  3166. .form-wrapper.is-order-scheduling ::v-deep .ele-pro-table > .el-pagination {
  3167. margin-top: auto !important;
  3168. }
  3169. .form-wrapper ::v-deep .el-table {
  3170. color: #364152;
  3171. border-color: #e5edf6;
  3172. font-size: 14px;
  3173. }
  3174. .form-wrapper ::v-deep .el-table__body-wrapper {
  3175. overflow-y: auto !important;
  3176. overflow-x: auto;
  3177. padding-bottom: 1px;
  3178. box-sizing: border-box;
  3179. }
  3180. .form-wrapper ::v-deep .el-table th.el-table__cell {
  3181. background: linear-gradient(180deg, #f8fbff 0%, #eef5fb 100%);
  3182. color: #1f2d3d;
  3183. font-weight: 600;
  3184. position: sticky;
  3185. top: 0;
  3186. z-index: 2;
  3187. }
  3188. .form-wrapper ::v-deep .el-table td.el-table__cell,
  3189. .form-wrapper ::v-deep .el-table th.el-table__cell {
  3190. border-bottom-color: #e9eff6;
  3191. }
  3192. .form-wrapper ::v-deep .el-table td.el-table__cell {
  3193. padding-top: 5px;
  3194. padding-bottom: 5px;
  3195. }
  3196. .form-wrapper ::v-deep .el-table__row:hover > td.el-table__cell {
  3197. background: #f5faff;
  3198. }
  3199. .form-wrapper ::v-deep .el-table__row.current-row > td.el-table__cell,
  3200. .form-wrapper
  3201. ::v-deep
  3202. .el-table__row.el-table__row--striped.current-row
  3203. > td.el-table__cell {
  3204. background: #eaf5ff;
  3205. }
  3206. .form-wrapper ::v-deep .el-link {
  3207. display: inline-flex;
  3208. align-items: center;
  3209. justify-content: center;
  3210. min-width: 42px;
  3211. height: 26px;
  3212. padding: 0 10px;
  3213. border: 1px solid #c8e0f4;
  3214. border-radius: 6px;
  3215. background: linear-gradient(180deg, #f6fbff 0%, #eaf5ff 100%);
  3216. color: #0f75bd;
  3217. font-weight: 600;
  3218. line-height: 24px;
  3219. transition: all 0.18s ease;
  3220. }
  3221. .form-wrapper ::v-deep .el-link:hover {
  3222. border-color: #8ec6f4;
  3223. background: linear-gradient(180deg, #eef8ff 0%, #dff0ff 100%);
  3224. color: #0964a3;
  3225. }
  3226. .form-wrapper ::v-deep .el-checkbox__inner {
  3227. border-color: #d8e1eb;
  3228. }
  3229. .form-wrapper ::v-deep .el-table__body-wrapper::-webkit-scrollbar,
  3230. .form-wrapper ::v-deep .el-table__fixed-body-wrapper::-webkit-scrollbar {
  3231. width: 8px;
  3232. height: 8px;
  3233. }
  3234. .form-wrapper ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb,
  3235. .form-wrapper
  3236. ::v-deep
  3237. .el-table__fixed-body-wrapper::-webkit-scrollbar-thumb {
  3238. border: 2px solid #fff;
  3239. border-radius: 999px;
  3240. background: #c9d4e2;
  3241. }
  3242. .form-wrapper ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb:hover,
  3243. .form-wrapper
  3244. ::v-deep
  3245. .el-table__fixed-body-wrapper::-webkit-scrollbar-thumb:hover {
  3246. background: #aeb9c8;
  3247. }
  3248. .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table {
  3249. display: flex;
  3250. flex-direction: column;
  3251. min-height: 0;
  3252. }
  3253. .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table > .el-table {
  3254. flex: 1 1 auto;
  3255. height: calc(100% - 44px) !important;
  3256. min-height: 0;
  3257. }
  3258. .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table > .el-pagination {
  3259. margin-top: auto !important;
  3260. }
  3261. .form-wrapper.is-plan-scheduling ::v-deep .plan-scheduling-pagination {
  3262. display: flex;
  3263. align-items: center;
  3264. justify-content: center;
  3265. flex: 0 0 44px;
  3266. height: 44px;
  3267. min-height: 44px;
  3268. padding: 0 10px;
  3269. box-sizing: border-box;
  3270. border-top: 1px solid #e5edf6;
  3271. background: linear-gradient(180deg, #ffffff 0%, #f1f6fb 100%);
  3272. box-shadow: 0 -3px 10px rgba(31, 45, 61, 0.04);
  3273. }
  3274. .form-wrapper.is-plan-scheduling
  3275. ::v-deep
  3276. .ele-pro-table
  3277. > .el-pagination
  3278. > * {
  3279. margin-top: 0 !important;
  3280. }
  3281. .form-wrapper.is-order-scheduling ::v-deep .order-plan-pagination {
  3282. display: flex;
  3283. align-items: center;
  3284. justify-content: center;
  3285. flex: 0 0 44px;
  3286. height: 44px;
  3287. min-height: 44px;
  3288. margin-top: 0 !important;
  3289. padding: 0 10px;
  3290. box-sizing: border-box;
  3291. border-top: 1px solid #e5edf6;
  3292. background: linear-gradient(180deg, #ffffff 0%, #f1f6fb 100%);
  3293. box-shadow: 0 -3px 10px rgba(31, 45, 61, 0.04);
  3294. }
  3295. .form-wrapper.is-order-scheduling ::v-deep .order-plan-pagination > * {
  3296. margin-top: 0 !important;
  3297. }
  3298. .plan-dot-line-side {
  3299. width: 100%;
  3300. height: 100%;
  3301. overflow: hidden;
  3302. padding-left: 10px;
  3303. box-sizing: border-box;
  3304. display: flex;
  3305. flex-direction: column;
  3306. }
  3307. .plan-dot-line-side.is-fullscreen {
  3308. position: fixed;
  3309. top: 24px;
  3310. right: 24px;
  3311. bottom: 24px;
  3312. left: 24px;
  3313. z-index: 3000;
  3314. width: auto !important;
  3315. height: auto !important;
  3316. background: #fff;
  3317. border-radius: 8px;
  3318. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  3319. padding: 20px;
  3320. }
  3321. .plan-dot-line-detail {
  3322. position: relative;
  3323. display: flex;
  3324. flex-direction: column;
  3325. height: 100%;
  3326. min-width: 0;
  3327. overflow: hidden;
  3328. border: 1px solid #d8e3ef;
  3329. border-radius: 10px;
  3330. background: linear-gradient(180deg, #ffffff 0%, #fafdff 100%);
  3331. box-shadow: 0 10px 26px rgba(26, 39, 55, 0.08);
  3332. }
  3333. .plan-dot-line-scrollable {
  3334. flex: 1;
  3335. overflow: hidden;
  3336. padding-bottom: 14px;
  3337. box-sizing: border-box;
  3338. min-height: 0;
  3339. }
  3340. .plan-dot-line-scrollable.is-order-dispatch-scroll {
  3341. padding-bottom: 0;
  3342. }
  3343. .plan-dot-line-scrollable.is-plan-config-scroll {
  3344. display: flex;
  3345. flex-direction: column;
  3346. padding-bottom: 0;
  3347. }
  3348. .plan-dot-line-scrollable.is-plan-config-scroll
  3349. ::v-deep
  3350. .factory-task-config-panel {
  3351. flex: 1 1 auto;
  3352. min-height: 0;
  3353. display: flex;
  3354. flex-direction: column;
  3355. overflow: hidden;
  3356. }
  3357. .plan-dot-line-scrollable.is-plan-config-scroll
  3358. ::v-deep
  3359. .factory-task-config-panel
  3360. .task-config-table-wrap {
  3361. flex: 1 1 auto;
  3362. height: 100%;
  3363. min-height: 0;
  3364. }
  3365. @media (max-width: 1440px) {
  3366. .scheduling-content {
  3367. margin-top: 8px;
  3368. }
  3369. .plan-dot-line-side {
  3370. padding-left: 8px;
  3371. }
  3372. .plan-dot-line-scrollable {
  3373. padding-bottom: 12px;
  3374. }
  3375. }
  3376. @media (max-width: 1180px) {
  3377. .scheduling-content {
  3378. height: auto !important;
  3379. }
  3380. .plan-dot-line-side {
  3381. padding-left: 0;
  3382. padding-top: 10px;
  3383. min-height: 460px;
  3384. }
  3385. .form-wrapper {
  3386. min-height: 260px;
  3387. }
  3388. .plan-dot-line-scrollable {
  3389. overflow: auto;
  3390. }
  3391. }
  3392. </style>
  3393. <style lang="scss">
  3394. .scheduling-dialog.el-dialog {
  3395. width: calc(100vw - 10px) !important;
  3396. max-width: none;
  3397. height: calc(100vh - 10px);
  3398. max-height: calc(100vh - 10px);
  3399. margin-top: 5px !important;
  3400. margin-bottom: 5px !important;
  3401. border: 1px solid rgba(190, 205, 222, 0.85);
  3402. border-radius: 12px;
  3403. background: #eef4fa;
  3404. display: flex;
  3405. flex-direction: column;
  3406. overflow: hidden;
  3407. box-shadow: 0 22px 64px rgba(15, 23, 42, 0.22);
  3408. .el-dialog__header {
  3409. flex: 0 0 auto;
  3410. padding: 8px 16px;
  3411. border-bottom: 1px solid #dce7f2;
  3412. background: linear-gradient(180deg, #ffffff 0%, #f7fafc 100%);
  3413. }
  3414. .el-dialog__title {
  3415. color: #1d2939;
  3416. font-size: 16px;
  3417. font-weight: 700;
  3418. letter-spacing: 0;
  3419. }
  3420. .el-dialog__body {
  3421. flex: 1 1 auto;
  3422. display: flex;
  3423. flex-direction: column;
  3424. min-height: 0;
  3425. padding: 6px 8px 6px;
  3426. background: radial-gradient(
  3427. circle at 12% 0,
  3428. rgba(31, 140, 235, 0.08) 0,
  3429. transparent 28%
  3430. ),
  3431. linear-gradient(90deg, rgba(31, 140, 235, 0.035) 1px, transparent 1px),
  3432. linear-gradient(180deg, rgba(22, 184, 166, 0.025) 1px, transparent 1px),
  3433. #eef4fa;
  3434. background-size: auto, 24px 24px, 24px 24px;
  3435. overflow: hidden;
  3436. }
  3437. .project-gantt {
  3438. flex: 0 0 auto;
  3439. }
  3440. .scheduling-content {
  3441. flex: 1 1 auto;
  3442. height: auto !important;
  3443. min-height: 0;
  3444. }
  3445. .el-dialog__footer {
  3446. flex: 0 0 auto;
  3447. padding: 5px 16px 6px;
  3448. border-top: 1px solid #dce7f2;
  3449. background: linear-gradient(180deg, #f9fbfd 0%, #f3f7fb 100%);
  3450. }
  3451. }
  3452. @media (max-height: 760px) {
  3453. .scheduling-dialog.el-dialog {
  3454. .el-dialog__header {
  3455. padding: 8px 14px;
  3456. }
  3457. .el-dialog__body {
  3458. padding: 6px 8px;
  3459. }
  3460. .el-dialog__footer {
  3461. padding: 4px 14px 5px;
  3462. }
  3463. }
  3464. }
  3465. @media (max-width: 1180px) {
  3466. .scheduling-dialog.el-dialog {
  3467. .el-dialog__body {
  3468. overflow: auto;
  3469. }
  3470. .scheduling-content {
  3471. flex: 0 0 auto;
  3472. min-height: 720px;
  3473. }
  3474. }
  3475. }
  3476. </style>