create.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. <script setup>
  2. import { ref, reactive, getCurrentInstance, computed } from "vue";
  3. import tpfTimeRange from "@/components/tpf-time-range/tpf-time-range.vue";
  4. import { getDeptId, getUserInfo } from "@/utils/auth.js";
  5. import { selectedDeptsEmployee, specifiedSimpleDepts } from "@/api/index.js";
  6. import { onLoad } from "@dcloudio/uni-app";
  7. import { useDataDictStore } from "@/store/modules/dataDict";
  8. import { createIotRdDailyReport, getRuiDuReportDetail } from "@/api/ruiDu.js";
  9. import DaTree from "@/components/da-tree/index.vue";
  10. const { appContext } = getCurrentInstance();
  11. const t = appContext.config.globalProperties.$t;
  12. const NON_PROD_FIELDS = [
  13. { key: "repairTime", label: "设备故障" },
  14. { key: "selfStopTime", label: "设备保养" },
  15. { key: "accidentTime", label: "工程质量" },
  16. { key: "complexityTime", label: "技术受限" },
  17. { key: "rectificationTime", label: "生产组织" },
  18. { key: "waitingStopTime", label: "不可抗力" },
  19. { key: "partyaDesign", label: "甲方设计" },
  20. { key: "partyaPrepare", label: "甲方准备" },
  21. { key: "partyaResource", label: "甲方资源" },
  22. { key: "relocationTime", label: "生产配合" },
  23. { key: "winterBreakTime", label: "待命" },
  24. { key: "otherNptTime", label: "其他非生产时间" },
  25. ];
  26. const defaultProps = {
  27. inputBorder: false,
  28. clearable: false,
  29. placeholder: "请输入",
  30. style: {
  31. "text-align": "right",
  32. },
  33. styles: {
  34. disableColor: "#fff",
  35. },
  36. };
  37. const original = {
  38. deptId: getDeptId(),
  39. startTime: "08:00",
  40. endTime: "08:00",
  41. ...NON_PROD_FIELDS.reduce((acc, field) => ({ ...acc, [field.key]: 0 }), {}),
  42. winterBreakTime: 24,
  43. };
  44. const formRef = ref();
  45. const formDataBaseRules = reactive({
  46. deptId: {
  47. rules: [
  48. {
  49. required: true,
  50. errorMessage: "请选择施工部门",
  51. },
  52. ],
  53. },
  54. constructionBrief: {
  55. rules: [
  56. {
  57. required: true,
  58. errorMessage: "请输入施工简报",
  59. },
  60. ],
  61. },
  62. createTime: {
  63. rules: [
  64. {
  65. required: true,
  66. errorMessage: "请选择施工时间",
  67. },
  68. ],
  69. },
  70. location: {
  71. rules: [
  72. {
  73. required: true,
  74. errorMessage: "请输入施工地点",
  75. },
  76. ],
  77. },
  78. responsiblePerson: {
  79. rules: [
  80. {
  81. required: true,
  82. errorMessage: "请选择带班干部",
  83. },
  84. ],
  85. },
  86. rdStatus: {
  87. rules: [
  88. {
  89. required: true,
  90. errorMessage: "请选择施工状态",
  91. },
  92. ],
  93. },
  94. dailyFuel: {
  95. rules: [
  96. {
  97. required: true,
  98. errorMessage: "请输入当日油耗",
  99. },
  100. ],
  101. },
  102. startTime: {
  103. rules: [
  104. {
  105. required: true,
  106. errorMessage: `${t("operation.PleaseSelect")}${t("ruiDu.timeNode")}`,
  107. },
  108. ],
  109. },
  110. endTime: {
  111. rules: [
  112. {
  113. required: true,
  114. errorMessage: `${t("operation.PleaseSelect")}${t("ruiDu.timeNode")}`,
  115. },
  116. ],
  117. },
  118. productionStatus: {
  119. rules: [
  120. {
  121. required: true,
  122. errorMessage: `${t("operation.PleaseFillIn")}${t(
  123. "ruiDu.dailyProductionDynamic"
  124. )}`,
  125. },
  126. ],
  127. },
  128. nextPlan: {
  129. rules: [
  130. {
  131. required: true,
  132. errorMessage: `请输入下步工作计划`,
  133. },
  134. ],
  135. },
  136. });
  137. function debounce(func, delay) {
  138. let timer;
  139. return function (...args) {
  140. if (timer) clearTimeout(timer);
  141. timer = setTimeout(() => func.apply(this, args), delay);
  142. };
  143. }
  144. const handleInputRaw = (val, field) => {
  145. let num = Number(val);
  146. if (val === "" || isNaN(num) || num < 0) {
  147. num = 0;
  148. } else if (num > 24) {
  149. num = 24;
  150. }
  151. form.value[field] = num;
  152. };
  153. const onInputChange = debounce(handleInputRaw, 500);
  154. const form = ref({ ...original });
  155. const userOptions = ref([]);
  156. const submitterNames = ref("");
  157. async function loadUserOptions() {
  158. const res = await selectedDeptsEmployee({ deptIds: [getDeptId()] });
  159. userOptions.value = res.data.map((item) => ({
  160. text: item.nickname,
  161. value: item.id,
  162. raw: item,
  163. }));
  164. }
  165. const deptOptions = ref([]);
  166. const treeData = ref([]);
  167. const deptLoading = ref(false);
  168. const handleTree = (data, id, parentId, children) => {
  169. if (!Array.isArray(data)) {
  170. console.warn("data must be an array");
  171. return [];
  172. }
  173. const config = {
  174. id: id || "id",
  175. parentId: parentId || "parentId",
  176. childrenList: children || "children",
  177. };
  178. const childrenListMap = {};
  179. const nodeIds = {};
  180. const tree = [];
  181. for (const d of data) {
  182. const parentId = d[config.parentId];
  183. if (childrenListMap[parentId] == null) {
  184. childrenListMap[parentId] = [];
  185. }
  186. nodeIds[d[config.id]] = d;
  187. childrenListMap[parentId].push(d);
  188. }
  189. for (const d of data) {
  190. const parentId = d[config.parentId];
  191. if (nodeIds[parentId] == null) {
  192. tree.push(d);
  193. }
  194. }
  195. for (const t of tree) {
  196. adaptToChildrenList(t);
  197. }
  198. function adaptToChildrenList(o) {
  199. if (childrenListMap[o[config.id]] !== null) {
  200. o[config.childrenList] = childrenListMap[o[config.id]];
  201. }
  202. if (o[config.childrenList]) {
  203. for (const c of o[config.childrenList]) {
  204. adaptToChildrenList(c);
  205. }
  206. }
  207. }
  208. return tree;
  209. };
  210. async function loadDept() {
  211. deptLoading.value = true;
  212. try {
  213. function sortTreeBySort(treeNodes) {
  214. if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes;
  215. const sortedNodes = [...treeNodes].sort((a, b) => {
  216. const sortA = a.sort != null ? a.sort : 999999;
  217. const sortB = b.sort != null ? b.sort : 999999;
  218. return sortA - sortB;
  219. });
  220. sortedNodes.forEach((node) => {
  221. node.disabled = node.type !== "3";
  222. if (node.children && Array.isArray(node.children)) {
  223. node.children = sortTreeBySort(node.children);
  224. }
  225. });
  226. return sortedNodes;
  227. }
  228. const depts = await specifiedSimpleDepts(getDeptId());
  229. deptOptions.value = depts.data.map((item) => ({
  230. text: item.name,
  231. value: item.id,
  232. raw: item,
  233. }));
  234. treeData.value = sortTreeBySort(handleTree(depts.data));
  235. } catch (error) {
  236. } finally {
  237. deptLoading.value = false;
  238. }
  239. }
  240. const popup = ref();
  241. const openPopup = () => {
  242. popup.value.open();
  243. };
  244. const selectDeptId = ref("");
  245. function handleTreeChange(values) {
  246. selectDeptId.value = values;
  247. }
  248. function handleConfirm() {
  249. form.value.deptId = selectDeptId.value;
  250. popup.value.close();
  251. }
  252. const rdStatusOptions = ref([]);
  253. const dictStore = useDataDictStore();
  254. function loadRdStatusOptions() {
  255. rdStatusOptions.value = dictStore.getStrDictOptions("rdStatus").map((v) => ({
  256. text: v.label,
  257. value: v.value,
  258. }));
  259. }
  260. const formatT = (arr) =>
  261. `${arr[0].toString().padStart(2, "0")}:${arr[1].toString().padStart(2, "0")}`;
  262. const id = ref();
  263. const isview = ref("create");
  264. const detail = ref();
  265. onLoad(async (query) => {
  266. if (dictStore.dataDict.length <= 0) {
  267. dictStore.loadDataDictList().then(() => {
  268. loadRdStatusOptions();
  269. });
  270. } else loadRdStatusOptions();
  271. await loadUserOptions();
  272. id.value = query.id;
  273. isview.value = query.isview ?? "create";
  274. await loadDept();
  275. const info = JSON.parse(JSON.parse(getUserInfo()));
  276. submitterNames.value = info.user.nickname;
  277. if (query.id && query.isview) {
  278. const res = await getRuiDuReportDetail({ id: query.id });
  279. detail.value = res.data;
  280. submitterNames.value = res.data.submitterNames;
  281. form.value = {
  282. deptId: res.data.deptId,
  283. location: res.data.location,
  284. responsiblePerson: res.data.responsiblePerson,
  285. startTime: formatT(res.data.startTime),
  286. endTime: formatT(res.data.endTime),
  287. rdStatus: res.data.rdStatus,
  288. dailyFuel: res.data.dailyFuel,
  289. productionStatus: res.data.productionStatus,
  290. nextPlan: res.data.nextPlan,
  291. externalRental: res.data.externalRental,
  292. malfunction: res.data.malfunction,
  293. otherNptReason: res.data.otherNptReason,
  294. createTime: res.data.createTime,
  295. constructionBrief: res.data.constructionBrief,
  296. ...NON_PROD_FIELDS.reduce(
  297. (acc, field) => ({ ...acc, [field.key]: res.data[field.key] }),
  298. {}
  299. ),
  300. };
  301. }
  302. });
  303. const reportDetailsTimeRangeRef = ref(null);
  304. const startTime = ref("00:00");
  305. const startDefaultTime = ref("08:00");
  306. const endTime = ref("24:00");
  307. const endDefaultTime = ref("08:00");
  308. const handleClickTimeRangeItem = () => {
  309. reportDetailsTimeRangeRef.value.open();
  310. };
  311. const reportDetailsTimeRange = (data) => {
  312. form.value.startTime = data[0];
  313. form.value.endTime = data[1];
  314. };
  315. const formLoading = ref(false);
  316. async function submitForm() {
  317. try {
  318. await formRef.value.validate();
  319. const totalNonProdTime = NON_PROD_FIELDS.reduce((sum, field) => {
  320. return sum + (Number(form.value[field.key]) || 0);
  321. }, 0);
  322. if (totalNonProdTime > 24) {
  323. uni.showToast({
  324. title: "非生产时间总和不能大于24小时",
  325. icon: "none",
  326. });
  327. return;
  328. }
  329. const otherTime = Number(form.value.otherNptTime) || 0;
  330. if (otherTime >= 1) {
  331. if (
  332. !form.value.otherNptReason ||
  333. form.value.otherNptReason.trim() === ""
  334. ) {
  335. uni.showToast({
  336. title: "其他非生产时间大于0小时,请填写其他非生产原因",
  337. icon: "none",
  338. duration: 2000,
  339. });
  340. return;
  341. }
  342. }
  343. await createIotRdDailyReport(form.value);
  344. uni.showToast({ title: t("operation.success"), icon: "none" });
  345. uni.navigateBack();
  346. } catch (error) {
  347. console.error("提交表单失败", error);
  348. } finally {
  349. formLoading.value = false;
  350. }
  351. }
  352. const disabled = computed(() => {
  353. if (isview.value !== "create") {
  354. if (isview.value === "time") {
  355. return !(
  356. detail.value?.contractName === null && detail.value?.taskName === null
  357. );
  358. }
  359. return true;
  360. }
  361. return false;
  362. });
  363. </script>
  364. <template>
  365. <view class="page">
  366. <scroll-view scroll-y="true" class="segmented-content">
  367. <view class="content">
  368. <uni-forms
  369. ref="formRef"
  370. labelWidth="auto"
  371. :rules="formDataBaseRules"
  372. :model="form"
  373. validateTrigger="submit"
  374. err-show-type="toast">
  375. <uni-forms-item label="施工队伍" name="deptId" required>
  376. <view class="deptName">
  377. <uni-data-select
  378. :clear="true"
  379. align="right"
  380. placeholder="请选择"
  381. :localdata="deptOptions"
  382. hideRight
  383. placement="bottom"
  384. :disabled="true"
  385. v-model="form.deptId" />
  386. <button
  387. class="popup-button"
  388. type="primary"
  389. size="mini"
  390. :disabled="deptLoading || disabled"
  391. @click="openPopup">
  392. 选择
  393. </button>
  394. </view>
  395. </uni-forms-item>
  396. <uni-forms-item label="施工日期" name="createTime" required>
  397. <uni-datetime-picker
  398. type="date"
  399. return-type="timestamp"
  400. :clear-icon="true"
  401. v-model="form.createTime"
  402. :border="false"
  403. :style="{
  404. 'text-align': 'right',
  405. 'float': 'right',
  406. }"
  407. :styles="{ disableColor: '#fff' }"
  408. :disabled="disabled" />
  409. </uni-forms-item>
  410. <uni-forms-item label="施工地点" name="location" required>
  411. <uni-easyinput
  412. type="textarea"
  413. autoHeight
  414. v-bind="defaultProps"
  415. v-model="form.location"
  416. :disabled="disabled"
  417. :maxlength="1000" />
  418. </uni-forms-item>
  419. <uni-forms-item label="带班干部" name="responsiblePerson" required>
  420. <uni-data-select
  421. :clear="true"
  422. multiple
  423. align="right"
  424. placeholder="请选择"
  425. :localdata="userOptions"
  426. placement="bottom"
  427. :disabled="disabled"
  428. v-model="form.responsiblePerson" />
  429. </uni-forms-item>
  430. <uni-forms-item label="填报人" name="submitterNames">
  431. <span class="readOnly">{{ submitterNames }}</span>
  432. </uni-forms-item>
  433. <uni-forms-item :label="`${$t('ruiDu.timeNode')}:`" required>
  434. <view
  435. class="item-content"
  436. @click="!disabled && handleClickTimeRangeItem()">
  437. <view
  438. class="time-range-item"
  439. v-if="form.startTime && form.endTime">
  440. {{ form.startTime }} 至 {{ form.endTime }}
  441. </view>
  442. <view class="time-range-item" v-else> '请选择' </view>
  443. </view>
  444. </uni-forms-item>
  445. <uni-forms-item label="施工状态" name="rdStatus" required>
  446. <uni-data-select
  447. :clear="true"
  448. align="right"
  449. placeholder="请选择"
  450. :localdata="rdStatusOptions"
  451. placement="bottom"
  452. :disabled="disabled"
  453. v-model="form.rdStatus" />
  454. </uni-forms-item>
  455. <uni-forms-item label="当日油耗(L)" name="dailyFuel" required>
  456. <uni-easyinput
  457. type="number"
  458. v-bind="defaultProps"
  459. :disabled="disabled"
  460. v-model.number="form.dailyFuel" />
  461. </uni-forms-item>
  462. <uni-forms-item label="当日生产动态" name="productionStatus" required>
  463. <uni-easyinput
  464. type="textarea"
  465. autoHeight
  466. v-bind="defaultProps"
  467. :disabled="disabled"
  468. v-model="form.productionStatus"
  469. :maxlength="1000" />
  470. </uni-forms-item>
  471. <uni-forms-item label="下步工作计划" name="nextPlan" required>
  472. <uni-easyinput
  473. type="textarea"
  474. autoHeight
  475. v-bind="defaultProps"
  476. :disabled="disabled"
  477. v-model="form.nextPlan"
  478. :maxlength="1000" />
  479. </uni-forms-item>
  480. <uni-forms-item label="外租设备" name="externalRental">
  481. <uni-easyinput
  482. type="textarea"
  483. autoHeight
  484. v-bind="defaultProps"
  485. :disabled="disabled"
  486. v-model="form.externalRental"
  487. :maxlength="1000" />
  488. </uni-forms-item>
  489. <uni-forms-item label="故障情况" name="malfunction">
  490. <uni-easyinput
  491. type="textarea"
  492. autoHeight
  493. v-bind="defaultProps"
  494. :disabled="disabled"
  495. v-model="form.malfunction"
  496. :maxlength="1000" />
  497. </uni-forms-item>
  498. <uni-forms-item label="施工简报" name="constructionBrief" required>
  499. <uni-easyinput
  500. type="textarea"
  501. autoHeight
  502. v-bind="defaultProps"
  503. v-model="form.constructionBrief"
  504. :disabled="disabled"
  505. :maxlength="1000" />
  506. </uni-forms-item>
  507. <uv-divider text="非生产时间" textPosition="left"></uv-divider>
  508. <uni-forms-item
  509. v-for="field in NON_PROD_FIELDS"
  510. :key="field.key"
  511. :label="field.label + '(H)'"
  512. :name="field.key">
  513. <uni-easyinput
  514. type="number"
  515. v-bind="defaultProps"
  516. v-model.number="form[field.key]"
  517. :disabled="isview === 'detail'"
  518. @input="(val) => onInputChange(val, field.key)" />
  519. </uni-forms-item>
  520. <uni-forms-item label="其他非生产原因" name="otherNptReason">
  521. <uni-easyinput
  522. type="textarea"
  523. autoHeight
  524. v-bind="defaultProps"
  525. v-model="form.otherNptReason"
  526. :disabled="isview === 'detail'"
  527. :maxlength="1000" />
  528. </uni-forms-item>
  529. </uni-forms>
  530. </view>
  531. </scroll-view>
  532. <view class="segmented-footer">
  533. <view class="footer-btn">
  534. <button
  535. :loading="formLoading"
  536. class="confirm-btn"
  537. type="primary"
  538. :disabled="isview === 'detail'"
  539. @click="submitForm()">
  540. 确定
  541. </button>
  542. </view>
  543. </view>
  544. </view>
  545. <tpf-time-range
  546. ref="reportDetailsTimeRangeRef"
  547. :startTime="startTime"
  548. :startDefaultTime="startDefaultTime"
  549. :endTime="endTime"
  550. :endDefaultTime="endDefaultTime"
  551. @timeRange="reportDetailsTimeRange"></tpf-time-range>
  552. <uni-popup ref="popup" type="bottom">
  553. <view class="popup-content">
  554. <view class="tree">
  555. <DaTree
  556. :data="treeData"
  557. labelField="name"
  558. valueField="id"
  559. disabledField="disabled"
  560. defaultExpandAll
  561. checkedDisabled
  562. :defaultCheckedKeys="form.deptId"
  563. @change="handleTreeChange"></DaTree>
  564. </view>
  565. <button class="mini-btn" type="primary" @click="handleConfirm">
  566. 确定
  567. </button>
  568. </view>
  569. </uni-popup>
  570. </template>
  571. <style lang="scss" scoped>
  572. @import "@/style/work-order-segmented.scss";
  573. .page {
  574. padding-bottom: 0;
  575. }
  576. .content {
  577. background-color: white;
  578. padding: 16px 16px;
  579. border-radius: 8px;
  580. box-sizing: border-box;
  581. }
  582. .uni-forms {
  583. margin-top: 10px;
  584. height: 100%;
  585. .uni-form {
  586. height: 100%;
  587. }
  588. .uni-forms-item {
  589. display: flex;
  590. align-items: center;
  591. flex: 1;
  592. margin-bottom: 6px;
  593. border-bottom: 1px dashed #cacccf;
  594. }
  595. :deep(.uni-forms-item__content) {
  596. text-align: right;
  597. .readOnly {
  598. padding-right: 10px;
  599. }
  600. }
  601. :deep(.uni-forms-item__label) {
  602. height: 44px;
  603. font-weight: 500;
  604. font-size: 14px;
  605. color: #333333 !important;
  606. width: max-content !important;
  607. }
  608. :deep(.uni-select) {
  609. border: none;
  610. text-align: right;
  611. padding-right: 0;
  612. .uniui-bottom:before {
  613. content: "\e6b5" !important;
  614. font-size: 16px !important;
  615. }
  616. }
  617. :deep(.uni-easyinput__content-textarea) {
  618. min-height: inherit;
  619. margin: 10px;
  620. }
  621. :deep(.is-disabled) {
  622. color: #333333 !important;
  623. }
  624. :deep(.uni-date-editor--x__disabled) {
  625. opacity: 1;
  626. .uni-date__x-input {
  627. color: #333333 !important;
  628. }
  629. }
  630. :deep(.icon-calendar) {
  631. display: none;
  632. }
  633. :deep(.uni-select--disabled) {
  634. background-color: #fff;
  635. }
  636. }
  637. .item-content {
  638. display: flex;
  639. align-items: center;
  640. justify-content: end;
  641. }
  642. .time-range-item {
  643. margin: 10px;
  644. }
  645. .footer-btn {
  646. display: flex;
  647. justify-content: flex-end;
  648. align-items: center;
  649. padding: 0 16px;
  650. height: 100%;
  651. gap: 0 32px;
  652. & > uni-button {
  653. margin: 0;
  654. }
  655. }
  656. :deep(.confirm-btn) {
  657. height: 38px !important;
  658. font-size: 16px !important;
  659. }
  660. :deep(.popup-button) {
  661. width: 62px;
  662. margin: 0;
  663. }
  664. :deep(.deptName) {
  665. display: flex;
  666. justify-content: end;
  667. align-items: center;
  668. gap: 0 10px;
  669. }
  670. @mixin flex {
  671. /* #ifndef APP-NVUE */
  672. display: flex;
  673. /* #endif */
  674. flex-direction: column;
  675. }
  676. @mixin height {
  677. /* #ifndef APP-NVUE */
  678. height: 100%;
  679. /* #endif */
  680. /* #ifdef APP-NVUE */
  681. flex: 1;
  682. /* #endif */
  683. }
  684. .popup-content {
  685. @include flex;
  686. justify-content: space-between;
  687. padding: 15px;
  688. height: 500px;
  689. background-color: #fff;
  690. .tree {
  691. flex: 1;
  692. max-height: 420px;
  693. }
  694. button {
  695. width: 100%;
  696. height: 44px;
  697. }
  698. }
  699. </style>