reportDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <template>
  2. <u-popup
  3. :show="show"
  4. mode="bottom"
  5. :round="10"
  6. :closeable="true"
  7. @close="cancel"
  8. bgColor="#fff"
  9. :customStyle="{ maxHeight: '85vh', overflowY: 'auto' }"
  10. >
  11. <view class="report_wrap">
  12. <view class="title">{{ title }}</view>
  13. <view class="section_title">报工信息</view>
  14. <view class="form_row">
  15. <view class="label required">实际开始时间</view>
  16. <picker
  17. mode="multiSelector"
  18. v-if="false"
  19. ></picker>
  20. <view class="picker_box" @click="!inputDis && openTimePicker('realStartTime')">
  21. <text v-if="form.realStartTime" class="val">{{ form.realStartTime }}</text>
  22. <text v-else class="placeholder">请选择实际开始时间</text>
  23. </view>
  24. </view>
  25. <view class="form_row">
  26. <view class="label required">实际结束时间</view>
  27. <view class="picker_box" @click="!inputDis && openTimePicker('realEndTime')">
  28. <text v-if="form.realEndTime" class="val">{{ form.realEndTime }}</text>
  29. <text v-else class="placeholder">请选择实际结束时间</text>
  30. </view>
  31. </view>
  32. <view class="form_row">
  33. <view class="label required">报工数</view>
  34. <input
  35. class="ipt"
  36. type="digit"
  37. v-model="form.reportQuantity"
  38. :disabled="inputDis"
  39. placeholder="请输入"
  40. @input="(e) => handleQuantityInput(e.detail.value, 'reportQuantity')"
  41. />
  42. </view>
  43. <view class="form_row">
  44. <view class="label required">损耗数</view>
  45. <input
  46. class="ipt"
  47. type="digit"
  48. v-model="form.lossQuantity"
  49. :disabled="inputDis"
  50. placeholder="请输入"
  51. @input="(e) => handleQuantityInput(e.detail.value, 'lossQuantity')"
  52. />
  53. </view>
  54. <view class="form_row">
  55. <view class="label">备注信息</view>
  56. <textarea
  57. class="ipt textarea"
  58. v-model="form.remark"
  59. :disabled="inputDis"
  60. placeholder="请输入"
  61. maxlength="-1"
  62. />
  63. </view>
  64. <view class="tip" v-if="!inputDis">
  65. 任务剩余可报数量:{{ remaining }}
  66. </view>
  67. <view class="section_title">工单信息</view>
  68. <view class="info_grid">
  69. <view class="info_item" v-for="item in fieldMap.orderList" :key="item.prop">
  70. <view class="info_label">{{ item.label }}</view>
  71. <view class="info_val">{{ current[item.prop] || '-' }}</view>
  72. </view>
  73. </view>
  74. <view class="section_title">任务信息</view>
  75. <view class="info_grid">
  76. <view class="info_item" v-for="item in fieldMap.taskList" :key="item.prop">
  77. <view class="info_label">{{ item.label }}</view>
  78. <view class="info_val">{{ current[item.prop] || '-' }}</view>
  79. </view>
  80. </view>
  81. <view v-if="title === '详情'" class="section_title">报工记录</view>
  82. <view v-if="title === '详情'" class="record_list">
  83. <view v-if="list.length === 0" class="empty">暂无记录</view>
  84. <view v-for="(rec, idx) in list" :key="idx" class="record_item">
  85. <view class="rec_row"><text class="rec_label">实际开始:</text>{{ rec.realStartTime || '-' }}</view>
  86. <view class="rec_row"><text class="rec_label">实际结束:</text>{{ rec.realEndTime || '-' }}</view>
  87. <view class="rec_row"><text class="rec_label">工时:</text>{{ rec.durationText || '-' }}</view>
  88. <view class="rec_row"><text class="rec_label">报工数:</text>{{ rec.reportQuantity || 0 }}</view>
  89. <view class="rec_row"><text class="rec_label">损耗数:</text>{{ rec.lossQuantity || 0 }}</view>
  90. <view class="rec_row"><text class="rec_label">备注:</text>{{ rec.remark || '-' }}</view>
  91. </view>
  92. </view>
  93. <view class="btns">
  94. <button class="btn cancel" @click="cancel">关闭</button>
  95. <button v-if="!inputDis" class="btn confirm" :disabled="loadingBtn" @click="submitAdd">报工</button>
  96. </view>
  97. </view>
  98. <u-datetime-picker
  99. :show="timeShow"
  100. v-model="timeValue"
  101. mode="datetime"
  102. @confirm="onTimeConfirm"
  103. @cancel="timeShow = false"
  104. @close="timeShow = false"
  105. :closeOnClickOverlay="true"
  106. ></u-datetime-picker>
  107. </u-popup>
  108. </template>
  109. <script>
  110. import {
  111. listWorkCenter,
  112. batchUpdateRealTime,
  113. listUpdateRealTimeRecord,
  114. } from "@/api/pda/workReport.js";
  115. export default {
  116. data() {
  117. return {
  118. show: false,
  119. title: "详情",
  120. timeShow: false,
  121. timeField: "",
  122. timeValue: Date.now(),
  123. form: {
  124. realEndTime: "",
  125. realStartTime: "",
  126. remark: "",
  127. reportQuantity: "",
  128. lossQuantity: "",
  129. workOrderCode: "",
  130. workOrderId: "",
  131. taskId: "",
  132. },
  133. current: {},
  134. list: [],
  135. reportNum: 0,
  136. lossNum: 0,
  137. loadingBtn: false,
  138. fieldMap: {
  139. orderList: [
  140. { label: "计划批次号", prop: "batchNo" },
  141. { label: "工单编码", prop: "mesWorkOrderCode" },
  142. { label: "产品编码", prop: "productCode" },
  143. { label: "产品名称", prop: "productName" },
  144. { label: "规格", prop: "specification" },
  145. { label: "生产订单编码", prop: "workOrderCode" },
  146. { label: "计划编号", prop: "productionPlanCode" },
  147. { label: "工艺路线", prop: "produceRoutingName" },
  148. { label: "要求生产数量", prop: "formingNum" },
  149. { label: "要求生产重量", prop: "formingWeight" },
  150. { label: "所属工厂", prop: "factoryName" },
  151. { label: "所属班组", prop: "assignTeamName" },
  152. { label: "计划开始时间", prop: "planStartTime" },
  153. { label: "计划结束时间", prop: "planCompleteTime" },
  154. ],
  155. taskList: [
  156. { label: "任务编码", prop: "assignCode" },
  157. { label: "工序名称", prop: "taskName" },
  158. { label: "执行类型", prop: "assigneeType" },
  159. { label: "执行对象", prop: "assigneeName" },
  160. { label: "工时", prop: "durationText" },
  161. { label: "任务开始时间", prop: "startTime" },
  162. { label: "任务结束时间", prop: "endTime" },
  163. { label: "任务数量", prop: "quantity" },
  164. { label: "任务重量", prop: "weight" },
  165. ],
  166. },
  167. };
  168. },
  169. computed: {
  170. inputDis() {
  171. return this.title === "详情";
  172. },
  173. remaining() {
  174. const req = Number(this.current.quantity) || 0;
  175. return Math.max(this.sub(req, this.add(this.reportNum, this.lossNum)), 0);
  176. },
  177. },
  178. methods: {
  179. open(type, row, form) {
  180. this.title = type === "report" ? "报工" : "详情";
  181. this.current = { ...row };
  182. this.list = [];
  183. if (form && form.realEndTime) {
  184. this.form = { ...form };
  185. } else {
  186. this.form = {
  187. realEndTime: form ? form.realEndTime || "" : "",
  188. realStartTime: form ? form.realStartTime || "" : "",
  189. remark: form ? form.remark || "" : "",
  190. reportQuantity: form ? form.reportQuantity : "",
  191. lossQuantity: form ? form.lossQuantity : 0,
  192. workOrderCode: form ? form.workOrderCode : "",
  193. workOrderId: form ? form.workOrderId : "",
  194. taskId: form ? form.taskId : "",
  195. };
  196. }
  197. this.reportNum = Number(this.current.reportQuantityReported) || 0;
  198. this.lossNum = Number(this.current.lossQuantityReported) || 0;
  199. if (type !== "report") {
  200. listUpdateRealTimeRecord(this.current.apsAssigneeId)
  201. .then((res) => {
  202. if (res) this.list = res;
  203. })
  204. .catch((err) => {
  205. uni.showToast({ title: err.message || "记录加载失败", icon: "none" });
  206. });
  207. }
  208. this.getFactory();
  209. this.show = true;
  210. },
  211. async getFactory() {
  212. if (!this.current.firstTaskId) return;
  213. try {
  214. const res = await listWorkCenter(this.current.firstTaskId);
  215. if (res && res.length) {
  216. this.$set(this.current, "factoryName", res[0].factoryName);
  217. }
  218. } catch (err) {
  219. // 忽略:仅展示字段
  220. }
  221. },
  222. cancel() {
  223. this.show = false;
  224. this.form = {
  225. realEndTime: "",
  226. realStartTime: "",
  227. remark: "",
  228. reportQuantity: "",
  229. lossQuantity: "",
  230. workOrderCode: "",
  231. workOrderId: "",
  232. taskId: "",
  233. };
  234. this.current = {};
  235. this.list = [];
  236. },
  237. openTimePicker(field) {
  238. this.timeField = field;
  239. const cur = this.form[field];
  240. this.timeValue = cur ? new Date(cur.replace(/-/g, "/")).getTime() : Date.now();
  241. this.timeShow = true;
  242. },
  243. onTimeConfirm(e) {
  244. const ts = e.value;
  245. const d = new Date(ts);
  246. const pad = (n) => String(n).padStart(2, "0");
  247. const val =
  248. d.getFullYear() +
  249. "-" +
  250. pad(d.getMonth() + 1) +
  251. "-" +
  252. pad(d.getDate()) +
  253. " " +
  254. pad(d.getHours()) +
  255. ":" +
  256. pad(d.getMinutes()) +
  257. ":" +
  258. pad(d.getSeconds());
  259. this.form[this.timeField] = val;
  260. this.timeShow = false;
  261. this.handleTimeChange();
  262. },
  263. handleTimeChange() {
  264. if (this.form.realStartTime && this.current.startTime) {
  265. const a = new Date(this.form.realStartTime.replace(/-/g, "/")).getTime();
  266. const b = new Date(String(this.current.startTime).replace(/-/g, "/")).getTime();
  267. if (a < b) {
  268. this.form.realStartTime = "";
  269. uni.showToast({ title: "实际开始时间不能早于任务开始时间", icon: "none" });
  270. return;
  271. }
  272. }
  273. if (this.form.realStartTime && this.form.realEndTime) {
  274. const a = new Date(this.form.realStartTime.replace(/-/g, "/")).getTime();
  275. const b = new Date(this.form.realEndTime.replace(/-/g, "/")).getTime();
  276. if (a > b) {
  277. this.form.realEndTime = "";
  278. uni.showToast({ title: "结束时间不能早于开始时间", icon: "none" });
  279. }
  280. }
  281. },
  282. handleQuantityInput(val, type) {
  283. let newVal = String(val || "").replace(/[^\d.]/g, "");
  284. if (newVal.startsWith(".")) newVal = "";
  285. const firstDot = newVal.indexOf(".");
  286. if (firstDot !== -1) {
  287. newVal =
  288. newVal.slice(0, firstDot + 1) +
  289. newVal.slice(firstDot + 1).replace(/\./g, "");
  290. }
  291. const match = newVal.match(/^(\d+)(\.\d{0,4})?/);
  292. this.form[type] = match ? match[0] : "";
  293. this.calculateQuantity(type);
  294. },
  295. calculateQuantity(type) {
  296. const curRep = Number(this.form.reportQuantity) || 0;
  297. const curLoss = Number(this.form.lossQuantity) || 0;
  298. if (this.add(curRep, curLoss) > this.remaining) {
  299. this.form[type] = "";
  300. uni.showToast({
  301. title: `本次报工数与损耗数之和不能超过任务剩余可报数量(剩余 ${this.remaining})`,
  302. icon: "none",
  303. });
  304. }
  305. },
  306. getDecimalLength(num) {
  307. return (num.toString().split(".")[1] || "").length;
  308. },
  309. toInteger(num) {
  310. const len = this.getDecimalLength(num);
  311. return { int: Math.round(num * Math.pow(10, len)), factor: Math.pow(10, len) };
  312. },
  313. add(a, b) {
  314. const { int: aInt, factor: aFactor } = this.toInteger(a);
  315. const { int: bInt, factor: bFactor } = this.toInteger(b);
  316. const maxFactor = Math.max(aFactor, bFactor);
  317. return (aInt * (maxFactor / aFactor) + bInt * (maxFactor / bFactor)) / maxFactor;
  318. },
  319. sub(a, b) {
  320. const { int: aInt, factor: aFactor } = this.toInteger(a);
  321. const { int: bInt, factor: bFactor } = this.toInteger(b);
  322. const maxFactor = Math.max(aFactor, bFactor);
  323. return (aInt * (maxFactor / aFactor) - bInt * (maxFactor / bFactor)) / maxFactor;
  324. },
  325. submitAdd() {
  326. if (!this.form.realStartTime) {
  327. return uni.showToast({ title: "请选择实际开始时间", icon: "none" });
  328. }
  329. if (!this.form.realEndTime) {
  330. return uni.showToast({ title: "请选择实际结束时间", icon: "none" });
  331. }
  332. if (this.form.reportQuantity === "" || this.form.reportQuantity === null) {
  333. return uni.showToast({ title: "请输入报工数量", icon: "none" });
  334. }
  335. if (this.form.lossQuantity === "" || this.form.lossQuantity === null) {
  336. return uni.showToast({ title: "请输入损耗数量", icon: "none" });
  337. }
  338. const curRep = Number(this.form.reportQuantity) || 0;
  339. const curLoss = Number(this.form.lossQuantity) || 0;
  340. if (this.add(curRep, curLoss) > this.remaining) {
  341. return uni.showToast({
  342. title: `本次报工数与损耗数之和不能超过任务剩余可报数量(剩余 ${this.remaining})`,
  343. icon: "none",
  344. });
  345. }
  346. const data = { ...this.form, apsAssigneeId: this.current.apsAssigneeId };
  347. this.loadingBtn = true;
  348. batchUpdateRealTime([data])
  349. .then(() => {
  350. this.loadingBtn = false;
  351. uni.showToast({ title: "操作成功", icon: "success" });
  352. this.$emit("success");
  353. this.cancel();
  354. })
  355. .catch((err) => {
  356. this.loadingBtn = false;
  357. uni.showToast({ title: err.message || "操作失败", icon: "none" });
  358. });
  359. },
  360. },
  361. };
  362. </script>
  363. <style lang="scss" scoped>
  364. .report_wrap {
  365. padding: 30rpx;
  366. .title {
  367. font-size: 32rpx;
  368. font-weight: 600;
  369. text-align: center;
  370. margin-bottom: 20rpx;
  371. }
  372. .section_title {
  373. font-size: 28rpx;
  374. font-weight: 600;
  375. color: #333;
  376. margin: 20rpx 0 16rpx;
  377. padding-left: 16rpx;
  378. border-left: 6rpx solid $theme-color;
  379. }
  380. .form_row {
  381. margin-bottom: 20rpx;
  382. .label {
  383. font-size: 26rpx;
  384. color: #333;
  385. margin-bottom: 8rpx;
  386. &.required::before {
  387. content: "*";
  388. color: red;
  389. margin-right: 4rpx;
  390. }
  391. }
  392. .picker_box {
  393. border: 1rpx solid #e0e0e0;
  394. border-radius: 6rpx;
  395. padding: 14rpx 16rpx;
  396. font-size: 26rpx;
  397. min-height: 60rpx;
  398. .placeholder {
  399. color: #aaa;
  400. }
  401. }
  402. .ipt {
  403. border: 1rpx solid #e0e0e0;
  404. border-radius: 6rpx;
  405. padding: 14rpx 16rpx;
  406. font-size: 26rpx;
  407. width: 100%;
  408. box-sizing: border-box;
  409. min-height: 72rpx;
  410. line-height: 44rpx;
  411. }
  412. .textarea {
  413. min-height: 120rpx;
  414. }
  415. }
  416. .tip {
  417. color: #ff9c00;
  418. font-size: 24rpx;
  419. margin: -8rpx 0 16rpx;
  420. }
  421. .info_grid {
  422. display: flex;
  423. flex-wrap: wrap;
  424. background: #f7f9fa;
  425. padding: 16rpx;
  426. border-radius: 8rpx;
  427. .info_item {
  428. width: 50%;
  429. padding: 8rpx 6rpx;
  430. font-size: 24rpx;
  431. box-sizing: border-box;
  432. .info_label {
  433. color: #999;
  434. margin-bottom: 4rpx;
  435. }
  436. .info_val {
  437. color: #333;
  438. word-break: break-all;
  439. }
  440. }
  441. }
  442. .record_list {
  443. .empty {
  444. text-align: center;
  445. color: #999;
  446. font-size: 26rpx;
  447. padding: 30rpx 0;
  448. }
  449. .record_item {
  450. background: #f7f9fa;
  451. padding: 16rpx;
  452. border-radius: 8rpx;
  453. margin-bottom: 12rpx;
  454. .rec_row {
  455. font-size: 24rpx;
  456. color: #333;
  457. line-height: 40rpx;
  458. .rec_label {
  459. color: #999;
  460. }
  461. }
  462. }
  463. }
  464. .btns {
  465. display: flex;
  466. gap: 20rpx;
  467. margin-top: 30rpx;
  468. position: sticky;
  469. bottom: 0;
  470. background: #fff;
  471. padding-top: 16rpx;
  472. .btn {
  473. flex: 1;
  474. height: 80rpx;
  475. line-height: 80rpx;
  476. border-radius: 8rpx;
  477. font-size: 28rpx;
  478. }
  479. .cancel {
  480. background: #f5f5f5;
  481. color: #333;
  482. }
  483. .confirm {
  484. background: $theme-color;
  485. color: #fff;
  486. }
  487. }
  488. }
  489. </style>