IotMainWorkOrderModify.vue 101 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115
  1. <template>
  2. <ContentWrap v-loading="formLoading">
  3. <el-form
  4. ref="formRef"
  5. :model="formData"
  6. :rules="formRules"
  7. v-loading="formLoading"
  8. style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
  9. label-width="130px"
  10. >
  11. <div class="base-expandable-content">
  12. <el-row>
  13. <el-col :span="8">
  14. <el-form-item :label="t('bomList.name')" prop="name">
  15. <el-input type="text" v-model="formData.name" disabled/>
  16. </el-form-item>
  17. </el-col>
  18. <el-col :span="8">
  19. <el-form-item :label="t('mainPlan.MaintenanceMethod')" prop="type">
  20. <el-select v-model="formData.outsourcingFlag" :placeholder="t('faultForm.choose')" clearable disabled>
  21. <el-option
  22. v-for="dict in getIntDictOptions(DICT_TYPE.PMS_ORDER_PROCESS_MODE)"
  23. :key="dict.value"
  24. :label="dict.label"
  25. :value="dict.value"
  26. />
  27. </el-select>
  28. </el-form-item>
  29. </el-col>
  30. <el-col :span="8">
  31. <el-form-item :label="t('mainPlan.MaintenanceCost')" prop="cost">
  32. <el-input
  33. v-model="formData.cost"
  34. placeholder="根据物料消耗自动生成"
  35. disabled
  36. />
  37. </el-form-item>
  38. </el-col>
  39. <el-col :span="8">
  40. <el-form-item :label="t('fault.start')" prop="actualStartTime">
  41. <el-date-picker
  42. style="width: 150%"
  43. v-model="formData.actualStartTime"
  44. type="datetime"
  45. value-format="x"
  46. :placeholder="t('fault.start')"
  47. disabled
  48. />
  49. </el-form-item>
  50. </el-col>
  51. <el-col :span="8">
  52. <el-form-item :label="t('fault.end')" prop="actualEndTime">
  53. <el-date-picker
  54. style="width: 150%"
  55. v-model="formData.actualEndTime"
  56. type="datetime"
  57. value-format="x"
  58. :placeholder="t('fault.end')"
  59. disabled
  60. />
  61. </el-form-item>
  62. </el-col>
  63. <el-col :span="8">
  64. <el-form-item :label="t('mainPlan.otherCost')" prop="otherCost">
  65. <el-input
  66. v-model="formData.otherCost"
  67. @input="handleInput(formData.otherCost, 'otherCost')"
  68. placeholder="其他费用"
  69. disabled
  70. />
  71. </el-form-item>
  72. </el-col>
  73. <el-col :span="8">
  74. <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
  75. <el-input type="text" v-model="formData.deviceCode" disabled/>
  76. </el-form-item>
  77. </el-col>
  78. <el-col :span="8">
  79. <el-form-item :label="t('iotDevice.name')" prop="deviceName">
  80. <el-input type="text" v-model="formData.deviceName" disabled/>
  81. </el-form-item>
  82. </el-col>
  83. <el-col :span="8">
  84. <el-form-item :label="t('faultForm.remark')" prop="remark">
  85. <el-input v-model="formData.remark" type="textarea" :placeholder="t('faultForm.rHolder')" />
  86. </el-form-item>
  87. </el-col>
  88. </el-row>
  89. </div>
  90. </el-form>
  91. </ContentWrap>
  92. <ContentWrap>
  93. <!-- 列表 -->
  94. <ContentWrap>
  95. <el-table ref="mainTableRef"
  96. v-loading="loading"
  97. :data="paginatedList"
  98. :stripe="true"
  99. @row-click="handleRowClick"
  100. :row-class-name="tableRowClassName"
  101. :show-overflow-tooltip="true"
  102. :header-cell-style="tableHeaderStyle"
  103. :span-method="handleSpanMethod"
  104. highlight-current-row
  105. @current-change="handleCurrentChangeTable">
  106. <!-- 序号列 -->
  107. <el-table-column
  108. type="index"
  109. :label="t('maintain.serial')"
  110. align="center"
  111. prop="serial"
  112. :width="columnWidths.serial"
  113. fixed="left"
  114. />
  115. <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
  116. <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" :width="columnWidths.deviceCode" fixed="left" v-if="false">
  117. <template #default="{ row }">
  118. <div class="full-content-cell"> <!-- 自定义样式 -->
  119. {{ row.deviceCode }}
  120. </div>
  121. </template>
  122. </el-table-column>
  123. <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" :width="columnWidths.deviceName" fixed="left" v-if="false">
  124. <template #default="{ row }">
  125. <div class="full-content-cell"> <!-- 自定义样式 -->
  126. {{ row.deviceName }}
  127. </div>
  128. </template>
  129. </el-table-column>
  130. <!-- 保养项组列 -->
  131. <el-table-column
  132. v-if="hasGroupInCurrentPage"
  133. :label="t('mainPlan.MaintItemsGroup')"
  134. align="center"
  135. prop="group"
  136. fixed="left"
  137. :width="columnWidths.group"
  138. :cell-class-name="groupCellClassName"
  139. >
  140. <template #default="{ row }">
  141. <div class="full-content-cell">
  142. {{ row.group }}
  143. </div>
  144. </template>
  145. </el-table-column>
  146. <el-table-column :label="t('mainPlan.MaintItems')" align="center" prop="name"
  147. :show-overflow-tooltip="false" :width="columnWidths.name" fixed="left">
  148. <template #default="{ row }">
  149. <div class="full-content-cell">
  150. <!-- 保养项 只显示'->'后的内容 -->
  151. {{ formatMaintItemName(row.name) }}
  152. </div>
  153. </template>
  154. </el-table-column>
  155. <!-- 消耗物料规则列 -->
  156. <el-table-column :label="t('mainPlan.consumeMaterials')" align="center" width="80">
  157. <template #default="scope">
  158. <el-switch
  159. v-model="scope.row.rule"
  160. :active-value="0"
  161. :inactive-value="1"
  162. />
  163. </template>
  164. </el-table-column>
  165. <el-table-column :label="t('mainPlan.mainStatus')" align="center" width="140">
  166. <template #default="scope">
  167. <div class="status-container">
  168. <el-switch
  169. v-model="scope.row.status"
  170. :active-value="1"
  171. :inactive-value="0"
  172. :disabled="scope.row.initialStatus === 1"
  173. @change="handleStatusChange(scope.row)"
  174. class="status-switch"
  175. />
  176. <span class="status-text">
  177. {{ getStatusText(scope.row) }}
  178. </span>
  179. </div>
  180. </template>
  181. </el-table-column>
  182. <el-table-column :label="t('main.mileage')" key="mileageRule" align="center"
  183. :width="columnWidths.mileageRule" v-if="false">
  184. <template #default="scope">
  185. <el-switch
  186. v-model="scope.row.mileageRule"
  187. :active-value="0"
  188. :inactive-value="1"
  189. :disabled="true"
  190. />
  191. </template>
  192. </el-table-column>
  193. <el-table-column :label="t('main.runTime')" key="runningTimeRule" align="center"
  194. :width="columnWidths.runningTimeRule" v-if="false">
  195. <template #default="scope">
  196. <el-switch
  197. v-model="scope.row.runningTimeRule"
  198. :active-value="0"
  199. :inactive-value="1"
  200. :disabled="true"
  201. />
  202. </template>
  203. </el-table-column>
  204. <el-table-column :label="t('main.date')" key="naturalDateRule" align="center"
  205. :width="columnWidths.naturalDateRule" v-if="false">
  206. <template #default="scope">
  207. <el-switch
  208. v-model="scope.row.naturalDateRule"
  209. :active-value="0"
  210. :inactive-value="1"
  211. :disabled="true"
  212. />
  213. </template>
  214. </el-table-column>
  215. <el-table-column :label="t('operationFillForm.sumTime')" align="center" prop="totalRunTime" v-if="hasTimeRuleInCurrentPage"
  216. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.totalRunTime">
  217. <template #default="{ row }">
  218. {{ row.totalRunTime ?? row.tempTotalRunTime }}
  219. </template>
  220. </el-table-column>
  221. <el-table-column :label="t('operationFillForm.sumKil')" align="center" prop="totalMileage" v-if="hasMileageRuleInCurrentPage"
  222. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.totalMileage">
  223. <template #default="{ row }">
  224. {{ row.totalMileage ?? row.tempTotalMileage }}
  225. </template>
  226. </el-table-column>
  227. <el-table-column :label="t('operationFillForm.mainSumTime')" align="center" prop="mainRuntime" v-if="hasTimeRuleInCurrentPage"
  228. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.mainRuntime">
  229. <template #default="{ row }">
  230. <el-tooltip
  231. :disabled="!row.mainRuntimeError"
  232. :content="row.mainRuntimeError"
  233. placement="top"
  234. effect="light"
  235. popper-class="main-runtime-tooltip"
  236. :show-after="0"
  237. >
  238. <div class="main-runtime-input-wrapper">
  239. <el-input-number
  240. v-model="row.mainRuntime"
  241. :precision="2"
  242. :min="0"
  243. :controls="false"
  244. style="width: 100%"
  245. :disabled="row.status === 1"
  246. @change="validateMainRuntime(row)"
  247. @blur="validateMainRuntime(row)"
  248. :class="{
  249. 'is-required-input': row.mainRuntimeError,
  250. 'error-input': row.mainRuntimeError
  251. }"
  252. />
  253. </div>
  254. </el-tooltip>
  255. </template>
  256. </el-table-column>
  257. <el-table-column :label="t('operationFillForm.mainSumKil')" align="center" prop="mainMileage" v-if="hasMileageRuleInCurrentPage"
  258. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.mainMileage">
  259. <template #default="{ row }">
  260. <el-tooltip
  261. :disabled="!row.mainMileageError"
  262. :content="row.mainMileageError"
  263. placement="top"
  264. effect="light"
  265. popper-class="main-runtime-tooltip"
  266. :show-after="0"
  267. >
  268. <div class="main-runtime-input-wrapper">
  269. <el-input-number
  270. v-model="row.mainMileage"
  271. :precision="2"
  272. :min="0"
  273. :controls="false"
  274. style="width: 100%"
  275. :disabled="row.status === 1"
  276. @change="validateMainMileage(row)"
  277. @blur="validateMainMileage(row)"
  278. :class="{
  279. 'is-required-input': row.mainMileageError,
  280. 'error-input': row.mainMileageError
  281. }"
  282. />
  283. </div>
  284. </el-tooltip>
  285. </template>
  286. </el-table-column>
  287. <el-table-column :label="t('mainPlan.lastMaintenanceDate')" prop="lastMaintenanceDate" :width="columnWidths.lastMaintenanceDate">
  288. <template #default="{ row }">
  289. <div class="full-content-cell">
  290. {{ row.lastMaintenanceDate }}
  291. </div>
  292. </template>
  293. </el-table-column>
  294. <!-- 保养里程 分组 -->
  295. <el-table-column v-if="hasMileageRuleInCurrentPage" label="保养里程" align="center">
  296. <el-table-column :label="t('mainPlan.lastMaintenanceMileage')" align="center" prop="lastRunningKilometers"
  297. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.lastRunningKilometers">
  298. <template #default="{ row }">
  299. {{ row.lastRunningKilometers }}
  300. </template>
  301. </el-table-column>
  302. <el-table-column :label="t('mainPlan.nextMaintenanceKm')"
  303. align="center" prop="nextMaintenanceKm" :width="columnWidths.nextMaintenanceKm">
  304. <template #default="{ row }">
  305. {{ row.nextMaintenanceKm ?? '-' }}
  306. </template>
  307. </el-table-column>
  308. <el-table-column :label="t('mainPlan.remainKm')"
  309. align="center" prop="remainKm" :width="columnWidths.remainKm">
  310. <template #default="{ row }">
  311. <span :class="{ 'negative-value': row.remainKm != null && row.remainKm < 0 }">
  312. {{ row.remainKm != null ? row.remainKm.toFixed(2) : '-' }}
  313. </span>
  314. </template>
  315. </el-table-column>
  316. </el-table-column>
  317. <!-- 保养时长 分组 -->
  318. <el-table-column v-if="hasTimeRuleInCurrentPage" label="保养时长" align="center">
  319. <el-table-column :label="t('mainPlan.lastMaintenanceOperationTime')" align="center" prop="lastRunningTime"
  320. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.lastRunningTime">
  321. <template #default="{ row }">
  322. {{ row.lastRunningTime }}
  323. </template>
  324. </el-table-column>
  325. <el-table-column :label="t('mainPlan.nextMaintenanceH')"
  326. align="center" prop="nextMaintenanceH" :width="columnWidths.nextMaintenanceH">
  327. <template #default="{ row }">
  328. {{ row.nextMaintenanceH ?? '-' }}
  329. </template>
  330. </el-table-column>
  331. <el-table-column :label="t('mainPlan.remainH')"
  332. align="center" prop="remainH" :width="columnWidths.remainH">
  333. <template #default="{ row }">
  334. <span :class="{ 'negative-value': row.remainH != null && row.remainH < 0 }">
  335. {{ row.remainH != null ? row.remainH.toFixed(2) : '-' }}
  336. </span>
  337. </template>
  338. </el-table-column>
  339. <el-table-column
  340. v-if="shouldShowRunningTimeColumn"
  341. :label="t('mainPlan.RunTimeCycle')"
  342. align="center"
  343. prop="nextRunningTime"
  344. :width="columnWidths.nextRunningTime"
  345. >
  346. <template #default="scope">
  347. <el-input-number
  348. v-if="scope.row.runningTimeRule === 0"
  349. v-model="scope.row.nextRunningTime"
  350. :precision="1"
  351. :min="1"
  352. :controls="false"
  353. style="width: 100%"
  354. @change="validateRunningTime(scope.row)"
  355. disabled
  356. />
  357. <span v-else>-</span>
  358. <!-- 错误提示 -->
  359. <div v-if="scope.row.timeError" class="error-text">
  360. {{ scope.row.timeError }}
  361. </div>
  362. </template>
  363. </el-table-column>
  364. </el-table-column>
  365. <!-- 保养日期 分组 -->
  366. <el-table-column v-if="hasDateRuleInCurrentPage" label="保养日期" align="center">
  367. <el-table-column :label="t('mainPlan.lastMaintenanceNaturalDate')" align="center" prop="tempLastNaturalDate"
  368. :formatter="erpPriceTableColumnFormatter" :width="columnWidths.tempLastNaturalDate">
  369. <template #default="{ row }">
  370. {{ row.tempLastNaturalDate }}
  371. </template>
  372. </el-table-column>
  373. <el-table-column :label="t('mainPlan.nextMaintDate')"
  374. align="center" prop="nextMaintenanceDate" :width="columnWidths.nextMaintenanceDate">
  375. <template #default="{ row }">
  376. {{ row.nextMaintenanceDate ?? '-' }}
  377. </template>
  378. </el-table-column>
  379. <el-table-column :label="t('mainPlan.remainDay')"
  380. align="center" prop="remainDay" :width="columnWidths.remainDay">
  381. <template #default="{ row }">
  382. <span :class="{ 'negative-value': row.remainDay != null && row.remainDay < 0 }">
  383. {{ row.remainDay ?? '-' }}
  384. </span>
  385. </template>
  386. </el-table-column>
  387. </el-table-column>
  388. <el-table-column :label="t('iotMaintain.operation')" align="center" prop="operation" :width="columnWidths.operation" fixed="right">
  389. <template #default="scope">
  390. <div class="horizontal-actions">
  391. <!-- 延迟保养按钮 -->
  392. <el-tooltip
  393. v-if="scope.row.status === 0"
  394. effect="dark"
  395. :content="t('stock.DelayMaintenance')"
  396. placement="top"
  397. :show-after="300"
  398. >
  399. <el-button
  400. type="primary"
  401. link
  402. :icon="Clock"
  403. @click="handleDropdownCommand({ action: 'delay', row: scope.row })"
  404. class="action-button"
  405. />
  406. </el-tooltip>
  407. </div>
  408. </template>
  409. </el-table-column>
  410. </el-table>
  411. <div style="margin-top: 20px; display: flex; justify-content: flex-end;">
  412. <el-pagination
  413. v-model:current-page="currentPage"
  414. v-model:page-size="pageSize"
  415. :page-sizes="[10]"
  416. :background="true"
  417. layout="total, sizes, prev, pager, next, jumper"
  418. :total="list.length"
  419. @current-change="handleCurrentChange"
  420. @size-change="handleSizeChange"
  421. />
  422. </div>
  423. </ContentWrap>
  424. <!-- 选择的物料列表 -->
  425. <ContentWrap>
  426. <div style="margin-bottom: 15px; display: flex; justify-content: flex-start; align-items: center; gap: 12px;">
  427. <!-- 2. 新增“选择物料”按钮,与现有按钮平齐 -->
  428. <el-button
  429. type="primary"
  430. :icon="Box"
  431. @click="handleSelectMaterial"
  432. >
  433. {{ t('stock.selectMaterial') }}
  434. </el-button>
  435. <!-- 原有“显示所有物料/当前保养项物料”按钮 -->
  436. <el-button
  437. type="primary"
  438. @click="toggleShowAllMaterials"
  439. :icon="showAllMaterials ? Star : StarFilled"
  440. >
  441. {{ showAllMaterials ? '显示当前保养项物料' : '显示所有物料' }}
  442. </el-button>
  443. <!-- 新增物料按钮 -->
  444. <el-button
  445. type="primary"
  446. :icon="Plus"
  447. @click="handleAddMaterial"
  448. >
  449. 新增物料
  450. </el-button>
  451. </div>
  452. <el-table v-loading="false" :data="displayedMaterialList" :stripe="true" :show-overflow-tooltip="true"
  453. v-if="showMaterialTable" :row-class-name="getMaterialRowClassName">
  454. <el-table-column :label="t('iotDevice.serial')" align="center" width="50px">
  455. <template #default="scope">
  456. {{ scope.$index + 1 }}
  457. </template>
  458. </el-table-column>
  459. <el-table-column label="" align="center" prop="deviceId" v-if="false"/>
  460. <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
  461. <el-table-column label="工厂id" align="center" prop="factoryId" v-if="false"/>
  462. <el-table-column label="工厂名称" align="center" prop="factory"/>
  463. <el-table-column label="成本中心id" align="center" prop="costCenterId" v-if="false"/>
  464. <el-table-column label="成本中心名称" align="center" prop="costCenter"/>
  465. <el-table-column label="库存地点id" align="center" prop="storageLocationId" v-if="false"/>
  466. <el-table-column label="库存地点名称" align="center" prop="projectDepartment"/>
  467. <el-table-column label="物料编码" align="center" prop="materialCode" >
  468. <template #default="{ row }">
  469. <el-input
  470. v-if="row.isNew"
  471. v-model="row.materialCode"
  472. placeholder="请输入物料编码"
  473. style="width: 100%"
  474. :disabled="isMaterialDisabled(row)"
  475. />
  476. <span v-else>{{ row.materialCode }}</span>
  477. </template>
  478. </el-table-column>
  479. <el-table-column label="物料名称" align="center" prop="materialName" >
  480. <template #default="{ row }">
  481. <!-- 新增物料行:显示tooltip和必填红色边框 -->
  482. <el-tooltip
  483. v-if="row.isNew"
  484. effect=""
  485. content=""
  486. placement="top"
  487. :disabled="row.materialName?.trim()"
  488. >
  489. <el-input
  490. v-model="row.materialName"
  491. placeholder="请输入物料名称(必填)"
  492. style="width: 100%"
  493. :class="{ 'is-required-input': row.isNew && !row.materialName?.trim() }"
  494. :disabled="isMaterialDisabled(row)"
  495. />
  496. </el-tooltip>
  497. <!-- 非新增行:正常显示 -->
  498. <span v-else>{{ row.materialName }}</span>
  499. </template>
  500. </el-table-column>
  501. <el-table-column label="单位" align="center" prop="unit" width="60px">
  502. <template #default="{ row }">
  503. <el-tooltip
  504. v-if="row.isNew"
  505. effect=""
  506. content=""
  507. placement=""
  508. :disabled="row.unit?.trim()"
  509. >
  510. <el-input
  511. v-model="row.unit"
  512. placeholder="单位(必填)"
  513. style="width: 100%"
  514. :class="{
  515. 'is-required-input': row.isNew && !row.unit?.trim()
  516. }"
  517. :disabled="isMaterialDisabled(row)"
  518. />
  519. </el-tooltip>
  520. <span v-else>{{ row.unit }}</span>
  521. </template>
  522. </el-table-column>
  523. <el-table-column label="单价(CNY/元)" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter" width="120px">
  524. <template #default="{ row }">
  525. <div class="unit-price-container">
  526. <el-input-number
  527. v-model="row.unitPrice"
  528. :precision="2"
  529. :min="0"
  530. :controls="false"
  531. style="width: 100%"
  532. class="unit-price-input"
  533. :class="{
  534. 'is-required-input': row.unitPrice <= 0
  535. }"
  536. :disabled="isMaterialDisabled(row)"
  537. />
  538. </div>
  539. </template>
  540. </el-table-column>
  541. <el-table-column label="消耗数量" align="center" prop="quantity" width="120px">
  542. <template #default="{ row }">
  543. <el-input-number
  544. v-model="row.quantity"
  545. :precision="4"
  546. :min="0"
  547. :controls="false"
  548. style="width: 100%"
  549. :class="{ 'is-required-input': row.quantity <= 0 }"
  550. :disabled="isMaterialDisabled(row)"
  551. placeholder="请输入(必填)"
  552. />
  553. <!-- </el-tooltip> -->
  554. </template>
  555. </el-table-column>
  556. <el-table-column label="库存数量" align="center" prop="totalInventoryQuantity" width="80px"/>
  557. <el-table-column label="来源" align="center" prop="materialSource" width="100px"/>
  558. <el-table-column label="操作" align="center" width="80" fixed="right">
  559. <template #default="scope">
  560. <el-tooltip
  561. effect="dark"
  562. content="删除物料"
  563. placement="top"
  564. :show-after="300"
  565. >
  566. <el-button
  567. type="danger"
  568. link
  569. :icon="Delete"
  570. @click="handleDeleteMaterialInTable(scope.row)"
  571. :disabled="isMaterialDisabled(scope.row)"
  572. class="action-button"
  573. />
  574. </el-tooltip>
  575. </template>
  576. </el-table-column>
  577. </el-table>
  578. </ContentWrap>
  579. </ContentWrap>
  580. <ContentWrap>
  581. <el-form>
  582. <el-form-item style="float: right">
  583. <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('common.save')}}</el-button>
  584. <el-button @click="close">{{t('common.cancel')}}</el-button>
  585. </el-form-item>
  586. </el-form>
  587. </ContentWrap>
  588. <!-- 新增配置对话框 -->
  589. <el-dialog
  590. v-model="configDialog.visible"
  591. :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
  592. width="600px"
  593. >
  594. <!-- 使用header插槽自定义标题 -->
  595. <template #header>
  596. <span>设备 <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong> 保养项配置</span>
  597. </template>
  598. <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
  599. <div class="form-group">
  600. <div class="group-title">{{ t('mainPlan.basicMaintenanceRecords') }}</div>
  601. <!-- 里程配置 -->
  602. <el-form-item
  603. v-if="configDialog.current?.mileageRule === 0"
  604. :label="t('mainPlan.lastMaintenanceMileage')"
  605. prop="lastRunningKilometers"
  606. >
  607. <el-input-number
  608. v-model="configDialog.form.lastRunningKilometers"
  609. :precision="2"
  610. :min="0"
  611. controls-position="right"
  612. :controls="false"
  613. style="width: 60%"
  614. :disabled="true"
  615. />
  616. </el-form-item>
  617. <!-- 推迟公里数 -->
  618. <el-form-item
  619. v-if="configDialog.current?.mileageRule === 0"
  620. :label="t('mainPlan.DelayKil')"
  621. prop="delayKilometers"
  622. >
  623. <el-input-number
  624. v-model="configDialog.form.delayKilometers"
  625. :precision="2"
  626. :min="0"
  627. controls-position="right"
  628. :controls="false"
  629. style="width: 60%"
  630. />
  631. </el-form-item>
  632. <!-- 运行时间配置 -->
  633. <el-form-item
  634. v-if="configDialog.current?.runningTimeRule === 0"
  635. :label="t('mainPlan.lastMaintenanceOperationTime')"
  636. prop="lastRunningTime"
  637. >
  638. <el-input-number
  639. v-model="configDialog.form.lastRunningTime"
  640. :precision="1"
  641. :min="0"
  642. controls-position="right"
  643. :controls="false"
  644. style="width: 60%"
  645. :disabled="true"
  646. />
  647. </el-form-item>
  648. <!-- 推迟时长 -->
  649. <el-form-item
  650. v-if="configDialog.current?.runningTimeRule === 0"
  651. :label="t('mainPlan.DelayDuration')"
  652. prop="delayDuration"
  653. >
  654. <el-input-number
  655. v-model="configDialog.form.delayDuration"
  656. :precision="2"
  657. :min="0"
  658. controls-position="right"
  659. :controls="false"
  660. style="width: 60%"
  661. />
  662. </el-form-item>
  663. <!-- 自然日期配置 -->
  664. <el-form-item
  665. v-if="configDialog.current?.naturalDateRule === 0"
  666. :label="t('mainPlan.lastMaintenanceNaturalDate')"
  667. prop="lastNaturalDate"
  668. >
  669. <el-date-picker
  670. v-model="configDialog.form.lastNaturalDate"
  671. type="date"
  672. placeholder="选择日期"
  673. format="YYYY-MM-DD"
  674. value-format="YYYY-MM-DD"
  675. style="width: 60%"
  676. :disabled="true"
  677. />
  678. </el-form-item>
  679. <!-- 推迟自然日期 -->
  680. <el-form-item
  681. v-if="configDialog.current?.naturalDateRule === 0"
  682. :label="t('mainPlan.DelayDate')"
  683. prop="delayNaturalDate"
  684. >
  685. <el-input-number
  686. v-model="configDialog.form.delayNaturalDate"
  687. :precision="2"
  688. :min="0"
  689. controls-position="right"
  690. :controls="false"
  691. style="width: 60%"
  692. />
  693. </el-form-item>
  694. <el-form-item
  695. :label="t('stock.DelayReason')"
  696. prop="delayReason"
  697. v-if="configDialog.current?.mileageRule === 0 ||
  698. configDialog.current?.runningTimeRule === 0 ||
  699. configDialog.current?.naturalDateRule === 0"
  700. >
  701. <el-input
  702. v-model="configDialog.form.delayReason"
  703. type="textarea"
  704. :rows="2"
  705. :placeholder="t('stock.DelayReason')"
  706. style="width: 60%"
  707. />
  708. </el-form-item>
  709. </div>
  710. <div class="form-group" v-if="configDialog.current?.mileageRule === 0">
  711. <div class="group-title">{{ t('mainPlan.operatingMileageRuleConfiguration') }}</div>
  712. <!-- 保养规则周期值 + 提前量 -->
  713. <el-form-item
  714. v-if="configDialog.current?.mileageRule === 0"
  715. :label="t('mainPlan.operatingMileageCycle')"
  716. prop="nextRunningKilometers"
  717. >
  718. <el-input-number
  719. v-model="configDialog.form.nextRunningKilometers"
  720. :precision="2"
  721. :min="0"
  722. controls-position="right"
  723. :controls="false"
  724. style="width: 60%"
  725. :disabled="true"
  726. />
  727. </el-form-item>
  728. <el-form-item
  729. v-if="configDialog.current?.mileageRule === 0"
  730. :label="t('mainPlan.OperatingMileageCycle_lead')"
  731. prop="kiloCycleLead"
  732. >
  733. <el-input-number
  734. v-model="configDialog.form.kiloCycleLead"
  735. :precision="2"
  736. :min="0"
  737. controls-position="right"
  738. :controls="false"
  739. style="width: 60%"
  740. :disabled="true"
  741. />
  742. </el-form-item>
  743. </div>
  744. <div class="form-group" v-if="configDialog.current?.runningTimeRule === 0">
  745. <div class="group-title">{{ t('mainPlan.RunTimeRuleConfiguration') }}</div>
  746. <el-form-item
  747. v-if="configDialog.current?.runningTimeRule === 0"
  748. :label="t('mainPlan.RunTimeCycle')"
  749. prop="nextRunningTime"
  750. >
  751. <el-input-number
  752. v-model="configDialog.form.nextRunningTime"
  753. :precision="1"
  754. :min="0"
  755. controls-position="right"
  756. :controls="false"
  757. style="width: 60%"
  758. :disabled="true"
  759. />
  760. </el-form-item>
  761. <el-form-item
  762. v-if="configDialog.current?.runningTimeRule === 0"
  763. :label="t('mainPlan.RunTimeCycle_Lead')"
  764. prop="timePeriodLead"
  765. >
  766. <el-input-number
  767. v-model="configDialog.form.timePeriodLead"
  768. :precision="1"
  769. :min="0"
  770. controls-position="right"
  771. :controls="false"
  772. style="width: 60%"
  773. :disabled="true"
  774. />
  775. </el-form-item>
  776. </div>
  777. <div class="form-group" v-if="configDialog.current?.naturalDateRule === 0">
  778. <div class="group-title">{{ t('mainPlan.NaturalDayRuleConfig') }}</div>
  779. <el-form-item
  780. v-if="configDialog.current?.naturalDateRule === 0"
  781. :label="t('mainPlan.NaturalDailyCycle') "
  782. prop="nextNaturalDate"
  783. >
  784. <el-input-number
  785. v-model="configDialog.form.nextNaturalDate"
  786. :min="0"
  787. controls-position="right"
  788. :controls="false"
  789. style="width: 60%"
  790. :disabled="true"
  791. />
  792. </el-form-item>
  793. <el-form-item
  794. v-if="configDialog.current?.naturalDateRule === 0"
  795. :label="t('mainPlan.NaturalDailyCycle_Lead') "
  796. prop="naturalDatePeriodLead"
  797. >
  798. <el-input-number
  799. v-model="configDialog.form.naturalDatePeriodLead"
  800. :min="0"
  801. controls-position="right"
  802. :controls="false"
  803. style="width: 60%"
  804. :disabled="true"
  805. />
  806. </el-form-item>
  807. </div>
  808. </el-form>
  809. <template #footer>
  810. <el-button @click="configDialog.visible = false">{{ t('common.cancel')}}</el-button>
  811. <el-button type="primary" @click="saveConfig">{{ t('common.ok')}}</el-button>
  812. </template>
  813. </el-dialog>
  814. <!-- 表单弹窗:添加/修改 -->
  815. <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
  816. <!-- 设备BOM节点绑定的物料列表 -->
  817. <DeviceBomMaterials ref="deviceBomMaterialsRef" />
  818. <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
  819. <MaterialListDrawer
  820. :model-value="drawerVisible"
  821. @update:model-value="val => drawerVisible = val"
  822. :node-id="currentBomNodeId"
  823. :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
  824. @delete="handleDeleteMaterial"
  825. :hide-extra-columns="hideExtraColumnsInDrawer"
  826. />
  827. </template>
  828. <script setup lang="ts">
  829. import * as UserApi from '@/api/system/user'
  830. import { useUserStore } from '@/store/modules/user'
  831. import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
  832. import { useRouter, useRoute } from 'vue-router'
  833. import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
  834. import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
  835. import { IotMainWorkOrderBomMaterialApi, IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
  836. import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
  837. import { useTagsViewStore } from '@/store/modules/tagsView'
  838. import * as DeptApi from "@/api/system/dept";
  839. import {erpPriceTableColumnFormatter} from "@/utils";
  840. import dayjs from 'dayjs'
  841. import MaterialListDrawer from "@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue";
  842. import WorkOrderMaterial from "@/views/pms/iotmainworkorder/WorkOrderMaterial.vue";
  843. import DeviceBomMaterials from "@/views/pms/iotmainworkorder/DeviceBomMaterials.vue";
  844. import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
  845. // 引入图标
  846. import { View, Clock, Box, Document, Star, StarFilled, Delete, Plus } from '@element-plus/icons-vue';
  847. /** 保养工单 退回后修改 */
  848. defineOptions({ name: 'IotMainWorkOrderModify' })
  849. const { t } = useI18n() // 国际化
  850. const message = useMessage() // 消息弹窗
  851. const { delView } = useTagsViewStore() // 视图操作
  852. const { currentRoute, push } = useRouter()
  853. const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
  854. const dept = ref() // 当前登录人所属部门对象
  855. const configFormRef = ref() // 配置弹出框对象
  856. const bomNodeId = ref() // 最新的bomNodeId
  857. const dialogTitle = ref('') // 弹窗的标题
  858. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  859. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  860. const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
  861. const drawerVisible = ref<boolean>(false)
  862. const currentBomNodeId = ref() // 当前选中的bom节点
  863. const showDrawer = ref()
  864. const list = ref<IotMainWorkOrderBomVO[]>([]) // 保养工单bom关联列表的数据
  865. const materialList = ref<IotMainWorkOrderBomMaterialVO[]>([]) // 保养工单bom关联物料列表
  866. const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
  867. const { params, name } = useRoute() // 查询参数
  868. const id = params.id
  869. // 控制抽屉额外列的显示
  870. const hideExtraColumnsInDrawer = ref(false)
  871. // 分页相关变量
  872. const currentPage = ref(1)
  873. const pageSize = ref(10)
  874. const tableRef = ref();
  875. const mainTableRef = ref(); // 添加表格ref
  876. // 新增响应式变量
  877. const maintItemsWidth = ref('auto')
  878. const lastNaturalDateWatchers = ref(new Map())
  879. const columnWidths = ref<Record<string, string>>({});
  880. // 物料列表相关变量
  881. const showMaterialTable = ref(true) // 控制物料列表显示
  882. const currentBomItem = ref(null) // 当前选中的保养项
  883. const bomMaterialsMap = ref({}) // 存储所有保养项物料列表的映射
  884. const showAllMaterials = ref(false) // 控制是否显示所有物料
  885. // 计算当前页是否有分组数据
  886. const hasGroupInCurrentPage = computed(() => {
  887. return paginatedList.value.some(item => item.group && item.group !== '');
  888. });
  889. // 计算属性:根据showAllMaterials决定显示的物料列表
  890. const displayedMaterialList = computed(() => {
  891. if (showAllMaterials.value) {
  892. // 显示所有物料
  893. const allMaterials = [];
  894. for (const key in bomMaterialsMap.value) {
  895. allMaterials.push(...bomMaterialsMap.value[key]);
  896. }
  897. return allMaterials;
  898. } else {
  899. // 显示当前选中保养项的物料
  900. return materialList.value;
  901. }
  902. });
  903. // 判断物料是否被禁用
  904. const isMaterialDisabled = (material) => {
  905. // 根据物料的bomNodeId找到对应的保养项
  906. const bomItem = list.value.find(item => item.bomNodeId === material.bomNodeId);
  907. // 如果保养项不存在,默认不禁用
  908. if (!bomItem) return false;
  909. // 如果保养项状态为已完成(status=1),则禁用该物料
  910. // if (bomItem.status === 1) return true;
  911. // 如果保养项rule=1(不消耗物料),则禁用
  912. if (bomItem.rule === 1) return true;
  913. return false;
  914. };
  915. // 物料表格行类名 - 用于设置灰色背景
  916. const getMaterialRowClassName = ({ row }) => {
  917. if (row.isNew) {
  918. return 'new-material-row';
  919. }
  920. if (isMaterialDisabled(row)) {
  921. return 'disabled-material-row';
  922. }
  923. return '';
  924. };
  925. // 计算属性:判断按钮是否应该禁用
  926. const isButtonsDisabled = computed(() => {
  927. if (!currentBomItem.value) return true;
  928. // 条件1:保养状态为已完成(status=1)
  929. if (currentBomItem.value.status === 1) return true;
  930. // 条件2:消耗物料字段为不消耗物料(rule=1)
  931. if (currentBomItem.value.rule === 1) return true;
  932. return false;
  933. });
  934. // 切换显示所有物料/当前保养项物料
  935. const toggleShowAllMaterials = () => {
  936. showAllMaterials.value = !showAllMaterials.value;
  937. // 切换显示模式时,需要同步更新 materialList
  938. if (!showAllMaterials.value && currentBomItem.value) {
  939. // 切换到显示当前保养项物料时,使用新的刷新方法
  940. refreshCurrentBomItemMaterials();
  941. } else if (showAllMaterials.value) {
  942. // 切换到显示所有物料
  943. const allMaterials = [];
  944. for (const key in bomMaterialsMap.value) {
  945. allMaterials.push(...bomMaterialsMap.value[key]);
  946. }
  947. materialList.value = allMaterials;
  948. }
  949. console.log('切换显示模式:', {
  950. showAllMaterials: showAllMaterials.value,
  951. materialCount: materialList.value.length
  952. });
  953. };
  954. // 分组合并计算逻辑
  955. const groupSpans = ref<Record<string, { span: number, index: number }>>({})
  956. const formData = ref({
  957. id: undefined,
  958. deptId: undefined,
  959. name: '',
  960. orderNumber: undefined,
  961. responsiblePerson: undefined,
  962. actualStartTime: undefined,
  963. actualEndTime: undefined,
  964. cost: undefined,
  965. otherCost: undefined,
  966. outsourcingFlag: 0,
  967. remark: undefined,
  968. status: undefined,
  969. devicePersons: '',
  970. deviceCode: '',
  971. deviceName: ''
  972. })
  973. const formRules = reactive({
  974. name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
  975. actualStartTime: [{
  976. required: true,
  977. message: t('fault.start') + '不能为空',
  978. trigger: 'change'
  979. }, {
  980. validator: (rule, value, callback) => {
  981. const now = dayjs();
  982. const start = value ? dayjs(Number(value)) : null;
  983. // 验证开始时间 <= 当前日期
  984. if (start && start.isAfter(now)) {
  985. callback(new Error(t('fault.start') + '不能超过当前日期'));
  986. return;
  987. }
  988. const end = formData.value.actualEndTime
  989. ? dayjs(Number(formData.value.actualEndTime))
  990. : null;
  991. // 只有当结束时间有效时,才比较开始和结束时间
  992. if (end && end.isValid() && !end.isAfter(now)) {
  993. if (start && start.isAfter(end)) {
  994. callback(new Error(t('fault.start') + '不能超过' + t('fault.end')));
  995. return;
  996. }
  997. }
  998. // 触发结束时间的重新校验
  999. if (formRef.value) {
  1000. formRef.value.validateField('actualEndTime', () => {});
  1001. }
  1002. callback();
  1003. },
  1004. trigger: 'change'
  1005. }],
  1006. actualEndTime: [{
  1007. required: true,
  1008. message: t('fault.end') + '不能为空',
  1009. trigger: 'change'
  1010. }, {
  1011. validator: (rule, value, callback) => {
  1012. const now = dayjs();
  1013. const end = value ? dayjs(Number(value)) : null;
  1014. // 验证结束时间 <= 当前日期
  1015. if (end && end.isAfter(now)) {
  1016. callback(new Error(t('fault.end') + '不能超过当前日期'));
  1017. return;
  1018. }
  1019. const start = formData.value.actualStartTime
  1020. ? dayjs(Number(formData.value.actualStartTime))
  1021. : null;
  1022. // 验证结束时间 >= 开始时间(仅当开始时间存在时)
  1023. if (start && end && end.isBefore(start)) {
  1024. callback(new Error(t('fault.end') + '必须大于等于' + t('fault.start')));
  1025. return;
  1026. }
  1027. callback();
  1028. },
  1029. trigger: 'change'
  1030. }]
  1031. })
  1032. const formRef = ref() // 表单 Ref
  1033. interface MaterialFormExpose {
  1034. open: (deptId: number, bomNodeId: number, row: any, type: string) => void
  1035. }
  1036. const materialFormRef = ref<MaterialFormExpose>();
  1037. const deviceBomMaterialsRef = ref<MaterialFormExpose>();
  1038. // 新增配置相关状态
  1039. const configDialog = reactive({
  1040. visible: false,
  1041. current: null as IotMaintenanceBomVO | null,
  1042. form: {
  1043. lastRunningKilometers: 0,
  1044. delayKilometers: 0,
  1045. lastRunningTime: 0,
  1046. delayDuration: 0,
  1047. lastNaturalDate: '',
  1048. delayNaturalDate: 0,
  1049. // 保养规则 周期
  1050. nextRunningKilometers: 0,
  1051. nextRunningTime: 0,
  1052. nextNaturalDate: 0,
  1053. // 提前量
  1054. kiloCycleLead: 0,
  1055. timePeriodLead: 0,
  1056. naturalDatePeriodLead: 0,
  1057. // 推迟原因
  1058. delayReason: ''
  1059. }
  1060. })
  1061. // 打开配置对话框
  1062. const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
  1063. configDialog.current = row
  1064. // 处理日期初始化(核心修改)
  1065. let initialDate = ''
  1066. if (row.lastNaturalDate) {
  1067. // 如果已有值:时间戳 -> 日期字符串
  1068. initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
  1069. } else {
  1070. // 如果无值:设置默认值避免1970问题
  1071. initialDate = ''
  1072. }
  1073. configDialog.form = {
  1074. lastRunningKilometers: row.lastRunningKilometers || 0,
  1075. delayKilometers: row.delayKilometers || 0,
  1076. lastRunningTime: row.lastRunningTime || 0,
  1077. delayDuration: row.delayDuration || 0,
  1078. lastNaturalDate: initialDate,
  1079. delayNaturalDate: row.delayNaturalDate || 0,
  1080. // 保养规则 周期值
  1081. nextRunningKilometers: row.nextRunningKilometers || 0,
  1082. nextRunningTime: row.nextRunningTime || 0,
  1083. nextNaturalDate: row.nextNaturalDate || 0,
  1084. // 提前量
  1085. kiloCycleLead: row.kiloCycleLead || 0,
  1086. timePeriodLead: row.timePeriodLead || 0,
  1087. naturalDatePeriodLead: row.naturalDatePeriodLead || 0,
  1088. // 推迟原因
  1089. delayReason: row.delayReason || ''
  1090. }
  1091. configDialog.visible = true
  1092. }
  1093. // 格式化 保养项名称 方法 只显示 -> 后面的内容
  1094. const formatMaintItemName = (name: string) => {
  1095. if (!name) return '';
  1096. // 包含'->'时只取后半部分
  1097. if (name.includes('->')) {
  1098. return name.split('->').pop()?.trim() || name;
  1099. }
  1100. // 不含'->'时显示完整内容
  1101. return name;
  1102. };
  1103. // 行合并方法 (优化后符合图片效果)
  1104. const handleSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
  1105. // 只处理保养项组列
  1106. if (column.property === 'group' && row.group) {
  1107. const groupKey = `${currentPage.value}-${row.group}`;
  1108. const spanInfo = groupSpans.value[groupKey];
  1109. if (spanInfo && rowIndex === spanInfo.index) {
  1110. return {
  1111. rowspan: spanInfo.span,
  1112. colspan: 1
  1113. };
  1114. } else {
  1115. return {
  1116. rowspan: 0,
  1117. colspan: 0
  1118. };
  1119. }
  1120. }
  1121. // 其他列不合并
  1122. return {
  1123. rowspan: 1,
  1124. colspan: 1
  1125. };
  1126. };
  1127. // 计算分组合并信息 (优化后符合图片效果)
  1128. const calculateGroupSpans = () => {
  1129. // 重置分组信息
  1130. groupSpans.value = {};
  1131. const pageKey = currentPage.value;
  1132. let currentGroup = '';
  1133. let groupStartIndex = 0;
  1134. let groupCount = 0;
  1135. // 先清除所有行的分组标记
  1136. paginatedList.value.forEach(item => {
  1137. delete item.isGroupFirstRow;
  1138. });
  1139. paginatedList.value.forEach((item, index) => {
  1140. // 获取当前行的分组(从name中提取)
  1141. const group = item.name && item.name.includes('->')
  1142. ? item.name.split('->')[0].trim()
  1143. : '';
  1144. // 如果分组变化
  1145. if (group !== currentGroup) {
  1146. // 保存上一个分组的信息
  1147. if (currentGroup && groupCount > 0) {
  1148. const groupKey = `${pageKey}-${currentGroup}`;
  1149. groupSpans.value[groupKey] = {
  1150. span: groupCount,
  1151. index: groupStartIndex
  1152. };
  1153. // 标记上一个分组的起始行(添加这行)
  1154. paginatedList.value[groupStartIndex].isGroupFirstRow = true;
  1155. }
  1156. // 开始新分组
  1157. currentGroup = group;
  1158. groupStartIndex = index;
  1159. groupCount = 1;
  1160. } else {
  1161. // 相同分组,计数增加
  1162. groupCount++;
  1163. }
  1164. });
  1165. // 保存最后一个分组的信息
  1166. if (currentGroup && groupCount > 0) {
  1167. const groupKey = `${pageKey}-${currentGroup}`;
  1168. groupSpans.value[groupKey] = {
  1169. span: groupCount,
  1170. index: groupStartIndex
  1171. };
  1172. // 标记最后一个分组的起始行(添加这行)
  1173. paginatedList.value[groupStartIndex].isGroupFirstRow = true;
  1174. }
  1175. };
  1176. // 运行时间周期 单行校验方法
  1177. const validateRunningTime = (row: IotMainWorkOrderBomVO) => {
  1178. if (row.runningTimeRule === 0 && (!row.nextRunningTime || row.nextRunningTime <= 0)) {
  1179. row.timeError = t('mainPlan.runningTimeCycleError');
  1180. return false;
  1181. }
  1182. row.timeError = '';
  1183. return true;
  1184. };
  1185. // 计算属性:获取当前页的数据
  1186. const paginatedList = computed(() => {
  1187. const start = (currentPage.value - 1) * pageSize.value
  1188. const end = start + pageSize.value
  1189. return list.value.slice(start, end)
  1190. })
  1191. // 页码改变处理
  1192. const handleCurrentChange = (newPage: number) => {
  1193. currentPage.value = newPage
  1194. }
  1195. // 每页条数改变处理(保持10条)
  1196. const handleSizeChange = (newSize: number) => {
  1197. pageSize.value = newSize
  1198. currentPage.value = 1 // 重置到第一页
  1199. }
  1200. // 处理状态变化
  1201. const handleStatusChange = (row: IotMainWorkOrderBomVO) => {
  1202. // 如果是从未完成(0)切换到完成(1)
  1203. if (row.status === 1 && row.initialStatus === 0) {
  1204. // 校验 mainRuntime
  1205. if (!validateMainRuntime(row)) {
  1206. message.error(`${row.deviceCode}-${formatMaintItemName(row.name)} ${t('mainPlan.mainRuntimeError')}`);
  1207. row.status = 0; // 重置状态
  1208. return;
  1209. }
  1210. // 校验 mainMileage
  1211. if (!validateMainMileage(row)) {
  1212. message.error(`${row.deviceCode}-${formatMaintItemName(row.name)} ${t('mainPlan.mainMileageError')}`);
  1213. row.status = 0; // 重置状态
  1214. return;
  1215. }
  1216. // 检查消耗物料规则:如果设置为不消耗物料(rule=1),则跳过物料校验
  1217. if (row.rule === 1) {
  1218. console.log(`保养项 ${row.name} 设置为不消耗物料,跳过物料校验`);
  1219. message.success(`${row.deviceCode}-${formatMaintItemName(row.name)} 已标记为完成`);
  1220. return;
  1221. }
  1222. // 校验物料数据
  1223. const isValid = validateBomItemMaterials(row.bomNodeId);
  1224. if (!isValid) {
  1225. // 获取无效物料信息用于提示
  1226. const invalidMaterials = getInvalidMaterialsInfo(row.bomNodeId);
  1227. const errorMessage = `请填写物料必填项\n无效物料:\n${invalidMaterials.join('\n')}`;
  1228. message.error(errorMessage);
  1229. // 重置状态为未完成
  1230. row.status = 0;
  1231. return;
  1232. }
  1233. }
  1234. console.log(`保养项 ${row.name} 状态改为: ${row.status === 1 ? '完成' : '未完成'}`);
  1235. // 这里可以添加状态变化后的业务逻辑
  1236. // 例如:如果状态变为完成,可以自动填充完成时间等
  1237. if (row.status === 1) {
  1238. // 保养完成时的逻辑
  1239. message.success(`${row.deviceCode}-${formatMaintItemName(row.name)} 已标记为完成`);
  1240. }
  1241. };
  1242. // 运行时间周期 全局校验方法(在submitForm中调用)
  1243. const validateAllRunningTimes = (): boolean => {
  1244. let isValid = true;
  1245. list.value.forEach(row => {
  1246. if (row.runningTimeRule === 0 && (!row.nextRunningTime || row.nextRunningTime <= 0)) {
  1247. isValid = false;
  1248. // 高亮标记错误行
  1249. row.timeError = t('mainPlan.runningTimeCycleError');
  1250. message.error(`${t('mainPlan.runningTimeCycleError')}: ${row.deviceCode}-${row.name}`);
  1251. }
  1252. });
  1253. return isValid;
  1254. };
  1255. // 校验 mainRuntime 的方法
  1256. const validateMainRuntime = (row: IotMainWorkOrderBomVO) => {
  1257. // 清除之前的错误
  1258. row.mainRuntimeError = '';
  1259. const mainRuntime = Number(row.mainRuntime) || 0;
  1260. const totalRunTime = Number(row.totalRunTime ?? row.tempTotalRunTime) || 0;
  1261. const lastRunningTime = Number(row.lastRunningTime) || 0;
  1262. // 如果 mainRuntime 为 0 或空,不显示错误(允许为空)如果设置了累计运行时长保养规则 mainRuntime 必填
  1263. /* if (!mainRuntime || mainRuntime === 0) {
  1264. return true;
  1265. } */
  1266. // 校验规则:lastRunningTime ≤ mainRuntime ≤ totalRunTime
  1267. if (mainRuntime < lastRunningTime) {
  1268. row.mainRuntimeError = `保养时累计运行时间不能小于上次保养时长(${lastRunningTime})`;
  1269. return false;
  1270. }
  1271. if (mainRuntime > totalRunTime) {
  1272. row.mainRuntimeError = `保养时累计运行时间不能大于累计运行时间(${totalRunTime})`;
  1273. return false;
  1274. }
  1275. // 校验通过时确保错误信息被清除
  1276. row.mainRuntimeError = '';
  1277. return true;
  1278. };
  1279. // 校验 mainMileage 的方法
  1280. const validateMainMileage = (row: IotMainWorkOrderBomVO) => {
  1281. // 清除之前的错误
  1282. row.mainMileageError = '';
  1283. const mainMileage = Number(row.mainMileage) || 0;
  1284. const totalMileage = Number(row.totalMileage ?? row.tempTotalMileage) || 0;
  1285. const lastRunningKilometers = Number(row.lastRunningKilometers) || 0;
  1286. // 如果 mainMileage 为 0 或空,不显示错误(允许为空)如果设置了累计运行时长保养规则 mainMileage 必填
  1287. /* if (!mainMileage || mainMileage === 0) {
  1288. return true;
  1289. } */
  1290. // 校验规则:lastRunningKilometers ≤ mainMileage ≤ totalMileage
  1291. if (mainMileage < lastRunningKilometers) {
  1292. row.mainMileageError = `保养时累计运行公里数不能小于上次保养里程数(${lastRunningKilometers})`;
  1293. return false;
  1294. }
  1295. if (mainMileage > totalMileage) {
  1296. row.mainMileageError = `保养时累计运行公里数不能大于累计运行公里数(${totalMileage})`;
  1297. return false;
  1298. }
  1299. // 校验通过时确保错误信息被清除
  1300. row.mainMileageError = '';
  1301. return true;
  1302. };
  1303. // 全局校验所有 mainRuntime
  1304. const validateAllMainRuntimes = (): boolean => {
  1305. let isValid = true;
  1306. list.value.forEach(row => {
  1307. if (!validateMainRuntime(row)) {
  1308. isValid = false;
  1309. }
  1310. });
  1311. return isValid;
  1312. };
  1313. // 全局校验所有 mainMileage
  1314. const validateAllMainMileages = (): boolean => {
  1315. let isValid = true;
  1316. list.value.forEach(row => {
  1317. if (!validateMainMileage(row)) {
  1318. isValid = false;
  1319. }
  1320. });
  1321. return isValid;
  1322. };
  1323. /** 新增物料 */
  1324. const handleAddMaterial = () => {
  1325. // 检查是否选中保养项
  1326. if (!currentBomItem.value) {
  1327. message.warning('请先在上方表格中选择一个保养项');
  1328. return;
  1329. }
  1330. // 创建空记录
  1331. const newMaterial = {
  1332. // 关联字段
  1333. deviceId: currentBomItem.value.deviceId,
  1334. bomNodeId: currentBomItem.value.bomNodeId,
  1335. // 不可编辑字段 - 设置为空
  1336. factory: '',
  1337. costCenter: '',
  1338. projectDepartment: '',
  1339. factoryId: undefined,
  1340. costCenterId: undefined,
  1341. storageLocationId: undefined,
  1342. totalInventoryQuantity: '',
  1343. // 可编辑字段 - 设置为初始值
  1344. materialCode: '',
  1345. materialName: '',
  1346. unit: '',
  1347. unitPrice: 0,
  1348. quantity: 0,
  1349. // 固定字段
  1350. materialSource: '手动添加',
  1351. // 临时标识(用于区分新记录)
  1352. isNew: true,
  1353. tempId: Date.now() + Math.random() // 临时ID用于唯一标识
  1354. };
  1355. // 生成唯一键
  1356. const uniqueKey = `${currentBomItem.value.bomNodeId}`;
  1357. // 更新 bomMaterialsMap
  1358. if (!bomMaterialsMap.value[uniqueKey]) {
  1359. bomMaterialsMap.value[uniqueKey] = [];
  1360. }
  1361. // 添加到物料映射表的最前面
  1362. bomMaterialsMap.value[uniqueKey].unshift(newMaterial);
  1363. // 关键修复:同步更新materialList引用(确保视图实时刷新)
  1364. if (!showAllMaterials.value) {
  1365. // 显示当前保养项物料时,强制让materialList指向最新的bomMaterialsMap数据
  1366. materialList.value = bomMaterialsMap.value[uniqueKey];
  1367. } else {
  1368. // 显示所有物料时,重新赋值触发响应式(因displayedMaterialList依赖bomMaterialsMap)
  1369. materialList.value = [...materialList.value];
  1370. }
  1371. message.success('已新增空白物料记录,请填写相关信息');
  1372. // 新增物料后重新计算费用
  1373. nextTick(() => {
  1374. calculateTotalCost();
  1375. });
  1376. };
  1377. const openMaterialForm = (row: any) => {
  1378. bomNodeId.value = row.bomNodeId;
  1379. console.log('这是一个对象:', row.bomNodeId)
  1380. const type = 'maintenance'
  1381. materialFormRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
  1382. }
  1383. // 查看当前保养项已经绑定的物料列表
  1384. const openDeviceBomMaterials = (row: any) => {
  1385. bomNodeId.value = row.bomNodeId;
  1386. const type = 'maintenance'
  1387. deviceBomMaterialsRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
  1388. }
  1389. // 刷新当前保养项的物料列表显示
  1390. const refreshCurrentBomItemMaterials = () => {
  1391. if (!currentBomItem.value) {
  1392. materialList.value = [];
  1393. return;
  1394. }
  1395. const uniqueKey = `${currentBomItem.value.bomNodeId}`;
  1396. // 确保从映射中获取最新数据
  1397. const currentMaterials = bomMaterialsMap.value[uniqueKey] || [];
  1398. // 强制更新 materialList 的引用,确保响应式更新
  1399. materialList.value = [...currentMaterials];
  1400. console.log('刷新物料列表:', {
  1401. currentBomItem: currentBomItem.value.name,
  1402. materialCount: materialList.value.length,
  1403. uniqueKey: uniqueKey,
  1404. showAllMaterials: showAllMaterials.value
  1405. });
  1406. }
  1407. const selectChoose = (selectedMaterial) => {
  1408. // 检查当前是否有选中的保养项
  1409. if (!currentBomItem.value) {
  1410. message.warning('请先选择一个保养项');
  1411. return;
  1412. }
  1413. const targetBomNodeId = currentBomItem.value.bomNodeId;
  1414. const targetDeviceId = currentBomItem.value.deviceId;
  1415. selectedMaterial.bomNodeId = bomNodeId.value
  1416. // 关联 bomNodeId
  1417. const processedMaterials = selectedMaterial.map(material => ({
  1418. ...material,
  1419. bomNodeId: targetBomNodeId, // 统一关联当前行的 bomNodeId
  1420. deviceId: targetDeviceId // 确保 deviceId 也正确关联
  1421. }));
  1422. // 生成唯一键
  1423. const uniqueKey = `${targetBomNodeId}`;
  1424. // 初始化物料映射(如果不存在)
  1425. if (!bomMaterialsMap.value[uniqueKey]) {
  1426. bomMaterialsMap.value[uniqueKey] = [];
  1427. }
  1428. // 避免重复添加
  1429. processedMaterials.forEach(newMaterial => {
  1430. // 检查是否已存在相同 工厂+成本中心+库存地点+bomNodeId + materialCode 的条目
  1431. const isExist = bomMaterialsMap.value[uniqueKey].some(item =>
  1432. item.bomNodeId === bomNodeId.value &&
  1433. item.materialCode === newMaterial.materialCode &&
  1434. item.factoryId === newMaterial.factoryId &&
  1435. item.costCenterId === newMaterial.costCenterId &&
  1436. item.storageLocationId === newMaterial.storageLocationId
  1437. );
  1438. if (!isExist) {
  1439. bomMaterialsMap.value[uniqueKey].push(newMaterial);
  1440. }
  1441. });
  1442. // 立即更新当前显示的物料列表
  1443. refreshCurrentBomItemMaterials();
  1444. // 选择物料后立即计算费用
  1445. nextTick(() => {
  1446. calculateTotalCost();
  1447. });
  1448. }
  1449. // 计算属性:判断是否需要显示运行时间周期列
  1450. const shouldShowRunningTimeColumn = computed(() => {
  1451. return list.value.some(item => item.runningTimeRule === 0);
  1452. });
  1453. /** 查看已经选择的物料 并编辑 */
  1454. const handleView = (row: IotMainWorkOrderBomVO) => {
  1455. currentBomNodeId.value = row.bomNodeId
  1456. drawerVisible.value = true
  1457. // 根据状态值设置是否隐藏额外列
  1458. hideExtraColumnsInDrawer.value = row.status === 1
  1459. console.log('当前bom节点:', currentBomNodeId.value)
  1460. }
  1461. // 计算保养金额
  1462. const calculateTotalCost = () => {
  1463. try {
  1464. // 创建保养项ID到消耗规则的映射
  1465. const ruleMap = new Map<number, number>();
  1466. list.value.forEach(item => {
  1467. ruleMap.set(item.bomNodeId, item.rule);
  1468. });
  1469. // 从 bomMaterialsMap 中获取所有保养项的所有物料
  1470. let allMaterials = [];
  1471. for (const key in bomMaterialsMap.value) {
  1472. allMaterials = allMaterials.concat(bomMaterialsMap.value[key]);
  1473. }
  1474. // 物料总金额 = ∑(单价 * 消耗数量)
  1475. const materialTotal = allMaterials.reduce((sum, item) => {
  1476. // 获取物料所属保养项的消耗规则
  1477. const rule = ruleMap.get(item.bomNodeId);
  1478. // 如果保养项设置为不消耗物料(rule=1),跳过计算
  1479. if (rule === 1) return sum;
  1480. const price = Number(item.unitPrice) || 0
  1481. const quantity = Number(item.quantity) || 0
  1482. return sum + (price * quantity)
  1483. }, 0)
  1484. // 保养费用 = 物料总金额
  1485. formData.value.cost = (materialTotal).toFixed(2)
  1486. console.log('计算保养费用:', {
  1487. 物料总数: allMaterials.length,
  1488. 参与计算的物料数: allMaterials.filter(item => {
  1489. const rule = ruleMap.get(item.bomNodeId);
  1490. return rule !== 1; // 过滤掉不消耗物料的项
  1491. }).length,
  1492. 总金额: materialTotal
  1493. });
  1494. } catch (error) {
  1495. console.error('计算保养费用错误:', error);
  1496. formData.value.cost = '0.00';
  1497. }
  1498. }
  1499. // 同时监听物料列表和保养项列表变化
  1500. watch(
  1501. [() => materialList.value, () => list.value],
  1502. () => {
  1503. calculateTotalCost();
  1504. },
  1505. { deep: true }
  1506. );
  1507. const handleInput = (value, obj) => {
  1508. // 1. 过滤非法字符(只允许数字和小数点)
  1509. let filtered = value.replace(/[^\d.]/g, '')
  1510. // 2. 处理多个小数点的情况
  1511. filtered = filtered.replace(/\.{2,}/g, '.')
  1512. // 3. 限制小数点后最多两位
  1513. let decimalParts = filtered.split('.')
  1514. if (decimalParts.length > 1) {
  1515. decimalParts = decimalParts.slice(0, 2)
  1516. filtered = decimalParts.join('.')
  1517. }
  1518. // 4. 处理以小数点开头的情况(自动补0)
  1519. if (filtered.startsWith('.')) {
  1520. filtered = '0' + filtered
  1521. }
  1522. // 5. 更新绑定值(同时处理连续输入多个0的情况)
  1523. formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
  1524. }
  1525. // 保存配置
  1526. const saveConfig = () => {
  1527. (configFormRef.value as any).validate((valid: boolean) => {
  1528. if (!valid) return
  1529. if (!configDialog.current) return
  1530. // 动态校验逻辑
  1531. const requiredFields = []
  1532. if (configDialog.current.mileageRule === 0) {
  1533. requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
  1534. }
  1535. if (configDialog.current.runningTimeRule === 0) {
  1536. requiredFields.push('nextRunningTime', 'timePeriodLead')
  1537. }
  1538. if (configDialog.current.naturalDateRule === 0) {
  1539. requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
  1540. }
  1541. const missingFields = requiredFields.filter(field =>
  1542. !configDialog.form[field as keyof typeof configDialog.form]
  1543. )
  1544. if (missingFields.length > 0) {
  1545. message.error('请填写所有必填项')
  1546. return
  1547. }
  1548. // 强制校验逻辑
  1549. if (configDialog.current.naturalDateRule === 0) {
  1550. if (!configDialog.form.lastNaturalDate) {
  1551. message.error('必须选择自然日期')
  1552. return
  1553. }
  1554. // 验证日期有效性
  1555. const dateValue = dayjs(configDialog.form.lastNaturalDate)
  1556. if (!dateValue.isValid()) {
  1557. message.error('日期格式不正确')
  1558. return
  1559. }
  1560. }
  1561. // 转换逻辑(关键修改)
  1562. const finalDate = configDialog.form.lastNaturalDate
  1563. ? dayjs(configDialog.form.lastNaturalDate).valueOf()
  1564. : null // 改为null而不是0
  1565. // 更新当前行的数据
  1566. Object.assign(configDialog.current, {
  1567. ...configDialog.form,
  1568. lastNaturalDate: finalDate
  1569. })
  1570. configDialog.visible = false
  1571. })
  1572. }
  1573. const queryParams = reactive({
  1574. workOrderId: id
  1575. })
  1576. // 获取指定bomNodeId的物料数量
  1577. const getMaterialCount = (bomNodeId: number) => {
  1578. const uniqueKey = `${bomNodeId}`;
  1579. const materials = bomMaterialsMap.value[uniqueKey] || [];
  1580. // 只计算有效的物料数量
  1581. return materials.filter(material =>
  1582. !isMaterialDisabled(material) &&
  1583. material.materialName &&
  1584. material.unit &&
  1585. (material.unitPrice || 0) > 0 &&
  1586. (material.quantity || 0) > 0
  1587. ).length;
  1588. }
  1589. // 校验保养项下物料数据的方法
  1590. const validateBomItemMaterials = (bomNodeId: number): boolean => {
  1591. const uniqueKey = `${bomNodeId}`;
  1592. const materials = bomMaterialsMap.value[uniqueKey] || [];
  1593. // 如果没有物料数据,直接返回true(允许完成)
  1594. if (materials.length === 0) {
  1595. return true;
  1596. }
  1597. // 检查所有物料的单价和消耗数量是否都大于0
  1598. const hasInvalidMaterial = materials.some(material => {
  1599. const unitPrice = Number(material.unitPrice) || 0;
  1600. const quantity = Number(material.quantity) || 0;
  1601. return unitPrice <= 0 || quantity <= 0;
  1602. });
  1603. return !hasInvalidMaterial;
  1604. };
  1605. // 获取校验失败的物料信息(用于错误提示)
  1606. const getInvalidMaterialsInfo = (bomNodeId: number): string[] => {
  1607. const uniqueKey = `${bomNodeId}`;
  1608. const materials = bomMaterialsMap.value[uniqueKey] || [];
  1609. const invalidMaterials = [];
  1610. materials.forEach(material => {
  1611. const unitPrice = Number(material.unitPrice) || 0;
  1612. const quantity = Number(material.quantity) || 0;
  1613. if (unitPrice <= 0 || quantity <= 0) {
  1614. invalidMaterials.push(`${material.materialName || material.materialCode} (单价: ${unitPrice}, 数量: ${quantity})`);
  1615. }
  1616. });
  1617. return invalidMaterials;
  1618. };
  1619. const handleDeleteMaterialInTable = (material) => {
  1620. // 确认删除对话框
  1621. message.confirm('确认删除该物料吗?', { title: '提示' }).then(() => {
  1622. const targetBomNodeId = material.bomNodeId;
  1623. // 修复:统一使用 bomNodeId 作为键(与新增物料保持一致)
  1624. const uniqueKey = `${targetBomNodeId}`;
  1625. console.log('删除物料调试信息:', {
  1626. material,
  1627. uniqueKey,
  1628. bomMaterialsMapKeys: Object.keys(bomMaterialsMap.value),
  1629. currentMaterials: bomMaterialsMap.value[uniqueKey] || []
  1630. });
  1631. // 从 bomMaterialsMap 中删除
  1632. if (bomMaterialsMap.value[uniqueKey]) {
  1633. let mapIndex = -1;
  1634. // 简化查找逻辑
  1635. if (material.isNew && material.tempId) {
  1636. // 新增物料:通过 tempId 查找
  1637. mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
  1638. item.isNew && item.tempId === material.tempId
  1639. );
  1640. } else {
  1641. // 已有物料:通过关键字段查找
  1642. mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
  1643. !item.isNew &&
  1644. item.factoryId === material.factoryId &&
  1645. item.costCenterId === material.costCenterId &&
  1646. item.storageLocationId === material.storageLocationId &&
  1647. item.materialCode === material.materialCode
  1648. );
  1649. }
  1650. if (mapIndex !== -1) {
  1651. // 执行删除
  1652. bomMaterialsMap.value[uniqueKey].splice(mapIndex, 1);
  1653. message.success('物料删除成功');
  1654. console.log('删除成功后的物料列表:', bomMaterialsMap.value[uniqueKey]);
  1655. // 关键修复:立即更新当前显示的物料列表
  1656. refreshCurrentBomItemMaterials();
  1657. // 删除物料后重新计算费用
  1658. nextTick(() => {
  1659. calculateTotalCost();
  1660. });
  1661. } else {
  1662. message.warning('未找到要删除的物料记录');
  1663. console.error('未找到匹配的物料记录:', material);
  1664. }
  1665. } else {
  1666. message.warning('未找到对应的物料列表');
  1667. }
  1668. }).catch(() => {
  1669. // 用户取消删除
  1670. });
  1671. };
  1672. const handleDeleteMaterial = (material) => {
  1673. // 根据唯一标识查找要删除的物料索引
  1674. const index = materialList.value.findIndex(item =>
  1675. item.bomNodeId === material.bomNodeId &&
  1676. item.factoryId === material.factoryId &&
  1677. item.costCenterId === material.costCenterId &&
  1678. item.storageLocationId === material.storageLocationId &&
  1679. item.materialCode === material.materialCode
  1680. );
  1681. if (index !== -1) {
  1682. materialList.value.splice(index, 1);
  1683. message.success('物料删除成功');
  1684. }
  1685. };
  1686. const close = () => {
  1687. delView(unref(currentRoute))
  1688. push({ name: 'IotMainWorkOrder', params:{}})
  1689. }
  1690. // 分组单元格类名方法
  1691. const groupCellClassName = ({ row, column }) => {
  1692. if (column.property === 'group' && row.isGroupFirstRow) {
  1693. return 'group-first-row';
  1694. }
  1695. return '';
  1696. };
  1697. /** 提交表单 */
  1698. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  1699. const submitForm = async () => {
  1700. // 校验表单
  1701. await formRef.value.validate()
  1702. // 运行时间周期全局校验
  1703. if (!validateAllRunningTimes()) {
  1704. return; // 校验失败则终止提交
  1705. }
  1706. // 保养时 累计运行时长 mainRuntime 全局校验
  1707. /* if (!validateAllMainRuntimes()) {
  1708. message.error(t('mainPlan.mainRuntimeError'));
  1709. return;
  1710. } */
  1711. // 保养时 累计运行公里数 mainMileage 全局校验
  1712. if (!validateAllMainMileages()) {
  1713. message.error(t('mainPlan.mainMileageError'));
  1714. return;
  1715. }
  1716. // 校验所有设置为完成状态的保养项的物料数据
  1717. const invalidBomItems = [];
  1718. list.value.forEach(row => {
  1719. // 只校验状态为完成(1)且消耗物料规则为消耗(0)的保养项
  1720. if (row.status === 1 && row.rule === '0') {
  1721. const isValid = validateBomItemMaterials(row.bomNodeId);
  1722. if (!isValid) {
  1723. invalidBomItems.push({
  1724. name: `${row.deviceCode}-${formatMaintItemName(row.name)}`,
  1725. invalidMaterials: getInvalidMaterialsInfo(row.bomNodeId)
  1726. });
  1727. }
  1728. }
  1729. });
  1730. // 如果有校验失败的保养项,显示错误信息并终止提交
  1731. if (invalidBomItems.length > 0) {
  1732. let errorMessage = '请填写物料必填项\n\n';
  1733. invalidBomItems.forEach((item, index) => {
  1734. errorMessage += `${index + 1}. ${item.name}:\n`;
  1735. item.invalidMaterials.forEach(material => {
  1736. errorMessage += ` - ${material}\n`;
  1737. });
  1738. errorMessage += '\n';
  1739. });
  1740. message.error(errorMessage);
  1741. return;
  1742. }
  1743. // 校验表格数据
  1744. const isValid = validateTableData()
  1745. if (!isValid) return
  1746. // 提交请求
  1747. formLoading.value = true
  1748. try {
  1749. const convertedList = list.value.map(item => ({
  1750. ...item,
  1751. lastNaturalDate: typeof item.lastNaturalDate === 'number'
  1752. ? item.lastNaturalDate
  1753. : (item.lastNaturalDate ? dayjs(item.lastNaturalDate).valueOf() : null)
  1754. }));
  1755. // 从 bomMaterialsMap 获取所有物料数据,而不是 materialList
  1756. const allMaterials = [];
  1757. for (const key in bomMaterialsMap.value) {
  1758. allMaterials.push(...bomMaterialsMap.value[key]);
  1759. }
  1760. const data = {
  1761. mainWorkOrder: formData.value,
  1762. mainWorkOrderBom: convertedList,
  1763. mainWorkOrderMaterials: allMaterials
  1764. }
  1765. await IotMainWorkOrderApi.modifyWorkOrder(data)
  1766. message.success(t('common.updateSuccess'))
  1767. close()
  1768. // 发送操作成功的事件
  1769. emit('success')
  1770. } finally {
  1771. formLoading.value = false
  1772. }
  1773. }
  1774. const validateDelayReason = (rule: any, value: any, callback: any) => {
  1775. const form = configDialog.form
  1776. const hasDelay =
  1777. (form.delayKilometers > 0) ||
  1778. (form.delayDuration > 0) ||
  1779. (form.delayNaturalDate > 0)
  1780. if (hasDelay && (!value || value.trim() === '')) {
  1781. callback(new Error('请填写推迟原因'))
  1782. } else {
  1783. callback()
  1784. }
  1785. }
  1786. // 新增表单校验规则
  1787. const configFormRules = reactive({
  1788. nextRunningKilometers: [{
  1789. required: true,
  1790. message: '里程周期必须填写',
  1791. trigger: 'blur'
  1792. }],
  1793. kiloCycleLead: [{
  1794. required: true,
  1795. message: '提前量必须填写',
  1796. trigger: 'blur'
  1797. }],
  1798. nextRunningTime: [{
  1799. required: true,
  1800. message: '时间周期必须填写',
  1801. trigger: 'blur'
  1802. }],
  1803. timePeriodLead: [{
  1804. required: true,
  1805. message: '提前量必须填写',
  1806. trigger: 'blur'
  1807. }],
  1808. nextNaturalDate: [{
  1809. required: true,
  1810. message: '自然日周期必须填写',
  1811. trigger: 'blur'
  1812. }],
  1813. naturalDatePeriodLead: [{
  1814. required: true,
  1815. message: '提前量必须填写',
  1816. trigger: 'blur'
  1817. }],
  1818. // 新增推迟原因验证规则
  1819. delayReason: [
  1820. { validator: validateDelayReason, trigger: ['blur', 'change'] }
  1821. ]
  1822. })
  1823. // 计算文本宽度的辅助函数
  1824. const getTextWidth = (text: string, fontSize = 14): number => {
  1825. if (!text) return 0;
  1826. const span = document.createElement('span')
  1827. span.style.visibility = 'hidden'
  1828. span.style.position = 'absolute'
  1829. span.style.whiteSpace = 'nowrap'
  1830. span.style.fontSize = `${fontSize}px` // 与表格实际字体一致
  1831. span.style.fontFamily = 'inherit'
  1832. span.style.fontWeight = 'normal';
  1833. span.innerText = text
  1834. document.body.appendChild(span)
  1835. const width = span.offsetWidth
  1836. document.body.removeChild(span)
  1837. return width
  1838. }
  1839. // 计算列宽的主函数
  1840. const calculateMaintItemsWidth = () => {
  1841. if (list.value.length === 0) {
  1842. maintItemsWidth.value = 'auto'
  1843. return
  1844. }
  1845. // 1. 计算表头文本宽度
  1846. const headerText = t('mainPlan.MaintItems')
  1847. const headerWidth = getTextWidth(headerText)
  1848. // 2. 计算内容最大宽度
  1849. let contentMaxWidth = 0
  1850. list.value.forEach(item => {
  1851. if (item.name) {
  1852. const width = getTextWidth(item.name.toString())
  1853. if (width > contentMaxWidth) {
  1854. contentMaxWidth = width
  1855. }
  1856. }
  1857. })
  1858. // 3. 取最大值 + 内边距(20px)
  1859. const maxWidth = Math.max(headerWidth, contentMaxWidth) + 20
  1860. maintItemsWidth.value = `${maxWidth}px`
  1861. }
  1862. // 计算下次保养公里数(通用函数)
  1863. const calculateNextMaintenanceKm = (row: IotMaintenanceBomVO) => {
  1864. // 验证条件:规则开启 + 两个值都存在且 > 0
  1865. const isValid = row.mileageRule === 0 &&
  1866. row.lastRunningKilometers > 0 &&
  1867. row.nextRunningKilometers > 0;
  1868. return isValid
  1869. ? (row.lastRunningKilometers + row.nextRunningKilometers)
  1870. : null; // 不满足条件返回null
  1871. };
  1872. // 计算剩余保养公里数(通用函数)
  1873. const calculateRemainKm = (row: IotMaintenanceBomVO) => {
  1874. // 确定使用的里程值(优先totalMileage)
  1875. const mileageValue = row.totalMileage ?? row.tempTotalMileage;
  1876. // 验证条件:规则开启 + 3个值都存在且 > 0
  1877. const isValid = row.mileageRule === 0 &&
  1878. row.lastRunningKilometers > 0 &&
  1879. mileageValue > 0 &&
  1880. row.nextRunningKilometers > 0;
  1881. if (!isValid) return 0;
  1882. const result = row.nextRunningKilometers - (mileageValue - row.lastRunningKilometers);
  1883. return parseFloat(result.toFixed(2));
  1884. };
  1885. // 计算下次保养运行时长(通用函数)
  1886. const calculateNextMaintenanceH = (row: IotMaintenanceBomVO) => {
  1887. // 验证条件:规则开启 + 两个值都存在且 > 0
  1888. const isValid = row.runningTimeRule === 0 &&
  1889. row.lastRunningTime > 0 &&
  1890. row.nextRunningTime > 0;
  1891. return isValid
  1892. ? (row.lastRunningTime + row.nextRunningTime)
  1893. : null; // 不满足条件返回null
  1894. };
  1895. // 计算剩余运行时间(通用函数)
  1896. const calculateRemainH = (row: IotMaintenanceBomVO) => {
  1897. // 确定使用的 运行时长 值(优先 totalRunTime)
  1898. const runTimeValue = row.totalRunTime ?? row.tempTotalRunTime;
  1899. // 验证条件:规则开启 + 3个值都存在且 > 0
  1900. const isValid = row.runningTimeRule === 0 &&
  1901. row.lastRunningTime > 0 &&
  1902. runTimeValue > 0 &&
  1903. row.nextRunningTime > 0;
  1904. if (!isValid) return 0;
  1905. const result = row.nextRunningTime - (runTimeValue - row.lastRunningTime);
  1906. return parseFloat(result.toFixed(2));
  1907. };
  1908. // 计算下次保养日期(通用函数)
  1909. const calculateNextMaintenanceDate = (row: IotMaintenanceBomVO) => {
  1910. // 验证条件:规则开启 + 两个值都存在且 > 0
  1911. const isValid = row.naturalDateRule === 0 &&
  1912. row.lastNaturalDate &&
  1913. row.nextNaturalDate;
  1914. return isValid
  1915. ? dayjs(row.lastNaturalDate).add(row.nextNaturalDate, 'day').format('YYYY-MM-DD')
  1916. : null; // 不满足条件返回null
  1917. };
  1918. // 计算 自然日期保养 剩余天数(通用函数)
  1919. const calculateRemainDay = (row: IotMaintenanceBomVO) => {
  1920. // 验证条件:规则开启 + 两个值都存在且有效
  1921. const isValid = row.naturalDateRule === 0 &&
  1922. row.lastNaturalDate !== null &&
  1923. row.nextNaturalDate !== null &&
  1924. row.nextNaturalDate > 0;
  1925. if (!isValid) {
  1926. return '-';
  1927. }
  1928. try {
  1929. // 上次保养日期:将时间戳转换为 Day.js 对象
  1930. const lastNaturalDate = dayjs(row.lastNaturalDate);
  1931. // 计算下次保养日期
  1932. const nextMaintenanceDate = lastNaturalDate.add(row.nextNaturalDate, 'day');
  1933. // 计算剩余天数(当前日期到下次保养日期的天数差)
  1934. return nextMaintenanceDate.diff(dayjs(), 'day');
  1935. } catch (error) {
  1936. console.error('计算保养剩余天数错误:', error);
  1937. return null;
  1938. }
  1939. };
  1940. const getStatusText = (row: any) => {
  1941. // 状态为1直接返回"完成"
  1942. if (row.status === 1) return t('mainPlan.completed');
  1943. // 状态为0时判断延迟字段
  1944. const delayDuration = Number(row.delayDuration) || 0;
  1945. const delayKilometers = Number(row.delayKilometers) || 0;
  1946. const delayNaturalDate = Number(row.delayNaturalDate) || 0;
  1947. // 任意延迟字段大于0 -> 延时
  1948. if (delayDuration > 0 || delayKilometers > 0 || delayNaturalDate > 0) {
  1949. return t('mainPlan.delayed');
  1950. }
  1951. // 否则显示保养中
  1952. return t('mainPlan.maintaining');
  1953. };
  1954. // 计算属性 - 检查当前页是否有开启的里程规则
  1955. const hasMileageRuleInCurrentPage = computed(() => {
  1956. return paginatedList.value.some(row => row.mileageRule === 0);
  1957. });
  1958. // 计算属性 - 检查当前页是否有开启的 运行时间 规则
  1959. const hasTimeRuleInCurrentPage = computed(() => {
  1960. return paginatedList.value.some(row => row.runningTimeRule === 0);
  1961. });
  1962. // 计算属性 - 检查当前页是否有开启的 自然日期 规则
  1963. const hasDateRuleInCurrentPage = computed(() => {
  1964. return paginatedList.value.some(row => row.naturalDateRule === 0);
  1965. });
  1966. // 统一计算所有列宽
  1967. const calculateAllColumnsWidth = () => {
  1968. const MIN_WIDTH = 70; // 最小列宽
  1969. const PADDING = 10; // 列内边距
  1970. const FIXED_COLUMN_PADDING = 10; // 固定列额外内边距
  1971. const GROUP_COLUMN_EXTRA = 10; // 分组列额外宽度
  1972. // 需要自适应的列配置
  1973. const autoColumns = [
  1974. { prop: 'serial', label: t('iotDevice.serial') },
  1975. // { prop: 'deviceCode', label: t('iotMaintain.deviceCode') },
  1976. // { prop: 'deviceName', label: t('iotMaintain.deviceName') },
  1977. { prop: 'group', label: t('mainPlan.MaintItemsGroup') },
  1978. {
  1979. prop: 'totalRunTime',
  1980. label: t('operationFillForm.sumTime'),
  1981. getValue: (row) => row.totalRunTime ?? row.tempTotalRunTime
  1982. },
  1983. {
  1984. prop: 'totalMileage',
  1985. label: t('operationFillForm.sumKil'),
  1986. getValue: (row) => row.totalMileage ?? row.tempTotalMileage
  1987. },
  1988. {
  1989. prop: 'mainRuntime',
  1990. label: t('operationFillForm.mainSumTime'),
  1991. getValue: (row) => row.mainRuntime
  1992. },
  1993. {
  1994. prop: 'mainMileage',
  1995. label: t('operationFillForm.mainSumKil'),
  1996. getValue: (row) => row.mainMileage
  1997. },
  1998. { prop: 'name', label: t('bomList.bomNode') },
  1999. { prop: 'lastMaintenanceDate', label: t('mainPlan.lastMaintenanceDate') },
  2000. { prop: 'mileageRule', label: t('main.mileage') },
  2001. { prop: 'runningTimeRule', label: t('main.runTime') },
  2002. { prop: 'naturalDateRule', label: t('main.date') },
  2003. { prop: 'lastRunningKilometers', label: t('mainPlan.lastMaintenanceMileage') },
  2004. { prop: 'nextMaintenanceKm', label: t('mainPlan.nextMaintenanceKm') },
  2005. { prop: 'remainKm', label: t('mainPlan.remainKm') },
  2006. { prop: 'lastRunningTime', label: t('mainPlan.lastMaintenanceOperationTime') },
  2007. { prop: 'nextMaintenanceH', label: t('mainPlan.nextMaintenanceH') },
  2008. { prop: 'remainH', label: t('mainPlan.remainH') },
  2009. { prop: 'nextRunningTime', label: t('mainPlan.RunTimeCycle') },
  2010. { prop: 'tempLastNaturalDate', label: t('mainPlan.lastMaintenanceNaturalDate') },
  2011. { prop: 'nextMaintenanceDate', label: t('mainPlan.nextMaintDate') },
  2012. { prop: 'remainDay', label: t('mainPlan.remainDay') },
  2013. { prop: 'rule', label: t('mainPlan.consumeMaterials') }, // 消耗物料规则列
  2014. { prop: 'operation', label: t('operationFill.operation') }
  2015. ];
  2016. const newWidths: Record<string, number> = {};
  2017. autoColumns.forEach(col => {
  2018. const headerText = col.label;
  2019. // 计算表头宽度
  2020. const headerWidth = getTextWidth(headerText) * 1.2;
  2021. // 计算内容最大宽度
  2022. let contentMaxWidth = 0;
  2023. if (col.prop === 'operation') {
  2024. // 操作列固定宽度(根据按钮数量)
  2025. contentMaxWidth = 50;
  2026. } else if (['mileageRule', 'runningTimeRule', 'naturalDateRule', 'rule'].includes(col.prop)) {
  2027. // 开关列固定宽度
  2028. contentMaxWidth = 80;
  2029. } else {
  2030. list.value.forEach(row => {
  2031. const text = col.getValue ? String(col.getValue(row)) : String(row[col.prop] || '');
  2032. const textWidth = getTextWidth(text);
  2033. if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
  2034. });
  2035. }
  2036. // 取最大值并添加内边距
  2037. let finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
  2038. // 为分组列增加额外宽度
  2039. if ([
  2040. 'group',
  2041. 'lastRunningKilometers',
  2042. 'nextMaintenanceKm',
  2043. 'remainKm',
  2044. 'lastRunningTime',
  2045. 'nextMaintenanceH',
  2046. 'remainH',
  2047. 'tempLastNaturalDate',
  2048. 'nextMaintenanceDate',
  2049. 'remainDay'
  2050. ].includes(col.prop)) {
  2051. finalWidth += GROUP_COLUMN_EXTRA;
  2052. }
  2053. newWidths[col.prop] = finalWidth;
  2054. });
  2055. // 固定列特殊处理 - 增加额外空间
  2056. ['serial', 'name'].forEach(prop => {
  2057. if (newWidths[prop]) {
  2058. newWidths[prop] += FIXED_COLUMN_PADDING;
  2059. }
  2060. });
  2061. // 转换为CSS宽度值
  2062. Object.keys(newWidths).forEach(prop => {
  2063. columnWidths.value[prop] = `${newWidths[prop]}px`;
  2064. });
  2065. };
  2066. // 为每一行建立lastNaturalDate到tempLastNaturalDate的同步
  2067. const setupNaturalDateSync = (row: IotMaintenanceBomVO) => {
  2068. // 如果该行已有watcher则跳过
  2069. if (lastNaturalDateWatchers.value.has(row.id)) return
  2070. // 为该行创建单独的watcher
  2071. const unwatch = watch(
  2072. () => row.lastNaturalDate,
  2073. (newVal) => {
  2074. // 转换日期格式 (时间戳 -> YYYY-MM-DD)
  2075. row.tempLastNaturalDate = newVal
  2076. ? dayjs(newVal).format('YYYY-MM-DD')
  2077. : null;
  2078. },
  2079. { immediate: true, deep: true }
  2080. )
  2081. // 保存watcher用于后续清理
  2082. lastNaturalDateWatchers.value.set(row.id, unwatch)
  2083. }
  2084. const tableHeaderStyle = ({ row, rowIndex }) => {
  2085. return {
  2086. border: '1px solid #333',
  2087. backgroundColor: rowIndex === 0 ? '#f0f9eb' : '#f5f7fa' // 分组行特殊背景
  2088. }
  2089. }
  2090. // 更新列表数据的函数
  2091. const updateListWithCumulativeData = (cumulativeData: any[]) => {
  2092. cumulativeData.forEach(item => {
  2093. // 根据 bomNodeId 找到对应的行
  2094. const targetRow = list.value.find(row => row.bomNodeId === item.bomNodeId)
  2095. if (targetRow) {
  2096. // 按照优先级设置 mainRuntime 字段的值 保养时长
  2097. // 优先级:mainRuntime > tempMainRunTime
  2098. if (item.mainRuntime && Number(item.mainRuntime) > 0) {
  2099. // 如果 mainRuntime 有值且大于0,使用 mainRuntime
  2100. targetRow.mainRuntime = Number(item.mainRuntime)
  2101. } else if (item.tempMainRunTime && Number(item.tempMainRunTime) > 0) {
  2102. // 如果 mainRuntime 没有有效值,但 tempMainRunTime 有值且大于0,使用 tempMainRunTime
  2103. targetRow.mainRuntime = Number(item.tempMainRunTime)
  2104. } else {
  2105. // 两个字段都没有有效值,保持原值或设置为0
  2106. targetRow.mainRuntime = 0
  2107. }
  2108. // 按照优先级设置 mainMileage 字段的值 保养里程数
  2109. // 优先级:mainMileage > tempMainMileage
  2110. if (item.mainMileage && Number(item.mainMileage) > 0) {
  2111. // 如果 mainMileage 有值且大于0,使用 mainMileage
  2112. targetRow.mainMileage = Number(item.mainMileage)
  2113. } else if (item.tempMainMileage && Number(item.tempMainMileage) > 0) {
  2114. // 如果 mainMileage 没有有效值,但 tempMainMileage 有值且大于0,使用 tempMainMileage
  2115. targetRow.mainMileage = Number(item.tempMainMileage)
  2116. } else {
  2117. // 两个字段都没有有效值,保持原值或设置为0
  2118. targetRow.mainMileage = 0
  2119. }
  2120. // 更新数据后立即重新校验,清除可能的错误状态
  2121. nextTick(() => {
  2122. validateMainRuntime(targetRow)
  2123. })
  2124. }
  2125. })
  2126. }
  2127. const fetchCumulativeData = async () => {
  2128. if (!formData.value.actualStartTime) {
  2129. console.warn('actualStartTime is empty, skip fetching cumulative data');
  2130. return;
  2131. }
  2132. try {
  2133. // 确保时间戳格式正确
  2134. const startTime = Number(formData.value.actualStartTime);
  2135. if (isNaN(startTime)) {
  2136. console.error('Invalid actualStartTime:', formData.value.actualStartTime);
  2137. return;
  2138. }
  2139. const queryParams = {
  2140. workOrderId: id,
  2141. actualStartTime: dayjs(startTime).format('YYYY-MM-DD')
  2142. }
  2143. console.log('Fetching cumulative data with params:', queryParams);
  2144. const response = await IotMainWorkOrderBomApi.maintenanceCumulativeValue(queryParams)
  2145. if (response && Array.isArray(response)) {
  2146. updateListWithCumulativeData(response);
  2147. // 在所有数据更新完成后,重新校验所有行的 mainRuntime
  2148. nextTick(() => {
  2149. list.value.forEach(row => {
  2150. // 校验 保养时累计运行时长
  2151. if (row.mainRuntime) {
  2152. validateMainRuntime(row);
  2153. }
  2154. // 校验 保养时累计运行公里数
  2155. if (row.mainMileage) {
  2156. validateMainMileage(row);
  2157. }
  2158. });
  2159. });
  2160. message.success('累计运行数据更新成功');
  2161. }
  2162. } catch (error) {
  2163. console.error('获取累计运行数据失败:', error);
  2164. message.error('获取累计运行数据失败');
  2165. }
  2166. }
  2167. // 监听保养开始时间变化
  2168. watch(
  2169. () => formData.value.actualStartTime,
  2170. async (newVal, oldVal) => {
  2171. console.log('actualStartTime changed:', { newVal, oldVal });
  2172. if (newVal && newVal !== oldVal) {
  2173. // await fetchCumulativeData();
  2174. }
  2175. },
  2176. { immediate: false, deep: true }
  2177. );
  2178. // 原有代码保持不变,新增“选择物料”按钮的点击处理方法
  2179. const handleSelectMaterial = () => {
  2180. // 校验是否已选中保养项(避免无选中项时点击按钮报错)
  2181. if (!currentBomItem.value) {
  2182. message.warning('请先在上方表格中选择一个保养项');
  2183. return;
  2184. }
  2185. // 调用现有方法打开物料选择窗口,传入当前选中的保养项
  2186. openMaterialForm(currentBomItem.value);
  2187. };
  2188. // Element UI表格当前行变化事件
  2189. const handleCurrentChangeTable = (currentRow) => {
  2190. if (currentRow) {
  2191. handleBomItemClick(currentRow);
  2192. }
  2193. };
  2194. // 监听分页数据和规则变化 - 重新布局表格
  2195. watch([paginatedList, hasMileageRuleInCurrentPage, hasTimeRuleInCurrentPage, hasDateRuleInCurrentPage], () => {
  2196. nextTick(() => {
  2197. tableRef.value?.doLayout();
  2198. calculateAllColumnsWidth(); // 重新计算列宽
  2199. });
  2200. });
  2201. // 监听分页变化时重新计算分组信息
  2202. watch([paginatedList, currentPage], () => {
  2203. calculateGroupSpans();
  2204. calculateAllColumnsWidth();
  2205. tableRef.value?.doLayout(); // 确保表格重新布局
  2206. });
  2207. // 监听分页数据变化,自动选中第一条
  2208. watch(() => paginatedList.value, (newList) => {
  2209. if (newList && newList.length > 0) {
  2210. // 使用Element UI的方法设置当前行
  2211. nextTick(() => {
  2212. if (mainTableRef.value) {
  2213. mainTableRef.value.setCurrentRow(newList[0]);
  2214. }
  2215. });
  2216. // 选中新分页的第一条
  2217. handleBomItemClick(newList[0]);
  2218. } else {
  2219. // 如果没有数据,清空选中状态
  2220. currentBomItem.value = null;
  2221. materialList.value = [];
  2222. }
  2223. }, { immediate: true });
  2224. // 下拉菜单命令处理
  2225. const handleDropdownCommand = (command: { action: string; row: IotMainWorkOrderBomVO }) => {
  2226. switch (command.action) {
  2227. case 'delay':
  2228. openConfigDialog(command.row);
  2229. break;
  2230. case 'material':
  2231. openMaterialForm(command.row);
  2232. break;
  2233. case 'deviceBomMaterials':
  2234. openDeviceBomMaterials(command.row);
  2235. break;
  2236. case 'detail':
  2237. handleView(command.row);
  2238. break;
  2239. }
  2240. };
  2241. /** 校验表格数据 */
  2242. const validateTableData = (): boolean => {
  2243. let isValid = true;
  2244. const errorMessages: string[] = []; // 通用错误集合
  2245. const materialRequiredErrors: string[] = []; // 物料缺失专用错误集合
  2246. // 1. 基础校验:工单明细是否存在
  2247. if (list.value.length === 0) {
  2248. message.error('工单明细不存在');
  2249. return false;
  2250. }
  2251. // 2. 校验设备状态
  2252. list.value.forEach((row, index) => {
  2253. const rowNumber = index + 1;
  2254. if (row.deviceCode === null || row.deviceName === null) {
  2255. errorMessages.push(`第${rowNumber}行设备状态错误`);
  2256. isValid = false;
  2257. }
  2258. });
  2259. // 3. 校验物料必填(仅在非委外模式下)
  2260. if (formData.value.outsourcingFlag !== 1) {
  2261. list.value.forEach((row, index) => {
  2262. const rowNumber = index + 1;
  2263. const deviceIdentifier = `${row.deviceCode}-${formatMaintItemName(row.name)}`;
  2264. // 检查消耗物料规则,如果为1(不消耗物料)则跳过校验
  2265. if (row.rule === 1) {
  2266. return; // 跳过当前保养项的物料校验
  2267. }
  2268. // 检查是否设置了推迟保养
  2269. const hasDelay =
  2270. (row.delayKilometers || 0) > 0 ||
  2271. (row.delayNaturalDate || 0) > 0 ||
  2272. (row.delayDuration || 0) > 0;
  2273. // 未设置推迟保养时,需要校验物料
  2274. if (!hasDelay) {
  2275. // 使用 bomMaterialsMap 获取物料,更可靠
  2276. const uniqueKey = `${row.bomNodeId}`;
  2277. const materials = bomMaterialsMap.value[uniqueKey] || [];
  2278. // 过滤有效的物料(非禁用、非无效状态)
  2279. const validMaterials = materials.filter(material =>
  2280. material.materialName && // 物料名称不为空
  2281. material.unit && // 单位不为空
  2282. (material.unitPrice || 0) > 0 && // 单价大于0
  2283. (material.quantity || 0) > 0 // 数量大于0
  2284. );
  2285. if (validMaterials.length === 0) {
  2286. materialRequiredErrors.push(`第${rowNumber}行【${deviceIdentifier}】未添加有效物料`);
  2287. isValid = false;
  2288. }
  2289. }
  2290. });
  2291. }
  2292. // 4. 智能错误提示
  2293. if (!isValid) {
  2294. // 构建错误消息HTML
  2295. let errorHtml = '';
  2296. // 添加通用错误
  2297. if (errorMessages.length > 0) {
  2298. errorHtml += errorMessages.join('<br>');
  2299. }
  2300. // 添加物料错误(带智能截断)
  2301. if (materialRequiredErrors.length > 0) {
  2302. if (errorHtml) errorHtml += '<br>'; // 添加换行分隔
  2303. if (materialRequiredErrors.length > 3) {
  2304. errorHtml += materialRequiredErrors.slice(0, 3).join('<br>');
  2305. errorHtml += `<br>...等共 ${materialRequiredErrors.length} 个保养项未添加物料`;
  2306. } else {
  2307. errorHtml += materialRequiredErrors.join('<br>');
  2308. }
  2309. }
  2310. // 添加标题
  2311. const title = "<span style='font-weight:bold;color:#f56c6c'></span>";
  2312. errorHtml = title + errorHtml;
  2313. // 显示带格式的错误消息
  2314. message.error({
  2315. message: errorHtml,
  2316. dangerouslyUseHTMLString: true,
  2317. duration: 8000 // 延长显示时间
  2318. });
  2319. }
  2320. return isValid;
  2321. };
  2322. /** 重置表单 */
  2323. const resetForm = () => {
  2324. formData.value = {
  2325. id: undefined,
  2326. deviceId: undefined,
  2327. status: undefined,
  2328. description: undefined,
  2329. pic: undefined,
  2330. remark: undefined,
  2331. deviceName: undefined,
  2332. processInstanceId: undefined,
  2333. auditStatus: undefined,
  2334. deptId: undefined
  2335. }
  2336. formRef.value?.resetFields()
  2337. }
  2338. // 防抖函数实现
  2339. function debounce(func: Function, wait: number) {
  2340. let timeout: ReturnType<typeof setTimeout> | null
  2341. return function executedFunction(...args: any[]) {
  2342. const later = () => {
  2343. clearTimeout(timeout!)
  2344. func(...args)
  2345. }
  2346. clearTimeout(timeout!)
  2347. timeout = setTimeout(later, wait)
  2348. }
  2349. }
  2350. // 响应窗口大小变化
  2351. const handleResize = debounce(calculateMaintItemsWidth, 300)
  2352. onMounted(async () => {
  2353. materialList.value = []
  2354. const deptId = useUserStore().getUser.deptId
  2355. // 查询当前登录人所属部门名称
  2356. dept.value = await DeptApi.getDept(deptId)
  2357. deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
  2358. formData.value.deptId = deptId
  2359. try{
  2360. formType.value = 'update'
  2361. // 查询保养工单 主表数据
  2362. const workOrder = await IotMainWorkOrderApi.getIotMainWorkOrder(id);
  2363. formData.value = workOrder
  2364. // 查询保养工单 明细数据
  2365. const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams);
  2366. list.value = []
  2367. if (Array.isArray(data)) {
  2368. list.value = data.map(item => {
  2369. // 记录初始状态,用于判断是否可编辑
  2370. const initialStatus = item.status;
  2371. // 提取分组名称
  2372. const group = item.name && item.name.includes('->')
  2373. ? item.name.split('->')[0].trim()
  2374. : '';
  2375. if (item.mileageRule === 0) {
  2376. item.nextMaintenanceKm = calculateNextMaintenanceKm(item);
  2377. item.remainKm = calculateRemainKm(item);
  2378. }
  2379. if (item.runningTimeRule === 0) {
  2380. item.nextMaintenanceH = calculateNextMaintenanceH(item);
  2381. item.remainH = calculateRemainH(item);
  2382. }
  2383. if (item.naturalDateRule === 0) {
  2384. item.nextMaintenanceDate = calculateNextMaintenanceDate(item);
  2385. item.remainDay = calculateRemainDay(item);
  2386. }
  2387. setupNaturalDateSync(item);
  2388. return {
  2389. ...item,
  2390. group,
  2391. initialStatus, // 保存初始状态
  2392. lastNaturalDate: item.lastNaturalDate,
  2393. lastMaintenanceDate: item.lastMaintenanceDate
  2394. ? dayjs(item.lastMaintenanceDate).format("YYYY-MM-DD") // 时间戳 → 日期字符串
  2395. : null, // 处理空值
  2396. // 设置consumableRule默认值,如果后端没有返回则默认为0
  2397. rule: item.rule !== undefined ? Number(item.rule) : 0
  2398. }
  2399. })
  2400. // 设备信息提取逻辑
  2401. if (list.value.length > 0) {
  2402. // 从第一个保养项提取设备信息(所有保养项共享相同设备)
  2403. formData.value.deviceCode = list.value[0].deviceCode;
  2404. formData.value.deviceName = list.value[0].deviceName;
  2405. }
  2406. }
  2407. // 查询当前保养工单已经关联的所有物料
  2408. const materials = await IotMainWorkOrderBomMaterialApi.getWorkOrderBomMaterials(queryParams);
  2409. // 重新初始化物料映射,确保接口数据完全覆盖
  2410. const tempBomMaterialsMap = {};
  2411. // 首先,用 getWorkOrderBomMaterials 返回的数据初始化映射(主数据源)
  2412. if (Array.isArray(materials) && materials.length > 0) {
  2413. materials.forEach(material => {
  2414. const uniqueKey = `${material.bomNodeId}`;
  2415. if (!tempBomMaterialsMap[uniqueKey]) {
  2416. tempBomMaterialsMap[uniqueKey] = [];
  2417. }
  2418. // 通过 bomNodeId 在 list 中查找对应的保养项,获取 deviceId
  2419. const correspondingItem = list.value.find(item => item.bomNodeId === material.bomNodeId);
  2420. const deviceId = correspondingItem ? correspondingItem.deviceId : null;
  2421. const materialWithDeviceId = {
  2422. ...material,
  2423. deviceId: deviceId
  2424. };
  2425. // 避免重复添加
  2426. const existingIndex = tempBomMaterialsMap[uniqueKey].findIndex(item =>
  2427. item.materialCode === material.materialCode &&
  2428. item.factoryId === material.factoryId &&
  2429. item.costCenterId === material.costCenterId &&
  2430. item.storageLocationId === material.storageLocationId
  2431. );
  2432. if (existingIndex === -1) {
  2433. tempBomMaterialsMap[uniqueKey].push(materialWithDeviceId);
  2434. } else {
  2435. tempBomMaterialsMap[uniqueKey][existingIndex] = materialWithDeviceId;
  2436. }
  2437. });
  2438. }
  2439. // 其次,对于 getWorkOrderBOMs 接口返回的 deviceBomMaterials,
  2440. // 只有当 getWorkOrderBomMaterials 中没有对应保养项的物料时,才使用 deviceBomMaterials 作为备用
  2441. list.value.forEach(item => {
  2442. const uniqueKey = `${item.bomNodeId}`;
  2443. // 如果 getWorkOrderBomMaterials 中没有这个保养项的物料,且 deviceBomMaterials 有值,则使用备用数据
  2444. if ((!tempBomMaterialsMap[uniqueKey] || tempBomMaterialsMap[uniqueKey].length === 0) &&
  2445. item.deviceBomMaterials && item.deviceBomMaterials.length > 0) {
  2446. tempBomMaterialsMap[uniqueKey] = item.deviceBomMaterials.map(material => ({
  2447. ...material,
  2448. materialName: material.name,
  2449. materialCode: material.code,
  2450. projectDepartment: material.storageLocation,
  2451. totalInventoryQuantity: material.stockQuantity,
  2452. deviceId: item.deviceId
  2453. }));
  2454. } else if (!tempBomMaterialsMap[uniqueKey]) {
  2455. // 确保每个保养项在映射中都有对应的数组(即使是空的)
  2456. tempBomMaterialsMap[uniqueKey] = [];
  2457. }
  2458. });
  2459. // 更新响应式映射
  2460. bomMaterialsMap.value = tempBomMaterialsMap;
  2461. // 如果有数据,选中第一个保养项并显示其物料
  2462. if (list.value.length > 0) {
  2463. // 确保第一个保养项的物料正确显示
  2464. const firstItem = list.value[0];
  2465. const uniqueKey = `${firstItem.bomNodeId}`;
  2466. materialList.value = bomMaterialsMap.value[uniqueKey] || [];
  2467. // 使用Element UI的方法设置当前行
  2468. nextTick(() => {
  2469. if (mainTableRef.value) {
  2470. mainTableRef.value.setCurrentRow(firstItem);
  2471. }
  2472. });
  2473. handleBomItemClick(firstItem);
  2474. }
  2475. // 页面初始化完成后立即计算费用
  2476. nextTick(() => {
  2477. calculateTotalCost();
  2478. });
  2479. } catch (error) {
  2480. console.error('数据加载失败:', error)
  2481. message.error('数据加载失败,请重试')
  2482. }
  2483. nextTick(() => {
  2484. calculateAllColumnsWidth()
  2485. window.addEventListener('resize', calculateAllColumnsWidth);
  2486. })
  2487. // 初始化 保养项 分组信息
  2488. calculateGroupSpans();
  2489. })
  2490. onUnmounted(async () => {
  2491. window.removeEventListener('resize', calculateAllColumnsWidth);
  2492. })
  2493. // 处理保养项点击事件
  2494. const handleBomItemClick = (item) => {
  2495. // 移除之前的高亮
  2496. /* if (currentBomItem.value) {
  2497. currentBomItem.value.isSelected = false;
  2498. } */
  2499. // 设置当前选中项
  2500. // item.isSelected = true;
  2501. currentBomItem.value = item;
  2502. // 切换到当前保养项物料视图
  2503. showAllMaterials.value = false;
  2504. // 使用新的刷新方法
  2505. refreshCurrentBomItemMaterials();
  2506. // 切换保养项时重新计算费用
  2507. nextTick(() => {
  2508. calculateTotalCost();
  2509. });
  2510. }
  2511. // 为表格行添加点击事件处理
  2512. // 在el-table上添加row-click事件
  2513. // <el-table ... @row-click="handleRowClick">
  2514. const handleRowClick = (row) => {
  2515. handleBomItemClick(row);
  2516. }
  2517. </script>
  2518. <style scoped>
  2519. .base-expandable-content {
  2520. overflow: hidden; /* 隐藏溢出的内容 */
  2521. transition: max-height 0.3s ease; /* 平滑过渡效果 */
  2522. }
  2523. :deep(.el-input-number .el-input__inner) {
  2524. text-align: left !important;
  2525. padding-left: 10px; /* 保持左侧间距 */
  2526. }
  2527. /* 分组容器样式 */
  2528. .form-group {
  2529. position: relative;
  2530. border: 1px solid #dcdfe6;
  2531. border-radius: 4px;
  2532. padding: 20px 15px 10px;
  2533. margin-bottom: 18px;
  2534. transition: border-color 0.2s;
  2535. }
  2536. /* 分组标题样式 */
  2537. .group-title {
  2538. position: absolute;
  2539. top: -10px;
  2540. left: 20px;
  2541. background: white;
  2542. padding: 0 8px;
  2543. color: #606266;
  2544. font-size: 12px;
  2545. font-weight: 500;
  2546. }
  2547. .error-text {
  2548. color: #f56c6c;
  2549. font-size: 12px;
  2550. margin-top: 5px;
  2551. }
  2552. :deep(.el-table__body) {
  2553. .el-table__cell {
  2554. .cell {
  2555. word-break: break-word;
  2556. max-width: 600px; /* 最大宽度限制 */
  2557. }
  2558. }
  2559. }
  2560. .full-content-cell {
  2561. white-space: nowrap; /* 禁止换行 */
  2562. overflow: visible; /* 允许内容溢出单元格 */
  2563. }
  2564. /* 分组表头样式 */
  2565. :deep(.el-table__header) {
  2566. border: 1px solid #dcdfe6 !important;
  2567. }
  2568. :deep(.el-table__header th) {
  2569. border-right: 1px solid #dcdfe6 !important;
  2570. border-bottom: 1px solid #dcdfe6 !important;
  2571. }
  2572. :deep(.el-table__header .is-group th) {
  2573. background-color: #f5f7fa !important;
  2574. border-bottom: 1px solid #dcdfe6 !important;
  2575. font-weight: 600;
  2576. position: relative;
  2577. }
  2578. :deep(.el-table__header .is-group th::after) {
  2579. display: none !important;
  2580. }
  2581. /* 分组标题下的子表头单元格 */
  2582. :deep(.el-table__header .el-table__cell:not(.is-group)) {
  2583. border-top: 1px solid #dcdfe6 !important; /* 添加顶部边框连接分组标题 */
  2584. }
  2585. /* 分组行样式 - 符合图片效果 */
  2586. :deep(.el-table .group-row) {
  2587. background-color: #f8f8f9; /* 轻微的背景色区分 */
  2588. }
  2589. /* 分组单元格样式 */
  2590. :deep(.el-table .group-cell) {
  2591. font-weight: 600; /* 加粗字体 */
  2592. vertical-align: middle; /* 垂直居中 */
  2593. }
  2594. /* 分组第一行单元格样式 - 添加底部边框 */
  2595. :deep(.el-table .group-first-row) {
  2596. position: relative;
  2597. }
  2598. /* 使用伪元素创建更明显的底部边框 */
  2599. :deep(.el-table .group-first-row::after) {
  2600. content: '';
  2601. position: absolute;
  2602. left: 0;
  2603. right: 0;
  2604. bottom: 0;
  2605. height: 2px;
  2606. background-color: #606266; /* 深灰色边框 */
  2607. z-index: 1;
  2608. }
  2609. /* 调整分组行的高度,使边框更明显 */
  2610. :deep(.el-table .el-table__row.group-first-row) {
  2611. border-bottom: none; /* 移除默认边框 */
  2612. }
  2613. /* 增强高亮行样式的特异性,确保覆盖Element UI的默认样式 */
  2614. :deep(.el-table__body tr.current-row>td) {
  2615. background-color: #d1eaff !important;
  2616. }
  2617. :deep(.el-table__body tr.current-row:hover>td) {
  2618. background-color: #b8dfff !important;
  2619. }
  2620. /* 物料列表操作区域样式 */
  2621. .material-list-header {
  2622. display: flex;
  2623. justify-content: space-between;
  2624. align-items: center;
  2625. margin-bottom: 15px;
  2626. }
  2627. .material-list-title {
  2628. margin: 0;
  2629. font-size: 16px;
  2630. font-weight: bold;
  2631. }
  2632. /* 单价列统一容器样式:强制居左,固定宽度,对齐基准一致 */
  2633. .unit-price-container {
  2634. width: 100%;
  2635. text-align: left; /* 强制内容居左 */
  2636. padding: 0 4px; /* 可选:添加轻微内边距,避免内容贴边 */
  2637. box-sizing: border-box;
  2638. }
  2639. /* 单价输入框样式:消除默认内边距差异,与静态文本对齐 */
  2640. :deep(.unit-price-input .el-input__inner) {
  2641. text-align: left !important; /* 覆盖el-input默认居中对齐 */
  2642. padding-left: 8px !important; /* 统一输入框内边距,与文本缩进匹配 */
  2643. padding-right: 8px !important;
  2644. }
  2645. /* 单价静态文本样式:与输入框保持一致的内边距和对齐 */
  2646. .unit-price-text {
  2647. display: inline-block; /* 转为行内块,支持padding */
  2648. width: 100%;
  2649. padding: 4px 8px; /* 与输入框内边距匹配(输入框默认height约32px,padding上下4px) */
  2650. box-sizing: border-box;
  2651. vertical-align: middle; /* 确保与输入框垂直对齐 */
  2652. }
  2653. /* 禁用物料行的样式 */
  2654. :deep(.disabled-material-row) {
  2655. background-color: #f5f7fa !important;
  2656. color: #c0c4cc !important;
  2657. cursor: not-allowed !important;
  2658. }
  2659. :deep(.disabled-material-row:hover>td) {
  2660. background-color: #f5f7fa !important;
  2661. }
  2662. :deep(.disabled-material-row .el-input.is-disabled .el-input__inner) {
  2663. background-color: #f5f7fa !important;
  2664. color: #c0c4cc !important;
  2665. cursor: not-allowed !important;
  2666. }
  2667. :deep(.disabled-material-row .el-input-number.is-disabled) {
  2668. opacity: 0.6;
  2669. cursor: not-allowed !important;
  2670. }
  2671. :deep(.disabled-material-row .el-button.is-disabled) {
  2672. opacity: 0.6;
  2673. cursor: not-allowed !important;
  2674. }
  2675. /* 新增物料行的样式 */
  2676. :deep(.el-table__body tr.new-material-row) {
  2677. background-color: #f0f9ff !important;
  2678. }
  2679. :deep(.el-table__body tr.new-material-row:hover>td) {
  2680. background-color: #e6f7ff !important;
  2681. }
  2682. /* 新增物料输入框样式 */
  2683. :deep(.new-material-row .el-input .el-input__inner) {
  2684. background-color: white !important;
  2685. border: 1px solid #dcdfe6 !important;
  2686. }
  2687. :deep(.new-material-row .el-input-number .el-input__inner) {
  2688. background-color: white !important;
  2689. border: 1px solid #dcdfe6 !important;
  2690. }
  2691. /* 必填输入框红色边框样式(含hover状态) */
  2692. :deep(.is-required-input .el-input__inner) {
  2693. border-color: #f56c6c !important; /* Element错误色 */
  2694. box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important; /* 错误阴影 */
  2695. }
  2696. :deep(.is-required-input .el-input-number__input),
  2697. :deep(.error-input .el-input-number__input) {
  2698. border-color: #f56c6c !important;
  2699. background-color: #fef0f0 !important;
  2700. box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important;
  2701. }
  2702. /* 鼠标悬停时保持红色边框(覆盖Element默认hover样式) */
  2703. :deep(.is-required-input .el-input__inner:hover) {
  2704. border-color: #f56c6c !important;
  2705. }
  2706. :deep(.is-required-input .el-input-number__input:hover),
  2707. :deep(.error-input .el-input-number__input:hover) {
  2708. border-color: #f56c6c !important;
  2709. background-color: #fef0f0 !important;
  2710. }
  2711. :deep(.is-required-input .el-input-number__input:focus),
  2712. :deep(.error-input .el-input-number__input:focus) {
  2713. border-color: #f56c6c !important;
  2714. background-color: #fef0f0 !important;
  2715. box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.2) !important;
  2716. }
  2717. /* 状态列容器样式 - 水平排列 */
  2718. .status-container {
  2719. display: flex;
  2720. align-items: center;
  2721. justify-content: center;
  2722. gap: 8px; /* 设置开关和文本之间的间距 */
  2723. white-space: nowrap; /* 防止换行 */
  2724. }
  2725. /* 状态开关样式 */
  2726. .status-switch {
  2727. flex-shrink: 0; /* 防止开关被压缩 */
  2728. }
  2729. /* 状态列样式优化 */
  2730. .status-text {
  2731. font-size: 12px;
  2732. color: #666;
  2733. min-width: 40px; /* 为文本设置最小宽度,确保对齐 */
  2734. text-align: left;
  2735. flex-shrink: 0; /* 防止文本被压缩 */
  2736. }
  2737. /* 禁用状态的switch样式 */
  2738. :deep(.el-switch.is-disabled) {
  2739. opacity: 0.6;
  2740. }
  2741. /* 已完成状态的特殊样式 */
  2742. :deep(.status-completed .el-switch__core) {
  2743. background-color: #67c23a;
  2744. border-color: #67c23a;
  2745. }
  2746. /* 保养中状态的样式 */
  2747. :deep(.status-maintaining .el-switch__core) {
  2748. background-color: #409eff;
  2749. border-color: #409eff;
  2750. }
  2751. /* 延迟状态的样式 */
  2752. :deep(.status-delayed .el-switch__core) {
  2753. background-color: #e6a23c;
  2754. border-color: #e6a23c;
  2755. }
  2756. /* 自定义淡红色背景的 tooltip */
  2757. :deep(.main-runtime-tooltip) {
  2758. background: #fef0f0 !important;
  2759. border: 1px solid #fbc4c4 !important;
  2760. color: #f56c6c !important;
  2761. max-width: 300px;
  2762. font-size: 12px;
  2763. padding: 8px 12px;
  2764. }
  2765. /* 隐藏 Tooltip 菱形箭头(核心需求) */
  2766. :deep(.main-runtime-tooltip .el-tooltip__arrow),
  2767. :deep(.main-runtime-tooltip .el-tooltip__arrow::before) {
  2768. display: none !important; /* 完全隐藏箭头及伪元素 */
  2769. width: 0 !important;
  2770. height: 0 !important;
  2771. }
  2772. /* 新增包装层样式,确保tooltip正确触发 */
  2773. .main-runtime-input-wrapper {
  2774. width: 100%;
  2775. position: relative;
  2776. }
  2777. /* 负数值红色样式 */
  2778. :deep(.negative-value) {
  2779. color: #f56c6c !important;
  2780. font-weight: 600;
  2781. }
  2782. /* 确保在表格单元格中正确显示 */
  2783. :deep(.el-table .cell .negative-value) {
  2784. display: inline-block;
  2785. width: 100%;
  2786. }
  2787. </style>