planDotLineDetail.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <template>
  2. <div v-loading="loading" class="pane-box plan-dot-line-detail">
  3. <div class="plan-time-bar">
  4. <span class="plan-time-item">
  5. <span class="plan-time-label">计划开始时间:</span>
  6. <span class="plan-time-value">{{ planStartDisplay }}</span>
  7. </span>
  8. <span class="plan-time-item">
  9. <span class="plan-time-label">计划结束时间:</span>
  10. <span class="plan-time-value">{{ planEndDisplay }}</span>
  11. </span>
  12. <span class="plan-time-item">
  13. <span class="plan-time-label">计划数量:</span>
  14. <span class="plan-time-value">{{ planNumDisplay }}</span>
  15. </span>
  16. </div>
  17. <div class="plan-dot-line">
  18. <div class="top-route">
  19. <div class="panel-title">工艺路线</div>
  20. <el-empty
  21. v-if="taskList.length === 0"
  22. description="暂无工艺路线"
  23. ></el-empty>
  24. <el-steps
  25. v-else
  26. :active="routeStepsActive"
  27. space="20px"
  28. align-center
  29. finish-status="success"
  30. class="route-steps"
  31. >
  32. <el-step
  33. v-for="(item, index) in taskList"
  34. :key="`route-step-${item._taskKey}`"
  35. :title="routeStepTitle(item, index)"
  36. :class="{ active: routeDesIndex === index }"
  37. ></el-step>
  38. </el-steps>
  39. </div>
  40. <div class="config-panel">
  41. <div class="panel-title">工艺配置</div>
  42. <el-empty
  43. v-if="taskList.length === 0"
  44. description="暂无工艺"
  45. ></el-empty>
  46. <div v-else class="task-config-table-wrap">
  47. <el-table
  48. :data="taskList"
  49. border
  50. size="small"
  51. row-key="_taskKey"
  52. max-height="420"
  53. class="config-table"
  54. >
  55. <el-table-column
  56. type="index"
  57. label="序号"
  58. width="60"
  59. align="center"
  60. class-name="config-index-cell"
  61. label-class-name="config-execution-team-header"
  62. />
  63. <el-table-column
  64. label="工序名称"
  65. min-width="100"
  66. show-overflow-tooltip
  67. class-name="process-name-cell"
  68. label-class-name="config-execution-team-header"
  69. align="center"
  70. >
  71. <template slot-scope="{ row, $index }">
  72. <span class="task-name-text">{{ taskName(row, $index) }}</span>
  73. </template>
  74. </el-table-column>
  75. <el-table-column
  76. label="工序并行"
  77. min-width="100"
  78. align="center"
  79. label-class-name="config-execution-team-header"
  80. class-name="config-meta-cell"
  81. >
  82. <template slot-scope="{ row }">
  83. {{ row.isParallelism ? '是' : '否' }}
  84. </template>
  85. </el-table-column>
  86. <el-table-column
  87. label="执行模式"
  88. min-width="100"
  89. align="center"
  90. label-class-name="config-execution-team-header"
  91. class-name="config-meta-cell"
  92. >
  93. <template slot-scope="{ row }">
  94. {{ executionTypeText(row.executionType) }}
  95. </template>
  96. </el-table-column>
  97. <el-table-column
  98. label="执行对象"
  99. min-width="130"
  100. align="center"
  101. label-class-name="config-execution-team-header"
  102. class-name="config-meta-cell"
  103. >
  104. <template slot-scope="{ row }">
  105. {{ executionObjectDisplay(row) }}
  106. </template>
  107. </el-table-column>
  108. <el-table-column
  109. label="执行开始时间"
  110. min-width="168"
  111. align="center"
  112. prop="executionStartTime"
  113. label-class-name="config-execution-team-header"
  114. class-name="config-meta-cell"
  115. />
  116. <el-table-column
  117. label="执行结束时间"
  118. min-width="168"
  119. align="center"
  120. prop="executionEndTime"
  121. label-class-name="config-execution-team-header"
  122. class-name="config-meta-cell"
  123. />
  124. </el-table>
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. </template>
  130. <script>
  131. import { getPlanDotLine } from '@/api/productionPlan/planDotLine';
  132. const EXEC_TYPE_MAP = { 0: '自制', 1: '请托', 2: '委外' };
  133. /** 与 planDotLine.vue 一致:0 自制、1 请托、2 委外 */
  134. const EXEC_TYPE = Object.freeze({ HOMEMADE: 0, ENTRUST: 1, OUTSOURCE: 2 });
  135. function formatDateTime(val) {
  136. if (val == null || val === '') return '';
  137. const str = String(val).trim();
  138. if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str)) return str;
  139. const d = new Date(val);
  140. if (Number.isNaN(d.getTime())) return '';
  141. const p = (n) => String(n).padStart(2, '0');
  142. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
  143. d.getHours()
  144. )}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
  145. }
  146. export default {
  147. name: 'PlanDotLineDetail',
  148. props: {
  149. /** 生产计划 id,无则展示空状态 */
  150. planId: {
  151. type: [Number, String],
  152. default: null
  153. },
  154. infoData: {
  155. type: Object,
  156. default: () => {}
  157. }
  158. },
  159. data() {
  160. return {
  161. loading: false,
  162. taskList: []
  163. };
  164. },
  165. computed: {
  166. routeStepsActive() {
  167. const list = this.taskList;
  168. if (!list.length) return 0;
  169. for (let i = 0; i < list.length; i++) {
  170. if (!this.isTaskDone(list[i])) return i;
  171. }
  172. return list.length;
  173. },
  174. routeDesIndex() {
  175. const list = this.taskList;
  176. if (!list.length) return -1;
  177. const a = this.routeStepsActive;
  178. return a >= list.length ? list.length - 1 : a;
  179. },
  180. planStartDisplay() {
  181. const t = this.infoData?.productionPlan?.startTime;
  182. return t ? formatDateTime(t) || String(t) : '—';
  183. },
  184. planEndDisplay() {
  185. const t = this.infoData?.productionPlan?.endTime;
  186. return t ? formatDateTime(t) || String(t) : '—';
  187. },
  188. planNumDisplay() {
  189. const t =
  190. this.infoData?.productionPlan?.productNum +
  191. this.infoData?.productionPlan?.measuringUnit;
  192. return t ? t : '—';
  193. }
  194. },
  195. watch: {
  196. planId(val) {
  197. if (val == null || val === '') {
  198. this.taskList = [];
  199. }
  200. }
  201. },
  202. methods: {
  203. /** 父级在切换到「布点详情」页签时调用,拉取接口 */
  204. async load() {
  205. const planId = this.planId;
  206. if (planId == null || planId === '') {
  207. this.taskList = [];
  208. return;
  209. }
  210. this.loading = true;
  211. try {
  212. const planData = await getPlanDotLine({ planId });
  213. const details = planData?.detailList;
  214. if (!Array.isArray(details) || details.length === 0) {
  215. this.taskList = [];
  216. return;
  217. }
  218. this.taskList = details
  219. .slice()
  220. .sort((a, b) => (a.taskSort ?? 0) - (b.taskSort ?? 0))
  221. .map((item, i) => this.normalizeItem(item, i));
  222. } catch {
  223. this.taskList = [];
  224. } finally {
  225. this.loading = false;
  226. }
  227. },
  228. normalizeItem(item, index) {
  229. return {
  230. ...item,
  231. _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
  232. executionStartTime: this.formatTime(item.executionStartTime),
  233. executionEndTime: this.formatTime(item.executionEndTime),
  234. executionType:
  235. item.executionType != null ? Number(item.executionType) : undefined
  236. };
  237. },
  238. taskName(item, index) {
  239. return item.taskName || item.name || `工艺${index + 1}`;
  240. },
  241. routeStepTitle(item, index) {
  242. return (
  243. item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
  244. );
  245. },
  246. isTaskDone(item) {
  247. return item.executionType != null && item.executionType !== '';
  248. },
  249. executionTypeText(val) {
  250. return EXEC_TYPE_MAP[val] ?? '';
  251. },
  252. executionObjectDisplay(row) {
  253. const t =
  254. row.executionType != null ? Number(row.executionType) : undefined;
  255. if (t === EXEC_TYPE.HOMEMADE) return row.executionTeamName ?? '';
  256. if (t === EXEC_TYPE.ENTRUST) return row.executionFactoryName ?? '';
  257. return '';
  258. },
  259. formatTime(val) {
  260. if (val == null || val === '') return '';
  261. const str = String(val).trim();
  262. if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str)) return str;
  263. const d = new Date(val);
  264. if (Number.isNaN(d.getTime())) return '';
  265. const p = (n) => String(n).padStart(2, '0');
  266. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
  267. d.getHours()
  268. )}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
  269. }
  270. }
  271. };
  272. </script>
  273. <style lang="scss" scoped>
  274. .pane-box {
  275. padding: 20px 0;
  276. }
  277. .plan-dot-line-detail .plan-dot-line,
  278. .plan-dot-line-detail .top-route,
  279. .plan-dot-line-detail .config-panel {
  280. width: 100%;
  281. max-width: 100%;
  282. min-width: 0;
  283. box-sizing: border-box;
  284. }
  285. .plan-dot-line-detail .plan-dot-line {
  286. min-height: 360px;
  287. }
  288. .plan-dot-line-detail .top-route,
  289. .plan-dot-line-detail .config-panel {
  290. border: 1px solid #ebeef5;
  291. border-radius: 4px;
  292. padding: 12px;
  293. background: #fff;
  294. }
  295. .plan-dot-line-detail .top-route {
  296. overflow: visible;
  297. }
  298. .plan-dot-line-detail .config-panel {
  299. margin-top: 12px;
  300. }
  301. .plan-dot-line-detail .panel-title {
  302. font-size: 14px;
  303. font-weight: 600;
  304. margin-bottom: 10px;
  305. }
  306. .plan-dot-line-detail .route-steps {
  307. width: 100%;
  308. overflow-x: auto;
  309. overflow-y: hidden;
  310. padding-bottom: 8px;
  311. box-sizing: content-box;
  312. }
  313. .plan-time-bar {
  314. display: flex;
  315. flex-wrap: wrap;
  316. align-items: center;
  317. gap: 12px 28px;
  318. margin-bottom: 12px;
  319. padding: 10px 12px;
  320. border: 1px solid #ebeef5;
  321. border-radius: 4px;
  322. background: #fafafa;
  323. font-size: 14px;
  324. }
  325. .plan-time-item {
  326. display: inline-flex;
  327. align-items: baseline;
  328. gap: 4px;
  329. }
  330. .plan-time-label {
  331. color: #606266;
  332. white-space: nowrap;
  333. }
  334. .plan-time-value {
  335. color: #303133;
  336. font-weight: 500;
  337. }
  338. .plan-dot-line-detail .route-steps ::v-deep {
  339. .el-steps {
  340. display: flex;
  341. flex-wrap: nowrap;
  342. align-items: flex-start;
  343. }
  344. .el-step {
  345. flex-shrink: 0;
  346. }
  347. .el-step__title {
  348. font-size: clamp(16px, 2.2vw, 16px);
  349. line-height: 1.35;
  350. max-width: 8em;
  351. margin-left: auto;
  352. margin-right: auto;
  353. white-space: normal;
  354. word-break: break-all;
  355. }
  356. .el-step.active .el-step__title {
  357. color: #157a2c;
  358. font-weight: 600;
  359. }
  360. .el-step__line {
  361. top: 14px;
  362. }
  363. .el-step__head.is-success {
  364. color: #157a2c;
  365. border-color: #157a2c;
  366. }
  367. .el-step__head.is-success .el-step__line {
  368. background-color: #157a2c;
  369. }
  370. .el-step__head.is-success .el-step__icon {
  371. color: #157a2c;
  372. border-color: #157a2c;
  373. }
  374. .el-step__head.is-success .el-step__line-inner {
  375. background-color: #157a2c;
  376. border-color: #157a2c;
  377. }
  378. .el-step__title.is-success,
  379. .el-step__description.is-success {
  380. color: #157a2c;
  381. }
  382. .el-step__icon {
  383. width: 30px;
  384. height: 30px;
  385. }
  386. .el-step__main {
  387. padding-top: 8px;
  388. margin-top: 0;
  389. }
  390. .el-step__description {
  391. margin-top: 0;
  392. padding-right: 0;
  393. }
  394. }
  395. @media (max-width: 576px) {
  396. .plan-dot-line-detail .top-route,
  397. .plan-dot-line-detail .config-panel {
  398. padding: 8px;
  399. }
  400. }
  401. .plan-dot-line-detail .task-config-table-wrap {
  402. width: 100%;
  403. }
  404. .plan-dot-line-detail .config-table ::v-deep {
  405. .el-table th > .cell,
  406. .el-table td > .cell {
  407. font-size: 14px;
  408. text-align: center;
  409. }
  410. th.config-execution-team-header > .cell,
  411. td.config-meta-cell > .cell {
  412. font-size: 14px;
  413. font-weight: 500;
  414. line-height: 1.4;
  415. }
  416. td.config-index-cell > .cell {
  417. font-size: 14px;
  418. }
  419. .el-table__body .cell {
  420. padding-left: 6px;
  421. padding-right: 6px;
  422. }
  423. td.process-name-cell .task-name-text {
  424. font-size: 14px;
  425. font-weight: 500;
  426. line-height: 1.4;
  427. }
  428. }
  429. </style>