index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. <template>
  2. <scroll-view class="entry-page" scroll-y>
  3. <view class="hero">
  4. <image class="hero-bg" src="/static/entry/bg.png" mode="widthFix" />
  5. <view class="hero-mask"></view>
  6. <view class="hero-content">
  7. <view class="status-row"> </view>
  8. <view class="profile-row">
  9. <view class="avatar-wrap">
  10. <text class="avatar-text">{{ avatarText }}</text>
  11. </view>
  12. <view class="profile-main">
  13. <view class="greeting">{{ greetingText }},{{ displayName }}</view>
  14. <view class="project-pill">
  15. <text class="project-name">{{ projectName }}</text>
  16. <uni-icons type="down" size="13" color="#ffffff" />
  17. </view>
  18. </view>
  19. <view
  20. class="notice-wrap"
  21. @click="navigatorTo('/pages/message/index')"
  22. >
  23. <uni-badge :text="messageCount" absolute="rightTop" size="small">
  24. <view class="notice-button">
  25. <image
  26. src="~@/static/home/message.png"
  27. style="
  28. width: 15px;
  29. height: 15px;
  30. background-color: transparent;
  31. "
  32. @click="navigatorTo('/pages/message/index')"
  33. />
  34. </view>
  35. </uni-badge>
  36. </view>
  37. <view class="hero-title"> 工作台 </view>
  38. </view>
  39. </view>
  40. </view>
  41. <view class="content-shell">
  42. <view class="section-card">
  43. <view class="section-title">核心功能</view>
  44. <scroll-view class="core-scroll" scroll-x show-scrollbar="false">
  45. <view class="core-list">
  46. <view
  47. v-for="item in coreFeatures"
  48. :key="item.title"
  49. class="core-item"
  50. :class="{ active: item.active }"
  51. >
  52. <view class="core-icon">
  53. <image
  54. :src="item.iconText"
  55. mode="aspectFit"
  56. style="
  57. width: 40px;
  58. height: 40px;
  59. background-color: transparent;
  60. "
  61. />
  62. </view>
  63. <view class="core-name">{{ item.title }}</view>
  64. <view class="core-desc">{{ item.desc }}</view>
  65. <view class="core-indicator" v-if="item.active"></view>
  66. </view>
  67. </view>
  68. </scroll-view>
  69. </view>
  70. <view class="section-card">
  71. <view class="section-title">PMS常用</view>
  72. <view class="tool-grid">
  73. <view
  74. v-for="item in commonTools"
  75. :key="item.title"
  76. class="tool-item"
  77. @click="navigatorTo(item.path)"
  78. >
  79. <image class="tool-icon" :src="item.icon" mode="aspectFit" />
  80. <view class="tool-title">{{ item.title }}</view>
  81. </view>
  82. </view>
  83. </view>
  84. <view class="section-card">
  85. <view class="section-head">
  86. <view class="section-title">日报入口</view>
  87. <view class="section-tip">
  88. <uni-icons type="personadd" size="16" color="#246BFF" />
  89. <text>按角色显示</text>
  90. </view>
  91. </view>
  92. <view class="daily-list">
  93. <view
  94. v-for="item in visibleDailyEntries"
  95. :key="item.title"
  96. class="daily-item"
  97. @click="navigatorTo(item.path)"
  98. >
  99. <image class="daily-icon" :src="item.icon" mode="aspectFit" />
  100. <view class="daily-content">
  101. <view class="daily-title">{{ item.title }}</view>
  102. <view class="daily-subtitle-wrap">
  103. <view class="daily-subtitle">{{ item.subtitle }}</view>
  104. <uni-icons type="right" size="12" color="#8090a8" />
  105. </view>
  106. </view>
  107. </view>
  108. </view>
  109. </view>
  110. </view>
  111. </scroll-view>
  112. </template>
  113. <script setup>
  114. import { computed, ref } from "vue";
  115. import { onShow } from "@dcloudio/uni-app";
  116. import dayjs from "dayjs";
  117. import { getLoginUserInfo } from "@/api/login";
  118. import { getUnreadMessageCount } from "@/api/message";
  119. import { getOvertimeTaskList } from "@/api/task";
  120. import { getUserInfo } from "@/utils/auth";
  121. const userInfo = ref({});
  122. const messageCount = ref(0);
  123. const todoStats = ref({
  124. maintenance: 0,
  125. fault: 0,
  126. });
  127. const coreFeatures = [
  128. {
  129. title: "PMS",
  130. desc: "设备生命周期管理",
  131. iconText: "/static/entry/pms.png",
  132. iconBg: "linear-gradient(135deg, #eaf2ff 0%, #d8e7ff 100%)",
  133. iconColor: "#246BFF",
  134. active: true,
  135. },
  136. {
  137. title: "QHSE",
  138. desc: "质量健康安全环保",
  139. iconText: "/static/entry/qhse.png",
  140. iconBg: "linear-gradient(135deg, #ecfff7 0%, #d8f8ea 100%)",
  141. iconColor: "#1eb980",
  142. },
  143. {
  144. title: "组态管理",
  145. desc: "流程与仪表组态",
  146. iconText: "/static/entry/zutai.png",
  147. iconBg: "linear-gradient(135deg, #f1edff 0%, #e0d9ff 100%)",
  148. iconColor: "#6d5efc",
  149. },
  150. {
  151. title: "智慧连油",
  152. desc: "润滑管理平台",
  153. iconText: "/static/entry/lianyou.png",
  154. iconBg: "linear-gradient(135deg, #e7fbff 0%, #d4f7fb 100%)",
  155. iconColor: "#10a8c6",
  156. },
  157. {
  158. title: "生产运营双周会",
  159. desc: "双周会管理与跟踪",
  160. iconText: "/static/entry/yunying.png",
  161. iconBg: "linear-gradient(135deg, #fff3e8 0%, #ffe3cc 100%)",
  162. iconColor: "#ff8a1f",
  163. },
  164. ];
  165. const commonTools = [
  166. {
  167. title: "运行记录",
  168. icon: "/static/entry/record.png",
  169. path: "/pages/recordFilling/list",
  170. },
  171. {
  172. title: "保养工单",
  173. icon: "/static/entry/baoyang.png",
  174. path: "/pages/maintenance/index",
  175. },
  176. {
  177. title: "设备维修",
  178. icon: "/static/entry/weixiu.png",
  179. path: "/pages/repair/index",
  180. },
  181. {
  182. title: "巡检工单",
  183. icon: "/static/entry/xunjian.png",
  184. path: "/pages/inspection/index",
  185. },
  186. {
  187. title: "故障上报",
  188. icon: "/static/entry/guzhang.png",
  189. path: "/pages/fault/index",
  190. },
  191. {
  192. title: "库存管理",
  193. icon: "/static/entry/kucun.png",
  194. path: "/pages/inventory/index",
  195. },
  196. {
  197. title: "保养查询",
  198. icon: "/static/entry/byquery.png",
  199. path: "/pages/maintenance/search",
  200. },
  201. {
  202. title: "设备台账",
  203. icon: "/static/entry/taizhang.png",
  204. path: "/pages/ledger/index",
  205. },
  206. {
  207. title: "设备状态变更",
  208. icon: "/static/entry/status.png",
  209. path: "/pages/statusChange/index",
  210. },
  211. {
  212. title: "设备责任人",
  213. icon: "/static/entry/zeren.png",
  214. path: "/pages/deviceUser/index",
  215. },
  216. ];
  217. const dailyEntries = computed(() => [
  218. {
  219. title: "瑞恒日报",
  220. subtitle: "进入瑞恒日报",
  221. path: "/pages/ruihen/index?type=edit",
  222. icon: "/static/entry/ruiheng.png",
  223. visible: !!userInfo.value.rhReportFlag,
  224. },
  225. {
  226. title: "瑞都日报",
  227. subtitle: "进入瑞都日报",
  228. path: "/pages/ruiDu/index",
  229. icon: "/static/entry/ruidu.png",
  230. visible: !!userInfo.value.rdReportFlag,
  231. },
  232. {
  233. title: "瑞鹰日报",
  234. subtitle: "进入瑞鹰日报",
  235. path: "/pages/ruiying/index?type=edit",
  236. icon: "/static/entry/ruiying.png",
  237. visible: !!userInfo.value.ryReportFlag,
  238. },
  239. ]);
  240. const visibleDailyEntries = computed(() => {
  241. const list = dailyEntries.value.filter((item) => item.visible);
  242. if (list.length) return list.slice(0, 3);
  243. return dailyEntries.value
  244. .map((item) => ({ ...item, visible: true }))
  245. .slice(0, 3);
  246. });
  247. const todoCards = computed(() => [
  248. {
  249. title: "保养工单",
  250. icon: "/static/home/baoyang.png",
  251. count: todoStats.value.maintenance,
  252. color: "#1eb980",
  253. },
  254. {
  255. title: "故障上报",
  256. icon: "/static/home/guzhang.png",
  257. count: todoStats.value.fault,
  258. color: "#ff4d4f",
  259. },
  260. ]);
  261. const currentTimeText = computed(() => dayjs().format("H:mm"));
  262. const greetingText = computed(() => {
  263. const hour = dayjs().hour();
  264. if (hour < 12) return "上午好";
  265. if (hour < 18) return "下午好";
  266. return "晚上好";
  267. });
  268. const displayName = computed(
  269. () => userInfo.value.nickname || userInfo.value.username || "张工",
  270. );
  271. const avatarText = computed(() =>
  272. String(displayName.value || "张").slice(0, 1),
  273. );
  274. const projectName = computed(
  275. () =>
  276. userInfo.value.deptName ||
  277. userInfo.value.postGroup ||
  278. userInfo.value.stationName ||
  279. "智慧工厂示例项目",
  280. );
  281. const parseCachedUserInfo = () => {
  282. const cache = getUserInfo();
  283. if (!cache) return {};
  284. try {
  285. const parsed = typeof cache === "string" ? JSON.parse(cache) : cache;
  286. if (typeof parsed === "string") {
  287. return JSON.parse(parsed)?.user || {};
  288. }
  289. return parsed?.user || parsed || {};
  290. } catch (error) {
  291. return {};
  292. }
  293. };
  294. const buildTodoStats = (list) => {
  295. const stats = {
  296. maintenance: 0,
  297. fault: 0,
  298. };
  299. list.forEach((item) => {
  300. const typeText = String(item?.type || "");
  301. if (typeText.includes("保养")) {
  302. stats.maintenance += 1;
  303. }
  304. if (typeText.includes("故障")) {
  305. stats.fault += 1;
  306. }
  307. });
  308. return stats;
  309. };
  310. const loadPageData = async () => {
  311. userInfo.value = parseCachedUserInfo();
  312. try {
  313. const [userRes, messageRes, todoRes] = await Promise.all([
  314. getLoginUserInfo(),
  315. getUnreadMessageCount(),
  316. getOvertimeTaskList({ pageNo: 1, pageSize: 50 }),
  317. ]);
  318. if (userRes?.code === 0 && userRes.data) {
  319. userInfo.value = userRes.data;
  320. }
  321. if (messageRes?.code === 0) {
  322. messageCount.value = messageRes.data || 0;
  323. }
  324. if (todoRes?.code === 0) {
  325. const todoList = todoRes.data?.list || [];
  326. todoStats.value = buildTodoStats(todoList);
  327. }
  328. } catch (error) {
  329. messageCount.value = messageCount.value || 0;
  330. }
  331. };
  332. const navigatorTo = (url) => {
  333. if (!url) return;
  334. uni.navigateTo({ url });
  335. };
  336. onShow(() => {
  337. loadPageData();
  338. });
  339. </script>
  340. <style lang="scss" scoped>
  341. .entry-page {
  342. height: 100vh;
  343. background: linear-gradient(180deg, #edf4ff 0%, #f7f9fc 28%, #f6f8fb 100%);
  344. }
  345. .hero {
  346. position: relative;
  347. height: 640rpx;
  348. overflow: hidden;
  349. }
  350. .hero-bg {
  351. position: absolute;
  352. inset: 0;
  353. width: 100%;
  354. height: 100%;
  355. }
  356. .hero-mask {
  357. position: absolute;
  358. inset: 0;
  359. height: 55%;
  360. background: linear-gradient(
  361. to bottom,
  362. rgba(255, 255, 255, 0) 0%,
  363. rgb(45, 114, 212, 0.4) 88%,
  364. rgb(45, 114, 212, 0.4) 89%,
  365. rgb(45, 114, 212, 0.7) 91%,
  366. rgb(41, 113, 214, 0.2) 95%,
  367. #f9fbfd 100%
  368. );
  369. pointer-events: none;
  370. }
  371. .hero-content {
  372. position: relative;
  373. z-index: 2;
  374. padding: calc(var(--status-bar-height) + 24rpx) 28rpx 0;
  375. }
  376. .hero-title {
  377. position: absolute;
  378. left: 0%;
  379. bottom: -110%;
  380. // background: #f7f9fc;
  381. background: linear-gradient(
  382. to right,
  383. #f7f9fc 0%,
  384. #f7f9fc 50%,
  385. rgba(253, 253, 254, 0.3) 85%,
  386. rgba(253, 253, 254, 0.2) 90%,
  387. rgba(253, 253, 254, 0) 100%
  388. );
  389. border-radius: 16rpx 0rpx 0rpx 0rpx;
  390. padding-top: 20rpx;
  391. padding-bottom: 50rpx;
  392. padding-left: 28rpx;
  393. width: 50%;
  394. height: 80rpx;
  395. font-size: 40rpx;
  396. font-weight: 700;
  397. }
  398. .status-row {
  399. display: flex;
  400. align-items: center;
  401. justify-content: space-between;
  402. margin-bottom: 42rpx;
  403. }
  404. .profile-row {
  405. display: flex;
  406. align-items: center;
  407. }
  408. .avatar-wrap {
  409. width: 82rpx;
  410. height: 82rpx;
  411. border-radius: 50%;
  412. background: rgba(255, 255, 255, 0.3);
  413. border: 2rpx solid rgba(255, 255, 255, 0.7);
  414. display: flex;
  415. align-items: center;
  416. justify-content: center;
  417. backdrop-filter: blur(8px);
  418. }
  419. .avatar-text {
  420. color: #ffffff;
  421. font-size: 34rpx;
  422. font-weight: 700;
  423. }
  424. .profile-main {
  425. flex: 1;
  426. min-width: 0;
  427. margin-left: 22rpx;
  428. }
  429. .greeting {
  430. color: #ffffff;
  431. font-size: 30rpx;
  432. font-weight: 700;
  433. line-height: 1.2;
  434. text-shadow: 0 6rpx 18rpx rgba(0, 31, 90, 0.18);
  435. }
  436. .project-pill {
  437. display: inline-flex;
  438. align-items: center;
  439. gap: 12rpx;
  440. max-width: 100%;
  441. margin-top: 18rpx;
  442. padding: 12rpx 18rpx;
  443. border-radius: 18rpx;
  444. background: rgba(255, 255, 255, 0.18);
  445. color: #ffffff;
  446. backdrop-filter: blur(8px);
  447. }
  448. .project-name {
  449. max-width: 360rpx;
  450. overflow: hidden;
  451. text-overflow: ellipsis;
  452. white-space: nowrap;
  453. font-size: 20rpx;
  454. }
  455. .notice-wrap {
  456. margin-left: 12rpx;
  457. padding-right: 28rpx;
  458. }
  459. .notice-button {
  460. width: 30rpx;
  461. height: 30rpx;
  462. display: flex;
  463. align-items: center;
  464. justify-content: center;
  465. }
  466. .notice-glyph {
  467. font-size: 32rpx;
  468. line-height: 1;
  469. }
  470. .content-shell {
  471. position: relative;
  472. z-index: 2;
  473. margin-top: -325rpx;
  474. padding: 0 24rpx calc(env(safe-area-inset-bottom) + 34rpx);
  475. }
  476. .page-title {
  477. margin: 0 6rpx 22rpx;
  478. color: #071426;
  479. font-size: 72rpx;
  480. font-weight: 800;
  481. letter-spacing: 1rpx;
  482. }
  483. .section-card {
  484. margin-bottom: 22rpx;
  485. padding: 26rpx 22rpx 24rpx;
  486. border-radius: 20rpx;
  487. background: rgba(255, 255, 255, 0.94);
  488. box-shadow: 0 14rpx 24rpx rgba(13, 43, 91, 0.02);
  489. border: 1rpx solid #eef3fc;
  490. }
  491. .section-head {
  492. display: flex;
  493. align-items: center;
  494. justify-content: space-between;
  495. gap: 16rpx;
  496. }
  497. .section-title {
  498. color: #101c2e;
  499. font-size: 36rpx;
  500. font-weight: 700;
  501. }
  502. .section-tip {
  503. display: flex;
  504. align-items: center;
  505. gap: 8rpx;
  506. color: #5572a3;
  507. font-size: 24rpx;
  508. }
  509. .core-scroll {
  510. margin-top: 22rpx;
  511. white-space: nowrap;
  512. }
  513. .core-list {
  514. display: inline-flex;
  515. gap: 18rpx;
  516. padding-bottom: 4rpx;
  517. }
  518. .core-item {
  519. position: relative;
  520. width: 188rpx;
  521. min-height: 238rpx;
  522. padding: 24rpx 18rpx 20rpx;
  523. border-radius: 22rpx;
  524. background: #f9fbfd;
  525. border: 1rpx solid #e5edf8;
  526. box-sizing: border-box;
  527. display: flex;
  528. flex-direction: column;
  529. align-items: center;
  530. }
  531. .core-item.active {
  532. border-color: #246bff;
  533. background: linear-gradient(180deg, #ffffff 0%, #dfebfd 100%);
  534. box-shadow: inset 0 0 0 2rpx rgba(36, 107, 255, 0.08);
  535. .core-name {
  536. color: #0b57f6;
  537. }
  538. }
  539. .core-icon {
  540. width: 84rpx;
  541. height: 84rpx;
  542. border-radius: 24rpx;
  543. display: flex;
  544. align-items: center;
  545. justify-content: center;
  546. text {
  547. font-size: 42rpx;
  548. font-weight: 800;
  549. }
  550. }
  551. .core-name {
  552. margin-top: 22rpx;
  553. color: #111d2f;
  554. font-size: 20rpx;
  555. font-weight: 700;
  556. white-space: normal;
  557. }
  558. .core-desc {
  559. margin-top: 10rpx;
  560. color: #6f7f96;
  561. font-size: 18rpx;
  562. line-height: 1.5;
  563. white-space: normal;
  564. }
  565. .core-indicator {
  566. position: absolute;
  567. left: 50%;
  568. bottom: 16rpx;
  569. width: 34rpx;
  570. height: 8rpx;
  571. margin-left: -17rpx;
  572. border-radius: 999rpx;
  573. background: #246bff;
  574. }
  575. .tool-grid {
  576. display: grid;
  577. grid-template-columns: repeat(5, minmax(0, 1fr));
  578. gap: 18rpx;
  579. margin-top: 22rpx;
  580. }
  581. .tool-item {
  582. min-height: 140rpx;
  583. padding: 22rpx 10rpx 18rpx;
  584. border-radius: 20rpx;
  585. background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
  586. border: 1rpx solid #e5edf8;
  587. display: flex;
  588. flex-direction: column;
  589. align-items: center;
  590. justify-content: center;
  591. box-sizing: border-box;
  592. }
  593. .tool-icon {
  594. width: 54rpx;
  595. height: 54rpx;
  596. }
  597. .tool-title {
  598. margin-top: 16rpx;
  599. color: #111d2f;
  600. font-size: 22rpx;
  601. line-height: 1.3;
  602. text-align: center;
  603. }
  604. .daily-list {
  605. display: grid;
  606. grid-template-columns: repeat(3, minmax(0, 1fr));
  607. gap: 5rpx;
  608. margin-top: 22rpx;
  609. }
  610. .daily-item {
  611. padding: 30rpx 0rpx;
  612. border-radius: 16rpx;
  613. background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
  614. border: 1rpx solid #e5edf8;
  615. display: flex;
  616. // flex-direction: column;
  617. // align-items: center;
  618. gap: 10rpx;
  619. // min-width: 0;
  620. }
  621. .daily-subtitle-wrap {
  622. display: flex;
  623. align-items: center;
  624. margin-top: 8rpx;
  625. gap: 8rpx;
  626. }
  627. .daily-icon {
  628. width: 64rpx;
  629. height: 64rpx;
  630. flex-shrink: 0;
  631. }
  632. .daily-content {
  633. flex: 1;
  634. min-width: 0;
  635. }
  636. .daily-title {
  637. color: #111d2f;
  638. font-size: 22rpx;
  639. font-weight: 700;
  640. }
  641. .daily-subtitle {
  642. // margin-top: 8rpx;
  643. color: #6f7f96;
  644. font-size: 16rpx;
  645. white-space: nowrap;
  646. overflow: hidden;
  647. text-overflow: ellipsis;
  648. }
  649. .todo-list {
  650. display: grid;
  651. grid-template-columns: repeat(2, minmax(0, 1fr));
  652. gap: 18rpx;
  653. margin-top: 22rpx;
  654. }
  655. .todo-item {
  656. min-width: 0;
  657. padding: 22rpx 20rpx;
  658. border-radius: 22rpx;
  659. background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
  660. border: 1rpx solid #e5edf8;
  661. display: flex;
  662. align-items: center;
  663. gap: 16rpx;
  664. }
  665. .todo-icon {
  666. width: 58rpx;
  667. height: 58rpx;
  668. flex-shrink: 0;
  669. }
  670. .todo-content {
  671. flex: 1;
  672. min-width: 0;
  673. }
  674. .todo-title {
  675. color: #111d2f;
  676. font-size: 26rpx;
  677. font-weight: 700;
  678. }
  679. .todo-subtitle {
  680. margin-top: 8rpx;
  681. color: #6f7f96;
  682. font-size: 22rpx;
  683. }
  684. .todo-right {
  685. display: flex;
  686. align-items: flex-end;
  687. gap: 6rpx;
  688. }
  689. .todo-count {
  690. font-size: 48rpx;
  691. font-weight: 800;
  692. line-height: 1;
  693. }
  694. .todo-unit {
  695. margin-bottom: 6rpx;
  696. color: #6f7f96;
  697. font-size: 22rpx;
  698. }
  699. </style>