IotMaintenancePlanEdit.vue 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944
  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="12">
  14. <el-form-item :label="t('main.planName')" prop="name">
  15. <el-input type="text" v-model="formData.name" />
  16. </el-form-item>
  17. </el-col>
  18. <el-col :span="12">
  19. <el-form-item :label="t('main.planCode')" prop="serialNumber">
  20. <el-input type="text" v-model="formData.serialNumber" disabled />
  21. </el-form-item>
  22. </el-col>
  23. <el-col :span="24">
  24. <el-form-item :label="t('iotMaintain.remark')" prop="remark">
  25. <el-input
  26. v-model="formData.remark"
  27. type="textarea"
  28. :placeholder="t('iotMaintain.remarkHolder')"
  29. />
  30. </el-form-item>
  31. </el-col>
  32. </el-row>
  33. </div>
  34. </el-form>
  35. </ContentWrap>
  36. <ContentWrap>
  37. <ContentWrap>
  38. <!-- 搜索工作栏 -->
  39. <el-form
  40. class="-mb-15px"
  41. :model="queryParams"
  42. ref="queryFormRef"
  43. :inline="true"
  44. label-width="68px"
  45. >
  46. <el-form-item>
  47. <el-button @click="openForm" type="warning">
  48. <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}</el-button
  49. >
  50. </el-form-item>
  51. </el-form>
  52. </ContentWrap>
  53. <!-- 列表 -->
  54. <ContentWrap>
  55. <el-table
  56. ref="tableRef"
  57. @header-dragend="handleHeaderDragEnd"
  58. v-loading="loading"
  59. :data="pagedList"
  60. :stripe="true"
  61. :show-overflow-tooltip="true"
  62. style="table-layout: fixed"
  63. border
  64. :header-cell-style="tableHeaderStyle"
  65. :cell-class-name="cellClassName"
  66. :max-height="420"
  67. scrollbar-always-on
  68. >
  69. <!-- 添加序号列 -->
  70. <el-table-column
  71. type="index"
  72. :label="t('iotDevice.serial')"
  73. :min-width="48"
  74. align="center"
  75. prop="serial"
  76. fixed="left"
  77. />
  78. <el-table-column label="设备id" align="center" prop="deviceId" v-if="false" />
  79. <el-table-column
  80. :label="t('iotMaintain.deviceCode')"
  81. align="center"
  82. prop="deviceCode"
  83. :min-width="columnWidths.deviceCode"
  84. fixed="left"
  85. />
  86. <el-table-column
  87. :label="t('iotMaintain.deviceName')"
  88. align="center"
  89. prop="deviceName"
  90. :min-width="columnWidths.deviceName"
  91. fixed="left"
  92. />
  93. <el-table-column
  94. :label="t('bomList.bomNode')"
  95. align="center"
  96. prop="name"
  97. :show-overflow-tooltip="false"
  98. :min-width="columnWidths.name"
  99. fixed="left"
  100. >
  101. <template #default="{ row }">
  102. <div class="full-content-cell">
  103. {{ row.name }}
  104. </div>
  105. </template>
  106. </el-table-column>
  107. <el-table-column
  108. :label="t('main.runTime')"
  109. align="center"
  110. key="runningTimeRule"
  111. prop="runningTimeRule"
  112. :min-width="columnWidths.runningTimeRule"
  113. >
  114. <template #default="scope">
  115. <el-switch
  116. v-model="scope.row.runningTimeRule"
  117. :active-value="0"
  118. :inactive-value="1"
  119. @change="handleRuleChange(scope.row, 'runningTime')"
  120. />
  121. </template>
  122. </el-table-column>
  123. <el-table-column
  124. :label="t('main.mileage')"
  125. align="center"
  126. key="mileageRule"
  127. prop="mileageRule"
  128. :min-width="columnWidths.mileageRule"
  129. >
  130. <template #default="scope">
  131. <el-switch
  132. v-model="scope.row.mileageRule"
  133. :active-value="0"
  134. :inactive-value="1"
  135. @change="handleRuleChange(scope.row, 'mileage')"
  136. />
  137. </template>
  138. </el-table-column>
  139. <el-table-column
  140. :label="t('main.date')"
  141. align="center"
  142. key="naturalDateRule"
  143. prop="naturalDateRule"
  144. :min-width="columnWidths.naturalDateRule"
  145. >
  146. <template #default="scope">
  147. <el-switch
  148. v-model="scope.row.naturalDateRule"
  149. :active-value="0"
  150. :inactive-value="1"
  151. @change="handleRuleChange(scope.row, 'date')"
  152. />
  153. </template>
  154. </el-table-column>
  155. <el-table-column
  156. :label="t('operationFillForm.sumTime')"
  157. align="center"
  158. prop="totalRunTime"
  159. :formatter="erpPriceTableColumnFormatter"
  160. :min-width="columnWidths.totalRunTime"
  161. >
  162. <template #default="{ row }">
  163. {{ row.totalRunTime ?? row.tempTotalRunTime }}
  164. </template>
  165. </el-table-column>
  166. <el-table-column
  167. :label="t('operationFillForm.sumKil')"
  168. align="center"
  169. prop="totalMileage"
  170. :formatter="erpPriceTableColumnFormatter"
  171. :min-width="columnWidths.totalMileage"
  172. >
  173. <template #default="{ row }">
  174. {{ row.totalMileage ?? row.tempTotalMileage }}
  175. </template>
  176. </el-table-column>
  177. <el-table-column
  178. label="tempTotalRunTime"
  179. align="center"
  180. prop="tempTotalRunTime"
  181. :formatter="erpPriceTableColumnFormatter"
  182. v-if="false"
  183. />
  184. <el-table-column
  185. label="tempTotalMileage"
  186. align="center"
  187. prop="tempTotalMileage"
  188. :formatter="erpPriceTableColumnFormatter"
  189. v-if="false"
  190. />
  191. <el-table-column
  192. :label="t('mainPlan.lastMaintenanceDate')"
  193. prop="lastMaintenanceDate"
  194. align="center"
  195. :min-width="columnWidths.lastMaintenanceDate"
  196. >
  197. <template #default="{ row }">
  198. <div class="full-content-cell">
  199. {{ row.lastMaintenanceDate }}
  200. </div>
  201. </template>
  202. </el-table-column>
  203. <!-- 保养时长 分组 -->
  204. <el-table-column v-if="hasTimeRuleInCurrentPage" label="保养时长" align="center">
  205. <el-table-column
  206. :label="t('mainPlan.lastMaintenanceOperationTime')"
  207. align="center"
  208. prop="lastRunningTime"
  209. :formatter="erpPriceTableColumnFormatter"
  210. :min-width="columnWidths.lastRunningTime"
  211. >
  212. <template #default="{ row }">
  213. {{ row.lastRunningTime }}
  214. </template>
  215. </el-table-column>
  216. <el-table-column
  217. :label="t('mainPlan.nextMaintenanceH')"
  218. align="center"
  219. prop="nextMaintenanceH"
  220. :min-width="columnWidths.nextMaintenanceH"
  221. >
  222. <template #default="{ row }">
  223. {{ row.nextMaintenanceH ?? '-' }}
  224. </template>
  225. </el-table-column>
  226. <el-table-column
  227. :label="t('mainPlan.remainH')"
  228. align="center"
  229. prop="remainH"
  230. :min-width="columnWidths.remainH"
  231. >
  232. <template #default="{ row }">
  233. {{ row.remainH ?? '-' }}
  234. </template>
  235. </el-table-column>
  236. </el-table-column>
  237. <!-- 保养里程 分组 -->
  238. <el-table-column v-if="hasMileageRuleInCurrentPage" label="保养里程" align="center">
  239. <el-table-column
  240. :label="t('mainPlan.lastMaintenanceMileage')"
  241. align="center"
  242. prop="lastRunningKilometers"
  243. :formatter="erpPriceTableColumnFormatter"
  244. :min-width="columnWidths.lastRunningKilometers"
  245. >
  246. <template #default="{ row }">
  247. {{ row.lastRunningKilometers }}
  248. </template>
  249. </el-table-column>
  250. <el-table-column
  251. :label="t('mainPlan.nextMaintenanceKm')"
  252. align="center"
  253. prop="nextMaintenanceKm"
  254. :min-width="columnWidths.nextMaintenanceKm"
  255. >
  256. <template #default="{ row }">
  257. {{ row.nextMaintenanceKm ?? '-' }}
  258. </template>
  259. </el-table-column>
  260. <el-table-column
  261. :label="t('mainPlan.remainKm')"
  262. align="center"
  263. prop="remainKm"
  264. :min-width="columnWidths.remainKm"
  265. >
  266. <template #default="{ row }">
  267. {{ row.remainKm ?? '-' }}
  268. </template>
  269. </el-table-column>
  270. </el-table-column>
  271. <!-- 保养时长 分组 -->
  272. <el-table-column v-if="hasDateRuleInCurrentPage" label="保养日期" align="center">
  273. <el-table-column
  274. :label="t('mainPlan.lastMaintenanceNaturalDate')"
  275. align="center"
  276. prop="tempLastNaturalDate"
  277. :formatter="erpPriceTableColumnFormatter"
  278. :min-width="columnWidths.tempLastNaturalDate"
  279. >
  280. <template #default="{ row }">
  281. {{ row.tempLastNaturalDate }}
  282. </template>
  283. </el-table-column>
  284. <el-table-column
  285. :label="t('mainPlan.nextMaintDate')"
  286. align="center"
  287. prop="nextMaintenanceDate"
  288. :min-width="columnWidths.nextMaintenanceDate"
  289. >
  290. <template #default="{ row }">
  291. {{ row.nextMaintenanceDate ?? '-' }}
  292. </template>
  293. </el-table-column>
  294. <el-table-column
  295. :label="t('mainPlan.remainDay')"
  296. align="center"
  297. prop="remainDay"
  298. :min-width="columnWidths.remainDay"
  299. >
  300. <template #default="{ row }">
  301. {{ row.remainDay ?? '-' }}
  302. </template>
  303. </el-table-column>
  304. </el-table-column>
  305. <el-table-column
  306. :label="t('operationFill.operation')"
  307. align="center"
  308. :min-width="columnWidths.operation"
  309. prop="operation"
  310. fixed="right"
  311. >
  312. <template #default="scope">
  313. <div style="display: flex; justify-content: center; align-items: center; width: 100%">
  314. <div>
  315. <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
  316. <el-button
  317. style="vertical-align: middle"
  318. link
  319. type="danger"
  320. @click="handleDelete(scope.row.deviceId + '-' + scope.row.bomNodeId)"
  321. >
  322. {{ t('modelTemplate.delete') }}
  323. </el-button>
  324. </div>
  325. <!-- 新增配置按钮 -->
  326. <div style="margin-left: 12px">
  327. <el-button link type="primary" @click="openConfigDialog(scope.row)">
  328. {{ t('modelTemplate.update') }}
  329. </el-button>
  330. </div>
  331. </div>
  332. </template>
  333. </el-table-column>
  334. </el-table>
  335. <!-- 添加分页组件 -->
  336. <!-- <el-pagination
  337. style="margin-top: 20px; justify-content: flex-end"
  338. :total="list.length"
  339. v-model:current-page="currentPage"
  340. v-model:page-size="pageSize"
  341. :page-sizes="[10, 20, 50, 100]"
  342. layout="total, sizes, prev, pager, next, jumper"
  343. @current-change="handlePageChange"
  344. @size-change="handleSizeChange"
  345. /> -->
  346. </ContentWrap>
  347. </ContentWrap>
  348. <ContentWrap>
  349. <el-form>
  350. <el-form-item style="float: right">
  351. <el-button @click="submitForm" type="primary" :disabled="formLoading">{{
  352. t('iotMaintain.save')
  353. }}</el-button>
  354. <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
  355. </el-form-item>
  356. </el-form>
  357. </ContentWrap>
  358. <MainPlanDeviceList ref="deviceFormRef" @choose="deviceChoose" />
  359. <!-- 新增配置对话框 -->
  360. <el-dialog
  361. v-model="configDialog.visible"
  362. :title="`设备 ${configDialog.current?.deviceCode + '-' + configDialog.current?.name} 保养配置`"
  363. width="600px"
  364. :close-on-click-modal="false"
  365. >
  366. <!-- 使用header插槽自定义标题 -->
  367. <template #header>
  368. <span
  369. >设备
  370. <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong>
  371. 保养项配置</span
  372. >
  373. </template>
  374. <el-form
  375. :model="configDialog.form"
  376. label-width="200px"
  377. :rules="configFormRules"
  378. ref="configFormRef"
  379. >
  380. <div class="form-group">
  381. <div class="group-title">{{ t('mainPlan.basicMaintenanceRecords') }}</div>
  382. <!-- 里程配置 -->
  383. <el-form-item
  384. v-if="configDialog.current?.mileageRule === 0"
  385. :label="t('mainPlan.lastMaintenanceMileage')"
  386. prop="lastRunningKilometers"
  387. >
  388. <el-input-number
  389. v-model="configDialog.form.lastRunningKilometers"
  390. :precision="2"
  391. :min="0"
  392. controls-position="right"
  393. :controls="false"
  394. style="width: 60%"
  395. />
  396. </el-form-item>
  397. <!-- 运行时间配置 -->
  398. <el-form-item
  399. v-if="configDialog.current?.runningTimeRule === 0"
  400. :label="t('mainPlan.lastMaintenanceOperationTime')"
  401. prop="lastRunningTime"
  402. >
  403. <el-input-number
  404. v-model="configDialog.form.lastRunningTime"
  405. :precision="1"
  406. :min="0"
  407. controls-position="right"
  408. :controls="false"
  409. style="width: 60%"
  410. />
  411. </el-form-item>
  412. <!-- 自然日期配置 -->
  413. <el-form-item
  414. v-if="configDialog.current?.naturalDateRule === 0"
  415. :label="t('mainPlan.lastMaintenanceNaturalDate')"
  416. prop="lastNaturalDate"
  417. >
  418. <el-date-picker
  419. v-model="configDialog.form.lastNaturalDate"
  420. type="date"
  421. placeholder="选择日期"
  422. format="YYYY-MM-DD"
  423. value-format="YYYY-MM-DD"
  424. style="width: 60%"
  425. />
  426. </el-form-item>
  427. </div>
  428. <div class="form-group" v-if="configDialog.current?.mileageRule === 0">
  429. <div class="group-title">{{ t('mainPlan.operatingMileageRuleConfiguration') }}</div>
  430. <!-- 保养规则周期值 + 提前量 -->
  431. <el-form-item
  432. v-if="configDialog.current?.mileageRule === 0"
  433. :label="t('mainPlan.operatingMileageCycle')"
  434. prop="nextRunningKilometers"
  435. >
  436. <el-input-number
  437. v-model="configDialog.form.nextRunningKilometers"
  438. :precision="2"
  439. :min="0"
  440. controls-position="right"
  441. :controls="false"
  442. style="width: 60%"
  443. />
  444. </el-form-item>
  445. <el-form-item
  446. v-if="configDialog.current?.mileageRule === 0"
  447. :label="t('mainPlan.OperatingMileageCycle_lead')"
  448. prop="kiloCycleLead"
  449. >
  450. <el-input-number
  451. v-model="configDialog.form.kiloCycleLead"
  452. :precision="2"
  453. :min="0"
  454. controls-position="right"
  455. :controls="false"
  456. style="width: 60%"
  457. />
  458. </el-form-item>
  459. </div>
  460. <div class="form-group" v-if="configDialog.current?.runningTimeRule === 0">
  461. <div class="group-title">{{ t('mainPlan.RunTimeRuleConfiguration') }}</div>
  462. <el-form-item
  463. v-if="configDialog.current?.runningTimeRule === 0"
  464. :label="t('mainPlan.RunTimeCycle')"
  465. prop="nextRunningTime"
  466. >
  467. <el-input-number
  468. v-model="configDialog.form.nextRunningTime"
  469. :precision="1"
  470. :min="0"
  471. controls-position="right"
  472. :controls="false"
  473. style="width: 60%"
  474. />
  475. </el-form-item>
  476. <el-form-item
  477. v-if="configDialog.current?.runningTimeRule === 0"
  478. :label="t('mainPlan.RunTimeCycle_Lead')"
  479. prop="timePeriodLead"
  480. >
  481. <el-input-number
  482. v-model="configDialog.form.timePeriodLead"
  483. :precision="1"
  484. :min="0"
  485. controls-position="right"
  486. :controls="false"
  487. style="width: 60%"
  488. />
  489. </el-form-item>
  490. </div>
  491. <div class="form-group" v-if="configDialog.current?.naturalDateRule === 0">
  492. <div class="group-title">{{ t('mainPlan.NaturalDayRuleConfig') }}</div>
  493. <el-form-item
  494. v-if="configDialog.current?.naturalDateRule === 0"
  495. :label="t('mainPlan.NaturalDailyCycle')"
  496. prop="nextNaturalDate"
  497. >
  498. <el-input-number
  499. v-model="configDialog.form.nextNaturalDate"
  500. :min="0"
  501. controls-position="right"
  502. :controls="false"
  503. style="width: 60%"
  504. />
  505. </el-form-item>
  506. <el-form-item
  507. v-if="configDialog.current?.naturalDateRule === 0"
  508. :label="t('mainPlan.NaturalDailyCycle_Lead')"
  509. prop="naturalDatePeriodLead"
  510. >
  511. <el-input-number
  512. v-model="configDialog.form.naturalDatePeriodLead"
  513. :min="0"
  514. controls-position="right"
  515. :controls="false"
  516. style="width: 60%"
  517. />
  518. </el-form-item>
  519. </div>
  520. <!-- 运行记录模板中 多个 累计运行时长 累计运行里程 属性匹配-->
  521. <div
  522. class="form-group"
  523. v-if="
  524. (configDialog.current?.runningTimeRule === 0 ||
  525. configDialog.current?.mileageRule === 0) &&
  526. (configDialog.current?.timeAccumulatedAttrs?.length ||
  527. configDialog.current?.mileageAccumulatedAttrs?.length) &&
  528. (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime)) &&
  529. (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
  530. "
  531. >
  532. <div class="group-title">{{ t('mainPlan.accumulatedParams') }}</div>
  533. <!-- 累计运行时长 -->
  534. <el-form-item
  535. v-if="
  536. configDialog.current?.runningTimeRule === 0 &&
  537. configDialog.current?.timeAccumulatedAttrs?.length &&
  538. (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime))
  539. "
  540. :label="t('mainPlan.accumulatedRunTime')"
  541. prop="accumulatedTimeOption"
  542. :rules="[
  543. {
  544. required:
  545. configDialog.current?.runningTimeRule === 0 &&
  546. configDialog.current?.timeAccumulatedAttrs?.length &&
  547. (configDialog.current.totalRunTime == null ||
  548. isNaN(configDialog.current.totalRunTime)),
  549. message: '请选择累计运行时长',
  550. trigger: 'change'
  551. }
  552. ]"
  553. >
  554. <el-select
  555. v-model="configDialog.form.accumulatedTimeOption"
  556. placeholder="请选择累计运行时长"
  557. style="width: 80%"
  558. clearable
  559. @change="handleAccumulatedTimeChange"
  560. >
  561. <el-option
  562. v-for="(item, index) in configDialog.current.timeAccumulatedAttrs"
  563. :key="`time-${item.pointName}-${index}`"
  564. :label="item.pointName"
  565. :value="item.pointName"
  566. />
  567. </el-select>
  568. </el-form-item>
  569. <!-- 累计运行公里数 -->
  570. <el-form-item
  571. v-if="
  572. configDialog.current?.mileageRule === 0 &&
  573. configDialog.current?.mileageAccumulatedAttrs?.length &&
  574. (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
  575. "
  576. :label="t('mainPlan.accumulatedMileage')"
  577. prop="accumulatedMileageOption"
  578. :rules="[
  579. {
  580. required:
  581. configDialog.current?.mileageRule === 0 &&
  582. configDialog.current?.mileageAccumulatedAttrs?.length &&
  583. (configDialog.current.totalMileage == null ||
  584. isNaN(configDialog.current.totalMileage)),
  585. message: '请选择累计运行公里数',
  586. trigger: 'change'
  587. }
  588. ]"
  589. >
  590. <el-select
  591. v-model="configDialog.form.accumulatedMileageOption"
  592. placeholder="请选择累计运行公里数"
  593. style="width: 80%"
  594. clearable
  595. @change="handleAccumulatedMileageChange"
  596. >
  597. <el-option
  598. v-for="(item, index) in configDialog.current.mileageAccumulatedAttrs"
  599. :key="`mileage-${item.pointName}-${index}`"
  600. :label="item.pointName"
  601. :value="item.pointName"
  602. />
  603. </el-select>
  604. </el-form-item>
  605. </div>
  606. </el-form>
  607. <template #footer>
  608. <el-button @click="configDialog.visible = false">{{ t('common.cancel') }}</el-button>
  609. <el-button type="primary" @click="saveConfig">{{ t('common.save') }}</el-button>
  610. </template>
  611. </el-dialog>
  612. </template>
  613. <script setup lang="ts">
  614. import { IotDeviceApi } from '@/api/pms/device'
  615. import * as UserApi from '@/api/system/user'
  616. import { useUserStore } from '@/store/modules/user'
  617. import { ref, computed, watch } from 'vue'
  618. import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
  619. import { IotMaintenancePlanApi } from '@/api/pms/maintenance'
  620. import { useTagsViewStore } from '@/store/modules/tagsView'
  621. import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
  622. import MainPlanDeviceList from '@/views/pms/maintenance/MainPlanDeviceList.vue'
  623. import * as DeptApi from '@/api/system/dept'
  624. import { erpPriceTableColumnFormatter } from '@/utils'
  625. import dayjs from 'dayjs'
  626. /** 保养计划 编辑 */
  627. defineOptions({ name: 'IotMainPlanEdit' })
  628. const { t } = useI18n() // 国际化
  629. const message = useMessage() // 消息弹窗
  630. const { delView } = useTagsViewStore() // 视图操作
  631. const { currentRoute, push } = useRouter()
  632. const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
  633. const dept = ref() // 当前登录人所属部门对象
  634. const configFormRef = ref() // 配置弹出框对象
  635. const dialogTitle = ref('') // 弹窗的标题
  636. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  637. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  638. const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
  639. const list = ref<IotMaintenanceBomVO[]>([]) // 设备bom关联列表的数据
  640. const lastNaturalDateWatchers = ref(new Map())
  641. const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
  642. const bomNodeColumnWidth = ref('auto')
  643. const tableRef = ref()
  644. const columnWidths = ref<Record<string, string>>({})
  645. // 分页相关变量
  646. const currentPage = ref(1)
  647. const pageSize = ref(10)
  648. // 计算分页后的数据
  649. const pagedList = computed(() => {
  650. console.log('list.value :>> ', list.value)
  651. return list.value
  652. // const start = (currentPage.value - 1) * pageSize.value
  653. // const end = start + pageSize.value
  654. // return list.value.slice(start, end)
  655. })
  656. // 处理页码变化
  657. const handlePageChange = (page: number) => {
  658. currentPage.value = page
  659. }
  660. // 处理每页数量变化
  661. const handleSizeChange = (size: number) => {
  662. pageSize.value = size
  663. currentPage.value = 1 // 重置到第一页
  664. }
  665. const { params, name } = useRoute() // 查询参数
  666. const id = params.id
  667. const formData = ref({
  668. id: undefined,
  669. deptId: undefined,
  670. name: '',
  671. serialNumber: undefined,
  672. responsiblePerson: undefined,
  673. remark: undefined,
  674. failureName: undefined,
  675. status: undefined,
  676. devicePersons: ''
  677. })
  678. const formRules = reactive({
  679. name: [{ required: true, message: '计划名称不能为空', trigger: 'blur' }],
  680. responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }]
  681. })
  682. const formRef = ref() // 表单 Ref
  683. // 新增配置相关状态
  684. const configDialog = reactive({
  685. visible: false,
  686. current: null as IotMaintenanceBomVO | null,
  687. form: {
  688. lastRunningKilometers: 0,
  689. lastRunningTime: 0,
  690. lastNaturalDate: '',
  691. // 保养规则 周期
  692. nextRunningKilometers: 0,
  693. nextRunningTime: 0,
  694. nextNaturalDate: 0,
  695. // 提前量
  696. kiloCycleLead: 0,
  697. timePeriodLead: 0,
  698. naturalDatePeriodLead: 0,
  699. // 多个累计时长 累计里程 匹配
  700. accumulatedTimeOption: null, // 累计运行时长选项
  701. accumulatedMileageOption: null // 累计运行公里数选项
  702. }
  703. })
  704. // 打开配置对话框
  705. const openConfigDialog = (row: IotMaintenanceBomVO) => {
  706. // 新增规则校验:至少一个规则开启
  707. if (row.mileageRule !== 0 && row.runningTimeRule !== 0 && row.naturalDateRule !== 0) {
  708. message.error('请先设置保养规则')
  709. return
  710. }
  711. configDialog.current = row
  712. // 处理日期初始化(核心修改)
  713. let initialDate = ''
  714. if (row.lastNaturalDate) {
  715. // 如果已有值:时间戳 -> 日期字符串
  716. initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
  717. } else {
  718. // 如果无值:设置默认值避免1970问题
  719. initialDate = ''
  720. }
  721. configDialog.form = {
  722. lastRunningKilometers: row.lastRunningKilometers || 0,
  723. lastRunningTime: row.lastRunningTime || 0,
  724. // 时间戳 -> Date对象(用于日期选择器)
  725. lastNaturalDate: row.lastNaturalDate ? dayjs(row.lastNaturalDate).format('YYYY-MM-DD') : null,
  726. // 保养规则 周期值
  727. nextRunningKilometers: row.nextRunningKilometers || 0,
  728. nextRunningTime: row.nextRunningTime || 0,
  729. nextNaturalDate: row.nextNaturalDate || 0,
  730. // 提前量
  731. kiloCycleLead: row.kiloCycleLead || 0,
  732. timePeriodLead: row.timePeriodLead || 0,
  733. naturalDatePeriodLead: row.naturalDatePeriodLead || 0,
  734. // 多个累计时长 累计里程 匹配
  735. accumulatedTimeOption: null, // 累计运行时长选项
  736. accumulatedMileageOption: null // 累计运行公里数选项
  737. }
  738. // 初始化累计参数选择
  739. /* configDialog.form.accumulatedTimeOption = row.isRuntimeFromTemp
  740. ? row.code
  741. : null;
  742. configDialog.form.accumulatedMileageOption = row.isMileageFromTemp
  743. ? row.type
  744. : null; */
  745. configDialog.form.accumulatedTimeOption = row.code || null
  746. configDialog.form.accumulatedMileageOption = row.type || null
  747. configDialog.visible = true
  748. }
  749. // 列宽调整后的处理
  750. const handleHeaderDragEnd = () => {
  751. nextTick(() => {
  752. tableRef.value?.doLayout()
  753. })
  754. }
  755. // 保存配置
  756. const saveConfig = () => {
  757. ;(configFormRef.value as any).validate((valid: boolean) => {
  758. if (!valid) return
  759. if (!configDialog.current) return
  760. // 累计运行时长配置 校验逻辑
  761. if (
  762. configDialog.current.runningTimeRule === 0 &&
  763. configDialog.current.timeAccumulatedAttrs?.length &&
  764. !configDialog.form.accumulatedTimeOption &&
  765. (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime))
  766. ) {
  767. message.error('请选择累计运行时长')
  768. return
  769. }
  770. // 累计运行公里数配置 校验逻辑
  771. if (
  772. configDialog.current.mileageRule === 0 &&
  773. configDialog.current.mileageAccumulatedAttrs?.length &&
  774. !configDialog.form.accumulatedMileageOption &&
  775. (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
  776. ) {
  777. message.error('请选择累计运行公里数')
  778. return
  779. }
  780. // 动态校验逻辑
  781. const requiredFields = []
  782. if (configDialog.current.mileageRule === 0) {
  783. requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
  784. }
  785. if (configDialog.current.runningTimeRule === 0) {
  786. requiredFields.push('nextRunningTime', 'timePeriodLead')
  787. }
  788. if (configDialog.current.naturalDateRule === 0) {
  789. requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
  790. }
  791. const missingFields = requiredFields.filter(
  792. (field) => !configDialog.form[field as keyof typeof configDialog.form]
  793. )
  794. if (missingFields.length > 0) {
  795. message.error('请填写所有必填项')
  796. return
  797. }
  798. // 强制校验逻辑
  799. if (configDialog.current.naturalDateRule === 0) {
  800. if (!configDialog.form.lastNaturalDate) {
  801. message.error('必须选择自然日期')
  802. return
  803. }
  804. // 验证日期有效性
  805. const dateValue = dayjs(configDialog.form.lastNaturalDate)
  806. if (!dateValue.isValid()) {
  807. message.error('日期格式不正确')
  808. return
  809. }
  810. }
  811. // 转换逻辑(关键修改)
  812. const finalDate = configDialog.form.lastNaturalDate
  813. ? dayjs(configDialog.form.lastNaturalDate).valueOf()
  814. : null // 改为null而不是0
  815. const updateData = {
  816. ...configDialog.form,
  817. lastNaturalDate: finalDate
  818. }
  819. // 关闭保养规则,无论是否有选择值,都清除相关数据
  820. // 处理累计运行时长
  821. if (configDialog.current.runningTimeRule !== 0) {
  822. configDialog.current.code = null
  823. configDialog.current.totalRunTime = null
  824. configDialog.form.accumulatedTimeOption = null // 清除选择值
  825. } else if (configDialog.form.accumulatedTimeOption) {
  826. // 查找选中的累计运行时长项
  827. const selectedTimeOption = configDialog.current.timeAccumulatedAttrs?.find(
  828. (item) => item.pointName === configDialog.form.accumulatedTimeOption
  829. )
  830. if (selectedTimeOption) {
  831. configDialog.current.code = selectedTimeOption.pointName
  832. // 优先使用接口值,没有则使用临时值
  833. configDialog.current.tempTotalRunTime = selectedTimeOption.totalRunTime
  834. configDialog.current.isRuntimeFromTemp = true
  835. // 只有接口未提供值时才使用临时值
  836. if (!configDialog.current.totalRunTime) {
  837. }
  838. }
  839. }
  840. // 处理累计运行公里数
  841. if (configDialog.current.mileageRule !== 0) {
  842. configDialog.current.type = null
  843. configDialog.current.totalMileage = null
  844. configDialog.form.accumulatedMileageOption = null // 清除选择值
  845. } else if (configDialog.form.accumulatedMileageOption) {
  846. // 查找选中的累计运行公里数项
  847. const selectedMileageOption = configDialog.current.mileageAccumulatedAttrs?.find(
  848. (item) => item.pointName === configDialog.form.accumulatedMileageOption
  849. )
  850. if (selectedMileageOption) {
  851. configDialog.current.type = selectedMileageOption.pointName
  852. configDialog.current.tempTotalMileage = selectedMileageOption.totalRunTime
  853. configDialog.current.isMileageFromTemp = true
  854. if (!configDialog.current.totalMileage) {
  855. }
  856. }
  857. }
  858. // 更新当前行的数据
  859. if (configDialog.current) {
  860. Object.assign(configDialog.current, updateData)
  861. // 重新计算 下次保养公里数 剩余公里数
  862. configDialog.current.nextMaintenanceKm = calculateNextMaintenanceKm(configDialog.current)
  863. configDialog.current.remainKm = calculateRemainKm(configDialog.current)
  864. // 重新计算 下次保养运行时长 剩余时长
  865. configDialog.current.nextMaintenanceH = calculateNextMaintenanceH(configDialog.current)
  866. configDialog.current.remainH = calculateRemainH(configDialog.current)
  867. // 重新计算 下次保养日期 剩余天数
  868. if (configDialog.form.lastNaturalDate) {
  869. configDialog.current.tempLastNaturalDate = dayjs(configDialog.form.lastNaturalDate).format(
  870. 'YYYY-MM-DD'
  871. )
  872. configDialog.current.nextMaintenanceDate = calculateNextMaintenanceDate(
  873. configDialog.current
  874. )
  875. configDialog.current.remainDay = calculateRemainDay(configDialog.current)
  876. }
  877. }
  878. configDialog.visible = false
  879. })
  880. }
  881. const queryParams = reactive({
  882. deviceIds: undefined,
  883. planId: id,
  884. bomFlag: 'b'
  885. })
  886. // 处理保养规则变化 取消保养规则 时 清空已经设置的相应保养规则数据
  887. const handleRuleChange = (
  888. row: IotMaintenanceBomVO,
  889. ruleType: 'mileage' | 'runningTime' | 'date'
  890. ) => {
  891. // 当规则关闭时(inactive-value=1)
  892. console.log('执行了保养规则变化事件' + row.totalRunTime + ' - ' + row.totalMileage)
  893. // 当前保养项行已经返回了 totalRunTime totalMileage 数据 不需要再清空 累计运行时长 累计公里数
  894. // 选择完设备匹配了保养项后 不能直接置空 因为可能是正常的累计时长 累计公里数
  895. if (ruleType === 'runningTime' && row.runningTimeRule === 1) {
  896. // 清除临时来源的值
  897. if (row.isRuntimeFromTemp) {
  898. row.totalRunTime = null
  899. row.tempTotalRunTime = null
  900. row.code = null
  901. // row.isRuntimeFromTemp = false;
  902. }
  903. // 强制清除配置对话框中的值(如果打开的是当前行)
  904. if (
  905. configDialog.current?.deviceId === row.deviceId &&
  906. configDialog.current?.bomNodeId === row.bomNodeId
  907. ) {
  908. configDialog.form.accumulatedTimeOption = null
  909. }
  910. } else if (ruleType === 'mileage' && row.mileageRule === 1) {
  911. if (row.isMileageFromTemp) {
  912. row.totalMileage = null
  913. row.tempTotalMileage = null
  914. row.type = null
  915. // row.isMileageFromTemp = false;
  916. }
  917. // 强制清除配置对话框中的值(如果打开的是当前行)
  918. if (
  919. configDialog.current?.deviceId === row.deviceId &&
  920. configDialog.current?.bomNodeId === row.bomNodeId
  921. ) {
  922. configDialog.form.accumulatedMileageOption = null
  923. }
  924. }
  925. // 如果配置对话框打开的是当前行,同步清除对话框中的选择值
  926. if (
  927. configDialog.visible &&
  928. configDialog.current &&
  929. configDialog.current.deviceId === row.deviceId &&
  930. configDialog.current.bomNodeId === row.bomNodeId
  931. ) {
  932. if (ruleType === 'runningTime') {
  933. configDialog.form.accumulatedTimeOption = null
  934. } else if (ruleType === 'mileage') {
  935. configDialog.form.accumulatedMileageOption = null
  936. }
  937. }
  938. // 规则变化后按新条件重新计算 下次保养公里数 剩余公里数
  939. if (ruleType === 'mileage') {
  940. if (row.mileageRule === 0) {
  941. row.nextMaintenanceKm = calculateNextMaintenanceKm(row)
  942. row.remainKm = calculateRemainKm(row)
  943. } else {
  944. row.nextMaintenanceKm = null
  945. row.remainKm = null
  946. }
  947. }
  948. // 规则变化后按新条件重新计算 下次保养时长 剩余时长
  949. if (ruleType === 'runningTime') {
  950. if (row.runningTimeRule === 0) {
  951. row.nextMaintenanceH = calculateNextMaintenanceH(row)
  952. row.remainH = calculateRemainH(row)
  953. } else {
  954. row.nextMaintenanceH = null
  955. row.remainH = null
  956. }
  957. }
  958. // 规则变化后按新条件重新计算 下次保养日期 剩余天数
  959. if (ruleType === 'date') {
  960. if (row.naturalDateRule === 0) {
  961. row.nextMaintenanceDate = calculateNextMaintenanceDate(row)
  962. row.remainDay = calculateRemainDay(row)
  963. } else {
  964. row.nextMaintenanceDate = null
  965. row.remainDay = null
  966. }
  967. }
  968. }
  969. const deviceChoose = async (selectedDevices) => {
  970. const newIds = selectedDevices.map((device) => device.id)
  971. deviceIds.value = [...new Set([...deviceIds.value, ...newIds])]
  972. const params = {
  973. deviceIds: newIds.join(',') // 明确传递数组参数
  974. }
  975. queryParams.deviceIds = JSON.parse(JSON.stringify(params.deviceIds))
  976. queryParams.bomFlag = 'b'
  977. // 根据选择的设备筛选出设备BOM中与保养相关的节点项
  978. const res = await IotDeviceApi.deviceAssociateBomList(queryParams)
  979. const rawData = res || []
  980. if (rawData.length === 0) {
  981. message.error('选择的设备不存在待保养BOM项')
  982. }
  983. if (!Array.isArray(rawData)) {
  984. console.error('接口返回数据结构异常:', rawData)
  985. return
  986. }
  987. // 创建当前列表的唯一键集合(关键修改)
  988. const existingKeys = new Set(list.value.map((item) => `${item.deviceId}-${item.bomNodeId}`))
  989. // 转换数据结构(根据你的接口定义调整)
  990. const newItems = rawData
  991. .filter((device) => {
  992. // 排除已存在的项(设备ID+bom节点ID)
  993. const key = `${device.id}-${device.bomNodeId}`
  994. return !existingKeys.has(key)
  995. })
  996. .map((device) => ({
  997. assetClass: device.assetClass,
  998. deviceCode: device.deviceCode,
  999. deviceName: device.deviceName,
  1000. deviceStatus: device.deviceStatus,
  1001. deptName: device.deptName,
  1002. name: device.name,
  1003. code: device.code,
  1004. assetProperty: device.assetProperty,
  1005. remark: null, // 初始化备注
  1006. deviceId: device.id, // 移除操作需要
  1007. bomNodeId: device.bomNodeId,
  1008. totalRunTime: device.totalRunTime,
  1009. totalMileage: device.totalMileage,
  1010. nextRunningKilometers: 0,
  1011. nextRunningTime: 0,
  1012. nextNaturalDate: 0,
  1013. lastNaturalDate: null, // 初始化为null而不是0
  1014. // 保养规则 提前量
  1015. kiloCycleLead: 0,
  1016. timePeriodLead: 0,
  1017. naturalDatePeriodLead: 0,
  1018. tempTotalRunTime: null,
  1019. tempTotalMileage: null,
  1020. isRuntimeFromTemp: false,
  1021. isMileageFromTemp: false,
  1022. // 添加累计时长参数列表 属性
  1023. timeAccumulatedAttrs: device.timeAccumulatedAttrs || [],
  1024. // 添加累计里程参数列表 属性
  1025. mileageAccumulatedAttrs: device.mileageAccumulatedAttrs || []
  1026. }))
  1027. // 获取选择的设备相关的id数组
  1028. newItems.forEach((item) => {
  1029. deviceIds.value.push(item.deviceId)
  1030. })
  1031. // 合并到现有列表(去重)
  1032. newItems.forEach((item) => {
  1033. const exists = list.value.some(
  1034. (existing) => existing.deviceId === item.deviceId && existing.bomNodeId === item.bomNodeId
  1035. )
  1036. if (!exists) {
  1037. list.value.push(item)
  1038. }
  1039. })
  1040. // 排序保养项
  1041. applySorting()
  1042. // 新增数据后自动跳转到第一页
  1043. currentPage.value = 1
  1044. }
  1045. const getTextWidth = (text: string, fontSize = 14) => {
  1046. const span = document.createElement('span')
  1047. span.style.visibility = 'hidden'
  1048. span.style.position = 'absolute'
  1049. span.style.whiteSpace = 'nowrap'
  1050. span.style.fontSize = `${fontSize}px`
  1051. span.style.fontFamily = 'inherit'
  1052. span.innerText = text
  1053. document.body.appendChild(span)
  1054. const width = span.offsetWidth
  1055. document.body.removeChild(span)
  1056. return width
  1057. }
  1058. // 计算下次保养公里数(通用函数)
  1059. const calculateNextMaintenanceKm = (row: IotMaintenanceBomVO) => {
  1060. // 验证条件:规则开启 + 两个值都存在且 > 0
  1061. const isValid =
  1062. row.mileageRule === 0 && row.lastRunningKilometers > 0 && row.nextRunningKilometers > 0
  1063. return isValid ? row.lastRunningKilometers + row.nextRunningKilometers : null // 不满足条件返回null
  1064. }
  1065. // 计算剩余保养公里数(通用函数)
  1066. const calculateRemainKm = (row: IotMaintenanceBomVO) => {
  1067. // 确定使用的里程值(优先totalMileage)
  1068. const mileageValue = row.totalMileage ?? row.tempTotalMileage
  1069. // 验证条件:规则开启 + 3个值都存在且 > 0
  1070. const isValid =
  1071. row.mileageRule === 0 &&
  1072. row.lastRunningKilometers > 0 &&
  1073. mileageValue > 0 &&
  1074. row.nextRunningKilometers > 0
  1075. return isValid
  1076. ? parseFloat(
  1077. (row.nextRunningKilometers - (mileageValue - row.lastRunningKilometers)).toFixed(2)
  1078. )
  1079. : null // 不满足条件返回null
  1080. }
  1081. // 计算下次保养运行时长(通用函数)
  1082. const calculateNextMaintenanceH = (row: IotMaintenanceBomVO) => {
  1083. // 验证条件:规则开启 + 两个值都存在且 > 0
  1084. const isValid = row.runningTimeRule === 0 && row.lastRunningTime > 0 && row.nextRunningTime > 0
  1085. return isValid ? row.lastRunningTime + row.nextRunningTime : null // 不满足条件返回null
  1086. }
  1087. // 计算剩余运行时间(通用函数)
  1088. const calculateRemainH = (row: IotMaintenanceBomVO) => {
  1089. // 确定使用的 运行时长 值(优先 totalRunTime)
  1090. const runTimeValue = row.totalRunTime ?? row.tempTotalRunTime
  1091. // 验证条件:规则开启 + 3个值都存在且 > 0
  1092. const isValid =
  1093. row.runningTimeRule === 0 &&
  1094. row.lastRunningTime > 0 &&
  1095. runTimeValue > 0 &&
  1096. row.nextRunningTime > 0
  1097. return isValid
  1098. ? parseFloat((row.nextRunningTime - (runTimeValue - row.lastRunningTime)).toFixed(2))
  1099. : null // 不满足条件返回null
  1100. }
  1101. // 计算下次保养日期(通用函数)
  1102. const calculateNextMaintenanceDate = (row: IotMaintenanceBomVO) => {
  1103. // 验证条件:规则开启 + 两个值都存在且 > 0
  1104. const isValid = row.naturalDateRule === 0 && row.lastNaturalDate && row.nextNaturalDate
  1105. return isValid
  1106. ? dayjs(row.lastNaturalDate).add(row.nextNaturalDate, 'day').format('YYYY-MM-DD')
  1107. : null // 不满足条件返回null
  1108. }
  1109. // 计算 自然日期保养 剩余天数(通用函数)
  1110. const calculateRemainDay = (row: IotMaintenanceBomVO) => {
  1111. // 验证条件:规则开启 + 两个值都存在且有效
  1112. const isValid =
  1113. row.naturalDateRule === 0 &&
  1114. row.lastNaturalDate !== null &&
  1115. row.nextNaturalDate !== null &&
  1116. row.nextNaturalDate > 0
  1117. if (!isValid) {
  1118. return null
  1119. }
  1120. try {
  1121. // 上次保养日期:将时间戳转换为 Day.js 对象
  1122. const lastNaturalDate = dayjs(row.lastNaturalDate)
  1123. // 计算下次保养日期
  1124. const nextMaintenanceDate = lastNaturalDate.add(row.nextNaturalDate, 'day')
  1125. // 计算剩余天数(当前日期到下次保养日期的天数差)
  1126. return nextMaintenanceDate.diff(dayjs(), 'day')
  1127. } catch (error) {
  1128. console.error('计算保养剩余天数错误:', error)
  1129. return null
  1130. }
  1131. }
  1132. // 单元格类名回调方法
  1133. const cellClassName = ({ row, column }) => {
  1134. // 只对序号列进行处理
  1135. if (column.type === 'index') {
  1136. // 检查该行所有启用的规则是否都已配置完整
  1137. if (checkRowFilled(row)) {
  1138. return 'all-filled' // 返回自定义类名
  1139. }
  1140. }
  1141. return ''
  1142. }
  1143. // 检查行数据是否完整填写
  1144. const checkRowFilled = (row: IotMaintenanceBomVO) => {
  1145. // 检查是否启用了至少一个规则
  1146. const hasRuleEnabled =
  1147. row.mileageRule === 0 || row.runningTimeRule === 0 || row.naturalDateRule === 0
  1148. if (!hasRuleEnabled) {
  1149. return false // 没有任何规则启用,不显示背景色
  1150. }
  1151. // 检查里程规则
  1152. const mileageFilled =
  1153. row.mileageRule !== 0
  1154. ? true // 规则未启用,视为已"填写"
  1155. : row.lastRunningKilometers > 0 &&
  1156. row.nextRunningKilometers > 0 &&
  1157. row.kiloCycleLead > 0 &&
  1158. // 检查累计里程参数是否已选择(当条件满足时)
  1159. (!(
  1160. row.mileageAccumulatedAttrs?.length &&
  1161. (row.totalMileage == null || isNaN(row.totalMileage))
  1162. ) ||
  1163. (row.mileageAccumulatedAttrs?.length &&
  1164. (row.totalMileage == null || isNaN(row.totalMileage)) &&
  1165. row.type))
  1166. // 检查运行时间规则
  1167. const runningTimeFilled =
  1168. row.runningTimeRule !== 0
  1169. ? true
  1170. : row.lastRunningTime > 0 &&
  1171. row.nextRunningTime > 0 &&
  1172. row.timePeriodLead > 0 &&
  1173. // 检查累计时间参数是否已选择(当条件满足时)
  1174. (!(
  1175. row.timeAccumulatedAttrs?.length &&
  1176. (row.totalRunTime == null || isNaN(row.totalRunTime))
  1177. ) ||
  1178. (row.timeAccumulatedAttrs?.length &&
  1179. (row.totalRunTime == null || isNaN(row.totalRunTime)) &&
  1180. row.code))
  1181. // 检查自然日期规则
  1182. const naturalDateFilled =
  1183. row.naturalDateRule !== 0
  1184. ? true
  1185. : row.lastNaturalDate && row.nextNaturalDate > 0 && row.naturalDatePeriodLead > 0
  1186. return mileageFilled && runningTimeFilled && naturalDateFilled
  1187. }
  1188. // 统一计算所有列宽
  1189. const calculateAllColumnsWidth = () => {
  1190. const MIN_WIDTH = 80 // 最小列宽
  1191. const PADDING = 10 // 列内边距
  1192. const FIXED_COLUMN_PADDING = 15 // 固定列额外内边距
  1193. // 需要自适应的列配置
  1194. const autoColumns = [
  1195. { prop: 'serial', label: t('iotDevice.serial') },
  1196. { prop: 'deviceCode', label: t('iotMaintain.deviceCode') },
  1197. { prop: 'deviceName', label: t('iotMaintain.deviceName') },
  1198. {
  1199. prop: 'totalRunTime',
  1200. label: t('operationFillForm.sumTime'),
  1201. getValue: (row) => row.totalRunTime ?? row.tempTotalRunTime
  1202. },
  1203. {
  1204. prop: 'totalMileage',
  1205. label: t('operationFillForm.sumKil'),
  1206. getValue: (row) => row.totalMileage ?? row.tempTotalMileage
  1207. },
  1208. { prop: 'name', label: t('bomList.bomNode') },
  1209. { prop: 'lastMaintenanceDate', label: t('mainPlan.lastMaintenanceDate') },
  1210. { prop: 'mileageRule', label: t('main.mileage') },
  1211. { prop: 'runningTimeRule', label: t('main.runTime') },
  1212. { prop: 'naturalDateRule', label: t('main.date') },
  1213. { prop: 'lastRunningKilometers', label: t('mainPlan.lastMaintenanceMileage') },
  1214. { prop: 'nextMaintenanceKm', label: t('mainPlan.nextMaintenanceKm') },
  1215. { prop: 'remainKm', label: t('mainPlan.remainKm') },
  1216. { prop: 'lastRunningTime', label: t('mainPlan.lastMaintenanceOperationTime') },
  1217. { prop: 'nextMaintenanceH', label: t('mainPlan.nextMaintenanceH') },
  1218. { prop: 'remainH', label: t('mainPlan.remainH') },
  1219. { prop: 'tempLastNaturalDate', label: t('mainPlan.lastMaintenanceNaturalDate') },
  1220. { prop: 'nextMaintenanceDate', label: t('mainPlan.nextMaintDate') },
  1221. { prop: 'remainDay', label: t('mainPlan.remainDay') },
  1222. { prop: 'operation', label: t('operationFill.operation') }
  1223. ]
  1224. const newWidths: Record<string, number> = {}
  1225. autoColumns.forEach((col) => {
  1226. const headerText = col.label
  1227. // 计算表头宽度
  1228. const headerWidth = getTextWidth(headerText)
  1229. // 计算内容最大宽度
  1230. let contentMaxWidth = 0
  1231. if (col.prop === 'operation') {
  1232. // 操作列固定宽度(根据按钮数量)
  1233. contentMaxWidth = 100
  1234. } else if (['mileageRule', 'runningTimeRule', 'naturalDateRule'].includes(col.prop)) {
  1235. // 开关列固定宽度
  1236. contentMaxWidth = headerWidth
  1237. } else {
  1238. list.value.forEach((row) => {
  1239. const text = col.getValue ? String(col.getValue(row)) : String(row[col.prop] || '')
  1240. const textWidth = getTextWidth(text)
  1241. if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
  1242. })
  1243. }
  1244. // 取最大值并添加内边距
  1245. newWidths[col.prop] = Math.max(headerWidth, contentMaxWidth)
  1246. })
  1247. // 固定列特殊处理 - 增加额外空间
  1248. // ;['serial', 'deviceCode', 'deviceName', 'name'].forEach((prop) => {
  1249. // if (newWidths[prop]) {
  1250. // newWidths[prop] += FIXED_COLUMN_PADDING
  1251. // }
  1252. // })
  1253. // 转换为CSS宽度值
  1254. Object.keys(newWidths).forEach((prop) => {
  1255. columnWidths.value[prop] = `${newWidths[prop]}px`
  1256. })
  1257. }
  1258. // 计算属性 - 检查当前页是否有开启的里程规则
  1259. const hasMileageRuleInCurrentPage = computed(() => {
  1260. return pagedList.value.some((row) => row.mileageRule === 0)
  1261. })
  1262. // 计算属性 - 检查当前页是否有开启的 运行时间 规则
  1263. const hasTimeRuleInCurrentPage = computed(() => {
  1264. return pagedList.value.some((row) => row.runningTimeRule === 0)
  1265. })
  1266. // 计算属性 - 检查当前页是否有开启的 自然日期 规则
  1267. const hasDateRuleInCurrentPage = computed(() => {
  1268. return pagedList.value.some((row) => row.naturalDateRule === 0)
  1269. })
  1270. // 为每一行建立lastNaturalDate到tempLastNaturalDate的同步
  1271. const setupNaturalDateSync = (row: IotMaintenanceBomVO) => {
  1272. // 如果该行已有watcher则跳过
  1273. if (lastNaturalDateWatchers.value.has(row.id)) return
  1274. // 为该行创建单独的watcher
  1275. const unwatch = watch(
  1276. () => row.lastNaturalDate,
  1277. (newVal) => {
  1278. // 转换日期格式 (时间戳 -> YYYY-MM-DD)
  1279. row.tempLastNaturalDate = newVal ? dayjs(newVal).format('YYYY-MM-DD') : null
  1280. },
  1281. { immediate: true, deep: true }
  1282. )
  1283. // 保存watcher用于后续清理
  1284. lastNaturalDateWatchers.value.set(row.id, unwatch)
  1285. }
  1286. const tableHeaderStyle = ({ row, rowIndex }) => {
  1287. return {
  1288. border: '1px solid #333',
  1289. backgroundColor: rowIndex === 0 ? '#f0f9eb' : '#f5f7fa' // 分组行特殊背景
  1290. }
  1291. }
  1292. // 在添加新行时建立同步关系
  1293. watch(
  1294. list,
  1295. (newList) => {
  1296. newList.forEach((row) => {
  1297. if (!lastNaturalDateWatchers.value.has(row.id)) {
  1298. setupNaturalDateSync(row)
  1299. }
  1300. })
  1301. },
  1302. { deep: true, immediate: true }
  1303. )
  1304. // 监听数据变化重新计算列宽
  1305. watch(
  1306. () => [...list.value],
  1307. () => {
  1308. calculateAllColumnsWidth()
  1309. },
  1310. { deep: true, immediate: true }
  1311. )
  1312. // 监听分页数据和规则变化 - 重新布局表格
  1313. watch(
  1314. [pagedList, hasMileageRuleInCurrentPage, hasTimeRuleInCurrentPage, hasDateRuleInCurrentPage],
  1315. () => {
  1316. nextTick(() => {
  1317. tableRef.value?.doLayout()
  1318. calculateAllColumnsWidth() // 重新计算列宽
  1319. })
  1320. }
  1321. )
  1322. const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
  1323. const openForm = () => {
  1324. deviceFormRef.value?.open()
  1325. }
  1326. const close = () => {
  1327. delView(unref(currentRoute))
  1328. push({ name: 'IotMaintenancePlan', params: {} })
  1329. }
  1330. /** 提交表单 */
  1331. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  1332. const submitForm = async () => {
  1333. // 校验表单
  1334. await formRef.value.validate()
  1335. // 校验表格数据
  1336. const isValid = validateTableData()
  1337. if (!isValid) return
  1338. // 提交请求
  1339. formLoading.value = true
  1340. try {
  1341. // 将值为NULL 的保养规则 设置为 1
  1342. list.value.forEach((item) => {
  1343. // 确保保养规则不为null
  1344. item.mileageRule = item.mileageRule ?? 1
  1345. item.runningTimeRule = item.runningTimeRule ?? 1
  1346. item.naturalDateRule = item.naturalDateRule ?? 1
  1347. })
  1348. // 转换日期格式
  1349. const convertedList = list.value.map((item) => ({
  1350. ...item,
  1351. lastNaturalDate:
  1352. typeof item.lastNaturalDate === 'number'
  1353. ? item.lastNaturalDate
  1354. : item.lastNaturalDate
  1355. ? dayjs(item.lastNaturalDate).valueOf()
  1356. : null
  1357. }))
  1358. const data = {
  1359. mainPlan: formData.value,
  1360. mainPlanBom: convertedList
  1361. }
  1362. if (formType.value === 'create') {
  1363. await IotMaintenancePlanApi.createIotMaintenancePlan(data)
  1364. message.success(t('common.createSuccess'))
  1365. close()
  1366. } else {
  1367. await IotMaintenancePlanApi.updatePlan(data)
  1368. message.success(t('common.updateSuccess'))
  1369. close()
  1370. }
  1371. // 发送操作成功的事件
  1372. emit('success')
  1373. } finally {
  1374. formLoading.value = false
  1375. }
  1376. }
  1377. // 新增表单校验规则
  1378. const configFormRules = reactive({
  1379. nextRunningKilometers: [
  1380. {
  1381. required: true,
  1382. message: '里程周期必须填写',
  1383. trigger: 'blur'
  1384. }
  1385. ],
  1386. kiloCycleLead: [
  1387. {
  1388. required: true,
  1389. message: '提前量必须填写',
  1390. trigger: 'blur'
  1391. }
  1392. ],
  1393. nextRunningTime: [
  1394. {
  1395. required: true,
  1396. message: '时间周期必须填写',
  1397. trigger: 'blur'
  1398. }
  1399. ],
  1400. timePeriodLead: [
  1401. {
  1402. required: true,
  1403. message: '提前量必须填写',
  1404. trigger: 'blur'
  1405. }
  1406. ],
  1407. nextNaturalDate: [
  1408. {
  1409. required: true,
  1410. message: '自然日周期必须填写',
  1411. trigger: 'blur'
  1412. }
  1413. ],
  1414. naturalDatePeriodLead: [
  1415. {
  1416. required: true,
  1417. message: '提前量必须填写',
  1418. trigger: 'blur'
  1419. }
  1420. ]
  1421. })
  1422. /** 校验表格数据 */
  1423. const validateTableData = (): boolean => {
  1424. let isValid = true
  1425. const errorMessages: string[] = []
  1426. const noRulesErrorMessages: string[] = [] // 未设置任何保养项规则 的错误提示信息
  1427. const noRules: string[] = [] // 行记录中设置了保养规则的记录数量
  1428. const configErrors: string[] = [] // 保养规则配置弹出框
  1429. let shouldBreak = false
  1430. if (list.value.length === 0) {
  1431. errorMessages.push('请至少添加一条设备保养明细')
  1432. isValid = false
  1433. // 直接返回无需后续校验
  1434. message.error('请至少添加一条设备保养明细')
  1435. return isValid
  1436. }
  1437. list.value.forEach((row, index) => {
  1438. if (shouldBreak) return
  1439. const rowNumber = index + 1 // 用户可见的行号从1开始
  1440. const deviceIdentifier = `${row.deviceCode}-${row.name}` // 设备标识
  1441. // 累计参数校验逻辑
  1442. if (
  1443. row.mileageRule === 0 &&
  1444. row.mileageAccumulatedAttrs?.length &&
  1445. (row.totalMileage == null || isNaN(row.totalMileage)) &&
  1446. !row.type
  1447. ) {
  1448. errorMessages.push(`第 ${rowNumber} 行(${deviceIdentifier}):请选择累计运行公里数参数`)
  1449. isValid = false
  1450. }
  1451. if (
  1452. row.runningTimeRule === 0 &&
  1453. row.timeAccumulatedAttrs?.length &&
  1454. (row.totalRunTime == null || isNaN(row.totalRunTime)) &&
  1455. !row.code
  1456. ) {
  1457. errorMessages.push(`第 ${rowNumber} 行(${deviceIdentifier}):请选择累计运行时长参数`)
  1458. isValid = false
  1459. }
  1460. // 校验逻辑
  1461. const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
  1462. if (ruleValue === 0) {
  1463. // 规则开启
  1464. if (!row[configField] || row[configField] <= 0) {
  1465. configErrors.push(
  1466. `第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`
  1467. )
  1468. isValid = false
  1469. }
  1470. }
  1471. }
  1472. // 里程校验逻辑
  1473. if (row.mileageRule === 0) {
  1474. // 假设 0 表示开启状态
  1475. if (!row.nextRunningKilometers || row.nextRunningKilometers <= 0) {
  1476. errorMessages.push(`第 ${rowNumber} 行:开启里程规则必须填写有效的里程周期`)
  1477. isValid = false
  1478. }
  1479. // 再校验配置值
  1480. checkConfig('里程', row.mileageRule, 'lastRunningKilometers')
  1481. } else {
  1482. noRules.push(`第 ${rowNumber} 行:未设置里程规则`)
  1483. }
  1484. // 运行时间校验逻辑
  1485. if (row.runningTimeRule === 0) {
  1486. if (!row.nextRunningTime || row.nextRunningTime <= 0) {
  1487. errorMessages.push(`第 ${rowNumber} 行:开启运行时间规则必须填写有效的时间周期`)
  1488. isValid = false
  1489. }
  1490. checkConfig('运行时间', row.runningTimeRule, 'lastRunningTime')
  1491. } else {
  1492. noRules.push(`第 ${rowNumber} 行:未设置运行时间规则`)
  1493. }
  1494. // 自然日期校验逻辑
  1495. if (row.naturalDateRule === 0) {
  1496. if (!row.nextNaturalDate) {
  1497. errorMessages.push(`第 ${rowNumber} 行:开启自然日期规则必须填写有效的自然日期周期`)
  1498. isValid = false
  1499. }
  1500. checkConfig('自然日期', row.naturalDateRule, 'lastNaturalDate')
  1501. } else {
  1502. noRules.push(`第 ${rowNumber} 行:未设置自然日期规则`)
  1503. }
  1504. // 如果选中的一行记录未设置任何保养规则 提示 ‘保养项未设置任何保养规则’
  1505. if (noRules.length === 3) {
  1506. isValid = false
  1507. shouldBreak = true // 设置标志变量为true,退出循环
  1508. noRulesErrorMessages.push('保养项至少设置1个保养规则')
  1509. }
  1510. noRules.length = 0
  1511. })
  1512. if (errorMessages.length > 0) {
  1513. message.error('设置保养规则后,请维护对应的周期值')
  1514. } else if (noRulesErrorMessages.length > 0) {
  1515. message.error(noRulesErrorMessages.pop())
  1516. } else if (configErrors.length > 0) {
  1517. message.error(configErrors.pop())
  1518. }
  1519. return isValid
  1520. }
  1521. // 修改后的排序应用方法
  1522. const applySorting = () => {
  1523. // 创建新数组并排序
  1524. const sortedList = sortDeviceList(list.value)
  1525. // 使用Vue的响应式方法更新数组
  1526. list.value = sortedList
  1527. // 重置分页到第一页
  1528. currentPage.value = 1
  1529. }
  1530. // 保养项排序函数
  1531. const sortDeviceList = (devices: IotMaintenanceBomVO[]) => {
  1532. // 使用slice()创建数组副本,避免修改原数组
  1533. return devices.slice().sort((a, b) => {
  1534. // 处理可能的空值
  1535. const aCode = a.deviceCode || ''
  1536. const bCode = b.deviceCode || ''
  1537. const aName = a.name || ''
  1538. const bName = b.name || ''
  1539. // 设备编码排序
  1540. if (aCode !== bCode) {
  1541. return aCode.localeCompare(bCode)
  1542. }
  1543. // 保养项名称排序
  1544. return aName.localeCompare(bName)
  1545. })
  1546. }
  1547. // 累计运行时长变更
  1548. const handleAccumulatedTimeChange = (option) => {}
  1549. // 累计运行公里数变更
  1550. const handleAccumulatedMileageChange = (option) => {}
  1551. /** 重置表单 */
  1552. const resetForm = () => {
  1553. formData.value = {
  1554. id: undefined,
  1555. failureName: undefined,
  1556. deviceId: undefined,
  1557. status: undefined,
  1558. ifStop: undefined,
  1559. failureTime: undefined,
  1560. failureInfluence: undefined,
  1561. failureSystem: undefined,
  1562. description: undefined,
  1563. pic: undefined,
  1564. solution: undefined,
  1565. maintainStartTime: undefined,
  1566. maintainEndTime: undefined,
  1567. remark: undefined,
  1568. deviceName: undefined,
  1569. processInstanceId: undefined,
  1570. auditStatus: undefined,
  1571. deptId: undefined
  1572. }
  1573. formRef.value?.resetFields()
  1574. }
  1575. onMounted(async () => {
  1576. // 如果已有数据(从缓存恢复),则跳过初始化
  1577. if (list.value.length > 0) return
  1578. const deptId = useUserStore().getUser.deptId
  1579. // 查询当前登录人所属部门名称
  1580. dept.value = await DeptApi.getDept(deptId)
  1581. // 根据当前登录人部门信息生成生成 保养计划 名称
  1582. formData.value.name = dept.value.name + ' - 保养计划'
  1583. formData.value.deptId = deptId
  1584. if (id) {
  1585. formType.value = 'update'
  1586. const plan = await IotMaintenancePlanApi.getIotMaintenancePlan(id)
  1587. deviceLabel.value = plan.deviceName
  1588. formData.value = plan
  1589. if (list.value.length === 0) {
  1590. // 查询保养计划明细
  1591. const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams)
  1592. list.value = []
  1593. if (Array.isArray(data)) {
  1594. list.value = data.map((item) => {
  1595. if (item.mileageRule === 0) {
  1596. item.nextMaintenanceKm = calculateNextMaintenanceKm(item)
  1597. item.remainKm = calculateRemainKm(item)
  1598. }
  1599. if (item.runningTimeRule === 0) {
  1600. item.nextMaintenanceH = calculateNextMaintenanceH(item)
  1601. item.remainH = calculateRemainH(item)
  1602. }
  1603. if (item.naturalDateRule === 0) {
  1604. item.nextMaintenanceDate = calculateNextMaintenanceDate(item)
  1605. item.remainDay = calculateRemainDay(item)
  1606. }
  1607. setupNaturalDateSync(item)
  1608. return {
  1609. ...item,
  1610. lastNaturalDate: item.lastNaturalDate,
  1611. /* tempLastNaturalDate: item.lastNaturalDate
  1612. ? dayjs(item.lastNaturalDate).format("YYYY-MM-DD") // 时间戳 → 日期字符串
  1613. : null, */
  1614. lastMaintenanceDate: item.lastMaintenanceDate
  1615. ? dayjs(item.lastMaintenanceDate).format('YYYY-MM-DD') // 时间戳 → 日期字符串
  1616. : null // 处理空值
  1617. }
  1618. })
  1619. applySorting()
  1620. }
  1621. }
  1622. } else {
  1623. formType.value = 'create'
  1624. const { wsCache } = useCache()
  1625. const userInfo = wsCache.get(CACHE_KEY.USER)
  1626. formData.value.responsiblePerson = userInfo.user.id
  1627. }
  1628. nextTick(() => {
  1629. calculateAllColumnsWidth()
  1630. window.addEventListener('resize', calculateAllColumnsWidth)
  1631. })
  1632. })
  1633. onUnmounted(async () => {
  1634. window.removeEventListener('resize', calculateAllColumnsWidth)
  1635. })
  1636. const handleDelete = async (str: string) => {
  1637. try {
  1638. const [deviceIdStr, bomNodeId] = str.split('-')
  1639. const deviceId = parseInt(deviceIdStr)
  1640. // 删除列表项
  1641. const index = list.value.findIndex((item) => item.deviceId + '-' + item.bomNodeId === str)
  1642. if (index !== -1) {
  1643. list.value.splice(index, 1)
  1644. // 删除保养项后对保养项重新排序
  1645. applySorting()
  1646. deviceIds.value = []
  1647. }
  1648. // 更新设备ID列表(需要检查是否还有该设备的其他项)
  1649. const hasOtherItems = list.value.some((item) => item.deviceId === deviceId)
  1650. if (!hasOtherItems) {
  1651. deviceIds.value = deviceIds.value.filter((id) => id !== deviceId)
  1652. }
  1653. // message.success('移除成功')
  1654. } catch (error) {
  1655. console.error('移除失败:', error)
  1656. message.error('移除失败')
  1657. }
  1658. // 检查是否需要调整页码
  1659. if (list.value.length > 0 && pagedList.value.length === 0) {
  1660. currentPage.value = Math.max(1, currentPage.value - 1)
  1661. }
  1662. }
  1663. </script>
  1664. <style scoped>
  1665. .base-expandable-content {
  1666. overflow: hidden; /* 隐藏溢出的内容 */
  1667. transition: max-height 0.3s ease; /* 平滑过渡效果 */
  1668. }
  1669. :deep(.el-input-number .el-input__inner) {
  1670. text-align: left !important;
  1671. padding-left: 10px; /* 保持左侧间距 */
  1672. }
  1673. /* 分组容器样式 */
  1674. .form-group {
  1675. position: relative;
  1676. border: 1px solid #dcdfe6;
  1677. border-radius: 4px;
  1678. padding: 20px 15px 10px;
  1679. margin-bottom: 18px;
  1680. transition: border-color 0.2s;
  1681. }
  1682. /* 分组标题样式 */
  1683. .group-title {
  1684. position: absolute;
  1685. top: -10px;
  1686. left: 20px;
  1687. background: white;
  1688. padding: 0 8px;
  1689. color: #606266;
  1690. font-size: 12px;
  1691. font-weight: 500;
  1692. }
  1693. /* 确保分页组件右对齐 */
  1694. .el-pagination {
  1695. display: flex;
  1696. justify-content: flex-end;
  1697. margin-top: 20px;
  1698. }
  1699. .full-content-cell {
  1700. white-space: nowrap; /* 禁止换行 */
  1701. overflow: visible; /* 允许内容溢出单元格 */
  1702. }
  1703. /* 确保表格容器可滚动 */
  1704. .table-container {
  1705. overflow-x: auto;
  1706. width: 100%;
  1707. /* 修复固定列错位 */
  1708. :deep(.el-table__fixed-body-wrapper) {
  1709. bottom: 8px !important;
  1710. }
  1711. }
  1712. /* 滚动条调整 */
  1713. :deep(.el-table__body-wrapper) {
  1714. overflow-x: auto;
  1715. padding-bottom: 12px;
  1716. }
  1717. /* 固定表格布局 */
  1718. el-table {
  1719. table-layout: fixed;
  1720. min-width: 100%;
  1721. }
  1722. /* 全局禁止换行和省略号 */
  1723. :deep(.el-table th > .cell),
  1724. :deep(.el-table td > .cell) {
  1725. white-space: nowrap !important;
  1726. overflow: visible !important;
  1727. text-overflow: clip !important;
  1728. }
  1729. /* 表头特别处理 */
  1730. :deep(.el-table__header) {
  1731. border: 1px solid #dcdfe6 !important;
  1732. /* .cell {
  1733. display: inline-block;
  1734. white-space: nowrap;
  1735. width: auto !important;
  1736. } */
  1737. }
  1738. :deep(.el-table__header tr) {
  1739. border-bottom: 1px solid #e4e7ed !important;
  1740. }
  1741. :deep(.el-table__header th) {
  1742. border-right: 1px solid #dcdfe6 !important;
  1743. border-bottom: 1px solid #dcdfe6 !important;
  1744. }
  1745. :deep(.el-table__header .is-group th) {
  1746. background-color: #f5f7fa !important;
  1747. border-bottom: 1px solid #dcdfe6 !important;
  1748. font-weight: 600;
  1749. position: relative;
  1750. }
  1751. :deep(.el-table__header .is-group th::after) {
  1752. display: none !important;
  1753. }
  1754. /* 分组标题下的子表头单元格 */
  1755. :deep(.el-table__header .el-table__cell:not(.is-group)) {
  1756. border-top: 1px solid #dcdfe6 !important; /* 添加顶部边框连接分组标题 */
  1757. }
  1758. /* 单元格内容强制不换行 */
  1759. :deep(.el-table__body) {
  1760. .cell {
  1761. white-space: nowrap !important;
  1762. }
  1763. }
  1764. :deep(.el-table__body-wrapper::-webkit-scrollbar) {
  1765. height: 12px;
  1766. }
  1767. /* 固定列样式修复 */
  1768. :deep(.el-table__fixed) {
  1769. height: calc(100% - 12px) !important;
  1770. box-shadow: none !important;
  1771. }
  1772. :deep(.el-table__fixed:before) {
  1773. background-color: transparent !important;
  1774. }
  1775. :deep(.el-table__fixed-body-wrapper) {
  1776. bottom: 12px !important;
  1777. }
  1778. /* 固定列内容对齐 */
  1779. :deep(.el-table__fixed .el-table__cell) {
  1780. background-color: var(--el-table-bg-color);
  1781. }
  1782. /* 日期选择器特殊处理 */
  1783. :deep(.el-date-editor) {
  1784. width: 100% !important;
  1785. min-width: 220px;
  1786. .el-input__wrapper {
  1787. padding: 0 30px 0 11px !important; /* 日历图标空间 */
  1788. }
  1789. }
  1790. /* 已完整填写行的背景色 */
  1791. :deep(.el-table .all-filled) {
  1792. background-color: #67c23a !important; /* 淡绿色背景 */
  1793. transition: background-color 0.3s ease; /* 平滑过渡效果 */
  1794. }
  1795. :deep(.el-table .cell) {
  1796. padding: 0;
  1797. }
  1798. </style>