index.vue 13 KB

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