index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <template>
  2. <view class="report-page">
  3. <scroll-view class="report-scroll" scroll-y>
  4. <view v-if="loading" class="state-card">
  5. <text class="state-text">数据加载中...</text>
  6. </view>
  7. <view v-else-if="!dataList.length" class="state-card empty">
  8. <text class="state-text">当前日期暂无数据</text>
  9. </view>
  10. <view v-else class="report-list">
  11. <view
  12. v-for="item in sortedList"
  13. :key="item.projectDeptId"
  14. class="report-card"
  15. >
  16. <view class="card-header">
  17. <view>
  18. <view class="dept-name">{{ item.projectDeptName || "--" }}</view>
  19. </view>
  20. <view class="rate-badge" @click="openTeamDetail(item)">
  21. 查看队伍明细
  22. </view>
  23. </view>
  24. <view class="metrics-grid">
  25. <!-- <view class="metric-item">
  26. <text class="metric-label">队伍总数</text>
  27. <text class="metric-value">{{
  28. formatNumber(item.teamCount)
  29. }}</text>
  30. </view> -->
  31. <view class="summary-row">
  32. <text class="summary-label">队伍总数</text>
  33. <text class="summary-value">
  34. {{ formatNumber(item.teamCount) }}
  35. </text>
  36. </view>
  37. <!-- <view class="metric-item">
  38. <text class="metric-label">施工队伍</text>
  39. <text class="metric-value">{{
  40. formatNumber(item.sgTeamCount)
  41. }}</text>
  42. </view> -->
  43. <view class="summary-row">
  44. <text class="summary-label">施工队伍</text>
  45. <text class="summary-value">
  46. {{ formatNumber(item.sgTeamCount) }}
  47. </text>
  48. </view>
  49. <!-- <view class="metric-item">
  50. <text class="metric-label">施工准备</text>
  51. <text class="metric-value">{{
  52. formatNumber(item.zbTeamCount)
  53. }}</text>
  54. </view> -->
  55. <view class="summary-row">
  56. <text class="summary-label">施工准备</text>
  57. <text class="summary-value">
  58. {{ formatNumber(item.zbTeamCount) }}
  59. </text>
  60. </view>
  61. <!-- <view class="metric-item">
  62. <text class="metric-label">待命</text>
  63. <text class="metric-value">{{
  64. formatNumber(item.dmTeamCount)
  65. }}</text>
  66. </view> -->
  67. <view class="summary-row">
  68. <text class="summary-label">待命</text>
  69. <text class="summary-value">
  70. {{ formatNumber(item.dmTeamCount) }}
  71. </text>
  72. </view>
  73. <!-- <view class="metric-item">
  74. <text class="metric-label">当日运行时效</text>
  75. <text class="metric-value">{{
  76. formatPercent(item.hourUtilizationRate)
  77. }}</text>
  78. </view> -->
  79. <view class="summary-row">
  80. <text class="summary-label">当日运行时效</text>
  81. <text class="summary-value">
  82. {{ formatPercent(item.hourUtilizationRate) }}
  83. </text>
  84. </view>
  85. <!-- <view class="metric-item">
  86. <text class="metric-label">平均运行时效</text>
  87. <text class="metric-value">{{
  88. formatPercent(item.yearHourUtilizationRate)
  89. }}</text>
  90. </view> -->
  91. <view class="summary-row">
  92. <text class="summary-label">平均运行时效</text>
  93. <text class="summary-value">
  94. {{ formatPercent(item.yearHourUtilizationRate) }}
  95. </text>
  96. </view>
  97. </view>
  98. <view class="summary-panel">
  99. <view class="metric-item">
  100. <text class="metric-label">日注气量(万方)</text>
  101. <text class="metric-value">{{
  102. formatDecimal(item.gasInjection)
  103. }}</text>
  104. </view>
  105. <view class="metric-item">
  106. <text class="metric-label">累计注气量(万方)</text>
  107. <text class="metric-value">{{
  108. formatDecimal(item.yearGasInjection)
  109. }}</text>
  110. </view>
  111. <view class="metric-item">
  112. <text class="metric-label">日注水量(万方)</text>
  113. <text class="metric-value">{{
  114. formatDecimal(item.waterInjection)
  115. }}</text>
  116. </view>
  117. <view class="metric-item">
  118. <text class="metric-label">累计注水量(万方)</text>
  119. <text class="metric-value">{{
  120. formatDecimal(item.yearWaterInjection)
  121. }}</text>
  122. </view>
  123. </view>
  124. <view class="desc-panel">
  125. <view class="desc-title">生产动态概述</view>
  126. <text class="desc-text">
  127. {{ item.productionSummary || "暂无生产动态概述" }}
  128. </text>
  129. </view>
  130. </view>
  131. </view>
  132. </scroll-view>
  133. <view class="filter-bar">
  134. <view class="filter-main">
  135. <text class="filter-label">创建时间</text>
  136. <uni-datetime-picker
  137. v-model="selectedDate"
  138. class="date-picker"
  139. type="date"
  140. :teleport="true"
  141. return-type="string"
  142. :clear-icon="false"
  143. :border="false"
  144. @change="handleDateChange"
  145. />
  146. </view>
  147. <button
  148. class="filter-button"
  149. type="primary"
  150. size="mini"
  151. @click="loadList"
  152. >
  153. 查询
  154. </button>
  155. </view>
  156. <uni-popup
  157. ref="teamDetailPopup"
  158. type="right"
  159. background-color="#fff"
  160. :is-mask-click="true"
  161. >
  162. <view class="team-drawer">
  163. <view class="team-drawer__header">
  164. <text class="team-drawer__title">
  165. {{ currentDetailTitle || "队伍明细" }}
  166. </text>
  167. <text class="team-drawer__close" @click="closeTeamDetail">关闭</text>
  168. </view>
  169. <view class="team-drawer__sub">
  170. 统计日期:{{ selectedDate || "--" }}
  171. </view>
  172. <scroll-view class="team-drawer__scroll" scroll-y>
  173. <view v-if="teamDetailLoading" class="team-state-card">
  174. <text class="team-state-text">数据加载中...</text>
  175. </view>
  176. <view
  177. v-else-if="!teamDetailList.length"
  178. class="team-state-card empty"
  179. >
  180. <text class="team-state-text">暂无队伍明细</text>
  181. </view>
  182. <view v-else class="team-detail-list">
  183. <view
  184. v-for="(team, index) in teamDetailList"
  185. :key="team.teamName || index"
  186. class="team-detail-card"
  187. >
  188. <view class="team-detail-card__title">
  189. {{ team.teamName || "--" }}
  190. </view>
  191. <view class="team-detail-grid">
  192. <view class="team-detail-item">
  193. <text class="team-detail-label">累计天数</text>
  194. <text class="team-detail-value">{{
  195. formatNumber(team.cumulativeDays)
  196. }}</text>
  197. </view>
  198. <view class="team-detail-item">
  199. <text class="team-detail-label">施工天数</text>
  200. <text class="team-detail-value">{{
  201. formatNumber(team.constructionDays)
  202. }}</text>
  203. </view>
  204. <view class="team-detail-item team-detail-item--full">
  205. <text class="team-detail-label">设备利用率</text>
  206. <text class="team-detail-value">{{
  207. formatPercent(team.utilizationRate)
  208. }}</text>
  209. </view>
  210. </view>
  211. </view>
  212. </view>
  213. </scroll-view>
  214. </view>
  215. </uni-popup>
  216. </view>
  217. </template>
  218. <script setup>
  219. import { computed, ref } from "vue";
  220. import { onMounted } from "vue";
  221. import dayjs from "dayjs";
  222. import { getRhRate, getRhTeamDetail } from "@/api/ruiHengReport";
  223. const selectedDate = ref(dayjs().subtract(1, "day").format("YYYY-MM-DD"));
  224. const dataList = ref([]);
  225. const loading = ref(false);
  226. const teamDetailPopup = ref(null);
  227. const teamDetailLoading = ref(false);
  228. const teamDetailList = ref([]);
  229. const currentDetailTitle = ref("");
  230. const buildQueryParams = () => {
  231. const baseDate = selectedDate.value
  232. ? dayjs(selectedDate.value)
  233. : dayjs().subtract(1, "day");
  234. return {
  235. createTime: [
  236. baseDate.startOf("day").format("YYYY-MM-DD HH:mm:ss"),
  237. baseDate.endOf("day").format("YYYY-MM-DD HH:mm:ss"),
  238. ],
  239. };
  240. };
  241. const sortedList = computed(() =>
  242. [...dataList.value].sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999)),
  243. );
  244. const loadList = async () => {
  245. loading.value = true;
  246. try {
  247. const response = await getRhRate(buildQueryParams());
  248. dataList.value = Array.isArray(response?.data) ? response.data : [];
  249. } catch (error) {
  250. dataList.value = [];
  251. uni.showToast({
  252. title: "数据加载失败",
  253. icon: "none",
  254. });
  255. } finally {
  256. loading.value = false;
  257. }
  258. };
  259. const handleDateChange = () => {
  260. loadList();
  261. };
  262. const openTeamDetail = async (item) => {
  263. currentDetailTitle.value = `${item.projectDeptName || "--"} 队伍明细`;
  264. teamDetailList.value = [];
  265. teamDetailLoading.value = true;
  266. teamDetailPopup.value?.open();
  267. try {
  268. const response = await getRhTeamDetail({
  269. deptId: item.projectDeptId,
  270. createTime: buildQueryParams().createTime,
  271. });
  272. teamDetailList.value = Array.isArray(response?.data) ? response.data : [];
  273. } catch (error) {
  274. teamDetailList.value = [];
  275. uni.showToast({
  276. title: "队伍明细加载失败",
  277. icon: "none",
  278. });
  279. } finally {
  280. teamDetailLoading.value = false;
  281. }
  282. };
  283. const closeTeamDetail = () => {
  284. teamDetailPopup.value?.close();
  285. };
  286. const formatNumber = (value) => {
  287. if (value === null || value === undefined || value === "") return "--";
  288. return value;
  289. };
  290. const formatDecimal = (value) => {
  291. if (value === null || value === undefined || value === "") return "--";
  292. return Number(value).toFixed(2);
  293. };
  294. const formatPercent = (value) => {
  295. if (value === null || value === undefined || value === "") return "--";
  296. return `${(Number(value) * 100).toFixed(2)}%`;
  297. };
  298. onMounted(() => {
  299. loadList();
  300. });
  301. </script>
  302. <style lang="scss" scoped>
  303. .report-page {
  304. max-height: 100vh;
  305. padding: 12px;
  306. box-sizing: border-box;
  307. }
  308. .filter-bar {
  309. display: flex;
  310. align-items: center;
  311. padding: 14px;
  312. border-radius: 18px;
  313. background: rgba(255, 255, 255, 0.94);
  314. box-shadow: 0 14px 32px rgba(25, 56, 104, 0.08);
  315. backdrop-filter: blur(10px);
  316. }
  317. .filter-bar > * + * {
  318. margin-left: 10px;
  319. }
  320. .filter-main {
  321. flex: 1;
  322. min-width: 0;
  323. display: flex;
  324. align-items: center;
  325. justify-content: space-between;
  326. }
  327. .filter-main > * + * {
  328. margin-left: 10rpx;
  329. }
  330. .filter-label {
  331. display: block;
  332. margin-bottom: 8px;
  333. color: #5f6f87;
  334. font-size: 12px;
  335. letter-spacing: 1px;
  336. }
  337. .date-picker {
  338. padding: 0 12px;
  339. border: 1px solid rgba(0, 64, 152, 0.1);
  340. border-radius: 12px;
  341. background: #f8fbff;
  342. }
  343. .filter-button {
  344. width: 76px;
  345. height: 38px;
  346. line-height: 40px;
  347. border-radius: 12px;
  348. background: linear-gradient(135deg, #004098 0%, #1f68d8 100%);
  349. font-size: 14px;
  350. }
  351. .filter-tip {
  352. margin: 10px 4px 0;
  353. color: #6d7c91;
  354. font-size: 12px;
  355. }
  356. .report-scroll {
  357. height: calc(100vh - 140px);
  358. padding-top: 12px;
  359. box-sizing: border-box;
  360. }
  361. .report-list {
  362. display: flex;
  363. flex-direction: column;
  364. padding-bottom: 18px;
  365. }
  366. .report-list > .report-card + .report-card {
  367. margin-top: 14px;
  368. }
  369. .report-card {
  370. padding: 16px;
  371. border: 1px solid rgba(0, 64, 152, 0.08);
  372. border-radius: 20px;
  373. background: rgba(255, 255, 255, 0.96);
  374. box-shadow: 0 18px 38px rgba(31, 57, 90, 0.08);
  375. }
  376. .card-header {
  377. display: flex;
  378. align-items: flex-start;
  379. justify-content: space-between;
  380. margin-bottom: 14px;
  381. }
  382. .card-header > * + * {
  383. margin-left: 12px;
  384. }
  385. .dept-name {
  386. color: #122033;
  387. font-size: 18px;
  388. font-weight: 700;
  389. line-height: 26px;
  390. }
  391. .dept-meta {
  392. margin-top: 4px;
  393. color: #7f8ca0;
  394. font-size: 12px;
  395. }
  396. .rate-badge {
  397. flex-shrink: 0;
  398. min-width: 84px;
  399. padding: 8px 12px;
  400. border-radius: 999px;
  401. background: linear-gradient(135deg, #e6f0ff 0%, #d8e7ff 100%);
  402. color: #004098;
  403. font-size: 14px;
  404. font-weight: 700;
  405. text-align: center;
  406. }
  407. .metrics-grid {
  408. display: grid;
  409. grid-template-columns: calc(50% - 5px) calc(50% - 5px);
  410. margin-bottom: 14px;
  411. margin-top: 14px;
  412. }
  413. .metrics-grid > *:nth-child(even) {
  414. margin-left: 10px;
  415. }
  416. .metrics-grid > *:nth-child(n + 3) {
  417. margin-top: 10px;
  418. }
  419. .metric-item {
  420. padding: 12px;
  421. border-radius: 14px;
  422. background: #f6f9fd;
  423. }
  424. .metric-label {
  425. display: block;
  426. color: #6e7e95;
  427. font-size: 12px;
  428. }
  429. .metric-value {
  430. display: block;
  431. margin-top: 6px;
  432. color: #18273b;
  433. font-size: 20px;
  434. font-weight: 700;
  435. }
  436. .summary-panel {
  437. display: grid;
  438. grid-template-columns: calc(50% - 7px) calc(50% - 7px);
  439. padding: 14px 0;
  440. // border-top: 1px solid #edf2f7;
  441. border-bottom: 1px solid #edf2f7;
  442. }
  443. .summary-panel > *:nth-child(even) {
  444. margin-left: 14px;
  445. }
  446. .summary-panel > *:nth-child(n + 3) {
  447. margin-top: 8px;
  448. }
  449. .summary-row {
  450. display: flex;
  451. align-items: center;
  452. justify-content: space-between;
  453. }
  454. .summary-row > * + * {
  455. margin-left: 10px;
  456. }
  457. .summary-label {
  458. color: #6d7c91;
  459. font-size: 12px;
  460. }
  461. .summary-value {
  462. color: #17253a;
  463. font-size: 13px;
  464. font-weight: 600;
  465. }
  466. .desc-panel {
  467. padding-top: 14px;
  468. }
  469. .desc-title {
  470. margin-bottom: 8px;
  471. color: #1f2f46;
  472. font-size: 14px;
  473. font-weight: 700;
  474. }
  475. .desc-text {
  476. color: #5f6f87;
  477. font-size: 13px;
  478. line-height: 22px;
  479. white-space: pre-wrap;
  480. }
  481. .state-card {
  482. margin-top: 18px;
  483. padding: 40px 20px;
  484. border-radius: 18px;
  485. background: rgba(255, 255, 255, 0.92);
  486. text-align: center;
  487. box-shadow: 0 12px 28px rgba(30, 54, 88, 0.06);
  488. }
  489. .state-card.empty {
  490. border: 1px dashed rgba(0, 64, 152, 0.16);
  491. }
  492. .state-text {
  493. color: #73839a;
  494. font-size: 14px;
  495. }
  496. .team-drawer {
  497. width: 82vw;
  498. max-width: 620rpx;
  499. height: 100vh;
  500. padding: 18px 14px calc(18px + env(safe-area-inset-bottom));
  501. box-sizing: border-box;
  502. background: linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
  503. display: flex;
  504. flex-direction: column;
  505. }
  506. .team-drawer__header {
  507. display: flex;
  508. align-items: center;
  509. justify-content: space-between;
  510. }
  511. .team-drawer__header > * + * {
  512. margin-left: 12px;
  513. }
  514. .team-drawer__title {
  515. min-width: 0;
  516. color: #122033;
  517. font-size: 18px;
  518. font-weight: 700;
  519. }
  520. .team-drawer__close {
  521. flex-shrink: 0;
  522. color: #004098;
  523. font-size: 14px;
  524. font-weight: 600;
  525. }
  526. .team-drawer__sub {
  527. margin-top: 8px;
  528. margin-bottom: 14px;
  529. color: #70819a;
  530. font-size: 12px;
  531. }
  532. .team-drawer__scroll {
  533. flex: 1;
  534. min-height: 0;
  535. padding-bottom: 50rpx;
  536. }
  537. .team-detail-list {
  538. display: flex;
  539. flex-direction: column;
  540. padding-bottom: 10px;
  541. }
  542. .team-detail-list > .team-detail-card + .team-detail-card {
  543. margin-top: 12px;
  544. }
  545. .team-detail-card {
  546. padding: 14px;
  547. border-radius: 16px;
  548. background: rgba(255, 255, 255, 0.96);
  549. box-shadow: 0 10px 24px rgba(31, 57, 90, 0.08);
  550. }
  551. .team-detail-card__title {
  552. margin-bottom: 12px;
  553. color: #18273b;
  554. font-size: 16px;
  555. font-weight: 700;
  556. }
  557. .team-detail-grid {
  558. display: grid;
  559. grid-template-columns: calc(50% - 5px) calc(50% - 5px);
  560. }
  561. .team-detail-grid > *:nth-child(even) {
  562. margin-left: 10px;
  563. }
  564. .team-detail-grid > *:nth-child(n + 3) {
  565. margin-top: 10px;
  566. }
  567. .team-detail-item {
  568. padding: 10px 12px;
  569. border-radius: 12px;
  570. background: #f6f9fd;
  571. }
  572. .team-detail-item--full {
  573. grid-column: 1 / -1;
  574. }
  575. .team-detail-label {
  576. display: block;
  577. color: #6e7e95;
  578. font-size: 12px;
  579. }
  580. .team-detail-value {
  581. display: block;
  582. margin-top: 6px;
  583. color: #18273b;
  584. font-size: 18px;
  585. font-weight: 700;
  586. }
  587. .team-state-card {
  588. margin-top: 8px;
  589. padding: 36px 20px;
  590. border-radius: 16px;
  591. background: rgba(255, 255, 255, 0.92);
  592. text-align: center;
  593. box-shadow: 0 12px 28px rgba(30, 54, 88, 0.06);
  594. }
  595. .team-state-card.empty {
  596. border: 1px dashed rgba(0, 64, 152, 0.16);
  597. }
  598. .team-state-text {
  599. color: #73839a;
  600. font-size: 14px;
  601. }
  602. </style>