index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. <template>
  2. <div class="big-screen-home">
  3. <!-- 顶部栏 -->
  4. <header class="header-bar">
  5. <div class="header-left">
  6. <span class="time-text">{{ currentTime }}</span>
  7. </div>
  8. <h1 class="system-title">沅陵县城市地下管网管控平台</h1>
  9. <div class="header-right">
  10. <span class="welcome-text">欢迎,<span class="username">浏览账号</span></span>
  11. <button class="header-btn" @click="enterBackend">进入后台</button>
  12. <button class="header-btn" @click="logout">退出系统</button>
  13. </div>
  14. </header>
  15. <!-- 功能模块入口(3D椭圆旋转环) -->
  16. <div class="ring-3d-container">
  17. <div
  18. class="module-item-3d"
  19. v-for="(item, index) in modules"
  20. :key="index"
  21. :style="moduleStyles[index]"
  22. @click="handleModuleClick(item)"
  23. @mouseenter="pauseRotation"
  24. @mouseleave="resumeRotation"
  25. >
  26. <div class="icon-wrapper">
  27. <div class="icon-circle">
  28. <i :class="item.iconClass" class="module-icon"></i>
  29. </div>
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script setup>
  36. import { ref, reactive, onMounted, onUnmounted } from 'vue'
  37. import { useRouter } from 'vue-router'
  38. import { DEFAULT_BACKEND_HOME } from '@/router'
  39. import useUserStore from '@/store/modules/user'
  40. import usePermissionStore from '@/store/modules/permission'
  41. import { ElMessageBox } from 'element-plus'
  42. const router = useRouter()
  43. const userStore = useUserStore()
  44. const permissionStore = usePermissionStore()
  45. const currentTime = ref('')
  46. let timer = null
  47. let rotationInterval = null
  48. let animationFrame = null
  49. let isPaused = ref(false)
  50. // 椭圆轨迹参数
  51. const radiusX = 500
  52. const radiusY = 100
  53. const modulesCount = ref(7)
  54. // 连续旋转核心变量
  55. let currentAngle = ref(0)
  56. const ANIMATION_DURATION = 500
  57. const ROTATE_INTERVAL = 5000
  58. const STEP_ANGLE = (2 * Math.PI) / 7
  59. // 动态模块(从路由获取)
  60. const modules = ref([])
  61. // 【优化1】缓存模块样式,避免频繁计算
  62. const moduleStyles = reactive({})
  63. // 【优化2】预计算三角函数值缓存
  64. const sinCache = new Map()
  65. const cosCache = new Map()
  66. const getCachedSin = (angle) => {
  67. const key = Math.round(angle * 1000)
  68. if (!sinCache.has(key)) {
  69. sinCache.set(key, Math.sin(angle))
  70. }
  71. return sinCache.get(key)
  72. }
  73. const getCachedCos = (angle) => {
  74. const key = Math.round(angle * 1000)
  75. if (!cosCache.has(key)) {
  76. cosCache.set(key, Math.cos(angle))
  77. }
  78. return cosCache.get(key)
  79. }
  80. // 【优化3】批量更新所有模块样式
  81. const updateAllModuleStyles = () => {
  82. const count = modulesCount.value
  83. for (let i = 0; i < count; i++) {
  84. const offsetAngle = (3 * Math.PI / 2) + (i * 2 * Math.PI) / count
  85. const totalAngle = currentAngle.value + offsetAngle
  86. // 使用缓存的计算
  87. const cosVal = getCachedCos(totalAngle)
  88. const sinVal = getCachedSin(totalAngle)
  89. const x = cosVal * radiusX
  90. const y = sinVal * radiusY
  91. const scale = 0.7 + (sinVal + 1) / 2 * 0.6
  92. const opacity = 0.6 + (sinVal + 1) / 2 * 0.4
  93. const zIndex = Math.floor(sinVal * 10) + 10
  94. moduleStyles[i] = {
  95. transform: `translate(calc(-50% + ${x}px), calc(10% + ${y}px)) scale(${scale})`,
  96. margin:'auto',
  97. opacity: opacity,
  98. zIndex: zIndex,
  99. position: 'absolute',
  100. left: '50%',
  101. top: '50%'
  102. }
  103. }
  104. }
  105. // 平滑旋转动画
  106. let startAngle = 0
  107. let targetAngle = 0
  108. let animationStartTime = 0
  109. const startRotateAnimation = () => {
  110. if (isPaused.value) return
  111. startAngle = currentAngle.value
  112. targetAngle = currentAngle.value + STEP_ANGLE
  113. animationStartTime = performance.now()
  114. animate()
  115. }
  116. const animate = () => {
  117. if (isPaused.value) return
  118. const now = performance.now()
  119. let progress = (now - animationStartTime) / ANIMATION_DURATION
  120. progress = Math.min(progress, 1)
  121. // 缓动函数:更丝滑
  122. const t = progress === 1 ? 1 : 1 - Math.pow(1 - progress, 3)
  123. currentAngle.value = startAngle + (targetAngle - startAngle) * t
  124. // 【优化4】只在角度变化时更新样式
  125. updateAllModuleStyles()
  126. if (progress < 1) {
  127. animationFrame = requestAnimationFrame(animate)
  128. }
  129. }
  130. // 暂停 / 恢复
  131. const pauseRotation = () => {
  132. isPaused.value = true
  133. }
  134. const resumeRotation = () => {
  135. isPaused.value = false
  136. // 恢复时立即开始新的旋转动画
  137. if (rotationInterval) {
  138. clearInterval(rotationInterval)
  139. rotationInterval = setInterval(() => {
  140. if (!isPaused.value) startRotateAnimation()
  141. }, ROTATE_INTERVAL)
  142. }
  143. }
  144. // ==============================================
  145. // 从文件1同步:更新时间
  146. // ==============================================
  147. const updateTime = () => {
  148. const now = new Date()
  149. const weekArr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  150. const year = now.getFullYear()
  151. const month = String(now.getMonth() + 1).padStart(2, '0')
  152. const day = String(now.getDate()).padStart(2, '0')
  153. const hours = String(now.getHours()).padStart(2, '0')
  154. const minutes = String(now.getMinutes()).padStart(2, '0')
  155. const seconds = String(now.getSeconds()).padStart(2, '0')
  156. const week = weekArr[now.getDay()]
  157. currentTime.value = `${year}/${month}/${day} ${hours}:${minutes}:${seconds} ${week}`
  158. }
  159. // ==============================================
  160. // 从文件1同步:路由处理
  161. // ==============================================
  162. function updateParentPath(routes) {
  163. return routes.map(parent => {
  164. const homeRoute = parent.children?.find(item => item.meta?.title === '首页')
  165. if (homeRoute) {
  166. return {
  167. ...parent,
  168. path: parent.path + '/' + homeRoute.path
  169. }
  170. }
  171. return { ...parent }
  172. })
  173. }
  174. // ==============================================
  175. // 加载动态驾驶舱
  176. // ==============================================
  177. const loadDynamicModules = () => {
  178. const subSystemRoutes = permissionStore.sidebarRouters.filter(route =>
  179. route.name
  180. )
  181. const newRoutes = updateParentPath(subSystemRoutes)
  182. const iconList = ['icon-operation', 'icon-sewage', 'icon-bi', 'icon-water', 'icon-flood', 'icon-123','icon-ps']
  183. const res = newRoutes.map((item, i) => ({
  184. name: item.meta?.title || '驾驶舱',
  185. iconClass: iconList[i] || 'icon-flood',
  186. path: item.path || '/'
  187. }))
  188. while (res.length < 6) {
  189. res.push({
  190. name: `驾驶舱${res.length + 1}`,
  191. iconClass: 'icon-flood',
  192. path: '/'
  193. })
  194. }
  195. modules.value = res.slice(0, 7)
  196. modulesCount.value = modules.value.length
  197. updateAllModuleStyles()
  198. }
  199. // ==============================================
  200. // 进入后台
  201. // ==============================================
  202. const enterBackend = () => {
  203. router.push(DEFAULT_BACKEND_HOME)
  204. }
  205. // ==============================================
  206. // 退出系统(与 Navbar 保持一致)
  207. // ==============================================
  208. const logout = () => {
  209. ElMessageBox.confirm('确定要退出登录吗?', '提示', {
  210. confirmButtonText: '确定',
  211. cancelButtonText: '取消',
  212. type: 'warning'
  213. }).then(() => {
  214. // Store 中的方法名是 logOut,logout() 不存在,调用会静默失败导致没有真正退出
  215. userStore.logOut().then(() => {
  216. // 整页跳转回登录页,刷新路由实例以清除已动态注册的路由
  217. location.href = '/login'
  218. })
  219. }).catch(() => {})
  220. }
  221. // ==============================================
  222. // 点击驾驶舱
  223. // ==============================================
  224. const handleModuleClick = (item) => {
  225. if (item.path) router.push(item.path)
  226. // let ip = "192.168.110.147";
  227. // let port = 81;
  228. // localStorage.removeItem("routeTitle");
  229. // localStorage.setItem("routeTitle", item.name);
  230. // window.location.replace(`http://${ip}:${port}?username=${encodeURIComponent(item.name)}`);
  231. };
  232. // ==============================================
  233. // 生命周期
  234. // ==============================================
  235. onMounted(() => {
  236. updateTime()
  237. timer = setInterval(updateTime, 1000)
  238. loadDynamicModules()
  239. // 启动旋转定时器
  240. rotationInterval = setInterval(() => {
  241. if (!isPaused.value) startRotateAnimation()
  242. }, ROTATE_INTERVAL)
  243. // 初始样式
  244. updateAllModuleStyles()
  245. })
  246. onUnmounted(() => {
  247. clearInterval(timer)
  248. if (rotationInterval) clearInterval(rotationInterval)
  249. if (animationFrame) cancelAnimationFrame(animationFrame)
  250. // 清理缓存
  251. sinCache.clear()
  252. cosCache.clear()
  253. })
  254. </script>
  255. <style scoped lang="scss">
  256. /* 样式保持不变,省略... */
  257. .big-screen-home {
  258. width: 100vw;
  259. height: 100vh;
  260. background-image: url("../../assets/portal/back.png");
  261. background-size: 100% 100%;
  262. color: #fff;
  263. position: relative;
  264. overflow: hidden;
  265. font-family: 'Microsoft YaHei', sans-serif;
  266. perspective: 1200px;
  267. }
  268. .header-bar {
  269. width: 100%;
  270. height: 70px;
  271. background-image: url("../../assets/portal/headerBackground.png");
  272. display: flex;
  273. align-items: center;
  274. justify-content: space-between;
  275. padding: 0 30px;
  276. box-sizing: border-box;
  277. position: relative;
  278. z-index: 100;
  279. .header-left {
  280. width: 300px;
  281. .time-text {
  282. font-size: 14px;
  283. color: #a0d8ff;
  284. }
  285. }
  286. .system-title {
  287. font-size: 36px;
  288. font-weight: 600;
  289. color: #fff;
  290. text-shadow: 0 0 10px rgba(0, 191, 255, 0.8);
  291. margin: 0;
  292. letter-spacing: 2px;
  293. }
  294. .header-right {
  295. width: 300px;
  296. display: flex;
  297. align-items: center;
  298. gap: 15px;
  299. .welcome-text {
  300. font-size: 14px;
  301. color: #a0d8ff;
  302. }
  303. .username {
  304. color: #fff;
  305. font-weight: bold;
  306. }
  307. .header-btn {
  308. background: rgba(0, 127, 255, 0.3);
  309. border: 1px solid #00bfff;
  310. color: #fff;
  311. padding: 5px 12px;
  312. border-radius: 4px;
  313. cursor: pointer;
  314. font-size: 13px;
  315. transition: all 0.2s;
  316. &:hover {
  317. background: rgba(0, 127, 255, 0.5);
  318. box-shadow: 0 0 8px rgba(0,191,255,0.5);
  319. }
  320. }
  321. }
  322. }
  323. .ring-3d-container {
  324. position: absolute;
  325. top: 65%;
  326. left: 50%;
  327. width: 0;
  328. height: 0;
  329. transform: translate(-50%, -50%);
  330. .module-item-3d {
  331. position: absolute;
  332. display: flex;
  333. flex-direction: column;
  334. align-items: center;
  335. cursor: pointer;
  336. filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
  337. will-change: transform, opacity;
  338. transition: filter 0.2s ease;
  339. &:hover {
  340. filter: drop-shadow(0 0 15px rgba(0, 191, 255, 0.8));
  341. z-index: 100 !important;
  342. }
  343. .icon-wrapper {
  344. .icon-circle {
  345. width: 150px;
  346. height: 80px;
  347. border-radius: 50%;
  348. display: flex;
  349. align-items: center;
  350. justify-content: center;
  351. .module-icon {
  352. width: 150px;
  353. height: 130px;
  354. &.icon-operation {
  355. background: url("../../assets/portal/smxyj.png") no-repeat center center;
  356. background-size: 100% 100%;
  357. }
  358. &.icon-sewage {
  359. background: url("../../assets/portal/yjzh.png") no-repeat center center;
  360. background-size: 100% 100%;
  361. }
  362. &.icon-bi {
  363. background: url("../../assets/portal/gwss.png") no-repeat center center;
  364. background-size: 100% 100%;
  365. }
  366. &.icon-water {
  367. background: url("../../assets/portal/rq.png") no-repeat center center;
  368. background-size: 100% 100%;
  369. }
  370. &.icon-flood {
  371. background: url("../../assets/portal/gs.png") no-repeat center center;
  372. background-size: 100% 100%;
  373. }
  374. &.icon-123 {
  375. background: url("../../assets/portal/ps.png") no-repeat center center;
  376. background-size: 100% 100%;
  377. }
  378. &.icon-ps {
  379. background: url("../../assets/portal/jg.png") no-repeat center center;
  380. background-size: 100% 100%;
  381. }
  382. }
  383. }
  384. }
  385. }
  386. }
  387. //@media (max-width: 1366px) {
  388. // .ring-3d-container .module-item-3d .icon-wrapper .icon-circle {
  389. // width: 70px;
  390. // height: 70px;
  391. //
  392. // .module-icon {
  393. // width: 35px;
  394. // height: 35px;
  395. // }
  396. // }
  397. //
  398. // .ring-3d-container .module-item-3d {
  399. // font-size: 12px;
  400. // padding: 3px 10px;
  401. // }
  402. //}
  403. </style>