PageHeader.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <template>
  2. <div class="header-wrapper">
  3. <div class="title">青浦区国资委国有资产管理系统</div>
  4. <div class="header-actions">
  5. <div v-if="isManageActive" class="manage-context">
  6. <BaseSelect
  7. class="manage-context__group"
  8. text="当前集团"
  9. :selected-text="filterStore.selectManageGroup"
  10. :options="manageGroupOptions"
  11. :select-width="128"
  12. :max-options-height="320"
  13. @selected="filterStore.setManageGroup"
  14. />
  15. <span class="manage-context__user">{{ reporterDisplay }}</span>
  16. </div>
  17. <button
  18. type="button"
  19. class="notify-bell"
  20. title="消息通知"
  21. @click="showNotify = true"
  22. >
  23. 通知
  24. <span v-if="unreadCount > 0" class="notify-bell__badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
  25. </button>
  26. <div class="nav-tabs">
  27. <BaseButton
  28. :class="{ 'nav-tab-item--active': isStatsActive }"
  29. text="数据统计"
  30. @click="goStats"
  31. />
  32. <BaseButton
  33. :class="{ 'nav-tab-item--active': isManageActive }"
  34. text="数据管理"
  35. @click="goManage"
  36. />
  37. </div>
  38. </div>
  39. <NotificationModal
  40. v-model:visible="showNotify"
  41. @unread-change="unreadCount = $event"
  42. />
  43. </div>
  44. </template>
  45. <script setup>
  46. import { computed, onMounted, ref } from 'vue';
  47. import { useRoute, useRouter } from 'vue-router';
  48. import BaseButton from '@/components/base/BaseButton.vue';
  49. import BaseSelect from '@/components/base/BaseSelect.vue';
  50. import NotificationModal from '@/components/layout/NotificationModal.vue';
  51. import { useFilterStore } from '@/store/filter';
  52. import { useNotificationStore } from '@/store/notification';
  53. import { useUserStore } from '@/store/user';
  54. import { GROUP_ORDER } from '@/utils/aggregateReportRecords';
  55. const route = useRoute();
  56. const router = useRouter();
  57. const filterStore = useFilterStore();
  58. const notificationStore = useNotificationStore();
  59. const userStore = useUserStore();
  60. const showNotify = ref(false);
  61. const unreadCount = ref(0);
  62. const isStatsActive = computed(() =>
  63. ['home', 'topic1', 'topic2', 'topic3'].includes(route.name)
  64. );
  65. const isManageActive = computed(() => route.path.startsWith('/manage'));
  66. const manageGroupOptions = GROUP_ORDER.map((group) => ({ text: group, value: group }));
  67. const reporterName = computed(
  68. () => userStore.userInfo?.username || userStore.userInfo?.name || '—'
  69. );
  70. const reporterRole = computed(
  71. () => userStore.userInfo?.roleName || userStore.userInfo?.role || '填报员'
  72. );
  73. const reporterDisplay = computed(() => `${reporterName.value} · ${reporterRole.value}`);
  74. const goStats = () => {
  75. router.push('/home');
  76. };
  77. const goManage = () => {
  78. router.push('/manage/dashboard');
  79. };
  80. onMounted(() => {
  81. notificationStore.refresh().catch(() => {});
  82. });
  83. </script>
  84. <style scoped>
  85. .header-wrapper {
  86. width: 100%;
  87. max-width: var(--page-max-width);
  88. height: 64px;
  89. padding: 10px 24px;
  90. background: var(--color-surface);
  91. display: flex;
  92. justify-content: space-between;
  93. align-items: center;
  94. position: sticky;
  95. top: 0;
  96. z-index: 1000;
  97. }
  98. .title {
  99. width: 448px;
  100. height: 40px;
  101. font-family: var(--font-semibold);
  102. font-weight: 600;
  103. font-size: 32px;
  104. line-height: 40px;
  105. letter-spacing: 0px;
  106. vertical-align: middle;
  107. color: var(--color-text-primary);
  108. flex-shrink: 0;
  109. }
  110. .header-actions {
  111. display: flex;
  112. align-items: center;
  113. gap: 16px;
  114. }
  115. .manage-context {
  116. display: flex;
  117. align-items: center;
  118. gap: 16px;
  119. flex-shrink: 0;
  120. }
  121. .manage-context__group {
  122. flex-shrink: 0;
  123. }
  124. .manage-context__group :deep(.filter-label) {
  125. color: var(--color-text-body);
  126. }
  127. .manage-context__group :deep(.filter-select) {
  128. background: var(--color-accent-soft);
  129. border-color: var(--color-accent-border-strong);
  130. }
  131. .manage-context__user {
  132. font-family: var(--font-regular);
  133. font-size: 14px;
  134. line-height: 20px;
  135. color: var(--color-text-body);
  136. white-space: nowrap;
  137. }
  138. .nav-tabs {
  139. height: 38px;
  140. display: flex;
  141. align-items: center;
  142. gap: 16px;
  143. }
  144. .notify-bell {
  145. position: relative;
  146. height: 38px;
  147. padding: 0 14px;
  148. border-radius: var(--radius-card);
  149. border: 1px solid var(--color-accent-border-strong);
  150. background: var(--color-accent-bg);
  151. cursor: pointer;
  152. font-family: var(--font-regular);
  153. font-size: 14px;
  154. color: var(--color-text-body);
  155. display: inline-flex;
  156. align-items: center;
  157. justify-content: center;
  158. flex-shrink: 0;
  159. }
  160. .notify-bell__badge {
  161. position: absolute;
  162. top: 4px;
  163. right: 4px;
  164. min-width: 18px;
  165. height: 18px;
  166. padding: 0 5px;
  167. border-radius: 9px;
  168. background: var(--color-danger);
  169. color: #fff;
  170. font-family: var(--font-medium);
  171. font-size: 11px;
  172. font-weight: 500;
  173. line-height: 18px;
  174. text-align: center;
  175. }
  176. @media screen and (max-width: 1440px) {
  177. .header-wrapper {
  178. width: 100%;
  179. }
  180. .manage-context {
  181. gap: 10px;
  182. }
  183. }
  184. </style>