newFactoryProductionScheduling.vue 135 KB

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