planDotLine.vue 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. <template>
  2. <ele-modal
  3. v-if="visible"
  4. custom-class="ele-dialog-form long-dialog-form plan-dot-line-dialog"
  5. :centered="true"
  6. :append-to-body="true"
  7. :visible.sync="dialogVisible"
  8. :close-on-click-modal="false"
  9. width="70%"
  10. :maxable="true"
  11. :resizable="true"
  12. :fullscreen.sync="modalFullscreen"
  13. :title="title"
  14. @close="onModalClose"
  15. @opened="onModalOpened"
  16. >
  17. <div class="plan-dot-line">
  18. <div v-if="currentPlan" class="plan-time-bar">
  19. <span class="plan-time-item">
  20. <span class="plan-time-label">计划开始时间:</span>
  21. <span class="plan-time-value">{{ planStartDisplay }}</span>
  22. </span>
  23. <span class="plan-time-item">
  24. <span class="plan-time-label">计划结束时间:</span>
  25. <span class="plan-time-value">{{ planEndDisplay }}</span>
  26. </span>
  27. <span class="plan-time-item">
  28. <span class="plan-time-label">计划数量:</span>
  29. <span class="plan-time-value">{{ planNumDisplay }}</span>
  30. </span>
  31. </div>
  32. <div class="top-route">
  33. <div class="panel-title">工艺路线</div>
  34. <el-empty
  35. v-if="taskList.length === 0"
  36. description="暂无工艺路线"
  37. ></el-empty>
  38. <el-steps
  39. v-else
  40. :active="routeStepsActive"
  41. space="20px"
  42. align-center
  43. finish-status="success"
  44. class="route-steps"
  45. >
  46. <el-step
  47. v-for="(item, index) in taskList"
  48. :key="`route-step-${item._taskKey}`"
  49. :title="routeStepTitle(item, index)"
  50. :class="{ active: routeDesIndex === index }"
  51. v-bind="routeErrorIndex === index ? { status: 'error' } : {}"
  52. ></el-step>
  53. </el-steps>
  54. </div>
  55. <div class="config-panel">
  56. <div class="panel-title">工艺配置</div>
  57. <el-empty
  58. v-if="taskList.length === 0"
  59. description="暂无工艺"
  60. ></el-empty>
  61. <div v-else class="task-config-table-wrap">
  62. <el-table
  63. :key="modalFullscreen ? 'fs' : 'nm'"
  64. ref="configTable"
  65. :height="modalFullscreen ? tableMaxHeight : undefined"
  66. :max-height="modalFullscreen ? undefined : tableMaxHeight"
  67. :data="taskList"
  68. border
  69. size="small"
  70. row-key="_taskKey"
  71. class="config-table"
  72. >
  73. <el-table-column
  74. type="index"
  75. label="序号"
  76. width="60"
  77. align="center"
  78. class-name="config-table-index-col"
  79. />
  80. <el-table-column
  81. label="工序名称"
  82. min-width="100"
  83. show-overflow-tooltip
  84. class-name="task-name-cell"
  85. align="center"
  86. >
  87. <template slot-scope="{ row, $index }">
  88. <span class="task-name-text">{{
  89. row.taskName || row.name || `工艺${$index + 1}`
  90. }}</span>
  91. </template>
  92. </el-table-column>
  93. <el-table-column label="工序并行" min-width="92" align="center">
  94. <template slot-scope="{ row }">
  95. <el-select
  96. v-model="row.isParallelism"
  97. placeholder="工序并行"
  98. class="config-table-control"
  99. @change="handleParallelismChange(row)"
  100. >
  101. <el-option label="否" :value="0" />
  102. <el-option label="是" :value="1" />
  103. </el-select>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="执行模式" min-width="100" align="center">
  107. <template slot-scope="{ row }">
  108. <el-select
  109. v-model="row.executionType"
  110. placeholder="执行模式"
  111. clearable
  112. class="config-table-control"
  113. @change="onExecutionTypeChange(row)"
  114. >
  115. <el-option
  116. v-for="opt in executionTypeOptions"
  117. :key="opt.value"
  118. :label="opt.label"
  119. :value="opt.value"
  120. />
  121. </el-select>
  122. </template>
  123. </el-table-column>
  124. <el-table-column label="执行对象" min-width="130" align="center">
  125. <template slot-scope="{ row }">
  126. <el-select
  127. v-if="execTypeNum(row) === EXEC_TYPE.HOMEMADE"
  128. v-model="row.executionTeamId"
  129. placeholder="请选择"
  130. clearable
  131. filterable
  132. class="config-table-control"
  133. @change="onExecutionObjectChange(row)"
  134. >
  135. <el-option
  136. v-for="team in teamOptions"
  137. :key="team.id"
  138. :label="team.name"
  139. :value="team.id"
  140. />
  141. </el-select>
  142. <el-select
  143. v-else-if="execTypeNum(row) === EXEC_TYPE.ENTRUST"
  144. v-model="row.executionFactoryId"
  145. placeholder="请选择"
  146. clearable
  147. filterable
  148. class="config-table-control"
  149. @change="onExecutionObjectChange(row)"
  150. >
  151. <el-option
  152. v-for="f in factoryList"
  153. :key="f.value"
  154. :label="f.label"
  155. :value="f.value"
  156. />
  157. </el-select>
  158. <el-select
  159. v-else-if="execTypeNum(row) === EXEC_TYPE.OUTSOURCE"
  160. :value="''"
  161. placeholder="委外无需选择"
  162. disabled
  163. class="config-table-control"
  164. />
  165. <el-select
  166. v-else
  167. :value="''"
  168. placeholder="请先选择执行模式"
  169. disabled
  170. class="config-table-control"
  171. />
  172. </template>
  173. </el-table-column>
  174. <el-table-column
  175. label="执行开始时间"
  176. min-width="168"
  177. align="center"
  178. >
  179. <template slot-scope="{ row }">
  180. <el-date-picker
  181. v-model="row.executionStartTime"
  182. type="datetime"
  183. value-format="yyyy-MM-dd HH:mm:ss"
  184. placeholder="执行开始时间"
  185. clearable
  186. class="config-table-control"
  187. @change="handleTimeChange(row, 'executionStartTime')"
  188. />
  189. </template>
  190. </el-table-column>
  191. <el-table-column
  192. label="执行结束时间"
  193. min-width="168"
  194. align="center"
  195. >
  196. <template slot-scope="{ row }">
  197. <div
  198. :class="{
  199. 'last-end-over-plan-wrap': isExecutionEndOverPlanEnd(row),
  200. 'end-over-delivery-wrap': isExecutionEndOverDelivery(row)
  201. }"
  202. >
  203. <el-date-picker
  204. v-model="row.executionEndTime"
  205. type="datetime"
  206. value-format="yyyy-MM-dd HH:mm:ss"
  207. placeholder="执行结束时间"
  208. clearable
  209. class="config-table-control"
  210. @change="handleTimeChange(row, 'executionEndTime')"
  211. />
  212. </div>
  213. </template>
  214. </el-table-column>
  215. </el-table>
  216. </div>
  217. </div>
  218. </div>
  219. <div slot="footer">
  220. <el-button size="mini" @click="onModalClose">取消</el-button>
  221. <el-button size="mini" type="warning" @click="handleClearAll"
  222. >一键清空</el-button
  223. >
  224. <el-button size="mini" type="success" @click="handleCache"
  225. >缓存</el-button
  226. >
  227. <el-button size="mini" type="primary" @click="handleSave">保存</el-button>
  228. </div>
  229. </ele-modal>
  230. </template>
  231. <script>
  232. import {
  233. getPlanDotLine,
  234. savePlanDotLine
  235. } from '@/api/productionPlan/planDotLine';
  236. import { getTaskListById } from '@/api/materialPlan';
  237. import { teamPage } from '@/api/mainData';
  238. import { getFactoryarea } from '@/api/saleOrder';
  239. const EXEC_TYPE = Object.freeze({ HOMEMADE: 0, ENTRUST: 1, OUTSOURCE: 2 });
  240. const EXEC_TYPE_OPTIONS = Object.freeze([
  241. { label: '自制', value: EXEC_TYPE.HOMEMADE },
  242. { label: '请托', value: EXEC_TYPE.ENTRUST },
  243. { label: '委外', value: EXEC_TYPE.OUTSOURCE }
  244. ]);
  245. function toSafeNumber(val) {
  246. if (val === '' || val == null) return undefined;
  247. const n = Number(val);
  248. return Number.isNaN(n) ? undefined : n;
  249. }
  250. function formatDateTime(val) {
  251. if (val == null || val === '') return '';
  252. const str = String(val).trim();
  253. if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str)) return str;
  254. const d = new Date(val);
  255. if (Number.isNaN(d.getTime())) return '';
  256. const p = (n) => String(n).padStart(2, '0');
  257. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
  258. d.getHours()
  259. )}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
  260. }
  261. /** 比较两个时间字符串,结束 < 开始 则返回 true */
  262. function isEndBeforeStart(startStr, endStr) {
  263. if (!startStr || !endStr) return false;
  264. const s = new Date(startStr).getTime();
  265. const e = new Date(endStr).getTime();
  266. return !Number.isNaN(s) && !Number.isNaN(e) && e < s;
  267. }
  268. /** 工序结束时间是否晚于计划结束时间(用于最后一道工序标红与提醒) */
  269. function isEndAfterPlanEnd(executionEndStr, planEndStr) {
  270. if (!executionEndStr || !planEndStr) return false;
  271. const e = new Date(executionEndStr).getTime();
  272. const p = new Date(planEndStr).getTime();
  273. return !Number.isNaN(e) && !Number.isNaN(p) && e > p;
  274. }
  275. /** 计划交付时间是日期字段,比较时应以该日期当天结束为上限 */
  276. function isTimeAfterDeliveryDate(timeStr, deliveryDateStr) {
  277. if (!timeStr || !deliveryDateStr) return false;
  278. const time = new Date(timeStr).getTime();
  279. const deliveryDate = new Date(deliveryDateStr);
  280. if (Number.isNaN(time) || Number.isNaN(deliveryDate.getTime()))
  281. return false;
  282. deliveryDate.setHours(23, 59, 59, 999);
  283. return time > deliveryDate.getTime();
  284. }
  285. export default {
  286. data() {
  287. return {
  288. visible: false,
  289. dialogVisible: false,
  290. title: '计划布点',
  291. taskList: [],
  292. teamOptions: [],
  293. currentPlan: null,
  294. planRoutingPayload: null,
  295. executionTypeOptions: EXEC_TYPE_OPTIONS,
  296. /** 与 ele-modal 全屏按钮同步,用于表格高度适配 */
  297. modalFullscreen: false,
  298. /** 普通弹窗下动态限制表格最大高度,避免写死 420 */
  299. tableMaxHeight: 420,
  300. /** 工艺路线步骤 error 状态(-1 表示无) */
  301. routeErrorIndex: -1,
  302. factoryList: [],
  303. /** 模板中比对执行模式用 */
  304. EXEC_TYPE
  305. };
  306. },
  307. computed: {
  308. /** 当前激活步骤:首个未配置执行模式的工序;全部已配置则为步骤总数(全部 finish) */
  309. routeStepsActive() {
  310. const list = this.taskList;
  311. if (!list.length) return 0;
  312. for (let i = 0; i < list.length; i++) {
  313. if (!this.isRouteItemDone(list[i])) return i;
  314. }
  315. return list.length;
  316. },
  317. /** 用于 :class="active" 高亮,与 active 同步(全部完成时高亮最后一环) */
  318. routeDesIndex() {
  319. const list = this.taskList;
  320. if (!list.length) return -1;
  321. const a = this.routeStepsActive;
  322. return a >= list.length ? list.length - 1 : a;
  323. },
  324. /** 顶部展示:与列表行字段 startTime / endTime 一致(见 productionPlan/index.vue 列配置) */
  325. planStartDisplay() {
  326. const t = this.currentPlan?.startTime;
  327. return t ? formatDateTime(t) || String(t) : '—';
  328. },
  329. planEndDisplay() {
  330. const t = this.currentPlan?.endTime;
  331. return t ? formatDateTime(t) || String(t) : '—';
  332. },
  333. planNumDisplay() {
  334. const t =
  335. this.currentPlan?.productNum + this.currentPlan?.measuringUnit;
  336. return t ? t : '—';
  337. },
  338. /** 最后一道工序执行结束时间是否晚于计划结束时间 */
  339. lastProcessEndAfterPlanEnd() {
  340. const list = this.taskList;
  341. const planEnd = this.currentPlan?.endTime;
  342. if (!list.length || !planEnd) return false;
  343. const last = list[list.length - 1];
  344. if (!last?.executionEndTime) return false;
  345. return isEndAfterPlanEnd(last.executionEndTime, planEnd);
  346. }
  347. },
  348. watch: {
  349. modalFullscreen() {
  350. // 先算好高度,key 变化会重建 el-table,重建后再 doLayout
  351. this.calcTableHeight();
  352. this.$nextTick(() => {
  353. this.$refs.configTable?.doLayout?.();
  354. // 全屏动画结束后再修正一次
  355. setTimeout(() => {
  356. this.calcTableHeight();
  357. this.$nextTick(() => {
  358. this.$refs.configTable?.doLayout?.();
  359. });
  360. }, 400);
  361. });
  362. }
  363. },
  364. beforeDestroy() {
  365. window.removeEventListener('resize', this.calcTableHeight);
  366. },
  367. methods: {
  368. onModalOpened() {
  369. window.addEventListener('resize', this.calcTableHeight);
  370. this.$nextTick(() => {
  371. this.calcTableHeight();
  372. });
  373. },
  374. calcTableHeight() {
  375. if (this.modalFullscreen) {
  376. const tableEl = this.$refs.configTable?.$el;
  377. const footerEl = tableEl
  378. ?.closest('.el-dialog')
  379. ?.querySelector('.el-dialog__footer');
  380. const footerH = footerEl ? footerEl.offsetHeight : 52;
  381. const wrapEl = tableEl?.closest('.task-config-table-wrap');
  382. if (wrapEl) {
  383. const top = wrapEl.getBoundingClientRect().top;
  384. const vh = window.innerHeight;
  385. this.tableMaxHeight = Math.max(
  386. 200,
  387. Math.floor(vh - top - footerH - 16)
  388. );
  389. } else {
  390. const vh = window.innerHeight || 800;
  391. this.tableMaxHeight = Math.max(200, vh - 260);
  392. }
  393. } else {
  394. const vh = window.innerHeight || 800;
  395. this.tableMaxHeight = Math.max(320, Math.floor(vh * 0.52));
  396. }
  397. this.$nextTick(() => {
  398. this.$refs.configTable?.doLayout?.();
  399. });
  400. },
  401. async open(data) {
  402. this.currentPlan = data || null;
  403. this.planRoutingPayload = null;
  404. await Promise.all([
  405. this.loadTeamOptions(),
  406. this.loadFactoryList(),
  407. this.loadTaskList()
  408. ]);
  409. this.visible = true;
  410. this.dialogVisible = true;
  411. },
  412. async loadFactoryList() {
  413. const par = { type: 1, size: 9999 };
  414. try {
  415. const res = await getFactoryarea(par);
  416. if (res.list?.length) {
  417. this.factoryList = res.list.map((el) => ({
  418. value: el.id,
  419. label: el.name
  420. }));
  421. } else {
  422. this.factoryList = [];
  423. }
  424. } catch {
  425. this.factoryList = [];
  426. }
  427. },
  428. async loadTaskList() {
  429. this.taskList = [];
  430. this.planRoutingPayload = null;
  431. if (!this.currentPlan?.id) return;
  432. let planData;
  433. try {
  434. planData = await getPlanDotLine({ planId: this.currentPlan.id });
  435. } catch (e) {
  436. this.$message.error(e.message || '获取工序布点信息失败');
  437. return;
  438. }
  439. this.planRoutingPayload = planData || {};
  440. const details = planData?.detailList;
  441. if (details?.length) {
  442. this.taskList = [...details]
  443. .sort((a, b) => (a.taskSort ?? 0) - (b.taskSort ?? 0))
  444. .map((item, i) => this.normalizeDetailRow(item, i));
  445. return;
  446. }
  447. if (!this.currentPlan.produceRoutingId) {
  448. this.$message.warning('暂无布点明细且缺少工艺路线,无法加载工序');
  449. return;
  450. }
  451. try {
  452. const list = await getTaskListById(this.currentPlan.produceRoutingId);
  453. this.taskList = (list || []).map((item, i) =>
  454. this.normalizeRoutingTaskRow(item, i)
  455. );
  456. } catch (e) {
  457. this.$message.error(e.message || '获取工序列表失败');
  458. }
  459. },
  460. normalizeDetailRow(item, index) {
  461. const execType = toSafeNumber(item.executionType) ?? EXEC_TYPE.HOMEMADE;
  462. const outsource = execType === EXEC_TYPE.OUTSOURCE;
  463. const entrust = execType === EXEC_TYPE.ENTRUST;
  464. const homemade = execType === EXEC_TYPE.HOMEMADE;
  465. return {
  466. ...item,
  467. _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
  468. isParallelism: toSafeNumber(item.isParallelism) === 1 ? 1 : 0,
  469. executionStartTime: formatDateTime(item.executionStartTime),
  470. executionEndTime: formatDateTime(item.executionEndTime),
  471. executionType: execType,
  472. executionFactoryId: entrust
  473. ? item.executionFactoryId ?? item.executionTeamId ?? ''
  474. : '',
  475. executionFactoryName: entrust
  476. ? item.executionFactoryName ?? item.executionTeamName ?? ''
  477. : '',
  478. executionTeamId: homemade ? item.executionTeamId ?? '' : '',
  479. executionTeamLeader:
  480. outsource || entrust ? '' : item.executionTeamLeader ?? '',
  481. executionTeamLeaderId:
  482. outsource || entrust ? '' : item.executionTeamLeaderId ?? '',
  483. executionTeamName:
  484. outsource || entrust ? '' : item.executionTeamName ?? ''
  485. };
  486. },
  487. normalizeRoutingTaskRow(item, index) {
  488. const plan = this.currentPlan;
  489. const taskId = item.id;
  490. return {
  491. ...item,
  492. _taskKey: item.id ?? `new-${taskId ?? index}-${index}`,
  493. taskId: taskId ?? undefined,
  494. taskName: item.name ?? item.taskName,
  495. taskCode: item.code ?? item.taskCode,
  496. taskSort: item.taskSort ?? index + 1,
  497. type: item.type ?? undefined,
  498. sourceTaskId: item.sourceTaskId,
  499. planId: plan.id,
  500. planCode: plan.code,
  501. routingId: plan.produceRoutingId,
  502. routingName: plan.produceRoutingName ?? plan.routingName,
  503. isParallelism: 0,
  504. executionType: EXEC_TYPE.HOMEMADE,
  505. executionTeamId: '',
  506. executionTeamLeader: '',
  507. executionTeamLeaderId: '',
  508. executionTeamName: '',
  509. executionStartTime: '',
  510. executionEndTime: '',
  511. executionFactoryId: '',
  512. executionFactoryName: ''
  513. };
  514. },
  515. execTypeNum(row) {
  516. return toSafeNumber(row.executionType);
  517. },
  518. clearExecutionTeamFields(row) {
  519. this.$set(row, 'executionTeamId', '');
  520. this.$set(row, 'executionTeamName', '');
  521. this.$set(row, 'executionTeamLeader', '');
  522. this.$set(row, 'executionTeamLeaderId', '');
  523. this.$set(row, 'executionFactoryId', '');
  524. this.$set(row, 'executionFactoryName', '');
  525. },
  526. onExecutionTypeChange(row) {
  527. this.clearExecutionTeamFields(row);
  528. },
  529. onExecutionObjectChange(row) {
  530. const t = this.execTypeNum(row);
  531. if (t === EXEC_TYPE.HOMEMADE) {
  532. const team = this.teamOptions.find(
  533. (x) => x.id === row.executionTeamId
  534. );
  535. row.executionTeamName = team?.name ?? '';
  536. row.executionTeamLeader = team?.leaderUserName ?? '';
  537. row.executionTeamLeaderId = team?.leaderUserId ?? '';
  538. row.executionFactoryId = '';
  539. row.executionFactoryName = '';
  540. } else if (t === EXEC_TYPE.ENTRUST) {
  541. const f = this.factoryList.find(
  542. (x) => x.value === row.executionFactoryId
  543. );
  544. row.executionFactoryName = f?.label ?? '';
  545. row.executionTeamId = '';
  546. row.executionTeamName = '';
  547. row.executionTeamLeader = '';
  548. row.executionTeamLeaderId = '';
  549. }
  550. },
  551. handleClearAll() {
  552. this.taskList.forEach((row) => {
  553. this.$set(row, 'executionType', EXEC_TYPE.HOMEMADE);
  554. this.$set(row, 'isParallelism', 0);
  555. this.clearExecutionTeamFields(row);
  556. this.$set(row, 'executionStartTime', '');
  557. this.$set(row, 'executionEndTime', '');
  558. });
  559. this.$message.success('已清空');
  560. },
  561. async loadTeamOptions() {
  562. try {
  563. const factoryId = this.$store.state.user.info.factoryId;
  564. const res = await teamPage({ pageNum: 1, size: -1, factoryId });
  565. this.teamOptions = (res?.list || []).map(
  566. ({ id, name, leaderUserName, leaderUserId }) => ({
  567. id,
  568. name,
  569. leaderUserName,
  570. leaderUserId
  571. })
  572. );
  573. } catch {
  574. this.teamOptions = [];
  575. }
  576. },
  577. isRouteItemDone(row) {
  578. return row.executionType != null && row.executionType !== '';
  579. },
  580. routeStepTitle(item, index) {
  581. return (
  582. item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
  583. );
  584. },
  585. getTaskLabel(item, index) {
  586. return (
  587. item?.taskTypeName ||
  588. item?.taskName ||
  589. item?.name ||
  590. (typeof index === 'number' ? `工艺${index + 1}` : '当前工艺')
  591. );
  592. },
  593. compareTaskTime(start, end) {
  594. if (!start || !end) return false;
  595. const startTime = new Date(start).getTime();
  596. const endTime = new Date(end).getTime();
  597. return (
  598. !Number.isNaN(startTime) &&
  599. !Number.isNaN(endTime) &&
  600. startTime < endTime
  601. );
  602. },
  603. isTaskParallel(row) {
  604. return toSafeNumber(row?.isParallelism) === 1;
  605. },
  606. getCurrentPlanStartTime() {
  607. return (
  608. this.currentPlan?.startTime ||
  609. this.currentPlan?.planStartTime ||
  610. this.currentPlan?.executionStartTime ||
  611. ''
  612. );
  613. },
  614. isExecutionStartBeforePlanStart(executionStartTime) {
  615. if (!executionStartTime) {
  616. return false;
  617. }
  618. const planStartTime = this.getCurrentPlanStartTime();
  619. if (!planStartTime) {
  620. return false;
  621. }
  622. const executionStart = new Date(executionStartTime).getTime();
  623. const planStart = new Date(planStartTime).getTime();
  624. return (
  625. !Number.isNaN(executionStart) &&
  626. !Number.isNaN(planStart) &&
  627. executionStart < planStart
  628. );
  629. },
  630. validateTaskSequence(row, options = {}) {
  631. const index = this.taskList.findIndex((item) => item === row);
  632. if (index < 0) {
  633. return true;
  634. }
  635. const prev = this.taskList[index - 1];
  636. const next = this.taskList[index + 1];
  637. const resetKey = options.resetKey;
  638. const checkPrev =
  639. (!resetKey || resetKey === 'executionStartTime') &&
  640. !this.isTaskParallel(row);
  641. const checkNext =
  642. (!resetKey || resetKey === 'executionEndTime') &&
  643. !this.isTaskParallel(next);
  644. if (
  645. checkPrev &&
  646. prev?.executionEndTime &&
  647. row.executionStartTime &&
  648. this.compareTaskTime(row.executionStartTime, prev.executionEndTime)
  649. ) {
  650. this.routeErrorIndex = index;
  651. this.$message.warning(
  652. `${this.getTaskLabel(
  653. row,
  654. index
  655. )}执行开始时间不能小于上一道工序${this.getTaskLabel(
  656. prev,
  657. index - 1
  658. )}的执行结束时间`
  659. );
  660. if (resetKey) {
  661. this.$set(row, resetKey, '');
  662. }
  663. return false;
  664. }
  665. if (
  666. checkNext &&
  667. next?.executionStartTime &&
  668. row.executionEndTime &&
  669. this.compareTaskTime(next.executionStartTime, row.executionEndTime)
  670. ) {
  671. this.routeErrorIndex = index + 1;
  672. this.$message.warning(
  673. `下一道工序${this.getTaskLabel(
  674. next,
  675. index + 1
  676. )}的执行开始时间不能小于上一道工序${this.getTaskLabel(
  677. row,
  678. index
  679. )}的执行结束时间`
  680. );
  681. if (resetKey) {
  682. this.$set(row, resetKey, '');
  683. }
  684. return false;
  685. }
  686. return true;
  687. },
  688. handleTimeChange(row, changeKey) {
  689. if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
  690. this.$message.warning('执行结束时间不能小于执行开始时间');
  691. this.$set(row, changeKey, '');
  692. return;
  693. }
  694. if (
  695. changeKey === 'executionStartTime' &&
  696. this.isTaskParallel(row) &&
  697. this.isExecutionStartBeforePlanStart(row.executionStartTime)
  698. ) {
  699. this.$message.warning('执行开始时间不能小于计划开始时间');
  700. this.$set(row, changeKey, '');
  701. return;
  702. }
  703. if (!this.validateTaskSequence(row, { resetKey: changeKey })) {
  704. return;
  705. }
  706. this.routeErrorIndex = -1;
  707. const deadline = this.currentPlan?.planDeliveryTime;
  708. if (
  709. changeKey === 'executionStartTime' &&
  710. deadline &&
  711. row.executionStartTime
  712. ) {
  713. if (isTimeAfterDeliveryDate(row.executionStartTime, deadline)) {
  714. this.$message.warning('时间不能大于计划交付时间');
  715. this.$set(row, changeKey, '');
  716. return;
  717. }
  718. }
  719. if (changeKey === 'executionEndTime' && row.executionEndTime) {
  720. const planEnd = this.currentPlan?.endTime;
  721. const overDelivery =
  722. !!deadline &&
  723. isTimeAfterDeliveryDate(row.executionEndTime, deadline);
  724. const overPlanEnd =
  725. !!planEnd && isEndAfterPlanEnd(row.executionEndTime, planEnd);
  726. if (overDelivery && overPlanEnd) {
  727. this.$message.warning(
  728. '执行结束时间晚于计划交付时间及计划结束时间,已保留填写内容,请留意核对'
  729. );
  730. } else if (overDelivery) {
  731. this.$message.warning(
  732. '执行结束时间晚于计划交付时间,已保留填写内容,请留意核对'
  733. );
  734. } else if (overPlanEnd) {
  735. this.$message.warning(
  736. '执行结束时间晚于计划结束时间,已保留填写内容,请留意核对'
  737. );
  738. }
  739. }
  740. },
  741. handleParallelismChange(row) {
  742. if (!row?.executionStartTime) {
  743. return;
  744. }
  745. if (this.isTaskParallel(row)) {
  746. if (this.isExecutionStartBeforePlanStart(row.executionStartTime)) {
  747. this.$message.warning('执行开始时间不能小于计划开始时间');
  748. this.$set(row, 'executionStartTime', '');
  749. }
  750. return;
  751. }
  752. this.validateTaskSequence(row, { resetKey: 'executionStartTime' });
  753. },
  754. /** 执行结束时间晚于计划交付时间(选择框标红,可保存) */
  755. isExecutionEndOverDelivery(row) {
  756. const deadline = this.currentPlan?.planDeliveryTime;
  757. return (
  758. !!deadline &&
  759. !!row?.executionEndTime &&
  760. isTimeAfterDeliveryDate(row.executionEndTime, deadline)
  761. );
  762. },
  763. /** 执行结束时间晚于计划结束时间(任意工序行标红,可保存) */
  764. isExecutionEndOverPlanEnd(row) {
  765. const planEnd = this.currentPlan?.endTime;
  766. return (
  767. !!planEnd &&
  768. !!row?.executionEndTime &&
  769. isEndAfterPlanEnd(row.executionEndTime, planEnd)
  770. );
  771. },
  772. validateTaskList() {
  773. this.routeErrorIndex = -1;
  774. const label = (t) => this.getTaskLabel(t);
  775. for (const item of this.taskList) {
  776. const execType = toSafeNumber(item.executionType);
  777. if (execType === undefined) {
  778. this.$message.warning(`${label(item)}请选择执行模式`);
  779. return false;
  780. }
  781. if (execType === EXEC_TYPE.HOMEMADE) {
  782. if (
  783. !item.executionTeamId ||
  784. !item.executionStartTime ||
  785. !item.executionEndTime
  786. ) {
  787. this.$message.warning(
  788. `${label(
  789. item
  790. )}执行模式为自制时,需选择执行班组并填写执行开始和结束时间`
  791. );
  792. return false;
  793. }
  794. }
  795. if (execType === EXEC_TYPE.ENTRUST) {
  796. if (
  797. !item.executionFactoryId ||
  798. !item.executionStartTime ||
  799. !item.executionEndTime
  800. ) {
  801. this.$message.warning(
  802. `${label(
  803. item
  804. )}执行模式为请托时,需选择工厂并填写执行开始和结束时间`
  805. );
  806. return false;
  807. }
  808. }
  809. if (execType === EXEC_TYPE.OUTSOURCE) {
  810. if (!item.executionStartTime || !item.executionEndTime) {
  811. this.$message.warning(
  812. `${label(item)}执行模式为委外时,需填写执行开始和结束时间`
  813. );
  814. return false;
  815. }
  816. }
  817. if (
  818. isEndBeforeStart(item.executionStartTime, item.executionEndTime)
  819. ) {
  820. this.$message.warning(
  821. `${label(item)}执行结束时间不能小于执行开始时间`
  822. );
  823. return false;
  824. }
  825. if (
  826. this.isTaskParallel(item) &&
  827. this.isExecutionStartBeforePlanStart(item.executionStartTime)
  828. ) {
  829. this.$message.warning(
  830. `${label(item)}执行开始时间不能小于计划开始时间`
  831. );
  832. return false;
  833. }
  834. const deadline = this.currentPlan?.planDeliveryTime;
  835. if (deadline) {
  836. const checkStart = (val, name) => {
  837. if (!val) return true;
  838. if (!isTimeAfterDeliveryDate(val, deadline)) return true;
  839. this.$message.warning(
  840. `${label(item)}${name}不能大于计划交付时间`
  841. );
  842. return false;
  843. };
  844. if (!checkStart(item.executionStartTime, '执行开始时间'))
  845. return false;
  846. }
  847. }
  848. for (const item of this.taskList) {
  849. if (!this.validateTaskSequence(item)) {
  850. return false;
  851. }
  852. }
  853. return true;
  854. },
  855. /** @param {0|1} isCache 缓存传 1,保存传 0 */
  856. buildSavePayload(isCache = 0) {
  857. const plan = this.currentPlan;
  858. const head = this.planRoutingPayload || {};
  859. const detailList = this.taskList.map((row) => {
  860. const execType = toSafeNumber(row.executionType) ?? 0;
  861. const detail = {
  862. executionEndTime: row.executionEndTime || undefined,
  863. executionStartTime: row.executionStartTime || undefined,
  864. executionTeamId:
  865. execType === EXEC_TYPE.HOMEMADE
  866. ? row.executionTeamId || undefined
  867. : undefined,
  868. executionTeamLeader: row.executionTeamLeader || '',
  869. executionTeamLeaderId: row.executionTeamLeaderId || '',
  870. executionTeamName: row.executionTeamName || '',
  871. executionType: execType,
  872. isParallelism: this.isTaskParallel(row) ? 1 : 0,
  873. planCode: row.planCode || plan?.code,
  874. planId: row.planId ?? plan?.id,
  875. routingId:
  876. row.routingId ?? plan?.produceRoutingId ?? head.routingId,
  877. routingName:
  878. row.routingName || plan?.produceRoutingName || head.routingName,
  879. sourceTaskId: row.sourceTaskId ?? row.taskId,
  880. taskCode: row.taskCode,
  881. taskId: row.taskId,
  882. taskName: row.taskName,
  883. taskSort: row.taskSort,
  884. type: toSafeNumber(row.type)
  885. };
  886. if (execType === EXEC_TYPE.ENTRUST) {
  887. detail.executionFactoryId = row.executionFactoryId || undefined;
  888. detail.executionFactoryName = row.executionFactoryName || '';
  889. }
  890. if (row.id != null) detail.id = row.id;
  891. return detail;
  892. });
  893. return {
  894. detailList,
  895. fileParam: head.fileParam ?? [],
  896. id: head.id,
  897. isCache,
  898. planCode: plan?.code ?? head.planCode,
  899. planId: plan?.id ?? head.planId,
  900. produceVersionId: head.produceVersionId,
  901. produceVersionName: head.produceVersionName,
  902. releaseTime: head.releaseTime,
  903. routingCode: head.routingCode,
  904. routingId: head.routingId ?? plan?.produceRoutingId,
  905. routingName: head.routingName ?? plan?.produceRoutingName,
  906. routingVersion: head.routingVersion
  907. };
  908. },
  909. async handleCache() {
  910. if (!this.validateTaskList()) return;
  911. const payload = this.buildSavePayload(1);
  912. const loading = this.$loading({ lock: true, text: '缓存中...' });
  913. try {
  914. await savePlanDotLine(payload);
  915. this.$emit('save', {
  916. plan: this.currentPlan,
  917. taskList: this.taskList,
  918. payload
  919. });
  920. this.$message.success('缓存成功');
  921. this.onModalClose();
  922. } catch (e) {
  923. this.$message.error(e.message || '缓存失败');
  924. } finally {
  925. loading.close();
  926. }
  927. },
  928. async handleSave() {
  929. if (!this.validateTaskList()) return;
  930. const payload = this.buildSavePayload(0);
  931. const loading = this.$loading({ lock: true, text: '保存中...' });
  932. try {
  933. await savePlanDotLine(payload);
  934. this.$emit('save', {
  935. plan: this.currentPlan,
  936. taskList: this.taskList,
  937. payload
  938. });
  939. this.$message.success('保存成功');
  940. this.onModalClose();
  941. } catch (e) {
  942. this.$message.error(e.message || '保存失败');
  943. } finally {
  944. loading.close();
  945. }
  946. },
  947. onModalClose() {
  948. window.removeEventListener('resize', this.calcTableHeight);
  949. this.modalFullscreen = false;
  950. this.visible = false;
  951. this.dialogVisible = false;
  952. this.taskList = [];
  953. this.currentPlan = null;
  954. this.planRoutingPayload = null;
  955. this.routeErrorIndex = -1;
  956. this.$emit('success');
  957. }
  958. }
  959. };
  960. </script>
  961. <style lang="scss" scoped>
  962. .plan-dot-line,
  963. .top-route,
  964. .config-panel {
  965. width: 100%;
  966. max-width: 100%;
  967. min-width: 0;
  968. box-sizing: border-box;
  969. }
  970. .plan-dot-line {
  971. min-height: 360px;
  972. }
  973. .plan-time-bar {
  974. display: flex;
  975. flex-wrap: wrap;
  976. align-items: center;
  977. gap: 12px 28px;
  978. margin-bottom: 12px;
  979. padding: 10px 12px;
  980. border: 1px solid #ebeef5;
  981. border-radius: 4px;
  982. background: #fafafa;
  983. font-size: 14px;
  984. }
  985. .plan-time-item {
  986. display: inline-flex;
  987. align-items: baseline;
  988. gap: 4px;
  989. }
  990. .plan-time-label {
  991. color: #606266;
  992. white-space: nowrap;
  993. }
  994. .plan-time-value {
  995. color: #303133;
  996. font-weight: 500;
  997. }
  998. .last-end-over-plan-wrap,
  999. .end-over-delivery-wrap {
  1000. width: 100%;
  1001. }
  1002. .last-end-over-plan-wrap ::v-deep .el-input__inner,
  1003. .end-over-delivery-wrap ::v-deep .el-input__inner {
  1004. color: #f56c6c;
  1005. border-color: #f56c6c;
  1006. }
  1007. .last-end-over-plan-wrap ::v-deep .el-input__inner:focus,
  1008. .end-over-delivery-wrap ::v-deep .el-input__inner:focus {
  1009. border-color: #f56c6c;
  1010. }
  1011. .top-route,
  1012. .config-panel {
  1013. border: 1px solid #ebeef5;
  1014. border-radius: 4px;
  1015. padding: 12px;
  1016. background: #fff;
  1017. }
  1018. /* 工艺路线区域随内容增高,不要出现纵向滚动条 */
  1019. .top-route {
  1020. overflow: visible;
  1021. }
  1022. .config-panel {
  1023. margin-top: 12px;
  1024. }
  1025. .panel-title {
  1026. font-size: 14px;
  1027. font-weight: 600;
  1028. margin-bottom: 10px;
  1029. }
  1030. .route-steps {
  1031. width: 100%;
  1032. overflow-x: auto;
  1033. overflow-y: hidden;
  1034. padding-bottom: 8px;
  1035. box-sizing: content-box;
  1036. }
  1037. .route-steps ::v-deep {
  1038. .el-steps {
  1039. display: flex;
  1040. flex-wrap: nowrap;
  1041. align-items: flex-start;
  1042. }
  1043. .el-step {
  1044. flex-shrink: 0;
  1045. }
  1046. .el-step__title {
  1047. font-size: clamp(16px, 2.2vw, 16px);
  1048. line-height: 1.35;
  1049. max-width: 8em;
  1050. margin-left: auto;
  1051. margin-right: auto;
  1052. white-space: normal;
  1053. word-break: break-all;
  1054. }
  1055. .el-step__title:not(.is-error) {
  1056. color: #157a2c;
  1057. }
  1058. .el-step.active .el-step__title:not(.is-error) {
  1059. font-weight: 600;
  1060. }
  1061. .el-step__line {
  1062. top: 14px;
  1063. }
  1064. /* 已完成步骤 success 色(默认绿)改为品牌绿 */
  1065. .el-step__head.is-success {
  1066. color: #157a2c;
  1067. border-color: #157a2c;
  1068. }
  1069. .el-step__head.is-success .el-step__line {
  1070. background-color: #157a2c;
  1071. }
  1072. .el-step__head.is-success .el-step__icon {
  1073. color: #157a2c;
  1074. border-color: #157a2c;
  1075. }
  1076. .el-step__head.is-success .el-step__line-inner {
  1077. background-color: #157a2c;
  1078. border-color: #157a2c;
  1079. }
  1080. /* 当前步骤 process:ele-admin 对 .is-text 使用主题色实心圆,需覆盖 background */
  1081. .el-step__head.is-process {
  1082. color: #157a2c;
  1083. border-color: #157a2c;
  1084. }
  1085. .el-step__head.is-process .el-step__icon {
  1086. color: #157a2c;
  1087. border-color: #157a2c;
  1088. }
  1089. .el-step__head.is-process .el-step__icon.is-text {
  1090. color: #fff;
  1091. border-color: #157a2c;
  1092. background: #157a2c;
  1093. }
  1094. .el-step__head.is-process .el-step__icon.is-text .el-step__icon-inner {
  1095. color: #fff;
  1096. }
  1097. .el-step__description.is-process {
  1098. color: #157a2c;
  1099. }
  1100. .el-step__icon {
  1101. width: 30px;
  1102. height: 30px;
  1103. }
  1104. .el-step__main {
  1105. padding-top: 8px;
  1106. margin-top: 0;
  1107. }
  1108. .el-step__description {
  1109. margin-top: 0;
  1110. padding-right: 0;
  1111. }
  1112. }
  1113. @media (max-width: 576px) {
  1114. .top-route,
  1115. .config-panel {
  1116. padding: 8px;
  1117. }
  1118. }
  1119. .task-config-table-wrap {
  1120. width: 100%;
  1121. }
  1122. .config-table ::v-deep {
  1123. /* 序号列:column 上的 style 无效,需 class-name + td 内 .cell */
  1124. td.config-table-index-col .cell {
  1125. font-size: 14px !important;
  1126. }
  1127. .el-table td > .cell {
  1128. font-size: 14px;
  1129. text-align: center;
  1130. }
  1131. .el-table__body .cell {
  1132. padding-left: 6px;
  1133. padding-right: 6px;
  1134. }
  1135. td.task-name-cell .task-name-text {
  1136. font-size: 14px;
  1137. line-height: 1.4;
  1138. }
  1139. /* 与工序名称一致:覆盖 el-select / date-editor 的 small 字号 */
  1140. .config-table-control .el-input__inner {
  1141. font-size: 14px;
  1142. }
  1143. .config-table-control {
  1144. width: 100% !important;
  1145. }
  1146. .config-table-control.el-date-editor {
  1147. max-width: 100%;
  1148. }
  1149. }
  1150. </style>
  1151. <!-- 弹窗 append-to-body 后表头不在带 scoped 的节点链上,需用弹窗 class 单独写表头 -->
  1152. <style lang="scss">
  1153. .plan-dot-line-dialog {
  1154. .task-config-table-wrap
  1155. .el-table__header-wrapper
  1156. th.el-table__cell
  1157. > .cell {
  1158. font-size: 15px !important;
  1159. line-height: 1.4 !important;
  1160. }
  1161. .top-route {
  1162. overflow: visible !important;
  1163. max-height: none !important;
  1164. }
  1165. .route-steps .el-step__head.is-success {
  1166. color: #157a2c;
  1167. border-color: #157a2c;
  1168. }
  1169. .route-steps .el-step__head.is-success .el-step__line {
  1170. background-color: #157a2c;
  1171. }
  1172. .route-steps .el-step__head.is-success .el-step__icon {
  1173. color: #157a2c;
  1174. border-color: #157a2c;
  1175. }
  1176. .route-steps .el-step__head.is-success .el-step__line-inner {
  1177. background-color: #157a2c;
  1178. border-color: #157a2c;
  1179. }
  1180. .route-steps .el-step__head.is-process {
  1181. color: #157a2c;
  1182. border-color: #157a2c;
  1183. }
  1184. .route-steps .el-step__head.is-process .el-step__icon {
  1185. color: #157a2c;
  1186. border-color: #157a2c;
  1187. }
  1188. .route-steps .el-step__head.is-process .el-step__icon.is-text {
  1189. color: #fff;
  1190. border-color: #157a2c;
  1191. background: #157a2c;
  1192. }
  1193. .route-steps
  1194. .el-step__head.is-process
  1195. .el-step__icon.is-text
  1196. .el-step__icon-inner {
  1197. color: #fff;
  1198. }
  1199. .route-steps .el-step__title:not(.is-error) {
  1200. color: #157a2c;
  1201. }
  1202. .route-steps .el-step__description.is-process {
  1203. color: #157a2c;
  1204. }
  1205. }
  1206. </style>