header.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. <template>
  2. <header
  3. class="fixed w-full top-0 z-100 bg-white border-b border-[#f0f2f5] shadow-sm"
  4. >
  5. <div
  6. class="max-w-[1200px] mx-auto flex items-center justify-between px-5 h-15"
  7. >
  8. <div class="flex items-center gap-2 cursor-pointer" @click="goHome">
  9. <img :src="logo" alt="logo" class="w-9 h-9 rounded-md" />
  10. <span
  11. style="text-wrap-mode: nowrap"
  12. class="text-[#02409b] text-[20px] font-bold hidden lg:flex"
  13. >KERUI DEEPOIL</span
  14. >
  15. <span class="text-[#828182] text-[14px]" style="text-wrap-mode: nowrap"
  16. >智慧经营平台</span
  17. >
  18. </div>
  19. <nav class="hidden lg:flex flex-1 mx-4 ml-80 text-sm">
  20. <ul class="flex items-center gap-6 text-[#303133] text-md">
  21. <!-- 首页 -->
  22. <li>
  23. <a
  24. class="cursor-pointer px-3 py-1.5 rounded-md transition-all duration-300"
  25. :class="
  26. router.currentRoute.value.path === '/'
  27. ? 'nav-item-active'
  28. : 'nav-item-default'
  29. "
  30. style="text-wrap-mode: nowrap"
  31. @click="router.push('/')"
  32. >
  33. 首页
  34. </a>
  35. </li>
  36. <!-- 流程门户 -->
  37. <li>
  38. <a
  39. style="text-wrap-mode: nowrap"
  40. class="cursor-pointer px-3 py-1.5 rounded-md transition-all duration-300"
  41. :class="
  42. router.currentRoute.value.path.startsWith('/flow')
  43. ? 'nav-item-active'
  44. : 'nav-item-default'
  45. "
  46. @click="goFlow"
  47. >
  48. 流程门户
  49. </a>
  50. </li>
  51. <!-- 驾驶舱门户 -->
  52. <li v-hasPermi="['portal:dashboard:view']">
  53. <a
  54. class="cursor-pointer px-3 py-1.5 rounded-md transition-all duration-300"
  55. :class="
  56. router.currentRoute.value.path.startsWith('/drive')
  57. ? 'nav-item-active'
  58. : 'nav-item-default'
  59. "
  60. @click="goDrive"
  61. style="text-wrap-mode: nowrap"
  62. >
  63. 驾驶舱门户
  64. </a>
  65. </li>
  66. <!-- 报表门户 (假设路径为 /report,请根据实际路由调整) -->
  67. <li>
  68. <a
  69. class="cursor-pointer px-3 py-1.5 rounded-md transition-all duration-300"
  70. :class="
  71. router.currentRoute.value.path.startsWith('/report')
  72. ? 'nav-item-active'
  73. : 'nav-item-default'
  74. "
  75. style="text-wrap-mode: nowrap"
  76. >
  77. 报表门户
  78. </a>
  79. </li>
  80. </ul>
  81. </nav>
  82. <div
  83. style="border-left: 1px solid #a5bbdb"
  84. class="hidden lg:flex items-center gap-3 h-[40%] pl-4"
  85. >
  86. <!-- 消息中心 -->
  87. <el-dropdown trigger="click" placement="bottom-end">
  88. <div class="flex items-center gap-2 cursor-pointer pr-6 pt-1">
  89. <el-badge
  90. :value="unreadMessageCount + oaUnreadCount"
  91. class="item"
  92. v-if="hasUnreadMessages || oaHasUnreadCount"
  93. >
  94. <Icon
  95. icon="mdi:bell"
  96. class="w-5 h-5 text-[#507698] hover:text-[#409EFF]"
  97. />
  98. </el-badge>
  99. <Icon
  100. v-else
  101. icon="mdi:bell"
  102. class="w-5 h-5 text-[#507698] hover:text-[#409EFF]"
  103. />
  104. </div>
  105. <template #dropdown>
  106. <el-dropdown-menu class="notification-dropdown">
  107. <div class="notification-tabs pl-2">
  108. <el-tabs v-model="activeTab" class="demo-tabs">
  109. <el-tab-pane label="OA" name="tasks">
  110. <template #label>
  111. <span class="custom-tabs-label">
  112. <span>OA</span>
  113. <el-badge
  114. :value="oaUnreadCount"
  115. class="item ml-1"
  116. v-if="oaHasUnreadCount"
  117. ></el-badge>
  118. </span>
  119. </template>
  120. <div class="tab-content">
  121. <div>
  122. <span
  123. v-if="oaHasUnreadCount"
  124. class="cursor-pointer text-blue-500"
  125. @click="oaMarkAllAsRead"
  126. >全部标为已读</span
  127. >
  128. </div>
  129. <!-- OA消息 -->
  130. <div
  131. class="task-item"
  132. v-for="(task, index) in oaMessagesList"
  133. :key="index"
  134. >
  135. <div class="task-info">
  136. <p class="task-title">
  137. <span
  138. v-if="task.status === '0'"
  139. class="inline-block h-2 w-2 bg-[#f56c6c] rounded-full"
  140. ></span>
  141. {{ task.title }}
  142. </p>
  143. <p class="message-desc">
  144. <span>{{ task.oaCreateTime }}</span>
  145. </p>
  146. </div>
  147. </div>
  148. <div v-if="!oaMessagesList.length" class="no-tasks">
  149. 暂无新消息
  150. </div>
  151. </div>
  152. </el-tab-pane>
  153. <el-tab-pane label="CRM" name="messages">
  154. <template #label>
  155. <span class="custom-tabs-label">
  156. <span>CRM</span>
  157. <el-badge
  158. :value="unreadMessageCount"
  159. class="item ml-1"
  160. v-if="hasUnreadMessages"
  161. ></el-badge>
  162. </span>
  163. </template>
  164. <div class="tab-content">
  165. <!-- 消息中心内容 -->
  166. <div>
  167. <span
  168. v-if="hasUnreadMessages"
  169. class="cursor-pointer text-blue-500"
  170. @click="markAllAsRead"
  171. >全部标为已读</span
  172. >
  173. </div>
  174. <div
  175. class="message-item"
  176. v-for="(item, index) in messages"
  177. :key="index"
  178. >
  179. <div class="message-icon"></div>
  180. <div class="message-text">
  181. <!-- 未读就显示小红点 -->
  182. <p class="message-title flex items-center gap-5">
  183. <span
  184. v-if="item.status === '0'"
  185. class="w-2 h-2 bg-[#f56c6c] rounded-full"
  186. ></span
  187. >{{ item.contentMajor }}
  188. </p>
  189. <p class="message-desc">
  190. {{ timestampToDateTime(item.createTime) }}
  191. </p>
  192. </div>
  193. </div>
  194. <div v-if="!messages.length" class="no-messages">
  195. 暂无新消息
  196. </div>
  197. </div>
  198. </el-tab-pane>
  199. </el-tabs>
  200. </div>
  201. </el-dropdown-menu>
  202. </template>
  203. </el-dropdown>
  204. <template v-if="isLoggedIn">
  205. <el-dropdown @command="onUserCommand" trigger="click">
  206. <span class="flex items-center gap-2 cursor-pointer pr-2">
  207. <div class="avatar-wrapper">
  208. <img
  209. :src="userAvatar || person"
  210. alt="avatar"
  211. class="w-8 h-8 rounded-full avatar-image"
  212. />
  213. </div>
  214. <span
  215. class="text-sm text-[#303133]"
  216. style="text-wrap-mode: nowrap"
  217. >{{ userName }}</span
  218. >
  219. </span>
  220. <template #dropdown>
  221. <el-dropdown-menu>
  222. <el-dropdown-item command="profile">
  223. <svg
  224. xmlns="http://www.w3.org/2000/svg"
  225. width="18"
  226. height="18"
  227. viewBox="0 0 16 16"
  228. >
  229. <g fill="none">
  230. <path
  231. fill="url(#SVG3BqCJdyi)"
  232. d="M11.5 8A1.5 1.5 0 0 1 13 9.5v.5c0 1.971-1.86 4-5 4s-5-2.029-5-4v-.5A1.5 1.5 0 0 1 4.5 8z"
  233. />
  234. <path
  235. fill="url(#SVGfKhxtenh)"
  236. d="M11.5 8A1.5 1.5 0 0 1 13 9.5v.5c0 1.971-1.86 4-5 4s-5-2.029-5-4v-.5A1.5 1.5 0 0 1 4.5 8z"
  237. />
  238. <path
  239. fill="url(#SVGJYCMTblH)"
  240. d="M8 1.5A2.75 2.75 0 1 1 8 7a2.75 2.75 0 0 1 0-5.5"
  241. />
  242. <defs>
  243. <linearGradient
  244. id="SVG3BqCJdyi"
  245. x1="5.378"
  246. x2="7.616"
  247. y1="8.798"
  248. y2="14.754"
  249. gradientUnits="userSpaceOnUse"
  250. >
  251. <stop offset=".125" stop-color="#9c6cfe" />
  252. <stop offset="1" stop-color="#7a41dc" />
  253. </linearGradient>
  254. <linearGradient
  255. id="SVGfKhxtenh"
  256. x1="8"
  257. x2="11.164"
  258. y1="7.286"
  259. y2="17.139"
  260. gradientUnits="userSpaceOnUse"
  261. >
  262. <stop stop-color="#885edb" stop-opacity="0" />
  263. <stop offset="1" stop-color="#e362f8" />
  264. </linearGradient>
  265. <linearGradient
  266. id="SVGJYCMTblH"
  267. x1="6.558"
  268. x2="9.361"
  269. y1="2.231"
  270. y2="6.707"
  271. gradientUnits="userSpaceOnUse"
  272. >
  273. <stop offset=".125" stop-color="#9c6cfe" />
  274. <stop offset="1" stop-color="#7a41dc" />
  275. </linearGradient>
  276. </defs>
  277. </g>
  278. </svg>
  279. <span class="pl-2">个人中心</span>
  280. </el-dropdown-item>
  281. <el-dropdown-item command="logout">
  282. <svg
  283. xmlns="http://www.w3.org/2000/svg"
  284. width="18"
  285. height="18"
  286. viewBox="0 0 24 24"
  287. >
  288. <g fill="none">
  289. <path
  290. fill="url(#SVG0pAmxd9w)"
  291. d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2"
  292. />
  293. <path
  294. fill="url(#SVGFnXqmeDt)"
  295. d="m15.53 8.47l-.084-.073a.75.75 0 0 0-.882-.007l-.094.08L12 10.939l-2.47-2.47l-.084-.072a.75.75 0 0 0-.882-.007l-.094.08l-.073.084a.75.75 0 0 0-.007.882l.08.094L10.939 12l-2.47 2.47l-.072.084a.75.75 0 0 0-.007.882l.08.094l.084.073a.75.75 0 0 0 .882.007l.094-.08L12 13.061l2.47 2.47l.084.072a.75.75 0 0 0 .882.007l.094-.08l.073-.084a.75.75 0 0 0 .007-.882l-.08-.094L13.061 12l2.47-2.47l.072-.084a.75.75 0 0 0 .007-.882z"
  296. />
  297. <defs>
  298. <linearGradient
  299. id="SVG0pAmxd9w"
  300. x1="5.125"
  301. x2="18.25"
  302. y1="3.25"
  303. y2="22.625"
  304. gradientUnits="userSpaceOnUse"
  305. >
  306. <stop stop-color="#f83f54" />
  307. <stop offset="1" stop-color="#ca2134" />
  308. </linearGradient>
  309. <linearGradient
  310. id="SVGFnXqmeDt"
  311. x1="8.685"
  312. x2="12.591"
  313. y1="12.332"
  314. y2="16.392"
  315. gradientUnits="userSpaceOnUse"
  316. >
  317. <stop stop-color="#fdfdfd" />
  318. <stop offset="1" stop-color="#fecbe6" />
  319. </linearGradient>
  320. </defs>
  321. </g>
  322. </svg>
  323. <span class="pl-2">退出登录</span>
  324. </el-dropdown-item>
  325. </el-dropdown-menu>
  326. </template>
  327. </el-dropdown>
  328. </template>
  329. <template v-else>
  330. <div
  331. style="text-wrap: nowrap"
  332. class="text-md flex items-center justify-center cursor-pointer h-full py-4"
  333. @click="login"
  334. >
  335. 登 录
  336. </div>
  337. </template>
  338. </div>
  339. <div class="lg:hidden">
  340. <el-dropdown trigger="click" placement="bottom-end">
  341. <div class="flex items-center gap-2 cursor-pointer pr-6 pt-1">
  342. <el-badge
  343. :value="unreadMessageCount + oaUnreadCount"
  344. class="item"
  345. v-if="hasUnreadMessages || oaHasUnreadCount"
  346. >
  347. <Icon
  348. icon="mdi:bell"
  349. class="w-5 h-5 text-gray-600 hover:text-[#409EFF]"
  350. />
  351. </el-badge>
  352. <Icon
  353. v-else
  354. icon="mdi:bell"
  355. class="w-5 h-5 text-gray-600 hover:text-[#409EFF]"
  356. />
  357. </div>
  358. <template #dropdown>
  359. <el-dropdown-menu class="notification-dropdown">
  360. <div class="notification-tabs pl-2">
  361. <el-tabs v-model="activeTab" class="demo-tabs">
  362. <el-tab-pane label="OA" name="tasks">
  363. <template #label>
  364. <span class="custom-tabs-label">
  365. <span>OA</span>
  366. <el-badge
  367. :value="oaUnreadCount"
  368. class="item ml-1"
  369. v-if="oaHasUnreadCount"
  370. ></el-badge>
  371. </span>
  372. </template>
  373. <div class="tab-content">
  374. <div>
  375. <span
  376. v-if="oaHasUnreadCount"
  377. class="cursor-pointer text-blue-500"
  378. @click="oaMarkAllAsRead"
  379. >全部标为已读</span
  380. >
  381. </div>
  382. <!-- OA消息 -->
  383. <div
  384. class="task-item"
  385. v-for="(task, index) in oaMessagesList"
  386. :key="index"
  387. >
  388. <div class="task-info">
  389. <p class="task-title">
  390. <span
  391. v-if="task.status === '0'"
  392. class="inline-block h-2 w-2 bg-[#f56c6c] rounded-full"
  393. ></span>
  394. {{ task.title }}
  395. </p>
  396. <p class="message-desc">
  397. <span>{{ task.oaCreateTime }}</span>
  398. </p>
  399. </div>
  400. </div>
  401. <div v-if="!oaMessagesList.length" class="no-tasks">
  402. 暂无新消息
  403. </div>
  404. </div>
  405. </el-tab-pane>
  406. <el-tab-pane label="CRM" name="messages">
  407. <template #label>
  408. <span class="custom-tabs-label">
  409. <span>CRM</span>
  410. <el-badge
  411. :value="unreadMessageCount"
  412. class="item ml-1"
  413. v-if="hasUnreadMessages"
  414. ></el-badge>
  415. </span>
  416. </template>
  417. <div class="tab-content">
  418. <!-- 消息中心内容 -->
  419. <div>
  420. <span
  421. v-if="hasUnreadMessages"
  422. class="cursor-pointer text-blue-500"
  423. @click="markAllAsRead"
  424. >全部标为已读</span
  425. >
  426. </div>
  427. <div
  428. class="message-item"
  429. v-for="(item, index) in messages"
  430. :key="index"
  431. >
  432. <div class="message-icon"></div>
  433. <div class="message-text">
  434. <!-- 未读就显示小红点 -->
  435. <p class="message-title flex items-center gap-5">
  436. <span
  437. v-if="item.status === '0'"
  438. class="w-2 h-2 bg-[#f56c6c] rounded-full"
  439. ></span
  440. >{{ item.contentMajor }}
  441. </p>
  442. <p class="message-desc">
  443. {{ timestampToDateTime(item.createTime) }}
  444. </p>
  445. </div>
  446. </div>
  447. <div v-if="!messages.length" class="no-messages">
  448. 暂无新消息
  449. </div>
  450. </div>
  451. </el-tab-pane>
  452. </el-tabs>
  453. </div>
  454. </el-dropdown-menu>
  455. </template>
  456. </el-dropdown>
  457. <el-button link @click="drawer = true">
  458. <i class="el-icon" />
  459. <Icon icon="fa:bars" class="icon" />
  460. </el-button>
  461. </div>
  462. </div>
  463. <el-drawer
  464. v-model="drawer"
  465. placement="right"
  466. size="80%"
  467. :with-header="false"
  468. >
  469. <div class="p-4 space-y-3">
  470. <ul class="flex flex-col gap-3 text-[#303133]">
  471. <li>
  472. <a
  473. class="block px-3 py-2 rounded-md transition-all duration-300"
  474. :class="
  475. router.currentRoute.value.path === '/'
  476. ? 'nav-item-active'
  477. : 'nav-item-default'
  478. "
  479. @click="
  480. router.push('/');
  481. drawer = false;
  482. "
  483. >
  484. 首页
  485. </a>
  486. </li>
  487. <li>
  488. <a
  489. class="block px-3 py-2 rounded-md transition-all duration-300"
  490. :class="
  491. router.currentRoute.value.path.startsWith('/flow')
  492. ? 'nav-item-active'
  493. : 'nav-item-default'
  494. "
  495. @click="goFlow"
  496. >
  497. 流程门户
  498. </a>
  499. </li>
  500. <li v-hasPermi="['portal:dashboard:view']">
  501. <a
  502. class="block px-3 py-2 rounded-md transition-all duration-300"
  503. :class="
  504. router.currentRoute.value.path.startsWith('/drive')
  505. ? 'nav-item-active'
  506. : 'nav-item-default'
  507. "
  508. @click="goDrive"
  509. >
  510. 驾驶舱门户
  511. </a>
  512. </li>
  513. <li>
  514. <a
  515. class="block px-3 py-2 rounded-md transition-all duration-300"
  516. :class="
  517. router.currentRoute.value.path.startsWith('/report')
  518. ? 'nav-item-active'
  519. : 'nav-item-default'
  520. "
  521. >
  522. 报表门户
  523. </a>
  524. </li>
  525. </ul>
  526. <div class="flex items-center gap-3 mt-3">
  527. <template v-if="isLoggedIn">
  528. <el-dropdown @command="onUserCommand" trigger="click">
  529. <span class="flex items-center gap-2 cursor-pointer pr-2">
  530. <div class="avatar-wrapper">
  531. <img
  532. :src="userAvatar || person"
  533. alt="avatar"
  534. class="w-8 h-8 rounded-full avatar-image"
  535. />
  536. </div>
  537. <span class="text-sm text-[#303133]">{{ userName }}</span>
  538. </span>
  539. <template #dropdown>
  540. <el-dropdown-menu>
  541. <el-dropdown-item command="profile">
  542. <span class="pl-2">个人中心</span>
  543. </el-dropdown-item>
  544. <el-dropdown-item command="logout">
  545. <span class="pl-2">退出登录</span>
  546. </el-dropdown-item>
  547. </el-dropdown-menu>
  548. </template>
  549. </el-dropdown>
  550. </template>
  551. <el-button
  552. v-else
  553. type="primary"
  554. class="flex-1 bg-[#0050b3]!"
  555. @click="login"
  556. >登录</el-button
  557. >
  558. </div>
  559. </div>
  560. </el-drawer>
  561. </header>
  562. </template>
  563. <script setup lang="ts">
  564. import { Icon } from "@iconify/vue";
  565. import { ref, computed, onMounted, onBeforeUnmount } from "vue";
  566. import { useRouter } from "vue-router";
  567. import logo from "@/assets/images/logo.png";
  568. import person from "@/assets/images/person.png";
  569. import { useUserStoreWithOut } from "@/stores/useUserStore";
  570. const userStore = useUserStoreWithOut();
  571. import {
  572. getNotifyMessages,
  573. getNotifyMessageList,
  574. markMessageAsRead,
  575. getUnreadNotifyMessageCount,
  576. getOANotifyMessages,
  577. getOANotifyMessageList,
  578. markOAMessageAsRead,
  579. } from "@api/user";
  580. import {
  581. getAccessToken,
  582. getRefreshToken,
  583. removeToken,
  584. setToken,
  585. } from "@utils/auth";
  586. import { deleteUserCache } from "@hooks/useCache";
  587. import { manualLogoutKey, reloginCancelKey } from "@/config/axios/service";
  588. // 新增消息中心状态
  589. const activeTab = ref("tasks");
  590. const messages = ref([]);
  591. const isLoggedIn = computed(
  592. () => !!userStore.isSetUser || !!userStore.user?.id,
  593. );
  594. const userAvatar = computed(() => userStore.user?.avatar || "");
  595. const userName = computed(() => userStore.user?.nickname || "");
  596. // 是否有未读消息
  597. const hasUnreadMessages = computed(() => {
  598. return messages.value.some((msg) => msg.status === "0");
  599. });
  600. // oa是否有未读
  601. const oaHasUnreadCount = computed(() => {
  602. return oaMessagesList.value.some((msg) => msg.status === "0");
  603. });
  604. // 未读消息数量
  605. const unreadMessageCount = computed(() => {
  606. return messages.value.filter((msg) => msg.status === "0").length;
  607. });
  608. // oa未读消息数量
  609. const oaUnreadCount = computed(() => {
  610. return oaMessagesList.value.filter((msg) => msg.status === "0").length;
  611. });
  612. // oa未读
  613. const oaMessagesList = ref([]);
  614. const unreadCount = ref(0); // 未读消息数量
  615. const getUnreadCount = async () => {
  616. if (!getAccessToken()) {
  617. unreadCount.value = 0;
  618. return;
  619. }
  620. const data = await getUnreadNotifyMessageCount();
  621. unreadCount.value = data;
  622. };
  623. let messageTimer: ReturnType<typeof setInterval> | undefined;
  624. let unreadTimer: ReturnType<typeof setInterval> | undefined;
  625. onMounted(async () => {
  626. if (isLoggedIn.value) {
  627. getUnreadCount();
  628. await getNotifyMessages(userStore.getUser.username);
  629. const messageList = await getNotifyMessageList(userStore.getUser.username);
  630. messages.value = messageList.filter((msg: any) => msg.status === "0");
  631. // oa消息
  632. await getOANotifyMessages(userStore.getUser.username);
  633. const oaMessageList = await getOANotifyMessageList(
  634. userStore.getUser.username,
  635. );
  636. oaMessagesList.value = oaMessageList.filter((msg) => msg.status === "0");
  637. }
  638. messageTimer = setInterval(
  639. async () => {
  640. if (isLoggedIn.value) {
  641. await getNotifyMessages(userStore.getUser.username);
  642. const messageList = await getNotifyMessageList(
  643. userStore.getUser.username,
  644. );
  645. messages.value = messageList.filter((msg: any) => msg.status === "0");
  646. // oa消息
  647. await getOANotifyMessages(userStore.getUser.username);
  648. const oaMessageList = await getOANotifyMessageList(
  649. userStore.getUser.username,
  650. );
  651. oaMessagesList.value = oaMessageList.filter(
  652. (msg: any) => msg.status === "0",
  653. );
  654. }
  655. },
  656. 1000 * 60 * 5,
  657. );
  658. unreadTimer = setInterval(
  659. () => {
  660. if (userStore.getIsSetUser && getAccessToken()) {
  661. console.log("轮询刷新小红点");
  662. getUnreadCount();
  663. } else {
  664. unreadCount.value = 0;
  665. }
  666. },
  667. 1000 * 60 * 1,
  668. );
  669. });
  670. onBeforeUnmount(() => {
  671. if (messageTimer) {
  672. clearInterval(messageTimer);
  673. }
  674. if (unreadTimer) {
  675. clearInterval(unreadTimer);
  676. }
  677. });
  678. function timestampToDateTime(timestamp) {
  679. // 兼容 10位(秒) / 13位(毫秒)
  680. const len = String(timestamp).length;
  681. const date = new Date(Number(timestamp) * (len === 10 ? 1000 : 1));
  682. // 年
  683. const year = date.getFullYear();
  684. // 月(0~11 → +1)
  685. const month = String(date.getMonth() + 1).padStart(2, "0");
  686. // 日
  687. const day = String(date.getDate()).padStart(2, "0");
  688. // 时
  689. const hours = String(date.getHours()).padStart(2, "0");
  690. // 分
  691. const minutes = String(date.getMinutes()).padStart(2, "0");
  692. // 秒
  693. const seconds = String(date.getSeconds()).padStart(2, "0");
  694. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  695. }
  696. const router = useRouter();
  697. const drawer = ref(false);
  698. // 全部标为已读
  699. const markAllAsRead = async () => {
  700. await markMessageAsRead(userStore.getUser.username);
  701. // 刷新消息列表
  702. const messageList = await getNotifyMessageList(userStore.getUser.username);
  703. messages.value = messageList.filter((msg: any) => msg.status === "0");
  704. };
  705. // oa全部标为已读
  706. const oaMarkAllAsRead = async () => {
  707. await markOAMessageAsRead(userStore.getUser.username);
  708. // 刷新消息列表
  709. const messageList = await getOANotifyMessageList(userStore.getUser.username);
  710. oaMessagesList.value = messageList.filter((msg: any) => msg.status === "0");
  711. };
  712. const goHome = () => {
  713. router.push({ path: "/" });
  714. };
  715. const login = () => {
  716. router.push({
  717. path: "/login",
  718. });
  719. };
  720. const goFlow = () => {
  721. router.push({ path: "/flow" });
  722. };
  723. const goDrive = () => {
  724. router.push({ path: "/drive" });
  725. };
  726. const onUserCommand = async (command: string) => {
  727. if (command === "logout") {
  728. // await userStore.loginOut();
  729. deleteUserCache(); // 删除用户缓存
  730. sessionStorage.setItem(manualLogoutKey, "true");
  731. sessionStorage.removeItem(reloginCancelKey);
  732. removeToken();
  733. window.location.href = "/login";
  734. }
  735. };
  736. </script>
  737. <style scoped>
  738. .avatar-wrapper {
  739. position: relative;
  740. overflow: hidden;
  741. border-radius: 50%;
  742. }
  743. .avatar-wrapper::before {
  744. content: "";
  745. position: absolute;
  746. top: 0;
  747. left: -100%;
  748. width: 50%;
  749. height: 100%;
  750. background: linear-gradient(
  751. 90deg,
  752. rgba(255, 255, 255, 0) 0%,
  753. rgba(255, 255, 255, 0.8) 50%,
  754. rgba(255, 255, 255, 0) 100%
  755. );
  756. transform: skewX(-25deg);
  757. transition: none;
  758. z-index: 1;
  759. opacity: 0;
  760. }
  761. .avatar-wrapper:hover::before {
  762. animation: shine 0.5s ease-out;
  763. }
  764. @keyframes shine {
  765. 0% {
  766. left: -100%;
  767. opacity: 0;
  768. }
  769. 10% {
  770. opacity: 1;
  771. }
  772. 100% {
  773. left: 100%;
  774. opacity: 0;
  775. }
  776. }
  777. .notification-dropdown {
  778. width: 400px !important;
  779. max-height: 500px;
  780. overflow: hidden;
  781. }
  782. .notification-tabs .el-tabs__header {
  783. margin-bottom: 0;
  784. padding: 10px;
  785. background-color: #f8f9fa;
  786. }
  787. .tab-content {
  788. max-height: 400px;
  789. overflow-y: auto;
  790. padding: 10px;
  791. }
  792. .message-item {
  793. display: flex;
  794. align-items: flex-start;
  795. padding: 12px 8px;
  796. border-bottom: 1px solid #eee;
  797. }
  798. .message-item:last-child {
  799. border-bottom: none;
  800. }
  801. .message-icon {
  802. margin-right: 12px;
  803. display: flex;
  804. align-items: center;
  805. }
  806. .message-text {
  807. flex: 1;
  808. }
  809. .message-title {
  810. font-weight: 500;
  811. color: #303133;
  812. margin-bottom: 4px;
  813. }
  814. .message-desc {
  815. font-size: 13px;
  816. color: #909399;
  817. line-height: 1.4;
  818. margin-bottom: 4px;
  819. }
  820. .message-time {
  821. font-size: 12px;
  822. color: #c0c4cc;
  823. }
  824. .no-messages,
  825. .no-tasks {
  826. text-align: center;
  827. padding: 20px;
  828. color: #909399;
  829. font-style: italic;
  830. }
  831. .task-item {
  832. display: flex;
  833. align-items: center;
  834. justify-content: space-between;
  835. padding: 12px 8px;
  836. border-bottom: 1px solid #eee;
  837. }
  838. .task-item:last-child {
  839. border-bottom: none;
  840. }
  841. .task-info {
  842. flex: 1;
  843. }
  844. .task-title {
  845. font-weight: 500;
  846. color: #303133;
  847. margin-bottom: 4px;
  848. }
  849. .task-desc {
  850. font-size: 13px;
  851. color: #909399;
  852. line-height: 1.4;
  853. margin-bottom: 4px;
  854. }
  855. .task-time {
  856. font-size: 12px;
  857. color: #c0c4cc;
  858. }
  859. .nav-item-default {
  860. background-color: #eeeeef; /* 浅灰色背景 */
  861. color: #5f6f83; /* Slate 500 灰色文字 */
  862. border-radius: 100px;
  863. }
  864. .nav-item-default:hover {
  865. background-color: #e2e8f0; /* 悬停时稍深一点的灰色 */
  866. color: #02409b; /* 品牌蓝文字 */
  867. }
  868. /* 激活状态:深蓝色背景,白色文字 */
  869. .nav-item-active {
  870. background-color: #063e8d !important; /* 指定的深蓝色背景 */
  871. color: #ffffff !important; /* 白色文字 */
  872. font-weight: 500; /* 稍微加粗 */
  873. box-shadow: 0 2px 4px rgba(6, 62, 141, 0.2); /* 可选:轻微阴影 */
  874. border-radius: 100px;
  875. }
  876. /* 确保移动端也生效,因为上面用了 block,可能需要调整一下内边距或显示方式 */
  877. @media (max-width: 1024px) {
  878. .nav-item-active,
  879. .nav-item-default {
  880. display: block;
  881. width: 100%;
  882. text-align: left;
  883. }
  884. }
  885. </style>