newFactoryProductionScheduling.vue 121 KB

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