report-form.vue 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894
  1. <script setup>
  2. import { onLoad } from "@dcloudio/uni-app";
  3. import { useDataDictStore } from "@/store/modules/dataDict";
  4. import {
  5. onMounted,
  6. reactive,
  7. ref,
  8. computed,
  9. getCurrentInstance,
  10. watch,
  11. } from "vue";
  12. import { useDebounceFn } from "@/utils/useDebounceFn.js";
  13. import tpfTimeRange from "@/components/tpf-time-range/tpf-time-range.vue";
  14. import deviceTransfer from "@/components/device-transfer/index.vue";
  15. import { getRuiDuReportAttrs, updateRuiDuReportBatch } from "@/api/ruiDu.js";
  16. import config from "@/utils/config";
  17. import dayjs from "dayjs";
  18. import { getTenantId, getAccessToken } from "@/utils/auth.js";
  19. const istime = ref("false");
  20. onLoad(options => {
  21. istime.value = options.istime;
  22. });
  23. const NON_PROD_FIELDS = [
  24. { key: "repairTime", label: "设备故障" },
  25. { key: "selfStopTime", label: "设备保养" },
  26. { key: "accidentTime", label: "工程质量" },
  27. { key: "complexityTime", label: "技术受限" },
  28. { key: "rectificationTime", label: "生产组织" },
  29. { key: "waitingStopTime", label: "不可抗力" },
  30. { key: "partyaDesign", label: "甲方设计" },
  31. { key: "partyaPrepare", label: "甲方准备" },
  32. { key: "partyaResource", label: "甲方资源" },
  33. { key: "relocationTime", label: "生产配合" },
  34. { key: "winterBreakTime", label: "待命" },
  35. { key: "otherNptTime", label: "其他非生产时间" },
  36. ];
  37. const { appContext } = getCurrentInstance();
  38. const t = appContext.config.globalProperties.$t;
  39. const { getIntDictOptions, getStrDictOptions, loadDataDictList } =
  40. useDataDictStore();
  41. const rdStatusRange = ref([]);
  42. const techniqueRange = ref([]);
  43. const handleInitSelect = () => {
  44. rdStatusRange.value = getStrDictOptions("rdStatus").map(item => {
  45. return {
  46. ...item,
  47. text: item.label,
  48. };
  49. });
  50. // 施工工艺
  51. techniqueRange.value = getIntDictOptions("rq_iot_project_technology_rd").map(
  52. item => {
  53. return {
  54. ...item,
  55. text: item.label,
  56. };
  57. }
  58. );
  59. };
  60. onMounted(() => {
  61. loadDataDictList().then(() => {
  62. handleInitSelect();
  63. });
  64. });
  65. const selectPlaceholder = computed(() => {
  66. return props.formDisable ? " " : t("operation.PleaseSelect");
  67. });
  68. const inputPlaceholder = computed(() => {
  69. return props.formDisable ? " " : t("operation.PleaseInput");
  70. });
  71. const props = defineProps({
  72. reportId: {
  73. type: String,
  74. default: "",
  75. },
  76. reportData: {
  77. type: Object,
  78. default: () => {},
  79. },
  80. formDisable: {
  81. type: Boolean,
  82. default: false, // 是否禁用表单
  83. },
  84. type: {
  85. type: String,
  86. default: "edit",
  87. },
  88. });
  89. const isRequired = computed(() => {
  90. return props.formDisable ? false : true;
  91. });
  92. const reportFormRef = ref(null);
  93. // 设备选择器
  94. const deviceTransferRef = ref(null);
  95. // 已选择的设备(名称)
  96. const selectedEquipmentNames = ref("");
  97. // 未选择的设备(名称)
  98. const unselectedEquipmentNames = ref("");
  99. const timeRangeRef = ref(null);
  100. const startTime = ref("00:00");
  101. const startDefaultTime = ref("08:00");
  102. const endTime = ref("24:00");
  103. const endDefaultTime = ref("08:00");
  104. const timeRange = data => {
  105. form.startTime = data[0];
  106. form.endTime = data[1];
  107. };
  108. const dailyFuel = ref(0);
  109. const form = reactive({
  110. startTime: startDefaultTime.value,
  111. endTime: endDefaultTime.value,
  112. platformIds: [],
  113. deviceIds: [],
  114. productionStatus: "",
  115. nextPlan: "",
  116. externalRental: "",
  117. faultDowntime: "",
  118. malfunction: "",
  119. attachments: [],
  120. reportFuels: [],
  121. reportDetails: [],
  122. });
  123. const formDataBaseRules = reactive({
  124. // 时间节点 - 开始时间
  125. startTime: {
  126. rules: [
  127. {
  128. required: true,
  129. errorMessage: `${t("operation.PleaseSelect")}${t("ruiDu.timeNode")}`,
  130. },
  131. ],
  132. },
  133. // 时间节点 - 结束时间
  134. endTime: {
  135. rules: [
  136. {
  137. required: true,
  138. errorMessage: `${t("operation.PleaseSelect")}${t("ruiDu.timeNode")}`,
  139. },
  140. ],
  141. },
  142. // 当日生产动态
  143. // productionStatus: {
  144. // rules: [
  145. // {
  146. // required: true,
  147. // errorMessage: `${t("operation.PleaseFillIn")}${t(
  148. // "ruiDu.dailyProductionDynamic"
  149. // )}`,
  150. // },
  151. // ],
  152. // },
  153. nextPlan: {
  154. rules: [
  155. {
  156. required: true,
  157. errorMessage: `请输入下步工作计划`,
  158. },
  159. ],
  160. },
  161. });
  162. function debounce(func, delay) {
  163. let timer;
  164. return function (...args) {
  165. if (timer) clearTimeout(timer);
  166. timer = setTimeout(() => func.apply(this, args), delay);
  167. };
  168. }
  169. const handleInputRaw = (val, id, field) => {
  170. let num = Number(val);
  171. if (val === "" || isNaN(num) || num < 0) {
  172. num = 0;
  173. } else if (num > 24) {
  174. num = 24;
  175. }
  176. form[id][field] = num;
  177. };
  178. const onInputChange = debounce(handleInputRaw, 500);
  179. const validate = async () => {
  180. if (istime.value === "true") {
  181. return await reportFormRef.value.validateField([
  182. "constructionBrief",
  183. ...form.platformIds.flatMap(pid => [
  184. ...NON_PROD_FIELDS.map(item => `${pid}.${item.key}`),
  185. `${pid}.otherNptReason`,
  186. ]),
  187. ]);
  188. } else return await reportFormRef.value.validate();
  189. };
  190. const submitForm = async () => {
  191. const deleteId = wellOptions.value.filter(
  192. o => !form.platformIds.includes(o.value)
  193. );
  194. deleteId.forEach(o => {
  195. delete form[o.value];
  196. });
  197. await validate();
  198. let error = false;
  199. form.platformIds.forEach(id => {
  200. const pair = form[id];
  201. // 计算所有时间字段的总和
  202. const totalTime =
  203. (pair.repairTime || 0) +
  204. (pair.selfStopTime || 0) +
  205. (pair.accidentTime || 0) +
  206. (pair.complexityTime || 0) +
  207. (pair.rectificationTime || 0) +
  208. (pair.waitingStopTime || 0) +
  209. (pair.partyaDesign || 0) +
  210. (pair.partyaPrepare || 0) +
  211. (pair.partyaResource || 0) +
  212. (pair.relocationTime || 0) +
  213. (pair.winterBreakTime || 0) +
  214. (pair.otherNptTime || 0);
  215. const wellName =
  216. wellOptions.value.find(item => item.value === id)?.text ??
  217. props.reportData.wellName ??
  218. "";
  219. // 检查总和是否超过24小时
  220. if (totalTime > 24) {
  221. error = true;
  222. uni.showToast({
  223. title: `${wellName || "平台井"}的时间总和不能超过24小时`,
  224. icon: "none",
  225. });
  226. return;
  227. }
  228. // 检查otherNptTime>0时是否填写了otherNptReason
  229. if ((pair.otherNptTime || 0) > 0 && !pair.otherNptReason) {
  230. error = true;
  231. uni.showToast({
  232. title: `${wellName || "平台井"}的其他时间大于0时必须填写原因`,
  233. icon: "none",
  234. });
  235. return;
  236. }
  237. });
  238. if (error) {
  239. return;
  240. }
  241. // // 处理表单数据
  242. const formDataCopy = JSON.parse(JSON.stringify(form));
  243. if (!formDataCopy.dailyFuel && formDataCopy.dailyFuel !== 0) {
  244. uni.showToast({
  245. title: "请输入当日油耗",
  246. icon: "none",
  247. });
  248. return;
  249. }
  250. const responseData = [];
  251. // // 处理施工工艺
  252. form.platformIds.forEach(id => {
  253. formDataCopy[id].extProperty.forEach(attr => {
  254. if (attr.dataType === "double") {
  255. attr.actualValue = Number(attr.actualValue);
  256. }
  257. });
  258. const attachments = JSON.parse(
  259. JSON.stringify(formDataCopy.attachments)
  260. ).map(item => {
  261. item.bizId = id;
  262. return item;
  263. });
  264. let platformWell = props.reportData.platformWell;
  265. if (platformWell === 1) {
  266. platformWell = id === props.reportData.id ? 1 : 2;
  267. }
  268. const data = {
  269. id,
  270. timeRange: ["1970-01-01T00:00:00.008Z", "1970-01-01T00:00:00.008Z"],
  271. projectDepartment: "",
  272. costCenter: "",
  273. dynamicFields: {},
  274. platformWell,
  275. companyId: props.reportData.companyId,
  276. deptId: props.reportData.deptId,
  277. attachments,
  278. deviceIds: formDataCopy.deviceIds,
  279. startTime: formDataCopy.startTime,
  280. endTime: formDataCopy.endTime,
  281. extProperty: formDataCopy[id].extProperty,
  282. externalRental: formDataCopy.externalRental,
  283. faultDowntime: formDataCopy.faultDowntime,
  284. malfunction: formDataCopy.malfunction,
  285. nextPlan: formDataCopy.nextPlan,
  286. ...(props.reportData.platformWell === 1
  287. ? {
  288. platformId: props.reportData.platforms.find(v => v.reportId === id)
  289. .id,
  290. }
  291. : {}),
  292. productionStatus: formDataCopy.productionStatus,
  293. rdStatus: formDataCopy[id].rdStatus,
  294. techniqueIds: formDataCopy[id].techniqueIds.map(v => v.toString()),
  295. reportFuels: formDataCopy.reportFuels.map(item => ({
  296. ...item,
  297. customFuel: Number(item.customFuel),
  298. reportId: id,
  299. })),
  300. dailyFuel: Number(formDataCopy.dailyFuel),
  301. reportDetails: formDataCopy.reportDetails,
  302. constructionBrief: formDataCopy.constructionBrief || "",
  303. repairTime: formDataCopy[id].repairTime || 0,
  304. selfStopTime: formDataCopy[id].selfStopTime || 0,
  305. accidentTime: formDataCopy[id].accidentTime || 0,
  306. complexityTime: formDataCopy[id].complexityTime || 0,
  307. rectificationTime: formDataCopy[id].rectificationTime || 0,
  308. waitingStopTime: formDataCopy[id].waitingStopTime || 0,
  309. partyaDesign: formDataCopy[id].partyaDesign || 0,
  310. partyaPrepare: formDataCopy[id].partyaPrepare || 0,
  311. partyaResource: formDataCopy[id].partyaResource || 0,
  312. relocationTime: formDataCopy[id].relocationTime || 0,
  313. winterBreakTime: formDataCopy[id].winterBreakTime || 0,
  314. otherNptTime: formDataCopy[id].otherNptTime || 0,
  315. otherNptReason: formDataCopy[id].otherNptReason || "",
  316. nonProduct: props.reportData.auditStatus === 20 ? "Y" : "",
  317. };
  318. responseData.push(data);
  319. });
  320. // 提交表单
  321. updateRuiDuReportBatch(responseData).then(res => {
  322. // 提交成功
  323. if (res.code === 0) {
  324. uni.showToast({ title: t("operation.success"), icon: "none" });
  325. // 返回上一页
  326. uni.navigateBack();
  327. } else {
  328. uni.showToast({ title: res.msg, icon: "none" });
  329. }
  330. });
  331. };
  332. const handleClickSelectDevice = () => {
  333. deviceTransferRef.value.open();
  334. };
  335. const handleEquipmentNames = deviceIds => {
  336. form.deviceIds = deviceIds || []; //施工设备
  337. const { selectedDevices = [] } = props.reportData || {};
  338. const deviceIdSet = new Set(deviceIds);
  339. // 已选择的设备(名称)
  340. selectedEquipmentNames.value = selectedDevices
  341. .filter(item => deviceIdSet.has(item.id))
  342. .map(item => item.deviceName)
  343. .join(",");
  344. // 未选择的设备(名称)
  345. unselectedEquipmentNames.value =
  346. selectedDevices
  347. .filter(item => !deviceIdSet.has(item.id))
  348. .map(item => item.deviceName)
  349. .join(",") || t("ruiDu.allEquipmentConstructed");
  350. };
  351. // 设备选择器回调
  352. const handleTransferChange = selectedIds => {
  353. // 更新已选择的设备及名称
  354. handleEquipmentNames(selectedIds);
  355. };
  356. const steps = ref([]);
  357. const formatT = arr =>
  358. `${arr[0].toString().padStart(2, "0")}:${arr[1].toString().padStart(2, "0")}`;
  359. const addReportDetailRow = () => {
  360. if (!form.reportDetails) {
  361. form.reportDetails = [];
  362. }
  363. form.reportDetails.push({
  364. startTime: "08:00",
  365. endTime: "08:00",
  366. duration: 0,
  367. constructionDetail: "",
  368. });
  369. };
  370. const removeReportDetailRow = index => {
  371. if (index === 0) {
  372. uni.showToast({ title: "至少填写一条生产动态", icon: "none" });
  373. return;
  374. }
  375. form.reportDetails?.splice(index, 1);
  376. };
  377. const formDataFormat = () => {
  378. // 处理时间范围
  379. timeRangeFormat();
  380. // 处理已选择的设备
  381. if (props.reportData?.deviceIds) {
  382. const { deviceIds = [] } = props.reportData || {};
  383. handleEquipmentNames(deviceIds);
  384. }
  385. if (props.reportData.platformWell === 1) {
  386. form.platformIds = props.reportData.platforms?.map(v => v.reportId) ?? [];
  387. } else {
  388. form.platformIds = [props.reportData.id];
  389. }
  390. // if (props.formDisable) {
  391. // } else {
  392. // form.platformIds = [props.reportData.id];
  393. // }
  394. if (props.reportData.platformWell === 1) {
  395. props.reportData.platforms.forEach(p => {
  396. form[p.reportId] = {
  397. rdStatus: p.rdStatus,
  398. techniqueIds: p.techniqueIds,
  399. extProperty: p.extProperty,
  400. repairTime: p.repairTime || 0,
  401. selfStopTime: p.selfStopTime || 0,
  402. accidentTime: p.accidentTime || 0,
  403. complexityTime: p.complexityTime || 0,
  404. rectificationTime: p.rectificationTime || 0,
  405. waitingStopTime: p.waitingStopTime || 0,
  406. partyaDesign: p.partyaDesign || 0,
  407. partyaPrepare: p.partyaPrepare || 0,
  408. partyaResource: p.partyaResource || 0,
  409. relocationTime: p.relocationTime || 0,
  410. winterBreakTime: p.winterBreakTime || 0,
  411. otherNptTime: p.otherNptTime || 0,
  412. otherNptReason: p.otherNptReason || "",
  413. };
  414. });
  415. } else {
  416. form[props.reportData.id] = {
  417. rdStatus: props.reportData.rdStatus,
  418. techniqueIds: props.reportData.techniqueIds,
  419. extProperty: props.reportData.extProperty,
  420. repairTime: props.reportData.repairTime || 0,
  421. selfStopTime: props.reportData.selfStopTime || 0,
  422. accidentTime: props.reportData.accidentTime || 0,
  423. complexityTime: props.reportData.complexityTime || 0,
  424. rectificationTime: props.reportData.rectificationTime || 0,
  425. waitingStopTime: props.reportData.waitingStopTime || 0,
  426. partyaDesign: props.reportData.partyaDesign || 0,
  427. partyaPrepare: props.reportData.partyaPrepare || 0,
  428. partyaResource: props.reportData.partyaResource || 0,
  429. relocationTime: props.reportData.relocationTime || 0,
  430. winterBreakTime: props.reportData.winterBreakTime || 0,
  431. otherNptTime: props.reportData.otherNptTime || 0,
  432. otherNptReason: props.reportData.otherNptReason || "",
  433. };
  434. }
  435. form.constructionBrief = props.reportData.constructionBrief || ""; // 施工简介
  436. form.reportDetails = (props.reportData.reportDetails || []).map(item => ({
  437. duration: item.duration || 0,
  438. constructionDetail: item.constructionDetail || "",
  439. startTime: formatT(item.startTime),
  440. endTime: formatT(item.endTime),
  441. }));
  442. if (!form.reportDetails.length) {
  443. addReportDetailRow();
  444. }
  445. // 当日生产动态
  446. form.productionStatus = props.reportData.productionStatus || ""; //当日生产动态
  447. // 下步工作计划
  448. form.nextPlan = props.reportData.nextPlan || ""; //下步工作计划
  449. // 外租设备
  450. form.externalRental = props.reportData.externalRental || ""; //外租设备
  451. // 故障情况
  452. form.malfunction = props.reportData.malfunction || ""; //故障情况
  453. // 故障误工H
  454. form.faultDowntime = props.reportData.faultDowntime || ""; //故障误工H
  455. // 附件
  456. form.attachments = props.reportData.attachments || [];
  457. // 提取变量,方便阅读
  458. const list1 = props.reportData.reportFuels;
  459. const list2 = props.reportData.reportedFuels;
  460. const validList = list1?.length > 0 ? list1 : list2?.length > 0 ? list2 : [];
  461. form.reportFuels = validList.map(v => ({
  462. ...v,
  463. // 这里保持你原有的数值处理逻辑
  464. customFuel: Number(
  465. Number(
  466. props.formDisable
  467. ? (v.customFuel ?? 0)
  468. : (v.customFuel ?? v.zhbdFuel ?? 0)
  469. ).toFixed(2)
  470. ),
  471. }));
  472. steps.value = (props.reportData.taskProgresses ?? []).map(v => ({
  473. title: v.rdStatusLabel,
  474. desc: v.createTime,
  475. }));
  476. initDailyFuel();
  477. // form.
  478. // 展示用的文件列表
  479. // attachmentsFileList.value =
  480. // props.reportData?.attachments?.map(item => ({
  481. // ...item,
  482. // name: item.filename,
  483. // url: item.filePath,
  484. // })) || [];
  485. };
  486. // 初始化时间范围
  487. const timeRangeFormat = () => {
  488. if (props.reportData.startTime) {
  489. const startArr = props.reportData.startTime;
  490. form.startTime = `${startArr[0].toString().padStart(2, "0")}:${startArr[1]
  491. .toString()
  492. .padStart(2, "0")}`;
  493. }
  494. if (props.reportData.endTime) {
  495. const endArr = props.reportData.endTime;
  496. form.endTime = `${endArr[0].toString().padStart(2, "0")}:${endArr[1]
  497. .toString()
  498. .padStart(2, "0")}`;
  499. }
  500. };
  501. watch(
  502. () => props.reportData,
  503. val => {
  504. if (val.id) {
  505. formDataFormat();
  506. }
  507. },
  508. { deep: true, immediate: true }
  509. );
  510. const wellOptions = computed(() => {
  511. return (
  512. props.reportData.platforms?.map(v => ({
  513. text: v.wellName,
  514. value: v.reportId,
  515. })) ?? []
  516. );
  517. });
  518. const handleClickTimeRange = () => {
  519. // 打开时间范围选择器
  520. timeRangeRef.value.open();
  521. };
  522. const reportDetailsTimeRangeRef = ref(null);
  523. const reportDetailIndex = ref(0);
  524. const handleClickTimeRangeItem = index => {
  525. reportDetailIndex.value = index;
  526. reportDetailsTimeRangeRef.value.open();
  527. };
  528. const calculateDuration = row => {
  529. if (!row.startTime || !row.endTime) {
  530. row.duration = 0;
  531. return;
  532. }
  533. const todayStr = dayjs().format("YYYY-MM-DD");
  534. const start = dayjs(`${todayStr} ${row.startTime}`);
  535. const end = dayjs(`${todayStr} ${row.endTime}`);
  536. let diffMinutes = end.diff(start, "minute");
  537. if (diffMinutes < 0) {
  538. diffMinutes += 1440;
  539. }
  540. row.duration = Number((diffMinutes / 60).toFixed(2));
  541. };
  542. const reportDetailsTimeRange = data => {
  543. form.reportDetails[reportDetailIndex.value].startTime = data[0];
  544. form.reportDetails[reportDetailIndex.value].endTime = data[1];
  545. calculateDuration(form.reportDetails[reportDetailIndex.value]);
  546. };
  547. const uploadRef = ref(null);
  548. const attachmentList = ref([]);
  549. const chooseFile = () => {
  550. uploadRef.value.chooseFile({
  551. count: 9,
  552. size: 50,
  553. success: files => {
  554. attachmentList.value = attachmentList.value.concat(files);
  555. attachmentList.value.forEach(file => {
  556. // 将等待上传和上传失败的文件提交上传到服务器
  557. // 提示:::如果接口不支持跨域,改成调用this.getTempFilePath(file)
  558. if (file.status === "waiting" || file.status === "fail") {
  559. uploadHandle(file);
  560. }
  561. });
  562. },
  563. });
  564. };
  565. const uploadHandle = file => {
  566. uploadRef.value.upload({
  567. url:
  568. config.default.apiUrl + config.default.apiUrlSuffix + "/rq/file/upload",
  569. file,
  570. name: "files",
  571. header: {
  572. "Authorization": getAccessToken() ? "Bearer " + getAccessToken() : "",
  573. "Tenant-id": getTenantId() ? getTenantId() : "1",
  574. "Device-id": "undefined",
  575. },
  576. method: "post",
  577. success: e => {
  578. file.status = "success";
  579. const result = JSON.parse(e.result);
  580. if (result.code !== 0) {
  581. uni.showToast({
  582. title: `【${file.name}】${t("operation.uploadFail")}`,
  583. icon: "none",
  584. });
  585. return;
  586. }
  587. form.attachments.push({
  588. bizId: props.reportId,
  589. category: "daily_report",
  590. filePath: result.data.files[0].filePath,
  591. filename: result.data.files[0].name,
  592. // fileSize: data.files[0].size,
  593. // fileType: data.files[0].type,
  594. remark: "",
  595. type: "attachment",
  596. });
  597. },
  598. fail: e => {
  599. file.status = "fail";
  600. console.error("上传异常:", err);
  601. uni.showToast({ title: t("operation.uploadFail"), icon: "none" });
  602. },
  603. });
  604. };
  605. // const uploadHandle = file => {
  606. // uploadRef.value.getTempFilePath({
  607. // file,
  608. // success: e => {
  609. // uni.uploadFile({
  610. // url: config.default.apiUrl + config.default.apiUrlSuffix + '/rq/file/upload',
  611. // header: {
  612. // 'Authorization': getAccessToken() ? 'Bearer ' + getAccessToken() : '',
  613. // 'tenant-id': getTenantId() ? getTenantId() : '1',
  614. // 'device-id': undefined,
  615. // },
  616. // name: 'files',
  617. // // #ifdef H5
  618. // file: e.result,
  619. // // #endif
  620. // // #ifndef H5
  621. // filePath: e.result,
  622. // // #endif
  623. // success: e => {
  624. // console.log('e :>> ', e);
  625. // // file.status = 'success';
  626. // // const result = JSON.parse(e.result);
  627. // // if (e.code !== 0) {
  628. // // uni.showToast({
  629. // // title: `【${file.name}】${t('operation.uploadFail')}`,
  630. // // icon: 'none',
  631. // // });
  632. // // return;
  633. // // }
  634. // // form.attachments.push({
  635. // // bizId: props.reportId,
  636. // // category: 'daily_report',
  637. // // filePath: data.files[0].filePath,
  638. // // filename: data.files[0].name,
  639. // // // fileSize: data.files[0].size,
  640. // // // fileType: data.files[0].type,
  641. // // remark: '',
  642. // // type: 'attachment',
  643. // // });
  644. // },
  645. // fail: e => {
  646. // console.log('e :>> ', e);
  647. // },
  648. // });
  649. // },
  650. // });
  651. // };
  652. // 删除附件
  653. const deleteFiles = index => {
  654. // 1. 从formData.attachments中移除选中项
  655. form.attachments.splice(index, 1);
  656. };
  657. function copyToPublicAndOpen(sourcePath, fileName) {
  658. // 获取 _downloads/ (公共下载目录) 的目录对象
  659. plus.io.resolveLocalFileSystemURL(
  660. "_downloads/",
  661. entryDir => {
  662. // 获取源文件对象
  663. plus.io.resolveLocalFileSystemURL(
  664. sourcePath,
  665. entryFile => {
  666. // 执行复制操作:将 sourcePath 复制到 _downloads/ 下,并重命名
  667. entryFile.copyTo(
  668. entryDir,
  669. fileName,
  670. newEntry => {
  671. console.log("文件已复制到公共目录:", newEntry.fullPath);
  672. uni.showToast({
  673. title: "已保存到下载目录",
  674. icon: "none",
  675. duration: 3000,
  676. });
  677. // 3. 预览打开
  678. // 注意:打开公共目录的文件推荐用 plus.runtime.openFile
  679. // uni.openDocument 有时对公共路径支持不好
  680. plus.runtime.openFile(
  681. newEntry.fullPath,
  682. {},
  683. e => {
  684. console.log("打开成功");
  685. },
  686. e => {
  687. console.error("打开失败", e);
  688. uni.showToast({ title: "无法打开文件", icon: "none" });
  689. }
  690. );
  691. },
  692. e => {
  693. console.error("复制文件失败:", e);
  694. uni.showToast({ title: "保存到公共目录失败", icon: "none" });
  695. }
  696. );
  697. },
  698. e => {
  699. console.error("读取源文件失败:", e);
  700. }
  701. );
  702. },
  703. e => {
  704. console.error("读取下载目录失败:", e);
  705. }
  706. );
  707. }
  708. function saveTempFileToDownloads(tempPath, fileName) {
  709. // 1. 获取系统 Downloads 目录对象
  710. // "_downloads/" 是 H5+ API 对安卓公共下载目录的映射
  711. plus.io.resolveLocalFileSystemURL(
  712. "_downloads/",
  713. entryDir => {
  714. // 2. 获取临时文件对象
  715. plus.io.resolveLocalFileSystemURL(
  716. tempPath,
  717. entryFile => {
  718. // 3. 执行复制:将临时文件复制到 Downloads 目录
  719. entryFile.copyTo(
  720. entryDir,
  721. fileName,
  722. newEntry => {
  723. console.log("文件路径:" + newEntry.fullPath);
  724. uni.showToast({
  725. title: "已保存至Downloads",
  726. icon: "none",
  727. });
  728. // 4. (可选) 打开预览
  729. plus.runtime.openFile(newEntry.fullPath);
  730. },
  731. e => {
  732. console.error("复制失败", e);
  733. uni.showToast({ title: "保存失败", icon: "none" });
  734. }
  735. );
  736. },
  737. e => {
  738. console.error("读取临时文件失败", e);
  739. }
  740. );
  741. },
  742. e => {
  743. console.error("无法访问下载目录", e);
  744. // 这里如果报错,通常是权限没给或者 Android 11+ 读写受限
  745. }
  746. );
  747. }
  748. // 下载文件
  749. const downloadFile = async file => {
  750. console.log("🚀 ~ downloadFile ~ file:", file);
  751. const { filePath: fileUrl, name: fileName } = file;
  752. if (!fileUrl) {
  753. uni.showToast({ title: t("operation.fileUrlEmpty"), icon: "none" });
  754. return;
  755. }
  756. // 获取平台
  757. const platform = uni.getSystemInfoSync().platform;
  758. console.log("🚀 ~ downloadFile ~ platform:", platform);
  759. // 判断平台
  760. if (platform === "android") {
  761. uni.downloadFile({
  762. url: fileUrl,
  763. success: res => {
  764. console.log("🚀 ~ downloadFile ~ res:", res);
  765. if (res.statusCode === 200) {
  766. saveTempFileToDownloads(res.tempFilePath, fileName);
  767. // uni.saveFile({
  768. // tempFilePath: res.tempFilePath,
  769. // success: res => {
  770. // // console.log('🚀 ~ downloadFile saveFile ~ res:', res);
  771. // uni.showToast({
  772. // title: t('operation.downloadSuccess'),
  773. // icon: 'none',
  774. // });
  775. // copyToPublicAndOpen(res.savedFilePath, fileName);
  776. // },
  777. // fail: err => {
  778. // console.log('🚀 ~ downloadFile saveFile ~ err:', err);
  779. // uni.showToast({
  780. // title: t('operation.downloadFail'),
  781. // icon: 'none',
  782. // });
  783. // },
  784. // });
  785. }
  786. },
  787. fail: err => {
  788. console.log("🚀 ~ downloadFile ~ err:", err);
  789. uni.showToast({
  790. title: t("operation.downloadFail"),
  791. icon: "none",
  792. });
  793. },
  794. });
  795. } else {
  796. try {
  797. // 2.1 处理文件名(避免特殊字符乱码,如中文、空格)
  798. const safeFileName = decodeURIComponent(fileName || "未命名文件"); // 解码URL编码的文件名
  799. // 2.2 创建隐藏的 <a> 标签(核心:利用 download 属性触发下载)
  800. const link = document.createElement("a");
  801. // 关键:设置 download 属性指定文件名(Web 端独有)
  802. link.download = safeFileName;
  803. // 设置文件地址(若跨域,需后端配置 CORS + Content-Disposition 响应头)
  804. link.href = fileUrl;
  805. // 隐藏 <a> 标签(不影响页面布局)
  806. link.style.display = "none";
  807. // 将 <a> 标签添加到文档中(否则部分浏览器无法触发点击)
  808. document.body.appendChild(link);
  809. // 2.3 模拟点击 <a> 标签触发下载
  810. link.click();
  811. // 2.4 下载后清理资源(避免内存泄漏)
  812. setTimeout(() => {
  813. document.body.removeChild(link); // 移除 <a> 标签
  814. URL.revokeObjectURL(link.href); // 释放 URL 资源(若使用 Blob 时必需)
  815. }, 100);
  816. } catch (err) {
  817. console.error("🚀 ~ Web 端下载失败:", err);
  818. // 若直接通过 <a> 标签下载失败(如跨域),尝试通过 Blob 流下载(场景2兼容)
  819. await downloadFileByBlob(fileUrl, fileName);
  820. }
  821. }
  822. };
  823. // 兼容场景2:通过 Blob 流下载(解决跨域或后端返回流的情况)
  824. const downloadFileByBlob = async (fileUrl, fileName) => {
  825. try {
  826. // 1. 发起请求获取文件流(注意:需设置 responseType: 'blob')
  827. const response = await fetch(fileUrl, {
  828. method: "GET",
  829. headers: {
  830. // 若需要登录态,添加 token(根据项目授权方式调整)
  831. Authorization: `Bearer ${uni.getStorageSync("token")}`,
  832. },
  833. });
  834. if (!response.ok) {
  835. throw new Error(`请求失败: ${response.status}`);
  836. }
  837. // 2. 将响应转换为 Blob 对象(根据文件类型设置 MIME,如 PDF 为 'application/pdf')
  838. const blob = await response.blob();
  839. // 3. 生成 Blob 临时 URL
  840. const blobUrl = URL.createObjectURL(blob);
  841. // 4. 用 <a> 标签触发下载(同场景1逻辑)
  842. const safeFileName = decodeURIComponent(fileName || "未命名文件");
  843. const link = document.createElement("a");
  844. link.download = safeFileName;
  845. link.href = blobUrl;
  846. link.style.display = "none";
  847. document.body.appendChild(link);
  848. link.click();
  849. // 5. 清理资源
  850. setTimeout(() => {
  851. document.body.removeChild(link);
  852. URL.revokeObjectURL(blobUrl); // 必须释放 Blob URL,避免内存泄漏
  853. uni.hideLoading();
  854. // uni.showToast({ title: t("operation.downloadSuccess"), icon: "success" });
  855. }, 100);
  856. } catch (err) {
  857. console.error("🚀 ~ Blob 下载失败:", err);
  858. uni.hideLoading();
  859. // uni.showToast({ title: t("operation.downloadFail"), icon: "error" });
  860. }
  861. };
  862. const removeSelectedItem = (platformId, value) => {
  863. form[platformId].techniqueIds = form[platformId].techniqueIds.filter(
  864. item => item !== value
  865. );
  866. getWorkloadInfoByTechnique(platformId);
  867. };
  868. const getWorkloadInfoByTechnique = platformId => {
  869. const ids = form[platformId].techniqueIds;
  870. if (!ids.length) {
  871. form[platformId].extProperty = [];
  872. return;
  873. }
  874. getRuiDuReportAttrs({
  875. techniqueIds: ids.join(","),
  876. }).then(res => {
  877. const { data = [] } = res;
  878. // 1. 按 "identifier+unit" 去重:用Map保证唯一,key为拼接字段
  879. const uniqueMap = new Map();
  880. data.forEach(item => {
  881. // 生成去重key(identifier和unit都存在才拼接,避免异常)
  882. const key =
  883. item.identifier && item.unit
  884. ? `${item.identifier}-${item.unit}`
  885. : Math.random().toString(36).slice(2, 11); // 异常情况用随机key避免重复
  886. uniqueMap.set(key, item); // 重复key会覆盖,实现去重
  887. });
  888. // 去重后的数组
  889. const uniqueData = Array.from(uniqueMap.values());
  890. // 2. 对比formData.extProperty,保留已有actualValue(避免覆盖用户输入)
  891. const handledData = uniqueData.map(newItem => {
  892. // 生成当前新项的去重key
  893. const newKey =
  894. newItem.identifier && newItem.unit
  895. ? `${newItem.identifier}-${newItem.unit}`
  896. : "";
  897. // 在原有extProperty中找匹配项
  898. const oldItem = form[platformId].extProperty.find(old => {
  899. const oldKey =
  900. old.identifier && old.unit ? `${old.identifier}-${old.unit}` : "";
  901. return newKey && oldKey && newKey === oldKey;
  902. });
  903. // 有匹配项则复用原有actualValue,无则用新项的(默认空)
  904. return {
  905. ...newItem,
  906. actualValue: oldItem?.actualValue ?? newItem.actualValue,
  907. };
  908. });
  909. // 3. 重新赋值给formData.extProperty(更新页面展示)
  910. form[platformId].extProperty = handledData;
  911. });
  912. };
  913. defineExpose({
  914. submitForm,
  915. });
  916. // 假设你已经定义了 props
  917. // const props = defineProps({ reportData: Object });
  918. // --- 1. 公共工具函数 (保持不变) ---
  919. const parseNumber = val => {
  920. let num = parseFloat(val);
  921. if (isNaN(num)) num = 0;
  922. if (num < 0) num = 0;
  923. return Number(num.toFixed(2));
  924. };
  925. // --- 2. 防抖逻辑定义 (保持不变) ---
  926. // 列表变化 -> 算总和
  927. const handleListChange = useDebounceFn(() => {
  928. let total = 0;
  929. form.reportFuels.forEach(item => {
  930. const formattedVal = parseNumber(item.customFuel);
  931. if (item.customFuel !== formattedVal) {
  932. item.customFuel = formattedVal;
  933. }
  934. total += formattedVal;
  935. });
  936. // 更新 dailyFuel,这会触发下面的 dailyFuel watcher
  937. dailyFuel.value = parseNumber(total);
  938. }, 500);
  939. // dailyFuel 变化 -> 格式化自身 & 同步 form
  940. const handleDailyFuelChange = useDebounceFn(() => {
  941. const formattedVal = parseNumber(dailyFuel.value);
  942. if (dailyFuel.value !== formattedVal) {
  943. dailyFuel.value = formattedVal;
  944. }
  945. form.dailyFuel = formattedVal;
  946. }, 500);
  947. // --- 3. 关键修改:初始化逻辑 ---
  948. const initDailyFuel = () => {
  949. // 获取 props 中的值 (转为数字以做判断)
  950. const propVal = props.reportData?.dailyFuel;
  951. const numPropVal = parseFloat(propVal);
  952. // 判断规则:如果有值且不是 NaN (根据需求,你也可以加上 > 0 的判断)
  953. // 这里假设只要 props 里有有效数字,就以 props 为准
  954. const hasPropValue = !isNaN(numPropVal) && propVal !== null && propVal !== "";
  955. if (hasPropValue) {
  956. // 【情况A】Props 有值:直接使用 Props
  957. const val = parseNumber(numPropVal);
  958. dailyFuel.value = val;
  959. form.dailyFuel = val;
  960. // // 顺便把列表里的每一项也格式化一下(可选)
  961. // form.reportFuels.forEach(item => {
  962. // item.customFuel = parseNumber(item.customFuel);
  963. // });
  964. } else {
  965. // 【情况B】Props 没值:根据列表计算初始值
  966. // 这里我们不使用防抖,直接立即计算一次,确保显示正确
  967. let total = 0;
  968. form.reportFuels.forEach(item => {
  969. // 初始化时顺便把列表里的脏数据格式化了
  970. const val = parseNumber(item.customFuel);
  971. item.customFuel = val;
  972. total += val;
  973. });
  974. const finalTotal = parseNumber(total);
  975. dailyFuel.value = finalTotal;
  976. form.dailyFuel = finalTotal;
  977. }
  978. };
  979. // --- 4. 监听器 (关键修改) ---
  980. // 监听列表:【注意】这里去掉了 immediate: true
  981. // 因为初始化我们已经在上面手动 initDailyFuel() 里做过了
  982. // 现在只监听用户后续的“修改”操作
  983. // watch(
  984. // () => form.reportFuels,
  985. // () => {
  986. // handleListChange();
  987. // },
  988. // { deep: true } // 只有 deep,没有 immediate
  989. // );
  990. // 监听 dailyFuel
  991. watch(
  992. () => dailyFuel.value,
  993. (newVal, oldVal) => {
  994. // 只有当值真的变了,才触发防抖更新
  995. // 避免初始化赋值时触发不必要的逻辑
  996. if (newVal !== oldVal) {
  997. handleDailyFuelChange();
  998. }
  999. }
  1000. );
  1001. </script>
  1002. <template>
  1003. <scroll-view scroll-y="true" class="report-form">
  1004. <scroll-view class="steps" scroll-x :scroll-y="false">
  1005. <uni-steps
  1006. :options="steps"
  1007. :active="steps.length - 1"
  1008. :style="{ width: `${steps.length * 100}px` }" />
  1009. </scroll-view>
  1010. <uni-forms
  1011. ref="reportFormRef"
  1012. labelWidth="140px"
  1013. :modelValue="form"
  1014. :rules="formDataBaseRules"
  1015. err-show-type="toast">
  1016. <uni-forms-item
  1017. v-if="reportData.platformWell === 1"
  1018. class="form-item well-form-item"
  1019. label="平台井"
  1020. :required="isRequired">
  1021. <uni-data-select
  1022. class="form-item-select"
  1023. :clear="false"
  1024. align="right"
  1025. placeholder="请选择平台井"
  1026. :disabled="formDisable && istime !== 'true'"
  1027. :localdata="wellOptions"
  1028. multiple
  1029. v-model="form.platformIds" />
  1030. </uni-forms-item>
  1031. <div class="content">
  1032. <uni-forms-item
  1033. class="form-item"
  1034. :label="`${$t('ruiDu.timeNode')}:`"
  1035. :required="isRequired">
  1036. <view
  1037. class="item-content"
  1038. @click="props.formDisable ? '' : handleClickTimeRange()">
  1039. <view class="time-range-item" v-if="form.startTime && form.endTime">
  1040. {{ form.startTime }} 至 {{ form.endTime }}
  1041. </view>
  1042. <view class="time-range-item" v-else>
  1043. {{ selectPlaceholder }}
  1044. </view>
  1045. </view>
  1046. </uni-forms-item>
  1047. <uni-forms-item
  1048. class="form-item"
  1049. :label="`${$t('ruiDu.constructionEquipment')}:`">
  1050. <view>
  1051. <uni-row class="item-content">
  1052. <button
  1053. class="mini-btn form-item-btn"
  1054. type="primary"
  1055. :disabled="formDisable"
  1056. v-if="!formDisable"
  1057. @click="handleClickSelectDevice">
  1058. {{ $t("device.selectDevice") }}
  1059. </button>
  1060. </uni-row>
  1061. <uni-row>
  1062. <uni-easyinput
  1063. style="text-align: right"
  1064. type="textarea"
  1065. :autoHeight="true"
  1066. :inputBorder="false"
  1067. :clearable="false"
  1068. :styles="{ disableColor: '#fff' }"
  1069. :placeholder="$t('ruiDu.unselectedEquipment')"
  1070. :disabled="true"
  1071. v-model="selectedEquipmentNames" />
  1072. </uni-row>
  1073. </view>
  1074. </uni-forms-item>
  1075. <uni-forms-item
  1076. class="form-item"
  1077. :label="`${$t('ruiDu.unselectedEquipment')}:`">
  1078. <uni-easyinput
  1079. style="text-align: right"
  1080. type="textarea"
  1081. :autoHeight="true"
  1082. :inputBorder="false"
  1083. :clearable="false"
  1084. :styles="{ disableColor: '#fff' }"
  1085. :placeholder="' '"
  1086. :disabled="true"
  1087. v-model="unselectedEquipmentNames" />
  1088. </uni-forms-item>
  1089. <uni-forms-item
  1090. :required="isRequired"
  1091. class="form-item"
  1092. label="当日油耗(L):">
  1093. <uni-easyinput
  1094. class="digit-item"
  1095. type="number"
  1096. :inputBorder="false"
  1097. :clearable="false"
  1098. :styles="{ disableColor: '#fff' }"
  1099. :placeholder="inputPlaceholder"
  1100. :disabled="formDisable"
  1101. v-model.number="dailyFuel" />
  1102. </uni-forms-item>
  1103. <!-- <uni-forms-item
  1104. class="form-item"
  1105. :label="`${$t('ruiDu.dailyProductionDynamic')}:`"
  1106. :required="isRequired"
  1107. name="productionStatus">
  1108. <uni-easyinput
  1109. style="text-align: right"
  1110. type="textarea"
  1111. :autoHeight="true"
  1112. :inputBorder="false"
  1113. :clearable="false"
  1114. :styles="{ disableColor: '#fff' }"
  1115. :placeholder="inputPlaceholder"
  1116. :disabled="formDisable"
  1117. v-model="form.productionStatus"
  1118. :maxlength="1000" />
  1119. </uni-forms-item> -->
  1120. <!-- 下步工作计划 -->
  1121. <uni-forms-item
  1122. class="form-item"
  1123. :required="isRequired"
  1124. :label="`${$t('ruiDu.nextWorkPlan')}:`"
  1125. name="nextPlan">
  1126. <uni-easyinput
  1127. style="text-align: right"
  1128. type="textarea"
  1129. :autoHeight="true"
  1130. :inputBorder="false"
  1131. :clearable="false"
  1132. :styles="{ disableColor: '#fff' }"
  1133. :placeholder="inputPlaceholder"
  1134. :disabled="formDisable"
  1135. v-model="form.nextPlan"
  1136. :maxlength="1000" />
  1137. </uni-forms-item>
  1138. <!-- 外租设备 -->
  1139. <uni-forms-item
  1140. class="form-item"
  1141. :label="`${$t('ruiDu.externalRentalEquipment')}:`"
  1142. name="externalRental">
  1143. <uni-easyinput
  1144. style="text-align: right"
  1145. type="textarea"
  1146. :autoHeight="true"
  1147. :inputBorder="false"
  1148. :clearable="false"
  1149. :styles="{ disableColor: '#fff' }"
  1150. :placeholder="inputPlaceholder"
  1151. :disabled="formDisable"
  1152. v-model="form.externalRental"
  1153. :maxlength="1000" />
  1154. </uni-forms-item>
  1155. <!-- 故障情况 -->
  1156. <uni-forms-item
  1157. class="form-item"
  1158. :label="`${$t('ruiDu.faultSituation')}:`"
  1159. name="malfunction">
  1160. <uni-easyinput
  1161. style="text-align: right"
  1162. type="textarea"
  1163. :autoHeight="true"
  1164. :inputBorder="false"
  1165. :clearable="false"
  1166. :styles="{ disableColor: '#fff' }"
  1167. :placeholder="inputPlaceholder"
  1168. :disabled="formDisable"
  1169. v-model="form.malfunction"
  1170. :maxlength="1000" />
  1171. </uni-forms-item>
  1172. <!-- 故障误工H -->
  1173. <uni-forms-item
  1174. class="form-item"
  1175. :label="`${$t('ruiDu.faultDowntimeH')}:`"
  1176. name="faultDowntime">
  1177. <uni-easyinput
  1178. class="digit-item"
  1179. type="digit"
  1180. :inputBorder="false"
  1181. :clearable="true"
  1182. :styles="{ disableColor: '#fff' }"
  1183. :placeholder="inputPlaceholder"
  1184. :disabled="formDisable"
  1185. v-model="form.faultDowntime"
  1186. :maxlength="1000" />
  1187. </uni-forms-item>
  1188. <uni-forms-item
  1189. v-if="type !== 'edit' || istime === 'true'"
  1190. class="form-item"
  1191. label="当日施工简报"
  1192. required
  1193. name="constructionBrief"
  1194. :rules="[
  1195. { required: istime === 'true', errorMessage: '请输入当日施工简报' },
  1196. ]">
  1197. <uni-easyinput
  1198. class="digit-item"
  1199. type="textarea"
  1200. :autoHeight="true"
  1201. :inputBorder="false"
  1202. :clearable="true"
  1203. :styles="{ disableColor: '#fff' }"
  1204. :placeholder="inputPlaceholder"
  1205. :disabled="istime !== 'true'"
  1206. v-model="form.constructionBrief"
  1207. :maxlength="2000" />
  1208. </uni-forms-item>
  1209. <uni-forms-item
  1210. class="form-item"
  1211. :label="`${$t('ruiDu.attachment')}:`"
  1212. :required="false"
  1213. name="attachments">
  1214. <view v-if="!formDisable" class="file-parent">
  1215. <view class="file-picker-container item-end">
  1216. <view class="file-size-limit">
  1217. {{ $t("ruiDu.fileSizeLimit") }}
  1218. </view>
  1219. <button
  1220. type="primary"
  1221. size="mini"
  1222. class="file-picker-btn"
  1223. @click="chooseFile">
  1224. {{ $t("ruiDu.selectFile") }}
  1225. </button>
  1226. </view>
  1227. <view class="file-list">
  1228. <view v-for="(file, index) in form.attachments" :key="index">
  1229. {{ file.filename }}
  1230. <button
  1231. @click="deleteFiles"
  1232. type="primary"
  1233. size="mini"
  1234. class="file-picker-btn"
  1235. >删除文件
  1236. </button>
  1237. </view>
  1238. </view>
  1239. </view>
  1240. <view class="file-list item-col" v-else>
  1241. <view v-for="(file, index) in form.attachments" :key="index">
  1242. <span>{{ file.filename }}</span>
  1243. <button
  1244. @click="downloadFile(file)"
  1245. type="primary"
  1246. size="mini"
  1247. class="file-picker-btn"
  1248. >下载文件</button
  1249. >
  1250. </view>
  1251. </view>
  1252. </uni-forms-item>
  1253. <!-- 附件 -->
  1254. </div>
  1255. <div class="content">
  1256. <div class="content-title">
  1257. 生产动态
  1258. <button
  1259. v-if="!formDisable && istime !== 'true'"
  1260. type="primary"
  1261. size="mini"
  1262. class="detail-btn"
  1263. @click="addReportDetailRow()">
  1264. 添加一行
  1265. </button>
  1266. </div>
  1267. <template v-for="(item, index) in form.reportDetails" :key="index">
  1268. <uv-divider v-if="index !== 0" class="divider"></uv-divider>
  1269. <uni-forms-item class="form-item" label="日期">
  1270. <uni-easyinput
  1271. class="digit-item"
  1272. :inputBorder="false"
  1273. :clearable="false"
  1274. :styles="{ disableColor: '#fff' }"
  1275. :placeholder="inputPlaceholder"
  1276. :disabled="true"
  1277. :model-value="
  1278. dayjs(reportData.createTime).format('YYYY-MM-DD')
  1279. " />
  1280. </uni-forms-item>
  1281. <uni-forms-item
  1282. class="form-item"
  1283. :label="`${$t('ruiDu.timeNode')}:`"
  1284. required>
  1285. <view
  1286. class="item-content"
  1287. @click="props.formDisable ? '' : handleClickTimeRangeItem(index)">
  1288. <view
  1289. class="time-range-item"
  1290. v-if="item.startTime && item.endTime">
  1291. {{ item.startTime }} 至 {{ item.endTime }}
  1292. </view>
  1293. <view class="time-range-item" v-else>
  1294. {{ selectPlaceholder }}
  1295. </view>
  1296. </view>
  1297. </uni-forms-item>
  1298. <uni-forms-item class="form-item" label="时长(H)">
  1299. <uni-easyinput
  1300. class="digit-item"
  1301. :inputBorder="false"
  1302. :clearable="false"
  1303. :styles="{ disableColor: '#fff' }"
  1304. :placeholder="inputPlaceholder"
  1305. :disabled="true"
  1306. :model-value="item.duration" />
  1307. </uni-forms-item>
  1308. <uni-forms-item
  1309. class="form-item"
  1310. label="施工详情"
  1311. required
  1312. :name="['reportDetails', index, 'constructionDetail']"
  1313. :rules="[{ required: true, errorMessage: '请输入施工详情' }]">
  1314. <uni-easyinput
  1315. class="digit-item"
  1316. type="textarea"
  1317. autoHeight
  1318. :inputBorder="false"
  1319. :clearable="true"
  1320. :styles="{ disableColor: '#fff' }"
  1321. :placeholder="inputPlaceholder"
  1322. :disabled="formDisable"
  1323. v-model="item.constructionDetail"
  1324. :maxlength="2000" />
  1325. </uni-forms-item>
  1326. <uni-forms-item
  1327. v-if="!formDisable && istime !== 'true'"
  1328. class="form-item"
  1329. label="操作">
  1330. <button
  1331. type="warn"
  1332. size="mini"
  1333. class="detail-btn"
  1334. @click="removeReportDetailRow(index)">
  1335. 删除
  1336. </button>
  1337. </uni-forms-item>
  1338. </template>
  1339. </div>
  1340. <div
  1341. v-for="(platformId, index) in form.platformIds"
  1342. :key="platformId"
  1343. class="content">
  1344. <div class="content-title">{{
  1345. wellOptions.find(item => item.value === platformId)?.text ??
  1346. reportData.wellName ??
  1347. ""
  1348. }}</div>
  1349. <uni-forms-item
  1350. class="form-item"
  1351. :label="`${$t('ruiDu.constructionStatus')}:`"
  1352. :required="isRequired"
  1353. :name="[platformId, 'rdStatus']"
  1354. :rules="[
  1355. {
  1356. required: true,
  1357. errorMessage: `${t('operation.PleaseSelect')}${t(
  1358. 'ruiDu.constructionStatus'
  1359. )}`,
  1360. },
  1361. ]">
  1362. <uni-data-select
  1363. class="form-item-select items-center"
  1364. :clear="false"
  1365. :align="'right'"
  1366. placement="top"
  1367. :localdata="rdStatusRange"
  1368. :placeholder="selectPlaceholder"
  1369. :disabled="formDisable"
  1370. v-model="form[platformId].rdStatus">
  1371. </uni-data-select>
  1372. </uni-forms-item>
  1373. <uni-forms-item
  1374. class="form-item"
  1375. :label="`${$t('ruiDu.constructionProcess')}:`"
  1376. :required="reportData.virtualProject !== 'Y' ? isRequired : false"
  1377. :name="[platformId, 'techniqueIds']"
  1378. :rules="
  1379. reportData.virtualProject !== 'Y'
  1380. ? [
  1381. {
  1382. required: true,
  1383. errorMessage: `${t('operation.PleaseSelect')}${t(
  1384. 'ruiDu.constructionProcess'
  1385. )}`,
  1386. },
  1387. ]
  1388. : []
  1389. ">
  1390. <uni-data-select
  1391. ref="uniDataSelect"
  1392. mode="underline"
  1393. :multiple="true"
  1394. :clear="false"
  1395. :localdata="techniqueRange"
  1396. :disabled="formDisable"
  1397. placement="top"
  1398. @change="getWorkloadInfoByTechnique(platformId)"
  1399. v-model="form[platformId].techniqueIds">
  1400. <template v-slot:selected="{ selectedItems }" class="form-item">
  1401. <view class="slot-box flex flex-row items-center justify-end">
  1402. <view
  1403. v-for="item in selectedItems"
  1404. :key="item.value"
  1405. class="slot-content-item selected items-center justify-start">
  1406. {{ item.text }}
  1407. <uni-icons
  1408. type="close"
  1409. size="18"
  1410. color="#909399"
  1411. v-if="!formDisable"
  1412. @click="
  1413. removeSelectedItem(platformId, item.value)
  1414. "></uni-icons>
  1415. </view>
  1416. <view
  1417. v-if="selectedItems.length == 0"
  1418. class="slot-content-item items-center justify-start">
  1419. {{ selectPlaceholder }}
  1420. </view>
  1421. </view>
  1422. </template>
  1423. <template v-slot:option="{ item, itemSelected }">
  1424. <view class="slot-item">
  1425. <uni-list-item class="flex-row items-center justify-between">
  1426. <template v-slot:body>
  1427. <text
  1428. class="slot-item-text justify-start"
  1429. :style="{ color: itemSelected ? '#007aff' : '#303133' }">
  1430. {{ item.text }}
  1431. </text>
  1432. </template>
  1433. <template v-slot:footer>
  1434. <uni-icons
  1435. class="items-center justify-center"
  1436. v-if="itemSelected"
  1437. type="checkmarkempty"
  1438. size="20"
  1439. color="#007aff"></uni-icons>
  1440. </template>
  1441. </uni-list-item>
  1442. </view>
  1443. </template>
  1444. <template v-slot:empty>
  1445. <view class="empty-box">
  1446. <view>{{ $t("common.noData") }}</view>
  1447. </view>
  1448. </template>
  1449. </uni-data-select>
  1450. </uni-forms-item>
  1451. <uni-forms-item
  1452. class="form-item"
  1453. v-for="(attr, index) in form[platformId].extProperty"
  1454. :key="index"
  1455. :label="`${attr.name}(${attr.unit}):`"
  1456. :required="attr.required == 1 && isRequired"
  1457. :name="[platformId, 'extProperty', index, 'actualValue']"
  1458. :rules="[
  1459. {
  1460. required: true,
  1461. errorMessage: `${t('operation.PleaseFillIn')}${attr.name}`,
  1462. },
  1463. ]">
  1464. <uni-easyinput
  1465. v-if="attr.dataType === 'double'"
  1466. class="digit-item"
  1467. type="digit"
  1468. :inputBorder="false"
  1469. :clearable="true"
  1470. :styles="{ disableColor: '#fff' }"
  1471. :placeholder="inputPlaceholder"
  1472. :disabled="formDisable"
  1473. v-model.number="form[platformId].extProperty[index].actualValue" />
  1474. <uni-easyinput
  1475. v-else
  1476. style="text-align: right"
  1477. :styles="{ disableColor: '#fff' }"
  1478. :inputBorder="false"
  1479. :clearable="true"
  1480. :placeholder="inputPlaceholder"
  1481. :disabled="formDisable"
  1482. v-model="form[platformId].extProperty[index].actualValue"
  1483. :type="'textarea'"></uni-easyinput>
  1484. </uni-forms-item>
  1485. <uv-divider text="非生产时间" textPosition="left"></uv-divider>
  1486. <uni-forms-item
  1487. class="form-item"
  1488. v-for="field in NON_PROD_FIELDS"
  1489. :key="field.key"
  1490. :label="field.label + '(H)'"
  1491. :name="[platformId, field.key]">
  1492. <uni-easyinput
  1493. type="number"
  1494. style="text-align: right"
  1495. :inputBorder="false"
  1496. :clearable="false"
  1497. :styles="{ disableColor: '#fff' }"
  1498. :disabled="istime !== 'true'"
  1499. v-model.number="form[platformId][field.key]"
  1500. @input="val => onInputChange(val, platformId, field.key)" />
  1501. </uni-forms-item>
  1502. <uni-forms-item
  1503. class="form-item"
  1504. label="其他非生产原因"
  1505. :name="[platformId, 'otherNptReason']">
  1506. <uni-easyinput
  1507. style="text-align: right"
  1508. type="textarea"
  1509. autoHeight
  1510. :inputBorder="false"
  1511. :clearable="false"
  1512. :styles="{ disableColor: '#fff' }"
  1513. v-model="form[platformId].otherNptReason"
  1514. :disabled="istime !== 'true'"
  1515. :maxlength="1000" />
  1516. </uni-forms-item>
  1517. </div>
  1518. <div
  1519. v-for="(fuel, index) in form.reportFuels"
  1520. :key="index"
  1521. class="content">
  1522. <div class="content-title">{{ fuel.deviceCode }}</div>
  1523. <uni-forms-item class="form-item" label="设备名称:">
  1524. <view
  1525. style="
  1526. text-align: right;
  1527. width: 100%;
  1528. padding-right: 10px;
  1529. box-sizing: border-box;
  1530. "
  1531. >{{ fuel.deviceName }}</view
  1532. >
  1533. </uni-forms-item>
  1534. <uni-forms-item class="form-item" label="发生日期:">
  1535. <view
  1536. style="
  1537. text-align: right;
  1538. width: 100%;
  1539. padding-right: 10px;
  1540. box-sizing: border-box;
  1541. "
  1542. >{{ dayjs(fuel.queryDate).format("YYYY-MM-DD") }}</view
  1543. >
  1544. </uni-forms-item>
  1545. <uni-forms-item class="form-item" label="中航北斗油耗(L):">
  1546. <view
  1547. style="
  1548. text-align: right;
  1549. width: 100%;
  1550. padding-right: 10px;
  1551. box-sizing: border-box;
  1552. "
  1553. >{{ (fuel.zhbdFuel ?? 0).toFixed(2) }}</view
  1554. >
  1555. </uni-forms-item>
  1556. <uni-forms-item class="form-item" label="实际油耗(L):">
  1557. <uni-easyinput
  1558. class="digit-item"
  1559. type="number"
  1560. :inputBorder="false"
  1561. :clearable="false"
  1562. :styles="{ disableColor: '#fff' }"
  1563. :placeholder="inputPlaceholder"
  1564. :disabled="formDisable"
  1565. v-model.number="fuel.customFuel"
  1566. @input="handleListChange" />
  1567. </uni-forms-item>
  1568. </div>
  1569. </uni-forms>
  1570. </scroll-view>
  1571. <lsj-upload ref="uploadRef"></lsj-upload>
  1572. <tpf-time-range
  1573. ref="timeRangeRef"
  1574. :startTime="startTime"
  1575. :startDefaultTime="startDefaultTime"
  1576. :endTime="endTime"
  1577. :endDefaultTime="endDefaultTime"
  1578. @timeRange="timeRange"></tpf-time-range>
  1579. <tpf-time-range
  1580. ref="reportDetailsTimeRangeRef"
  1581. :startTime="startTime"
  1582. :startDefaultTime="startDefaultTime"
  1583. :endTime="endTime"
  1584. :endDefaultTime="endDefaultTime"
  1585. @timeRange="reportDetailsTimeRange"></tpf-time-range>
  1586. <device-transfer
  1587. ref="deviceTransferRef"
  1588. :allList="reportData.selectedDevices"
  1589. :selected="form.deviceIds"
  1590. @confirm="handleTransferChange" />
  1591. </template>
  1592. <style lang="scss" scoped>
  1593. @import "@/style/work-order-form.scss";
  1594. .report-form {
  1595. height: 100%;
  1596. color: #333;
  1597. }
  1598. .steps {
  1599. :deep(.uni-scroll-view > .uni-scroll-view) {
  1600. padding: 10px 0 20px 0;
  1601. }
  1602. }
  1603. :deep(.uni-textarea-textarea:disabled),
  1604. :deep(.uni-input-input:disabled) {
  1605. color: #333;
  1606. }
  1607. :deep(.uni-date-x) {
  1608. color: #333;
  1609. }
  1610. .digit-item {
  1611. text-align: right;
  1612. :deep(.uni-easyinput__content-input) {
  1613. padding-right: 10px;
  1614. }
  1615. }
  1616. .well-form-item {
  1617. background-color: #fff;
  1618. border-bottom: none;
  1619. padding: 0 10px;
  1620. }
  1621. .form-item-select {
  1622. width: 100%;
  1623. }
  1624. .form-item-btn {
  1625. margin: 10px 0 0 0;
  1626. width: 80px;
  1627. text-align: center;
  1628. }
  1629. .digit-item {
  1630. text-align: right;
  1631. :deep(.uni-easyinput__content-input) {
  1632. padding-right: 10px;
  1633. }
  1634. }
  1635. .file-picker-container {
  1636. width: 100%;
  1637. }
  1638. .file-picker-btn {
  1639. margin-left: unset;
  1640. margin-right: unset;
  1641. }
  1642. .file-size-limit {
  1643. font-size: 10px;
  1644. color: #ff4500;
  1645. padding: 0 10px;
  1646. }
  1647. .file-parent {
  1648. display: flex;
  1649. flex-direction: column;
  1650. align-items: end;
  1651. padding: 10px 0;
  1652. }
  1653. .file-list {
  1654. display: flex;
  1655. flex-direction: column;
  1656. align-items: end;
  1657. justify-content: end;
  1658. max-width: 100%;
  1659. & > view {
  1660. width: 100%;
  1661. padding: 10px;
  1662. padding-right: 0;
  1663. display: flex;
  1664. align-items: center;
  1665. justify-content: space-between;
  1666. & > span {
  1667. flex: 1;
  1668. word-break: break-all;
  1669. overflow-wrap: anywhere;
  1670. margin-right: 10px;
  1671. }
  1672. & > .file-picker-btn {
  1673. flex-shrink: 0;
  1674. margin-left: 0;
  1675. }
  1676. }
  1677. }
  1678. .slot-box {
  1679. width: 100%;
  1680. flex-wrap: wrap;
  1681. }
  1682. .slot-content-item {
  1683. font-size: 12px;
  1684. color: #6a6a6a;
  1685. &.selected {
  1686. background: #f4f4f5;
  1687. // color: #909399;
  1688. margin: 5px;
  1689. padding: 5px;
  1690. border-radius: 3px;
  1691. }
  1692. }
  1693. .slot-item-text {
  1694. width: 90%;
  1695. }
  1696. .content {
  1697. margin-top: 10px;
  1698. padding: 8px;
  1699. background-color: #fff;
  1700. }
  1701. .content-title {
  1702. font-weight: 600;
  1703. height: 44px;
  1704. display: flex;
  1705. align-items: center;
  1706. border-bottom: 1px dashed #cacccf;
  1707. }
  1708. .item-content {
  1709. display: flex;
  1710. align-items: center;
  1711. justify-content: end;
  1712. }
  1713. .item-end {
  1714. display: flex;
  1715. align-items: end;
  1716. justify-content: end;
  1717. }
  1718. .item-col {
  1719. justify-content: end;
  1720. }
  1721. .time-range-item {
  1722. margin: 10px;
  1723. }
  1724. .detail-btn {
  1725. margin: 0;
  1726. margin-left: auto;
  1727. float: right;
  1728. }
  1729. .divider {
  1730. margin: 0;
  1731. transform: translateY(-1px);
  1732. :deep(.uv-line) {
  1733. border-width: 2px !important;
  1734. border-color: rgb(41, 121, 255) !important;
  1735. }
  1736. }
  1737. </style>