index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <!-- 顶部栏 -->
  2. <template>
  3. <div
  4. class="w-full bg-[var(--default-bg-color)]"
  5. :class="[
  6. tabStyle === 'tab-card' || tabStyle === 'tab-google' ? 'mb-5 max-sm:mb-3 !bg-box' : ''
  7. ]"
  8. >
  9. <div
  10. class="relative box-border flex-b items-center h-15 leading-15 select-none"
  11. :class="[
  12. tabStyle === 'tab-card' || tabStyle === 'tab-google'
  13. ? 'border-b border-[var(--art-card-border)]'
  14. : ''
  15. ]"
  16. >
  17. <div class="flex-c flex-1 min-w-0 leading-15" style="display: flex">
  18. <!-- 系统信息 -->
  19. <div class="flex-c c-p" @click="toHome" v-if="isTopMenu">
  20. <ArtLogo class="pl-4.5" />
  21. <p v-if="width >= 1400" class="my-0 mx-2 ml-2 text-lg">{{ AppConfig.systemInfo.name }}</p>
  22. </div>
  23. <ArtLogo
  24. class="!hidden pl-3.5 overflow-hidden align-[-0.15em] fill-current"
  25. @click="toHome"
  26. />
  27. <!-- 菜单按钮 -->
  28. <ArtIconButton
  29. v-if="isLeftMenu && shouldShowMenuButton"
  30. icon="ri:menu-2-fill"
  31. class="ml-3 max-sm:ml-[7px]"
  32. @click="visibleMenu"
  33. />
  34. <!-- 刷新按钮 - 已隐藏 -->
  35. <ArtIconButton
  36. v-if="shouldShowRefreshButton"
  37. icon="ri:refresh-line"
  38. class="!ml-3 refresh-btn max-sm:!hidden"
  39. :style="{ marginLeft: !isLeftMenu ? '10px' : '0' }"
  40. @click="reload"
  41. />
  42. <!-- 快速入口 - 已隐藏 -->
  43. <!-- <ArtFastEnter v-if="shouldShowFastEnter && width >= headerBarFastEnterMinWidth">
  44. <ArtIconButton icon="ri:function-line" class="ml-3" />
  45. </ArtFastEnter> -->
  46. <!-- 面包屑 -->
  47. <ArtBreadcrumb
  48. v-if="(shouldShowBreadcrumb && isLeftMenu) || (shouldShowBreadcrumb && isDualMenu)"
  49. />
  50. <!-- 顶部菜单 -->
  51. <ArtHorizontalMenu v-if="isTopMenu" :list="menuList" />
  52. <!-- 混合菜单-顶部 -->
  53. <ArtMixedMenu v-if="isTopLeftMenu" :list="menuList" />
  54. </div>
  55. <div class="flex-c gap-2.5 leading-none">
  56. <!-- 全屏按钮 -->
  57. <ArtIconButton
  58. v-if="shouldShowFullscreen"
  59. :icon="isFullscreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-fill'"
  60. :class="[!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn', 'ml-3']"
  61. class="max-md:!hidden"
  62. @click="toggleFullScreen"
  63. />
  64. <!-- 国际化按钮 - 已隐藏 -->
  65. <!-- <ElDropdown
  66. @command="changeLanguage"
  67. popper-class="langDropDownStyle"
  68. v-if="shouldShowLanguage"
  69. >
  70. <ArtIconButton icon="ri:translate-2" class="language-btn text-[19px]" />
  71. <template #dropdown>
  72. <ElDropdownMenu>
  73. <div v-for="item in languageOptions" :key="item.value" class="lang-btn-item">
  74. <ElDropdownItem
  75. :command="item.value"
  76. :class="{ 'is-selected': locale === item.value }"
  77. >
  78. <span class="menu-txt">{{ item.label }}</span>
  79. <ArtSvgIcon icon="ri:check-fill" v-if="locale === item.value" />
  80. </ElDropdownItem>
  81. </div>
  82. </ElDropdownMenu>
  83. </template>
  84. </ElDropdown> -->
  85. <!-- 通知按钮 - 已隐藏 -->
  86. <!-- <ArtIconButton
  87. v-if="shouldShowNotification"
  88. icon="ri:notification-2-line"
  89. class="notice-button relative"
  90. @click="visibleNotice"
  91. >
  92. <div class="absolute top-2 right-2 size-1.5 !bg-danger rounded-full"></div>
  93. </ArtIconButton> -->
  94. <!-- 聊天按钮 - 已隐藏 -->
  95. <!-- <ArtIconButton
  96. v-if="shouldShowChat"
  97. icon="ri:message-3-line"
  98. class="chat-button relative"
  99. @click="openChat"
  100. >
  101. <div class="breathing-dot absolute top-2 right-2 size-1.5 !bg-success rounded-full"></div>
  102. </ArtIconButton> -->
  103. <!-- 主题切换按钮 - 已隐藏 -->
  104. <!-- <ArtIconButton
  105. v-if="shouldShowThemeToggle"
  106. @click="themeAnimation"
  107. :icon="isDark ? 'ri:sun-fill' : 'ri:moon-line'"
  108. /> -->
  109. <!-- 用户头像、菜单 -->
  110. <ArtUserMenu />
  111. </div>
  112. </div>
  113. <!-- 标签页 -->
  114. <ArtWorkTab />
  115. <!-- 通知组件 - 已隐藏 -->
  116. <!-- <ArtNotification v-model:value="showNotice" ref="notice" /> -->
  117. </div>
  118. </template>
  119. <script setup lang="ts">
  120. import { useI18n } from 'vue-i18n'
  121. import { useRouter } from 'vue-router'
  122. import { storeToRefs } from 'pinia'
  123. import { useFullscreen, useWindowSize } from '@vueuse/core'
  124. import { LanguageEnum, MenuTypeEnum } from '@/enums/appEnum'
  125. import { useSettingStore } from '@/store/modules/setting'
  126. import { useUserStore } from '@/store/modules/user'
  127. import { useMenuStore } from '@/store/modules/menu'
  128. import AppConfig from '@/config'
  129. import { languageOptions } from '@/locales'
  130. import { mittBus } from '@/utils/sys'
  131. import { themeAnimation } from '@/utils/ui/animation'
  132. import { useCommon } from '@/hooks/core/useCommon'
  133. import { useHeaderBar } from '@/hooks/core/useHeaderBar'
  134. import ArtUserMenu from './widget/ArtUserMenu.vue'
  135. defineOptions({ name: 'ArtHeaderBar' })
  136. const router = useRouter()
  137. const { locale } = useI18n()
  138. const { width } = useWindowSize()
  139. const settingStore = useSettingStore()
  140. const userStore = useUserStore()
  141. const menuStore = useMenuStore()
  142. // 顶部栏功能配置
  143. const {
  144. shouldShowMenuButton,
  145. shouldShowRefreshButton,
  146. shouldShowFastEnter,
  147. shouldShowBreadcrumb,
  148. shouldShowFullscreen,
  149. shouldShowNotification,
  150. shouldShowChat,
  151. shouldShowLanguage,
  152. shouldShowThemeToggle,
  153. fastEnterMinWidth: headerBarFastEnterMinWidth
  154. } = useHeaderBar()
  155. const { menuOpen, menuType, isDark, tabStyle } = storeToRefs(settingStore)
  156. const { language } = storeToRefs(userStore)
  157. const { menuList } = storeToRefs(menuStore)
  158. const showNotice = ref(false)
  159. const notice = ref(null)
  160. // 菜单类型判断
  161. const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
  162. const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
  163. const isTopMenu = computed(() => menuType.value === MenuTypeEnum.TOP)
  164. const isTopLeftMenu = computed(() => menuType.value === MenuTypeEnum.TOP_LEFT)
  165. const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
  166. onMounted(() => {
  167. initLanguage()
  168. document.addEventListener('click', bodyCloseNotice)
  169. })
  170. onUnmounted(() => {
  171. document.removeEventListener('click', bodyCloseNotice)
  172. })
  173. /**
  174. * 切换全屏状态
  175. */
  176. const toggleFullScreen = (): void => {
  177. toggleFullscreen()
  178. }
  179. /**
  180. * 切换菜单显示/隐藏状态
  181. */
  182. const visibleMenu = (): void => {
  183. settingStore.setMenuOpen(!menuOpen.value)
  184. }
  185. const { homePath } = useCommon()
  186. const { refresh } = useCommon()
  187. /**
  188. * 跳转到首页
  189. */
  190. const toHome = (): void => {
  191. router.push(homePath.value)
  192. }
  193. /**
  194. * 刷新页面
  195. * @param {number} time - 延迟时间,默认为0毫秒
  196. */
  197. const reload = (time: number = 0): void => {
  198. setTimeout(() => {
  199. refresh()
  200. }, time)
  201. }
  202. /**
  203. * 初始化语言设置
  204. */
  205. const initLanguage = (): void => {
  206. locale.value = language.value
  207. }
  208. /**
  209. * 切换系统语言
  210. * @param {LanguageEnum} lang - 目标语言类型
  211. */
  212. const changeLanguage = (lang: LanguageEnum): void => {
  213. if (locale.value === lang) return
  214. locale.value = lang
  215. userStore.setLanguage(lang)
  216. reload(50)
  217. }
  218. /**
  219. * 点击页面其他区域关闭通知面板
  220. * @param {Event} e - 点击事件对象
  221. */
  222. const bodyCloseNotice = (e: any): void => {
  223. if (!showNotice.value) return
  224. const target = e.target as HTMLElement
  225. // 检查是否点击了通知按钮或通知面板内部
  226. const isNoticeButton = target.closest('.notice-button')
  227. const isNoticePanel = target.closest('.art-notification-panel')
  228. if (!isNoticeButton && !isNoticePanel) {
  229. showNotice.value = false
  230. }
  231. }
  232. /**
  233. * 切换通知面板显示状态
  234. */
  235. const visibleNotice = (): void => {
  236. showNotice.value = !showNotice.value
  237. }
  238. /**
  239. * 打开聊天窗口
  240. */
  241. const openChat = (): void => {
  242. mittBus.emit('openChat')
  243. }
  244. </script>
  245. <style lang="scss" scoped>
  246. /* Custom animations */
  247. @keyframes rotate180 {
  248. 0% {
  249. transform: rotate(0);
  250. }
  251. 100% {
  252. transform: rotate(180deg);
  253. }
  254. }
  255. @keyframes shake {
  256. 0% {
  257. transform: rotate(0);
  258. }
  259. 25% {
  260. transform: rotate(-5deg);
  261. }
  262. 50% {
  263. transform: rotate(5deg);
  264. }
  265. 75% {
  266. transform: rotate(-5deg);
  267. }
  268. 100% {
  269. transform: rotate(0);
  270. }
  271. }
  272. @keyframes expand {
  273. 0% {
  274. transform: scale(1);
  275. }
  276. 50% {
  277. transform: scale(1.1);
  278. }
  279. 100% {
  280. transform: scale(1);
  281. }
  282. }
  283. @keyframes shrink {
  284. 0% {
  285. transform: scale(1);
  286. }
  287. 50% {
  288. transform: scale(0.9);
  289. }
  290. 100% {
  291. transform: scale(1);
  292. }
  293. }
  294. @keyframes moveUp {
  295. 0% {
  296. transform: translateY(0);
  297. }
  298. 50% {
  299. transform: translateY(-3px);
  300. }
  301. 100% {
  302. transform: translateY(0);
  303. }
  304. }
  305. @keyframes breathing {
  306. 0% {
  307. opacity: 0.4;
  308. transform: scale(0.9);
  309. }
  310. 50% {
  311. opacity: 1;
  312. transform: scale(1.1);
  313. }
  314. 100% {
  315. opacity: 0.4;
  316. transform: scale(0.9);
  317. }
  318. }
  319. /* Hover animation classes */
  320. .refresh-btn:hover :deep(.art-svg-icon) {
  321. animation: rotate180 0.5s;
  322. }
  323. .language-btn:hover :deep(.art-svg-icon) {
  324. animation: moveUp 0.4s;
  325. }
  326. .setting-btn:hover :deep(.art-svg-icon) {
  327. animation: rotate180 0.5s;
  328. }
  329. .full-screen-btn:hover :deep(.art-svg-icon) {
  330. animation: expand 0.6s forwards;
  331. }
  332. .exit-full-screen-btn:hover :deep(.art-svg-icon) {
  333. animation: shrink 0.6s forwards;
  334. }
  335. .notice-button:hover :deep(.art-svg-icon) {
  336. animation: shake 0.5s ease-in-out;
  337. }
  338. .chat-button:hover :deep(.art-svg-icon) {
  339. animation: shake 0.5s ease-in-out;
  340. }
  341. /* Breathing animation for chat dot */
  342. .breathing-dot {
  343. animation: breathing 1.5s ease-in-out infinite;
  344. }
  345. /* iPad breakpoint adjustments */
  346. @media screen and (width <= 768px) {
  347. .logo2 {
  348. display: block !important;
  349. }
  350. }
  351. @media screen and (width <= 640px) {
  352. .btn-box {
  353. width: 40px;
  354. }
  355. }
  356. </style>