daily-team-statistic.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. <template>
  2. <view class="page report-page">
  3. <view class="mode-tabs">
  4. <view
  5. v-for="item in modeOptions"
  6. :key="item.value"
  7. class="mode-tab"
  8. :class="{ active: currentMode === item.value }"
  9. @click="switchMode(item.value)">
  10. {{ item.label }}
  11. </view>
  12. </view>
  13. <z-paging
  14. ref="paging"
  15. v-model="dataList"
  16. class="report-paging"
  17. :fixed="false"
  18. :default-page-size="10"
  19. @query="queryList">
  20. <view class="report-list">
  21. <view class="report-card" v-for="item in dataList" :key="item.id">
  22. <view class="card-header">
  23. <view>
  24. <view class="card-date">{{ formatDate(item.createTime) }}</view>
  25. </view>
  26. <view class="status-tag">
  27. {{ rdStatusDict[item.rdStatus] || item.rdStatus || "--" }}
  28. </view>
  29. </view>
  30. <view class="card-body">
  31. <view class="field-row">
  32. <text class="field-label">施工队伍</text>
  33. <text class="field-value">{{ item.deptName || "--" }}</text>
  34. </view>
  35. <view class="field-row">
  36. <text class="field-label">任务</text>
  37. <text class="field-value">{{ item.taskName || "--" }}</text>
  38. </view>
  39. <view class="field-row brief-row">
  40. <text class="field-label">施工简报</text>
  41. <UniTooltip
  42. :content="item.constructionBrief || '--'"
  43. placement="top">
  44. <text class="field-value brief-text">{{
  45. item.constructionBrief || "--"
  46. }}</text>
  47. </UniTooltip>
  48. </view>
  49. </view>
  50. <view v-if="item.lastGroupIdFlag" class="summary-panel">
  51. <view class="summary-header" @click="toggleSummary(item.id)">
  52. <text>组汇总</text>
  53. <uni-icons
  54. :type="isSummaryExpanded(item.id) ? 'up' : 'down'"
  55. color="#004098"
  56. size="16" />
  57. </view>
  58. <view v-if="isSummaryExpanded(item.id)" class="summary-grid">
  59. <view
  60. class="summary-item"
  61. v-for="field in summaryFields"
  62. :key="field.key">
  63. <text class="summary-label">{{ field.label }}</text>
  64. <text class="summary-value">{{
  65. formatValue(item[field.key])
  66. }}</text>
  67. </view>
  68. </view>
  69. </view>
  70. </view>
  71. </view>
  72. </z-paging>
  73. <UniFab
  74. :pattern="fabPattern"
  75. horizontal="right"
  76. vertical="bottom"
  77. direction="horizontal"
  78. :popMenu="false"
  79. @fabClick="openFilterPopup" />
  80. <uni-popup
  81. ref="filterPopup"
  82. type="bottom"
  83. background-color="#fff"
  84. border-radius="10px 10px 0 0">
  85. <view class="filter-popup">
  86. <view class="filter-header">
  87. <text class="filter-action" @click="closeFilterPopup">取消</text>
  88. <text class="filter-title">筛选条件</text>
  89. <text class="filter-action primary" @click="applyFilter">确定</text>
  90. </view>
  91. <view class="filter-body">
  92. <view class="filter-item">
  93. <view class="filter-label">项目</view>
  94. <uni-easyinput
  95. v-model="filterForm.contractName"
  96. :inputBorder="false"
  97. :styles="inputStyles"
  98. placeholder="请输入项目" />
  99. </view>
  100. <view v-if="currentMode === 'team'" class="filter-item">
  101. <view class="filter-label">任务</view>
  102. <uni-easyinput
  103. v-model="filterForm.taskName"
  104. :inputBorder="false"
  105. :styles="inputStyles"
  106. placeholder="请输入任务" />
  107. </view>
  108. <view class="filter-item">
  109. <view class="filter-label">创建时间</view>
  110. <uni-datetime-picker
  111. v-model="filterForm.createTime"
  112. type="datetimerange"
  113. return-type="string"
  114. :border="false"
  115. placeholder="请选择" />
  116. </view>
  117. <view v-if="currentMode === 'well'" class="filter-item well-tree-item">
  118. <view class="filter-label">井</view>
  119. <uni-easyinput
  120. v-model="wellSearchKey"
  121. :inputBorder="false"
  122. :styles="inputStyles"
  123. placeholder="请输入井名" />
  124. <view class="dept-selected">
  125. {{ selectedWellText || "请选择" }}
  126. </view>
  127. <view class="tree well-tree">
  128. <DaTree
  129. :key="wellTreeRenderKey"
  130. :data="wellTreeData"
  131. labelField="label"
  132. valueField="value"
  133. childrenField="children"
  134. defaultExpandAll
  135. checkedDisabled
  136. :filterValue="wellSearchKey"
  137. :defaultCheckedKeys="selectedWellKey"
  138. @change="handleWellTreeChange" />
  139. </view>
  140. </view>
  141. <view v-if="currentMode === 'team'" class="filter-item dept-item">
  142. <view class="filter-label">队伍</view>
  143. <view class="dept-selected">
  144. {{ selectedDeptName || "请选择" }}
  145. </view>
  146. <view class="tree">
  147. <DaTree
  148. :data="treeData"
  149. labelField="name"
  150. valueField="id"
  151. disabledField="disabled"
  152. defaultExpandAll
  153. checkedDisabled
  154. :defaultCheckedKeys="filterForm.deptId"
  155. @change="handleTreeChange" />
  156. </view>
  157. </view>
  158. </view>
  159. <view class="filter-footer">
  160. <button class="filter-button reset" @click="resetFilter">重置</button>
  161. <button class="filter-button" type="primary" @click="applyFilter"
  162. >搜索</button
  163. >
  164. </view>
  165. </view>
  166. </uni-popup>
  167. </view>
  168. </template>
  169. <script setup>
  170. import { computed, onMounted, reactive, ref } from "vue";
  171. import dayjs from "dayjs";
  172. import DaTree from "@/components/da-tree/index.vue";
  173. import UniFab from "@/uni_modules/uni-fab/components/uni-fab/uni-fab.vue";
  174. import UniTooltip from "@/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue";
  175. import {
  176. getTaskWellNames,
  177. getRuiduReportTeamPage,
  178. getRuiduReportWellPage,
  179. } from "@/api/ruiduReport";
  180. import { specifiedSimpleDepts } from "@/api";
  181. import { getDeptId } from "@/utils/auth";
  182. import { useDataDictStore } from "@/store/modules/dataDict";
  183. const paging = ref(null);
  184. const filterPopup = ref(null);
  185. const dataList = ref([]);
  186. const deptOptions = ref([]);
  187. const treeData = ref([]);
  188. const wellTreeData = ref([]);
  189. const wellTreeRenderKey = ref(0);
  190. const selectedWellKey = ref("");
  191. const wellSearchKey = ref("");
  192. const expandedSummaryIds = ref([]);
  193. const currentMode = ref("well");
  194. const dictStore = useDataDictStore();
  195. const rdStatusDict = reactive({});
  196. const modeOptions = [
  197. { label: "井", value: "well" },
  198. { label: "队伍", value: "team" },
  199. ];
  200. const summaryFields = [
  201. { label: "桥塞", key: "groupIdBridgePlug" },
  202. { label: "趟数", key: "groupIdRunCount" },
  203. { label: "井数", key: "groupIdCumulativeWorkingWell" },
  204. { label: "小时H", key: "groupIdHourCount" },
  205. { label: "油耗L", key: "groupIdFuel" },
  206. { label: "水方量", key: "groupIdWaterVolume" },
  207. { label: "泵车台次", key: "groupIdPumpTrips" },
  208. { label: "段数", key: "groupIdCumulativeWorkingLayers" },
  209. { label: "仪表/混砂", key: "groupIdMixSand" },
  210. ];
  211. const inputStyles = reactive({
  212. backgroundColor: "#f7f8fa",
  213. color: "#333",
  214. });
  215. const fabPattern = reactive({
  216. color: "#fff",
  217. backgroundColor: "#fff",
  218. selectedColor: "#fff",
  219. buttonColor: "#004098",
  220. iconColor: "#fff",
  221. icon: "search",
  222. });
  223. const getDefaultCreateTime = () => {
  224. const end = dayjs().endOf("day").format("YYYY-MM-DD HH:mm:ss");
  225. const start = dayjs().subtract(6, "day").startOf("day").format("YYYY-MM-DD HH:mm:ss");
  226. return [start, end];
  227. };
  228. const filterForm = reactive({
  229. contractName: "",
  230. taskName: "",
  231. wellName: "",
  232. deptId: getDeptId(),
  233. createTime: getDefaultCreateTime(),
  234. });
  235. const selectedDeptName = computed(() => {
  236. const current = deptOptions.value.find(
  237. (item) => String(item.value) === String(filterForm.deptId)
  238. );
  239. return current?.text || "";
  240. });
  241. const selectedWellText = computed(() => {
  242. if (filterForm.wellName) return `井号:${filterForm.wellName}`;
  243. if (filterForm.contractName) return `项目:${filterForm.contractName}`;
  244. return "";
  245. });
  246. const handleTree = (
  247. data,
  248. id = "id",
  249. parentId = "parentId",
  250. children = "children"
  251. ) => {
  252. if (!Array.isArray(data)) return [];
  253. const childrenListMap = {};
  254. const nodeIds = {};
  255. const tree = [];
  256. for (const item of data) {
  257. const itemParentId = item[parentId];
  258. if (childrenListMap[itemParentId] == null) {
  259. childrenListMap[itemParentId] = [];
  260. }
  261. nodeIds[item[id]] = item;
  262. childrenListMap[itemParentId].push(item);
  263. }
  264. for (const item of data) {
  265. if (nodeIds[item[parentId]] == null) {
  266. tree.push(item);
  267. }
  268. }
  269. const adaptToChildrenList = (node) => {
  270. if (childrenListMap[node[id]] != null) {
  271. node[children] = childrenListMap[node[id]];
  272. }
  273. if (node[children]) {
  274. node[children].forEach(adaptToChildrenList);
  275. }
  276. };
  277. tree.forEach(adaptToChildrenList);
  278. return tree;
  279. };
  280. const sortDeptTree = (nodes) => {
  281. if (!Array.isArray(nodes)) return [];
  282. return [...nodes]
  283. .sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999))
  284. .map((node) => ({
  285. ...node,
  286. children: sortDeptTree(node.children),
  287. }));
  288. };
  289. const loadDeptOptions = async () => {
  290. try {
  291. const response = await specifiedSimpleDepts(getDeptId());
  292. const list = response?.data || [];
  293. deptOptions.value = list.map((item) => ({
  294. text: item.name,
  295. value: item.id,
  296. }));
  297. treeData.value = sortDeptTree(handleTree(list));
  298. } catch (error) {
  299. treeData.value = [];
  300. deptOptions.value = [];
  301. }
  302. };
  303. const loadWellOptions = async () => {
  304. try {
  305. const response = await getTaskWellNames({
  306. companyId: 163,
  307. wellName: "",
  308. });
  309. const list = response?.data || [];
  310. const parentMap = new Map();
  311. const tree = [];
  312. list.forEach((item) => {
  313. if (item.type === "1") {
  314. const node = {
  315. label: item.projectName,
  316. value: `project-${item.projectId}`,
  317. type: "1",
  318. rawData: item,
  319. children: [],
  320. };
  321. parentMap.set(item.projectId, node);
  322. tree.push(node);
  323. }
  324. });
  325. list.forEach((item) => {
  326. if (item.type === "2") {
  327. const parent = parentMap.get(item.projectId);
  328. const node = {
  329. label: item.wellName,
  330. value: `well-${item.projectId}-${item.wellName}`,
  331. type: "2",
  332. rawData: item,
  333. };
  334. if (parent) {
  335. parent.children.push(node);
  336. }
  337. }
  338. });
  339. wellTreeData.value = tree;
  340. } catch (error) {
  341. wellTreeData.value = [];
  342. }
  343. };
  344. const queryList = (pageNo, pageSize) => {
  345. const request =
  346. currentMode.value === "well"
  347. ? getRuiduReportTeamPage({
  348. pageNo,
  349. pageSize,
  350. contractName: filterForm.contractName,
  351. taskName: filterForm.wellName,
  352. createTime: filterForm.createTime,
  353. })
  354. : getRuiduReportWellPage({
  355. pageNo,
  356. pageSize,
  357. deptId: filterForm.deptId,
  358. contractName: filterForm.contractName,
  359. taskName: filterForm.taskName,
  360. createTime: filterForm.createTime,
  361. });
  362. request
  363. .then((res) => {
  364. const list = res.data?.list || [];
  365. const summaryIds = list
  366. .filter((item) => item.lastGroupIdFlag)
  367. .map((item) => String(item.id));
  368. expandedSummaryIds.value =
  369. pageNo === 1
  370. ? summaryIds
  371. : Array.from(new Set([...expandedSummaryIds.value, ...summaryIds]));
  372. paging.value?.complete(list);
  373. })
  374. .catch(() => {
  375. paging.value?.complete(false);
  376. });
  377. };
  378. const switchMode = (mode, reload = true) => {
  379. if (currentMode.value === mode) return;
  380. currentMode.value = mode;
  381. expandedSummaryIds.value = [];
  382. if (reload) {
  383. paging.value?.reload();
  384. }
  385. };
  386. const openFilterPopup = () => {
  387. filterPopup.value?.open();
  388. };
  389. const closeFilterPopup = () => {
  390. filterPopup.value?.close();
  391. };
  392. const applyFilter = () => {
  393. closeFilterPopup();
  394. paging.value?.reload();
  395. };
  396. const resetFilter = () => {
  397. filterForm.contractName = "";
  398. filterForm.taskName = "";
  399. filterForm.wellName = "";
  400. filterForm.deptId = getDeptId();
  401. filterForm.createTime = getDefaultCreateTime();
  402. selectedWellKey.value = "";
  403. wellSearchKey.value = "";
  404. wellTreeRenderKey.value += 1;
  405. };
  406. const handleTreeChange = (value) => {
  407. filterForm.deptId = value;
  408. };
  409. const handleWellTreeChange = (value, item) => {
  410. const node = item?.originItem;
  411. selectedWellKey.value = value;
  412. if (node?.type === "1") {
  413. filterForm.contractName = node.rawData?.projectName || node.label || "";
  414. filterForm.wellName = "";
  415. return;
  416. }
  417. if (node?.type === "2") {
  418. filterForm.wellName = node.rawData?.wellName || node.label || "";
  419. filterForm.contractName = "";
  420. }
  421. };
  422. const toggleSummary = (id) => {
  423. const key = String(id);
  424. if (expandedSummaryIds.value.includes(key)) {
  425. expandedSummaryIds.value = expandedSummaryIds.value.filter(
  426. (item) => item !== key
  427. );
  428. return;
  429. }
  430. expandedSummaryIds.value = [...expandedSummaryIds.value, key];
  431. };
  432. const isSummaryExpanded = (id) => {
  433. return expandedSummaryIds.value.includes(String(id));
  434. };
  435. const formatDate = (time) => {
  436. return time ? dayjs(time).format("YYYY-MM-DD") : "--";
  437. };
  438. const formatValue = (value) => {
  439. if (value === 0) return "0";
  440. return value ?? "--";
  441. };
  442. const loadRdStatusDict = async () => {
  443. if (dictStore.dataDict.length <= 0) {
  444. await dictStore.loadDataDictList();
  445. }
  446. dictStore.getStrDictOptions("rdStatus").forEach((item) => {
  447. rdStatusDict[item.value] = item.label;
  448. });
  449. };
  450. onMounted(() => {
  451. loadDeptOptions();
  452. loadWellOptions();
  453. loadRdStatusDict();
  454. });
  455. </script>
  456. <style lang="scss" scoped>
  457. .report-page {
  458. display: flex;
  459. flex-direction: column;
  460. min-height: 0;
  461. padding: 10px !important;
  462. overflow: hidden;
  463. }
  464. .mode-tabs {
  465. flex-shrink: 0;
  466. height: 42px;
  467. padding: 4px;
  468. margin-bottom: 8px;
  469. display: flex;
  470. gap: 4px;
  471. background: #ffffff;
  472. border-radius: 8px;
  473. box-sizing: border-box;
  474. }
  475. .mode-tab {
  476. flex: 1;
  477. height: 34px;
  478. line-height: 34px;
  479. text-align: center;
  480. color: #5c6675;
  481. font-size: 14px;
  482. border-radius: 6px;
  483. &.active {
  484. color: #ffffff;
  485. background: #004098;
  486. font-weight: 600;
  487. }
  488. }
  489. .report-paging {
  490. flex: 1;
  491. min-height: 0;
  492. height: auto;
  493. }
  494. .report-list {
  495. padding: 8px 6px;
  496. box-sizing: border-box;
  497. }
  498. .report-card {
  499. margin-bottom: 14px;
  500. overflow: hidden;
  501. background: #ffffff;
  502. border: 1px solid rgba(0, 64, 152, 0.08);
  503. border-radius: 8px;
  504. box-shadow: 0 6px 18px rgba(35, 54, 79, 0.08);
  505. }
  506. .card-header {
  507. min-height: 52px;
  508. padding: 14px 16px 10px;
  509. display: flex;
  510. align-items: center;
  511. justify-content: space-between;
  512. box-sizing: border-box;
  513. border-bottom: 1px solid #edf1f7;
  514. }
  515. .card-date {
  516. color: #000000;
  517. font-weight: 700;
  518. font-size: 18px;
  519. line-height: 24px;
  520. }
  521. .status-tag {
  522. max-width: 112px;
  523. height: 24px;
  524. line-height: 24px;
  525. padding: 0 9px;
  526. border-radius: 12px;
  527. background: #004098;
  528. color: #ffffff;
  529. font-size: 12px;
  530. font-weight: 600;
  531. overflow: hidden;
  532. text-overflow: ellipsis;
  533. white-space: nowrap;
  534. box-sizing: border-box;
  535. }
  536. .card-body {
  537. padding: 12px 16px 6px;
  538. box-sizing: border-box;
  539. }
  540. .field-row {
  541. display: flex;
  542. align-items: flex-start;
  543. min-height: 30px;
  544. font-size: 14px;
  545. }
  546. .field-label {
  547. width: 70px;
  548. flex-shrink: 0;
  549. color: #000000;
  550. font-weight: 600;
  551. font-size: 13px;
  552. line-height: 22px;
  553. }
  554. .field-value {
  555. min-width: 0;
  556. flex: 1;
  557. color: #233044;
  558. font-weight: 500;
  559. font-size: 14px;
  560. line-height: 22px;
  561. }
  562. .summary-grid {
  563. display: grid;
  564. grid-template-columns: repeat(2, minmax(0, 1fr));
  565. gap: 8px;
  566. }
  567. .summary-item {
  568. min-width: 0;
  569. padding: 9px 10px;
  570. border-radius: 6px;
  571. background: #f7f8fa;
  572. box-sizing: border-box;
  573. }
  574. .summary-label {
  575. display: block;
  576. color: #7a8494;
  577. font-size: 12px;
  578. line-height: 17px;
  579. }
  580. .summary-value {
  581. display: block;
  582. margin-top: 4px;
  583. color: #233044;
  584. font-weight: 700;
  585. font-size: 15px;
  586. line-height: 20px;
  587. overflow: hidden;
  588. text-overflow: ellipsis;
  589. white-space: nowrap;
  590. }
  591. .brief-row {
  592. padding-top: 4px;
  593. }
  594. .brief-row :deep(.uni-tooltip) {
  595. min-width: 0;
  596. flex: 1;
  597. display: block;
  598. }
  599. .brief-text {
  600. display: block;
  601. overflow: hidden;
  602. text-overflow: ellipsis;
  603. white-space: nowrap;
  604. }
  605. .summary-panel {
  606. margin: 0 16px 14px;
  607. border: 1px solid rgba(0, 64, 152, 0.12);
  608. border-radius: 8px;
  609. overflow: hidden;
  610. }
  611. .summary-header {
  612. height: 38px;
  613. padding: 0 12px;
  614. display: flex;
  615. align-items: center;
  616. justify-content: space-between;
  617. color: #004098;
  618. font-weight: 600;
  619. font-size: 14px;
  620. background: #eef5ff;
  621. box-sizing: border-box;
  622. }
  623. .summary-grid {
  624. padding: 10px;
  625. background: #ffffff;
  626. }
  627. .filter-popup {
  628. height: 86vh;
  629. max-height: 86vh;
  630. display: flex;
  631. flex-direction: column;
  632. background: #fff;
  633. padding-bottom: env(safe-area-inset-bottom);
  634. }
  635. .filter-header {
  636. height: 48px;
  637. padding: 0 16px;
  638. display: flex;
  639. align-items: center;
  640. justify-content: space-between;
  641. border-bottom: 1px solid #f0f0f0;
  642. box-sizing: border-box;
  643. }
  644. .filter-title {
  645. font-weight: 600;
  646. color: #333;
  647. font-size: 16px;
  648. }
  649. .filter-action {
  650. min-width: 48px;
  651. color: #666;
  652. font-size: 14px;
  653. &.primary {
  654. color: #004098;
  655. text-align: right;
  656. }
  657. }
  658. .filter-body {
  659. flex: 1;
  660. overflow-y: auto;
  661. padding: 12px 16px;
  662. box-sizing: border-box;
  663. }
  664. .filter-item {
  665. margin-bottom: 14px;
  666. }
  667. .filter-label {
  668. margin-bottom: 8px;
  669. color: #333;
  670. font-weight: 500;
  671. font-size: 14px;
  672. }
  673. .dept-selected {
  674. min-height: 36px;
  675. line-height: 36px;
  676. padding: 0 10px;
  677. margin-bottom: 8px;
  678. background: #f7f8fa;
  679. color: #666;
  680. border-radius: 4px;
  681. box-sizing: border-box;
  682. }
  683. .tree {
  684. height: 360px;
  685. overflow: hidden;
  686. border: 1px solid #f0f0f0;
  687. border-radius: 4px;
  688. }
  689. .filter-footer {
  690. display: flex;
  691. gap: 10px;
  692. padding: 10px 16px 14px;
  693. border-top: 1px solid #f0f0f0;
  694. box-sizing: border-box;
  695. }
  696. .filter-button {
  697. flex: 1;
  698. height: 38px;
  699. line-height: 38px;
  700. font-size: 14px;
  701. margin: 0;
  702. &.reset {
  703. color: #004098;
  704. background: #fff;
  705. border: 1px solid #004098;
  706. }
  707. }
  708. </style>