index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  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. gap: 10px;
  312. padding: 14px;
  313. border-radius: 18px;
  314. background: rgba(255, 255, 255, 0.94);
  315. box-shadow: 0 14px 32px rgba(25, 56, 104, 0.08);
  316. backdrop-filter: blur(10px);
  317. }
  318. .filter-main {
  319. flex: 1;
  320. min-width: 0;
  321. display: flex;
  322. align-items: center;
  323. justify-content: space-between;
  324. gap: 10rpx;
  325. }
  326. .filter-label {
  327. display: block;
  328. margin-bottom: 8px;
  329. color: #5f6f87;
  330. font-size: 12px;
  331. letter-spacing: 1px;
  332. }
  333. .date-picker {
  334. padding: 0 12px;
  335. border: 1px solid rgba(0, 64, 152, 0.1);
  336. border-radius: 12px;
  337. background: #f8fbff;
  338. }
  339. .filter-button {
  340. width: 76px;
  341. height: 38px;
  342. line-height: 40px;
  343. border-radius: 12px;
  344. background: linear-gradient(135deg, #004098 0%, #1f68d8 100%);
  345. font-size: 14px;
  346. }
  347. .filter-tip {
  348. margin: 10px 4px 0;
  349. color: #6d7c91;
  350. font-size: 12px;
  351. }
  352. .report-scroll {
  353. height: calc(100vh - 140px);
  354. padding-top: 12px;
  355. box-sizing: border-box;
  356. }
  357. .report-list {
  358. display: flex;
  359. flex-direction: column;
  360. gap: 14px;
  361. padding-bottom: 18px;
  362. }
  363. .report-card {
  364. padding: 16px;
  365. border: 1px solid rgba(0, 64, 152, 0.08);
  366. border-radius: 20px;
  367. background: rgba(255, 255, 255, 0.96);
  368. box-shadow: 0 18px 38px rgba(31, 57, 90, 0.08);
  369. }
  370. .card-header {
  371. display: flex;
  372. align-items: flex-start;
  373. justify-content: space-between;
  374. gap: 12px;
  375. margin-bottom: 14px;
  376. }
  377. .dept-name {
  378. color: #122033;
  379. font-size: 18px;
  380. font-weight: 700;
  381. line-height: 26px;
  382. }
  383. .dept-meta {
  384. margin-top: 4px;
  385. color: #7f8ca0;
  386. font-size: 12px;
  387. }
  388. .rate-badge {
  389. flex-shrink: 0;
  390. min-width: 84px;
  391. padding: 8px 12px;
  392. border-radius: 999px;
  393. background: linear-gradient(135deg, #e6f0ff 0%, #d8e7ff 100%);
  394. color: #004098;
  395. font-size: 14px;
  396. font-weight: 700;
  397. text-align: center;
  398. }
  399. .metrics-grid {
  400. display: grid;
  401. grid-template-columns: repeat(2, minmax(0, 1fr));
  402. gap: 10px;
  403. margin-bottom: 14px;
  404. margin-top: 14px;
  405. }
  406. .metric-item {
  407. padding: 12px;
  408. border-radius: 14px;
  409. background: #f6f9fd;
  410. }
  411. .metric-label {
  412. display: block;
  413. color: #6e7e95;
  414. font-size: 12px;
  415. }
  416. .metric-value {
  417. display: block;
  418. margin-top: 6px;
  419. color: #18273b;
  420. font-size: 20px;
  421. font-weight: 700;
  422. }
  423. .summary-panel {
  424. display: grid;
  425. grid-template-columns: repeat(2, minmax(0, 1fr));
  426. gap: 8px 14px;
  427. padding: 14px 0;
  428. // border-top: 1px solid #edf2f7;
  429. border-bottom: 1px solid #edf2f7;
  430. }
  431. .summary-row {
  432. display: flex;
  433. align-items: center;
  434. justify-content: space-between;
  435. gap: 10px;
  436. }
  437. .summary-label {
  438. color: #6d7c91;
  439. font-size: 12px;
  440. }
  441. .summary-value {
  442. color: #17253a;
  443. font-size: 13px;
  444. font-weight: 600;
  445. }
  446. .desc-panel {
  447. padding-top: 14px;
  448. }
  449. .desc-title {
  450. margin-bottom: 8px;
  451. color: #1f2f46;
  452. font-size: 14px;
  453. font-weight: 700;
  454. }
  455. .desc-text {
  456. color: #5f6f87;
  457. font-size: 13px;
  458. line-height: 22px;
  459. white-space: pre-wrap;
  460. }
  461. .state-card {
  462. margin-top: 18px;
  463. padding: 40px 20px;
  464. border-radius: 18px;
  465. background: rgba(255, 255, 255, 0.92);
  466. text-align: center;
  467. box-shadow: 0 12px 28px rgba(30, 54, 88, 0.06);
  468. }
  469. .state-card.empty {
  470. border: 1px dashed rgba(0, 64, 152, 0.16);
  471. }
  472. .state-text {
  473. color: #73839a;
  474. font-size: 14px;
  475. }
  476. .team-drawer {
  477. width: 82vw;
  478. max-width: 620rpx;
  479. height: 100vh;
  480. padding: 18px 14px calc(18px + env(safe-area-inset-bottom));
  481. box-sizing: border-box;
  482. background: linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
  483. display: flex;
  484. flex-direction: column;
  485. }
  486. .team-drawer__header {
  487. display: flex;
  488. align-items: center;
  489. justify-content: space-between;
  490. gap: 12px;
  491. }
  492. .team-drawer__title {
  493. min-width: 0;
  494. color: #122033;
  495. font-size: 18px;
  496. font-weight: 700;
  497. }
  498. .team-drawer__close {
  499. flex-shrink: 0;
  500. color: #004098;
  501. font-size: 14px;
  502. font-weight: 600;
  503. }
  504. .team-drawer__sub {
  505. margin-top: 8px;
  506. margin-bottom: 14px;
  507. color: #70819a;
  508. font-size: 12px;
  509. }
  510. .team-drawer__scroll {
  511. flex: 1;
  512. min-height: 0;
  513. padding-bottom: 50rpx;
  514. }
  515. .team-detail-list {
  516. display: flex;
  517. flex-direction: column;
  518. gap: 12px;
  519. padding-bottom: 10px;
  520. }
  521. .team-detail-card {
  522. padding: 14px;
  523. border-radius: 16px;
  524. background: rgba(255, 255, 255, 0.96);
  525. box-shadow: 0 10px 24px rgba(31, 57, 90, 0.08);
  526. }
  527. .team-detail-card__title {
  528. margin-bottom: 12px;
  529. color: #18273b;
  530. font-size: 16px;
  531. font-weight: 700;
  532. }
  533. .team-detail-grid {
  534. display: grid;
  535. grid-template-columns: repeat(2, minmax(0, 1fr));
  536. gap: 10px;
  537. }
  538. .team-detail-item {
  539. padding: 10px 12px;
  540. border-radius: 12px;
  541. background: #f6f9fd;
  542. }
  543. .team-detail-item--full {
  544. grid-column: 1 / -1;
  545. }
  546. .team-detail-label {
  547. display: block;
  548. color: #6e7e95;
  549. font-size: 12px;
  550. }
  551. .team-detail-value {
  552. display: block;
  553. margin-top: 6px;
  554. color: #18273b;
  555. font-size: 18px;
  556. font-weight: 700;
  557. }
  558. .team-state-card {
  559. margin-top: 8px;
  560. padding: 36px 20px;
  561. border-radius: 16px;
  562. background: rgba(255, 255, 255, 0.92);
  563. text-align: center;
  564. box-shadow: 0 12px 28px rgba(30, 54, 88, 0.06);
  565. }
  566. .team-state-card.empty {
  567. border: 1px dashed rgba(0, 64, 152, 0.16);
  568. }
  569. .team-state-text {
  570. color: #73839a;
  571. font-size: 14px;
  572. }
  573. </style>