index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. <template>
  2. <el-container
  3. v-loading="isAppLoadings" class="hc-layout-box"
  4. :class="{ 'yn-theme': !isYunNanProject, 'is-no-layout': !isNullES(isLayout) && isLayout === 'no' }"
  5. >
  6. <div v-if="appTheme === 'dark'" class="hc-app-bg-box">
  7. <img :src="appViewBg" alt="">
  8. </div>
  9. <el-header class="hc-layout-header">
  10. <div v-if="!isYunNanProject" class="hc-layout-header-logo logo-transition" :style="`width: ${isCollapse ? '0px' : '200px'};`" @click="logoClick">
  11. <!-- <img id="logo-icon" :src="appLogoIcon" alt=""> -->
  12. <img v-show="!isCollapse" id="logo-name" :src="appLogoName" alt="" @load="handleImageLoad">
  13. </div>
  14. <div v-else class="hc-layout-header-logo logo-transition" @click="logoClick">
  15. <img id="logo-icon" :src="appLogoIcon" alt="" @load="handleImageLoad">
  16. <!-- <img v-show="!isCollapse" id="logo-name" :src="appLogoName" alt=""> -->
  17. <div class="ml-2">
  18. <div class="text-18px font-900">工程项目档案资料管理系统</div>
  19. <div class="text-12px">Engineering Project File Manegement Platform</div>
  20. </div>
  21. </div>
  22. <div class="header-top-collapse-bar" @click="collapseChange">
  23. <HcIcon v-if="isCollapse" name="menu-unfold" />
  24. <HcIcon v-else name="menu-fold" />
  25. </div>
  26. <div class="header-top-menu-bar">
  27. <HcTopMenuBar v-if="!isYunNanProject" @load="topMenuLoad" @change="topMenuChange" />
  28. </div>
  29. <div class="header-content-bar">
  30. <HcCascader v-if="userRoleId !== website.role_id" @send="cascaderSend" @change="cascaderChange" />
  31. <div v-else class="hc-header-project-name-box" @click="userProjectClick">
  32. <HcIcon name="arrow-right" class="project-icon" />
  33. <div class="truncate">{{ projectInfoData?.projectName ?? '请先选择项目' }}</div>
  34. </div>
  35. <hc-upload-bar v-if="!isYunNanProject" />
  36. <hc-upload-bar v-else style="color:black" />
  37. <HelpInfoBar v-if="!isYunNanProject" />
  38. <ConfigBar v-if="!isYunNanProject" />
  39. <UserInfoBar @load="userInfoLoad" />
  40. </div>
  41. </el-header>
  42. <el-container class="hc-layout-container">
  43. <el-aside v-if="isAsideMenu" class="hc-layout-aside" :class="[isCollapse ? 'is-collapse' : '']" :width="isCollapse ? '90px' : '200px'">
  44. <MenuBar :collapse="isCollapse" :cur="menuBarKey" :datas="menuBarData" :msg-count="msgCount" @change="menuBarChange" />
  45. </el-aside>
  46. <el-main class="hc-layout-main">
  47. <div class="hc-router-menu-bar">
  48. <RouterMenu @load="routerMenuLoad" />
  49. </div>
  50. <div id="hc-main-box" class="hc-main-page">
  51. <div class="hc-main-body">
  52. <router-view v-if="reloadRouter" v-slot="{ Component }">
  53. <transition name="fade-transform">
  54. <keep-alive :max="10" exclude="HcDataV">
  55. <component :is="Component" :msg-count="msgCount" :msg-change="msgChange" />
  56. </keep-alive>
  57. </transition>
  58. </router-view>
  59. </div>
  60. </div>
  61. </el-main>
  62. </el-container>
  63. <hc-reminder v-model="isReminderShow" :text="isReminderText" />
  64. </el-container>
  65. </template>
  66. <script setup>
  67. import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
  68. import { getArrValue, getObjValue, isNullES, useClick } from 'js-fast-way'
  69. import { HcSocket } from '~src/plugins/HcSocket'
  70. import { useRoute, useRouter } from 'vue-router'
  71. import { useAppStore } from '~src/store'
  72. import { initButtons } from '~sto/app'
  73. import { HcAnnouncement } from 'hc-vue3-ui'
  74. import { useProject } from '~sto/useProject'
  75. import website from '~src/config'
  76. import appLogoIcon from '~src/assets/logo/yunnan.png'
  77. //初始组合式
  78. const router = useRouter()
  79. const useRoutes = useRoute()
  80. const store = useAppStore()
  81. const reloadRouter = ref(false)
  82. const userRoleId = ref(store.getRoleId)
  83. const projectInfoData = ref(store.getProjectInfo)
  84. const projectId = ref(store.getProjectId)
  85. // 判断是否为云南项目
  86. const isYunNanProject = ref(projectId.value === '1904814720589430785')
  87. // 添加watch来更新isYunNanProject
  88. watch(() => projectId.value, (newProjectId) => {
  89. isYunNanProject.value = newProjectId === '1904814720589430785'
  90. })
  91. //子组件
  92. import HcTopMenuBar from './modules/HcTopMenu.vue'
  93. import HcCascader from './modules/Cascader.vue'
  94. import UserInfoBar from './modules/UserInfoBar.vue'
  95. import HelpInfoBar from './modules/HelpInfoBar.vue'
  96. import ConfigBar from './modules/ConfigBar.vue'
  97. import RouterMenu from './modules/RouterMenu.vue'
  98. import MenuBar from '~src/layout/modules/MenuBar.vue'
  99. import appViewBg from '~src/assets/view/bg.png'
  100. // logo
  101. const appLogoName = ref(store.getLogoName)
  102. const appTheme = ref(store.getTheme)
  103. //菜单数据
  104. const menuBarKey = ref('')
  105. const menuBarData = ref([])
  106. const isLayout = ref('')
  107. //获取项目信息
  108. const { isAppLoading } = useProject()
  109. // 在 setup 中添加
  110. //渲染完成
  111. onMounted(async () => {
  112. const layout = useRoutes?.query?.layout, layout2 = store.isLayout
  113. isLayout.value = layout ?? layout2
  114. annRefs.value = []
  115. initButtons()
  116. // 确保初始化时加载样式
  117. currentStyle = await loadStyles()
  118. })
  119. //监听layout
  120. watch(() => [useRoutes?.query?.layout, store.isLayout], ([layout, layout2]) => {
  121. isLayout.value = layout ?? layout2
  122. }, { deep: true })
  123. //监听
  124. watch(() => store.getTheme, (theme) => {
  125. appTheme.value = theme
  126. })
  127. //监听项目信息变化
  128. const isAppLoadings = ref(true)
  129. watch(() => isAppLoading.value, (res) => {
  130. if (!website.localModel) {
  131. reloadRouter.value = res
  132. isAppLoadings.value = !res
  133. } else {
  134. if (res) {
  135. setTimeout(() => {
  136. isAppLoadings.value = false
  137. }, 1000)
  138. } else {
  139. isAppLoadings.value = true
  140. }
  141. }
  142. }, { immediate:true })
  143. //路由信息
  144. const routerMenuLoad = ({ key }) => {
  145. menuBarKey.value = key
  146. }
  147. // 是否折叠
  148. const isCollapse = ref(false)
  149. const collapseChange = () => {
  150. const bool = !isCollapse.value
  151. isCollapse.value = bool
  152. store.setCollapse(bool)
  153. }
  154. //顶部菜单导航
  155. const isAsideMenu = ref(true)
  156. const topMenuLoad = () => {
  157. isAsideMenu.value = false
  158. }
  159. // 修改菜单数据的监听逻辑
  160. watch(() => store.getMenus, (val) => {
  161. if (isYunNanProject.value) {
  162. const filterAndRenameMenus = (menus) => {
  163. return menus.map(menu => {
  164. // 创建新对象,而不是修改原对象
  165. const newMenu = { ...menu }
  166. // 过滤掉个人中心和系统设置
  167. if (newMenu.name === '个人中心' || newMenu.name === '系统设置') {
  168. return null
  169. }
  170. // 重命名档案收集为文件管理
  171. if (newMenu.name === '档案收集') {
  172. newMenu.name = '文件管理'
  173. }
  174. // 递归处理子菜单
  175. if (newMenu.children && newMenu.children.length > 0) {
  176. newMenu.children = filterAndRenameMenus(newMenu.children)
  177. }
  178. return newMenu
  179. }).filter(Boolean) // 过滤掉null值
  180. }
  181. menuBarData.value = filterAndRenameMenus(getArrValue(val))
  182. }
  183. }, { immediate: true })
  184. //顶部菜单导航被点击// 修改顶部菜单切换方法
  185. const topMenuChange = (data) => {
  186. if (!isYunNanProject.value && !isNullES(data)) {
  187. menuBarData.value = data
  188. isAsideMenu.value = true
  189. }
  190. }
  191. //菜单被点击
  192. const menuBarChange = ({ code }) => {
  193. menuBarKey.value = code
  194. if (code === 'statistics-datav') {
  195. // 如果跳转到数据看板,记录当前路由作为来源路径
  196. const currentRoute = router.currentRoute.value
  197. router.push({
  198. name: code,
  199. query: {
  200. from: currentRoute.fullPath,
  201. },
  202. })
  203. } else {
  204. router.push({ name: code })
  205. }
  206. }
  207. //消息数量
  208. const msgCount = ref({
  209. allCount: 0,
  210. taskCount: 0,
  211. messageCount: 0,
  212. messageCount_1: 0,
  213. messageCount_2: 0,
  214. messageCount_3: 0,
  215. messageCount_4: 0,
  216. messageCount_5: 0,
  217. })
  218. const msgChange = ref(0)
  219. //用户信息
  220. const userId = ref('')
  221. const userInfoLoad = ({ user_id }) => {
  222. userId.value = user_id
  223. }
  224. //项目合同段的ID
  225. let socket
  226. const cascaderSend = async ({ projectId, contractId }) => {
  227. await useClick()
  228. if (isNullES(contractId)) {
  229. //本地模式
  230. if (website.localModel) {
  231. window.$message?.error('项目信息不存在,请联系管理员')
  232. reloadRouter.value = false
  233. }
  234. return
  235. }
  236. //本地模式
  237. if (website.localModel) {
  238. setTimeout(() => {
  239. reloadRouter.value = true
  240. }, 1000)
  241. } else {
  242. reloadRouter.value = true
  243. }
  244. //链接webSocket
  245. if (!isNullES(socket)) socket.close()
  246. socket = new HcSocket({ projectId, contractId, userId: userId.value }, (res) => {
  247. socketData(res?.data)
  248. })
  249. }
  250. //长链接消息
  251. let annUpdateRef
  252. const annRefs = ref([])
  253. const isReminderShow = ref(false)
  254. const isReminderText = ref('')
  255. const socketData = async (res) => {
  256. console.log('socket:', res)
  257. const { type, data } = getObjValue(res)
  258. if (type === 'msgUpdateMsg') {
  259. closeAnnUpdate()
  260. //内容为空时,代表公告已经取消,由于前面已经关闭,所以不再创建
  261. if (isNullES(data)) return
  262. await nextTick()
  263. //系统更新公告,直接替换
  264. annUpdateRef = await HcAnnouncement({ type: 'update', data: data })
  265. } else if (type === 'msgSystemMsg') {
  266. //内容为空时,代表公告已经取消,由于前面已经关闭,所以不再创建
  267. if (isNullES(data)) {
  268. closeAnnFun()
  269. return
  270. }
  271. await nextTick()
  272. //普通公告,追加公告
  273. const ref = await HcAnnouncement({ type: 'system', data: data })
  274. annRefs.value.push(ref)
  275. } else if (type === 'msgLink') {
  276. if (store.isLogin) {
  277. socket.send('getMsg')
  278. store.setIsLogin(false)
  279. }
  280. } else if (type === 'msgCountDown') {
  281. //倒计时更新
  282. if (isNullES(data) || data <= 0) {
  283. closeReminder()
  284. return
  285. }
  286. isReminderText.value = `系统将在${data}秒后,进行更新`
  287. setReminderText(data - 1)
  288. isReminderShow.value = true
  289. }
  290. }
  291. //倒计时
  292. let timeRef
  293. let startTime
  294. const setReminderText = (totalTime) => {
  295. startTime = performance.now()
  296. const step = () => {
  297. const elapsedTime = Math.floor((performance.now() - startTime) / 1000)
  298. const remainingTime = totalTime - elapsedTime
  299. if (remainingTime < 0) {
  300. closeReminder()
  301. return
  302. }
  303. isReminderText.value = `系统将在${remainingTime}秒后,进行更新`
  304. requestAnimationFrame(step)
  305. }
  306. requestAnimationFrame(step)
  307. }
  308. //关闭倒计时
  309. const closeReminder = () => {
  310. isReminderShow.value = false
  311. if (!isNullES(timeRef)) {
  312. clearTimeout(timeRef)
  313. timeRef = null
  314. }
  315. }
  316. // 项目切换
  317. const cascaderChange = () => {
  318. reloadRouter.value = false
  319. }
  320. //首页
  321. const logoClick = () => {
  322. router.push({ name: 'statistics-datav' })
  323. }
  324. const userProjectClick = () => {
  325. router.push({ path: '/user/project' })
  326. }
  327. //关闭普通公告
  328. const closeAnnFun = () => {
  329. const refs = annRefs.value
  330. for (let i = 0; i < refs.length; i++) {
  331. if (!isNullES(refs[i])) {
  332. refs[i]?.close()
  333. }
  334. }
  335. annRefs.value = []
  336. }
  337. //关闭系统更新公告
  338. const closeAnnUpdate = () => {
  339. if (!isNullES(annUpdateRef)) {
  340. annUpdateRef.close()
  341. annUpdateRef = null
  342. }
  343. }
  344. //页面卸载
  345. onUnmounted(() => {
  346. if (!isNullES(socket)) socket.close()
  347. closeAnnFun()
  348. closeAnnUpdate()
  349. return () => {
  350. if (currentStyle) {
  351. document.head.removeChild(currentStyle)
  352. }
  353. }
  354. })
  355. // 动态加载样式
  356. const loadStyles = async () => {
  357. try {
  358. // 1. 首先移除所有已存在的样式
  359. const existingStyles = document.head.querySelectorAll('style[data-theme]')
  360. existingStyles.forEach(style => {
  361. document.head.removeChild(style)
  362. })
  363. // 2. 创建新的样式元素
  364. const styleElement = document.createElement('style')
  365. styleElement.type = 'text/css'
  366. // 添加标识,方便后续清理
  367. styleElement.setAttribute('data-theme', isYunNanProject.value ? 'yunnan' : 'default')
  368. // 3. 加载对应的样式模块
  369. const module = isYunNanProject.value
  370. ? await import('./test-yn/index-yn.scss?inline')
  371. : await import('./index.scss?inline')
  372. styleElement.textContent = module.default
  373. document.head.appendChild(styleElement)
  374. currentStyle = styleElement
  375. // 4. 强制重新计算样式
  376. document.documentElement.style.display = 'none'
  377. document.documentElement.offsetHeight // 触发重排
  378. document.documentElement.style.display = ''
  379. return styleElement
  380. } catch (error) {
  381. console.error('加载样式失败:', error)
  382. return null
  383. }
  384. }
  385. let currentStyle = null
  386. const imageLoaded = ref(false)
  387. const handleImageLoad = () => {
  388. imageLoaded.value = true
  389. }
  390. // 监听项目类型变化,动态切换样式
  391. watch(() => isYunNanProject.value, async (newVal) => {
  392. imageLoaded.value = false
  393. await loadStyles()
  394. console.log('样式已更新:', newVal ? 'yunnan' : 'default')
  395. }, { immediate: true })
  396. // 添加watch来更新isYunNanProject
  397. watch(() =>store.getProjectId, (newProjectId) => {
  398. isYunNanProject.value = newProjectId === '1904814720589430785'
  399. //重新加载菜单
  400. let val = store.getMenus
  401. if (!isYunNanProject.value) {
  402. return
  403. }
  404. const filterAndRenameMenus = (menus) => {
  405. return menus.map(menu => {
  406. // 创建新对象,而不是修改原对象
  407. const newMenu = { ...menu }
  408. // 过滤掉个人中心和系统设置
  409. if (newMenu.name === '个人中心' || newMenu.name === '系统设置') {
  410. return null
  411. }
  412. // 重命名档案收集为文件管理
  413. if (newMenu.name === '档案收集') {
  414. newMenu.name = '文件管理'
  415. }
  416. // 递归处理子菜单
  417. if (newMenu.children && newMenu.children.length > 0) {
  418. newMenu.children = filterAndRenameMenus(newMenu.children)
  419. }
  420. return newMenu
  421. }).filter(Boolean) // 过滤掉null值
  422. }
  423. menuBarData.value = filterAndRenameMenus(getArrValue(val))
  424. isAsideMenu.value = true
  425. })
  426. // 组件卸载时清理样式
  427. </script>
  428. <style lang="scss" scoped>
  429. .logo-transition {
  430. transition: all 0.3s ease;
  431. img {
  432. max-height: 28px;
  433. transition: all 0.3s ease;
  434. will-change: transform;
  435. }
  436. &.hc-layout-header-logo {
  437. display: flex;
  438. align-items: center;
  439. height: 100%;
  440. overflow: hidden;
  441. }
  442. }
  443. // 设置图片初始状态
  444. #logo-name, #logo-icon {
  445. height: 28px;
  446. width: auto;
  447. object-fit: contain;
  448. }
  449. </style>