index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <div class="header-bar">
  3. <div class="header-bar__menu">
  4. <div v-for="(item, index) in menu" :key="index" class="header-bar__menu-item">
  5. <div
  6. v-if="!item.children || item.children.length === 0"
  7. @click="getItemClick(item)"
  8. class="header-bar__menu-item-content flex"
  9. >
  10. <img v-if="getItemIcon(item)" :src="getItemIcon(item)" class="header-bar__menu-icon" />
  11. <span class="header-bar__menu-name">{{ getItemName(item) }}</span>
  12. </div>
  13. <div v-else class="header-bar__submenu">
  14. <div
  15. class="header-bar__submenu-header"
  16. @click.stop="toggleSubmenu(index)"
  17. >
  18. <img v-if="getItemIcon(item)" :src="getItemIcon(item)" class="header-bar__menu-icon" />
  19. <span class="header-bar__menu-name">{{ getItemName(item) }}</span>
  20. </div>
  21. <!-- 二级菜单 -->
  22. <div
  23. class="header-bar__submenu-body"
  24. :class="{ 'submenu-open': submenuOpen[index] }"
  25. >
  26. <div
  27. v-for="(child, childIndex) in item.children"
  28. :key="childIndex"
  29. class="header-bar__submenu-item"
  30. @click="getItemClick(child)"
  31. >
  32. <div
  33. v-if="!child.children || child.children.length === 0"
  34. class="header-bar__submenu-item-content flex left"
  35. >
  36. <img v-if="getItemIcon(child)" :src="getItemIcon(child)" class="header-bar__menu-icon" />
  37. <span class="header-bar__menu-name">{{ getItemName(child) }}</span>
  38. </div>
  39. <div v-else class="header-bar__submenu-third-level">
  40. <div
  41. class="header-bar__submenu-third-header"
  42. @click.stop="toggleThirdLevel(index, childIndex)"
  43. >
  44. <img v-if="getItemIcon(child)" :src="getItemIcon(child)" class="header-bar__menu-icon" />
  45. <span class="header-bar__menu-name">{{ getItemName(child) }}</span>
  46. </div>
  47. <!-- 三级菜单 -->
  48. <div
  49. class="header-bar__submenu-third-body"
  50. :class="{ 'submenu-open': thirdLevelOpen[`${index}-${childIndex}`] }"
  51. >
  52. <div
  53. v-for="(grandChild, grandChildIndex) in child.children"
  54. :key="grandChildIndex"
  55. class="header-bar__submenu-third-item"
  56. @click="getItemClick(grandChild)"
  57. >
  58. <img v-if="getItemIcon(grandChild)" :src="getItemIcon(grandChild)" class="header-bar__menu-icon" />
  59. <span class="header-bar__menu-name">{{ getItemName(grandChild) }}</span>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. <!-- 设备状态按钮 -->
  68. <div
  69. v-if="showDeviceStatus"
  70. class="header-bar__menu-item"
  71. @click="openDeviceStatus"
  72. title="设备状态"
  73. >
  74. <div class="header-bar__menu-item-content flex">
  75. <span class="header-bar__menu-name">设备状态</span>
  76. </div>
  77. </div>
  78. <!-- 设备状态按钮 -->
  79. <!-- <div
  80. v-if="showDeviceStatus"
  81. class="header-bar__menu-item"
  82. @click="OneClickRelease"
  83. title="一键释放"
  84. >
  85. <div class="header-bar__menu-item-content flex">
  86. <span class="header-bar__menu-name">一键释放</span>
  87. </div>
  88. </div>-->
  89. </div>
  90. <div class="header-bar__title">
  91. <span class="header-bar__text">
  92. <slot name="title">{{ title }}</slot>
  93. </span>
  94. </div>
  95. <div class="header-bar__buttons">
  96. <!-- 版本信息 - 始终显示 -->
  97. <div class="header-bar__button header-bar__button__version">
  98. <span class="version-text" @click="openOTA" title="点击查看版本详情">
  99. 当前版本:{{ currentVersion }}
  100. </span>
  101. </div>
  102. <!-- 使用手册 - 始终显示 -->
  103. <div class="header-bar__button header-bar__button__manual">
  104. <span class="manual-text" @click="openManual" title="点击查看使用手册">
  105. 使用手册
  106. </span>
  107. </div>
  108. <!-- 用户信息 - 仅在需要时显示 -->
  109. <div class="header-bar__button header-bar__button__user" v-if="showUser">
  110. <el-dropdown>
  111. <span class="el-dropdown-link">
  112. {{ (useUserInfoStore.userInfo as any).account_name || (useUserInfoStore.userInfo as any).real_name || (useUserInfoStore.userInfo as any).login_name }}
  113. </span>
  114. <template #dropdown>
  115. <el-dropdown-menu>
  116. <el-dropdown-item @click="loginOut">退出登录</el-dropdown-item>
  117. </el-dropdown-menu>
  118. </template>
  119. </el-dropdown>
  120. </div>
  121. </div>
  122. </div>
  123. <!-- 设备状态日志弹窗 -->
  124. <DeviceStatusDialog />
  125. <div class="header-bar_blank"></div>
  126. </template>
  127. <script setup lang="ts">
  128. import {defineProps, reactive, onMounted, onUnmounted, ref} from 'vue'
  129. import useUserInfo from '@/stores/modules/user'
  130. import { useRouter } from 'vue-router'
  131. import iconsz from './assets/shezhi@2x.png'
  132. import iconykq from './assets/yaokong@2x.png'
  133. import gengxin from './assets/gengxin.svg'
  134. import icpList from '@/utils/ipc'
  135. import { getRouterUrl } from '@/utils/appfun'
  136. import client from '@/stores/modules/client'
  137. import packageJson from '@/../../package.json';
  138. import { getCameraMachineDoc } from '@/apis/other';
  139. import useDeviceStatusStore from '@/stores/modules/deviceStatus';
  140. import DeviceStatusDialog from '@/components/device-status-dialog/index.vue';
  141. import socket from "@/stores/modules/socket";
  142. import { ElMessageBox } from 'element-plus'
  143. // 设备状态 store
  144. const deviceStatusStore = useDeviceStatusStore();
  145. // 设备状态图标
  146. const deviceStatusIcon = ref(iconsz);
  147. // 是否显示设备状态按钮
  148. /*const showDeviceStatus = ref(true);*/
  149. const clientStore = client()
  150. const useUserInfoStore = useUserInfo()
  151. const socketStore = socket()
  152. const currentVersion = ref(packageJson.version);
  153. // 定义 menu 项的类型
  154. interface MenuItem {
  155. name?: string //名称
  156. icon?: string // 图标
  157. click?: () => void // 点击事件
  158. type?: keyof typeof menuType // 类型 menuType里面的值
  159. children?: MenuItem[] // 支持嵌套菜单
  160. }
  161. // 定义 props
  162. const props = defineProps({
  163. title: {
  164. type: String,
  165. required: true
  166. },
  167. menu: {
  168. type: Array as () => MenuItem[],
  169. default: () => []
  170. },
  171. showUser: {
  172. type: Boolean,
  173. default: false
  174. },
  175. showDeviceStatus: {
  176. type: Boolean,
  177. default: false // 默认显示设备状态按钮
  178. }
  179. })
  180. const Router = useRouter()
  181. const submenuOpen = reactive<{ [key: number]: boolean }>({})
  182. const thirdLevelOpen = reactive<{ [key: string]: boolean }>({})
  183. const toggleSubmenu = (index: number) => {
  184. submenuOpen[index] = !submenuOpen[index]
  185. }
  186. const toggleThirdLevel = (parentIndex: number, childIndex: number) => {
  187. const key = `${parentIndex}-${childIndex}`
  188. thirdLevelOpen[key] = !thirdLevelOpen[key]
  189. }
  190. const menuType = reactive({
  191. setting: {
  192. name: '设置',
  193. icon: iconsz,
  194. click: openSetting
  195. },
  196. remoteControl: {
  197. name: '模拟遥控器',
  198. icon: iconykq,
  199. click: openRemoteControl
  200. },
  201. OneClickRelease: {
  202. name: '一键释放',
  203. click: OneClickRelease
  204. },
  205. developer: {
  206. name: '初始设备调频设置',
  207. icon: iconsz,
  208. click: openDeveloper
  209. },
  210. ota: {
  211. name: '当前版本:'+currentVersion.value,
  212. click: openOTA
  213. }
  214. })
  215. function getItemClick(item: MenuItem) {
  216. const menuItem = item.type ? { ...menuType[item.type], ...item } : item
  217. if (menuItem && menuItem.click) {
  218. menuItem.click()
  219. }
  220. // 关闭所有展开的一级和三级菜单
  221. for (const key in submenuOpen) {
  222. submenuOpen[key] = false
  223. }
  224. for (const key in thirdLevelOpen) {
  225. thirdLevelOpen[key] = false
  226. }
  227. }
  228. function getItemName(item: MenuItem) {
  229. const menuItem = item.type ? { ...menuType[item.type], ...item } : item
  230. return menuItem.name
  231. }
  232. function getItemIcon(item: MenuItem) {
  233. const menuItem = item.type ? { ...menuType[item.type], ...item } : item
  234. return menuItem.icon
  235. }
  236. function openSetting() {
  237. const { href } = Router.resolve({
  238. name: 'setting',
  239. query: {
  240. type: 0
  241. }
  242. })
  243. clientStore.ipc.removeAllListeners(icpList.utils.openMain)
  244. let params = {
  245. title: '设置',
  246. width: 3840,
  247. height: 2160,
  248. frame: true,
  249. id: 'seeting',
  250. url: getRouterUrl(href)
  251. }
  252. clientStore.ipc.send(icpList.utils.openMain, params)
  253. }
  254. function openRemoteControl() {
  255. const { href } = Router.resolve({
  256. name: 'RemoteControl'
  257. })
  258. clientStore.ipc.removeAllListeners(icpList.utils.openMain)
  259. let params = {
  260. title: '模拟遥控器',
  261. width: 350,
  262. height: 600,
  263. frame: true,
  264. id: 'RemoteControl',
  265. url: getRouterUrl(href)
  266. }
  267. clientStore.ipc.send(icpList.utils.openMain, params)
  268. }
  269. function openDeveloper() {
  270. const { href } = Router.resolve({
  271. name: 'developer'
  272. })
  273. clientStore.ipc.removeAllListeners(icpList.utils.openMain)
  274. let params = {
  275. title: '初始设备调频设置',
  276. width: 900,
  277. height: 700,
  278. frame: true,
  279. id: 'developer',
  280. url: getRouterUrl(href)
  281. }
  282. clientStore.ipc.send(icpList.utils.openMain, params)
  283. }
  284. function openOTA() {
  285. const { href } = Router.resolve({
  286. name: 'ota'
  287. })
  288. clientStore.ipc.removeAllListeners(icpList.utils.openMain)
  289. let params = {
  290. title: '版本更新',
  291. width: 900,
  292. height: 700,
  293. frame: true,
  294. id: 'ota',
  295. url: getRouterUrl(href)
  296. }
  297. clientStore.ipc.send(icpList.utils.openMain, params)
  298. }
  299. async function openManual() {
  300. try {
  301. const response = await getCameraMachineDoc({
  302. machine_type: 0
  303. });
  304. if (response.data && response.data.url) {
  305. // 使用系统默认浏览器打开URL
  306. clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
  307. let params = {
  308. action: 'openExternal',
  309. params: response.data.url
  310. };
  311. clientStore.ipc.send(icpList.utils.shellFun, params);
  312. } else {
  313. console.error('获取使用手册URL失败');
  314. }
  315. } catch (error) {
  316. console.error('获取使用手册失败:', error);
  317. }
  318. }
  319. function loginOut() {
  320. useUserInfoStore.loginOut({})
  321. useUserInfoStore.updateLoginShow(true)
  322. }
  323. // 打开设备状态弹窗
  324. function openDeviceStatus() {
  325. deviceStatusStore.openDialog();
  326. }
  327. //一键释放
  328. async function OneClickRelease(){
  329. await ElMessageBox.confirm(
  330. `确定要一键释放设备吗?`,
  331. '提示',
  332. {
  333. confirmButtonText: '确定',
  334. cancelButtonText: '取消',
  335. type: 'warning',
  336. },
  337. )
  338. socketStore.sendMessage({
  339. type: 'oneclick_release',
  340. data:"oneclick_release"
  341. })
  342. }
  343. onMounted(() => {
  344. document.addEventListener('click', handleOutsideClick);
  345. // 初始化设备状态监听
  346. deviceStatusStore.initListener();
  347. })
  348. onUnmounted(() => {
  349. document.removeEventListener('click', handleOutsideClick);
  350. // 清理监听
  351. deviceStatusStore.cleanup();
  352. })
  353. function handleOutsideClick(event: MouseEvent) {
  354. const menuElement = document.querySelector('.header-bar__menu')
  355. if (menuElement && !menuElement.contains(event.target as Node)) {
  356. // 关闭所有一级菜单
  357. for (const key in submenuOpen) {
  358. submenuOpen[key] = false
  359. }
  360. // 关闭所有三级菜单
  361. for (const key in thirdLevelOpen) {
  362. thirdLevelOpen[key] = false
  363. }
  364. }
  365. }
  366. </script>
  367. <style lang="scss" scoped>
  368. .header-bar_blank {
  369. width: 100%;
  370. height: 30px;
  371. }
  372. .header-bar {
  373. position: fixed;
  374. app-drag: drag;
  375. -webkit-app-region: drag;
  376. top: 0;
  377. width: 100%;
  378. height: 30px;
  379. z-index: 100000;
  380. background: #fff;
  381. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  382. }
  383. .header-bar__menu {
  384. position: absolute;
  385. top: 0;
  386. left: 0;
  387. display: flex;
  388. align-items: center;
  389. }
  390. .header-bar__menu-item {
  391. display: flex;
  392. justify-content: center;
  393. align-items: center;
  394. padding: 2px 10px;
  395. min-height: 30px;
  396. cursor: pointer;
  397. &:hover {
  398. background: #dcdcdc;
  399. }
  400. }
  401. .header-bar__menu-icon {
  402. width: 16px;
  403. height: 16px;
  404. margin-right: 5px;
  405. }
  406. .header-bar__menu-name {
  407. font-size: 14px;
  408. }
  409. .header-bar__title {
  410. position: absolute;
  411. top: 0;
  412. left: 50%;
  413. transform: translateX(-50%);
  414. display: flex;
  415. height: 30px;
  416. line-height: 30px;
  417. align-items: center;
  418. justify-content: center;
  419. }
  420. .header-bar__logo {
  421. width: 30px;
  422. height: 30px;
  423. margin-right: 10px;
  424. }
  425. .header-bar__text {
  426. font-weight: 600;
  427. font-size: 14px;
  428. color: #474747;
  429. }
  430. .header-bar__buttons {
  431. position: absolute;
  432. top: 0;
  433. right: 0;
  434. display: flex;
  435. align-items: center;
  436. }
  437. .header-bar__button {
  438. min-width: 30px;
  439. height: 30px;
  440. border: none;
  441. cursor: pointer;
  442. display: flex;
  443. align-items: center;
  444. justify-content: center;
  445. }
  446. .header-bar__button__user {
  447. padding: 0 10px;
  448. }
  449. .header-bar__button__version {
  450. padding: 0 8px;
  451. margin-right: 5px;
  452. }
  453. .version-text {
  454. font-size: 12px;
  455. color: #666;
  456. cursor: pointer;
  457. padding: 2px 6px;
  458. border-radius: 3px;
  459. transition: all 0.2s ease;
  460. }
  461. .header-bar__button__manual {
  462. padding: 0 8px;
  463. margin-right: 5px;
  464. }
  465. .manual-text {
  466. font-size: 12px;
  467. color: #666;
  468. cursor: pointer;
  469. padding: 2px 6px;
  470. border-radius: 3px;
  471. transition: all 0.2s ease;
  472. }
  473. .header-bar__button:hover {
  474. background-color: #e0e0e0;
  475. }
  476. .header-bar__button-icon {
  477. width: 16px;
  478. height: 16px;
  479. }
  480. .header-bar__submenu {
  481. position: relative;
  482. display: flex;
  483. flex-direction: column;
  484. }
  485. .header-bar__submenu-header {
  486. display: flex;
  487. align-items: center;
  488. padding: 2px 10px;
  489. cursor: pointer;
  490. }
  491. .header-bar__submenu-body {
  492. position: absolute;
  493. top: 100%;
  494. left: 0;
  495. background: white;
  496. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  497. z-index: 1000;
  498. min-width: 160px;
  499. border-radius: 4px;
  500. overflow: hidden;
  501. display: none;
  502. margin-top: 5px;
  503. &.submenu-open {
  504. display: block;
  505. }
  506. }
  507. .header-bar__submenu-item {
  508. display: flex;
  509. align-items: center;
  510. padding: 6px 12px;
  511. white-space: nowrap;
  512. &:hover {
  513. background-color: #f0f0f0;
  514. }
  515. }
  516. .header-bar__submenu-third-level {
  517. position: relative;
  518. display: flex;
  519. flex-direction: column;
  520. }
  521. .header-bar__submenu-third-header {
  522. display: flex;
  523. align-items: center;
  524. cursor: pointer;
  525. &:hover {
  526. background-color: #f0f0f0;
  527. }
  528. }
  529. .header-bar__submenu-third-body {
  530. position: fixed;
  531. margin-left: 150px;
  532. top: 0;
  533. left: 100%;
  534. background: white;
  535. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  536. z-index: 1000;
  537. min-width: 160px;
  538. border-radius: 4px;
  539. overflow: hidden;
  540. display: none;
  541. &.submenu-open {
  542. display: block;
  543. }
  544. }
  545. </style>