index.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081
  1. <template>
  2. <div class="large-screen">
  3. <!-- 地图 -->
  4. <mapScene ref="mapSceneRef" v-show="state.activeIndex === '1' && !state.showVideoPlayer"></mapScene>
  5. <div class="fusion-bg" v-show="state.activeIndex === '2'">
  6. <img src="@/assets/images/昆山水文站.jpg" alt="昆山水文站" />
  7. </div>
  8. <div class="business-bg" v-show="state.activeIndex === '3'">
  9. <!-- 圩区总览 -->
  10. <template v-if="!state.selectedPolderDetail">
  11. <img src="@/assets/images/圩区.png" alt="圩区" />
  12. <div class="business-marker-btn zhangxi" @mouseenter="showPolderInfo('zhangxi')" @mouseleave="hidePolderInfo">
  13. <div class="polder-popup" v-if="state.hoverPolder === 'zhangxi'">
  14. <div class="polder-popup-title">张西联圩</div>
  15. <div class="polder-popup-item"><span class="label">圩内水位:</span><span class="value">2.71m</span></div>
  16. <div class="polder-popup-item"><span class="label">圩外水位:</span><span class="value">3.10m</span></div>
  17. <div class="polder-popup-item"><span class="label">开闸流量:</span><span class="value">0.5 m³/s</span></div>
  18. <div class="polder-popup-item"><span class="label">开泵流量:</span><span class="value">12.8 m³/s</span></div>
  19. </div>
  20. </div>
  21. <div class="business-marker-btn zhangdong" @mouseenter="showPolderInfo('zhangdong')" @mouseleave="hidePolderInfo">
  22. <div class="polder-popup" v-if="state.hoverPolder === 'zhangdong'">
  23. <div class="polder-popup-title">张东联圩</div>
  24. <div class="polder-popup-item"><span class="label">圩内水位:</span><span class="value">2.65m</span></div>
  25. <div class="polder-popup-item"><span class="label">圩外水位:</span><span class="value">3.05m</span></div>
  26. <div class="polder-popup-item"><span class="label">开闸流量:</span><span class="value">0.4 m³/s</span></div>
  27. <div class="polder-popup-item"><span class="label">开泵流量:</span><span class="value">10.5 m³/s</span></div>
  28. </div>
  29. </div>
  30. <div class="business-marker-btn konggang" @click="selectPolderDetail('konggang')" @mouseenter="showPolderInfo('konggang')" @mouseleave="hidePolderInfo">
  31. <div class="polder-popup polder-popup-left" v-if="state.hoverPolder === 'konggang'">
  32. <div class="polder-popup-title">孔港联圩</div>
  33. <div class="polder-popup-item"><span class="label">圩内水位:</span><span class="value">2.71m</span></div>
  34. <div class="polder-popup-item"><span class="label">圩外水位:</span><span class="value">3.10m</span></div>
  35. <div class="polder-popup-item"><span class="label">开闸流量:</span><span class="value">0.3 m³/s</span></div>
  36. <div class="polder-popup-item"><span class="label">开泵流量:</span><span class="value">8.5 m³/s</span></div>
  37. </div>
  38. </div>
  39. </template>
  40. <!-- 孔港联圩详情 -->
  41. <template v-if="state.selectedPolderDetail === 'konggang'">
  42. <img :src="getKonggangImage()" alt="孔港联圩" />
  43. <div class="polder-back-btn" @click="backToPolderOverview">
  44. <span>返回</span>
  45. </div>
  46. <!-- 侧边开关控制 -->
  47. <div class="polder-layer-controls">
  48. <div class="toggle-item">
  49. <span class="toggle-text">泵站</span>
  50. <div class="toggle-switch" :class="{ active: state.showGate }" @click="state.showGate = !state.showGate">
  51. <div class="toggle-knob"></div>
  52. </div>
  53. </div>
  54. <div class="toggle-item">
  55. <span class="toggle-text">水文站</span>
  56. <div class="toggle-switch" :class="{ active: state.showHydrology }" @click="state.showHydrology = !state.showHydrology">
  57. <div class="toggle-knob"></div>
  58. </div>
  59. </div>
  60. </div>
  61. </template>
  62. </div>
  63. <div class="history-bg" v-show="state.activeIndex === '4'">
  64. <video v-if="!state.showHistoryImage" ref="historyVideoRef" class="history-bg-video" loop autoplay muted>
  65. <source :src="state.historyVideoSrc" type="video/mp4">
  66. </video>
  67. <img v-else class="history-bg-image" :src="state.historyImageSrc" alt="历史背景" />
  68. </div>
  69. <div class="culture-bg" v-show="state.activeIndex === '5'">
  70. <video class="culture-bg-video" loop autoplay muted>
  71. <source src="@/assets/video/水文化.mp4" type="video/mp4">
  72. </video>
  73. </div>
  74. <div class="science-bg" v-show="state.activeIndex === '5'">
  75. <img src="@/assets/images/背景图.png" alt="背景图" />
  76. </div>
  77. <div class="study-bg" v-show="state.activeIndex === '6'">
  78. <img v-if="state.studyBgImage === 'default'" src="@/assets/images/昆山水文站.jpg" alt="昆山水文站" />
  79. <img v-else src="@/assets/images/image.png" alt="技术实践区" />
  80. </div>
  81. <!-- 视频背景 -->
  82. <div class="video-background" v-if="state.showVideoPlayer">
  83. <video
  84. ref="videoPlayerRef"
  85. class="video-bg-player"
  86. src="@/assets/昆山水文站.mp4"
  87. autoplay
  88. loop
  89. muted
  90. @ended="closeVideoPlayer"
  91. ></video>
  92. <div class="video-close-btn" @click="closeVideoPlayer">
  93. <span>返回</span>
  94. </div>
  95. </div>
  96. <!-- 地图切换按钮 -->
  97. <div class="map-switch-btns" v-show="state.activeIndex === '1'">
  98. <div class="map-switch-btn" :class="{ active: state.currentMapMode === 'suzhou' }" @click="switchToSuzhouMap">
  99. <span>苏州地图</span>
  100. </div>
  101. <div class="map-switch-btn" :class="{ active: state.currentMapMode === 'kunshan' }" @click="switchToKunshanMap">
  102. <span>昆山地图</span>
  103. </div>
  104. </div>
  105. <div class="vignette-overlay"></div>
  106. <div class="large-screen-wrap" id="large-screen">
  107. <m-header title="水务水文融合系统" sub-text="Hydrological Visualization System">
  108. <!--左侧 天气 -->
  109. <template v-slot:left>
  110. <div class="m-header-weather"><span>小雪</span><span>-4℃</span></div>
  111. </template>
  112. <!--右侧 日期 -->
  113. <template v-slot:right>
  114. <div class="m-header-date"><span>2025-12-11</span><span>17:53:16</span></div>
  115. </template>
  116. </m-header>
  117. <!-- 顶部菜单 - 仅在昆山地图模式下显示 -->
  118. <div class="top-menu" v-show="state.currentMapMode === 'kunshan'">
  119. <mMenu :default-active="state.activeIndex" @select="handleMenuSelect">
  120. <mMenuItem index="1">区域总览</mMenuItem>
  121. <mMenuItem index="2">融合体系</mMenuItem>
  122. <mMenuItem index="3">综合业务</mMenuItem>
  123. <div class="top-menu-mid-space"></div>
  124. <mMenuItem index="4">历史沿革</mMenuItem>
  125. <mMenuItem index="5">水文科普</mMenuItem>
  126. <mMenuItem index="6">研学联建</mMenuItem>
  127. </mMenu>
  128. </div>
  129. <!-- 区域总览内容 -->
  130. <template v-if="state.activeIndex === '1'">
  131. <!-- 顶部统计卡片 -->
  132. <div class="top-count-card">
  133. <mCountCard v-for="(item, index) in state.totalView" :info="item" :key="index"></mCountCard>
  134. </div>
  135. <!-- 左边布局 图表 -->
  136. <div class="left-wrap">
  137. <div class="left-wrap-3d">
  138. <!-- 中心简介 -->
  139. <BulkCommoditySalesChart></BulkCommoditySalesChart>
  140. <!-- 近年水位变化 -->
  141. <EconomicTrendChart></EconomicTrendChart>
  142. </div>
  143. </div>
  144. <!-- 右边布局 图表 -->
  145. <div class="right-wrap">
  146. <div class="right-wrap-3d">
  147. <!-- 水利设施分布 -->
  148. <PurposeSpecialFunds> </PurposeSpecialFunds>
  149. <!-- 用水量分析 -->
  150. <ElectricityUsage></ElectricityUsage>
  151. </div>
  152. </div>
  153. <!-- 底部托盘 -->
  154. <div class="bottom-tray">
  155. <!-- svg线条动画 -->
  156. <mSvglineAnimation class="bottom-svg-line-left" :width="721" :height="57" color="#00BFFF" :strokeWidth="2"
  157. :dir="[0, 1]" :length="50"
  158. path="M1 56.6105C1 31.5123 185.586 10.0503 451.904 1.35519C458.942 1.12543 465.781 4.00883 470.505 9.22964L484.991 25.2383C487.971 28.4775 492.938 30.4201 498.254 30.4201H720.142">
  159. </mSvglineAnimation>
  160. <mSvglineAnimation class="bottom-svg-line-left bottom-svg-line-right" :width="721" :height="57" color="#00BFFF"
  161. :strokeWidth="2" :dir="[0, 1]" :length="50"
  162. path="M1 56.6105C1 31.5123 185.586 10.0503 451.904 1.35519C458.942 1.12543 465.781 4.00883 470.505 9.22964L484.991 25.2383C487.971 28.4775 492.938 30.4201 498.254 30.4201H720.142">
  163. </mSvglineAnimation>
  164. <!-- 做箭头 -->
  165. <div class="bottom-tray-arrow">
  166. <img src="@/assets/images/bottom-menu-arrow-big.svg" alt="" />
  167. <img src="@/assets/images/bottom-menu-arrow-small.svg" alt="" />
  168. </div>
  169. <!-- 底部菜单 -->
  170. <div class="bottom-menu">
  171. <div class="bottom-menu-item is-active"><span>区域总览</span></div>
  172. <div class="bottom-menu-item"><span>水质监测</span></div>
  173. <div class="bottom-menu-item"><span>水文数据</span></div>
  174. <div class="bottom-menu-item"><span>防洪预警</span></div>
  175. </div>
  176. <!-- 右箭头 -->
  177. <div class="bottom-tray-arrow is-reverse">
  178. <img src="@/assets/images/bottom-menu-arrow-big.svg" alt="" />
  179. <img src="@/assets/images/bottom-menu-arrow-small.svg" alt="" />
  180. </div>
  181. </div>
  182. <!-- 雷达 -->
  183. <div class="bottom-radar">
  184. <mRadar></mRadar>
  185. </div>
  186. </template>
  187. <!-- 融合体系内容 -->
  188. <WaterResourceContent v-if="state.activeIndex === '2'" />
  189. <!-- 水文测站内容 -->
  190. <WaterStationContent v-if="state.activeIndex === '3'" />
  191. <!-- 历史沿革内容 -->
  192. <HistoryContent v-if="state.activeIndex === '4'" @changeVideo="handleVideoChange" />
  193. <!-- 水文科普内容 -->
  194. <ScienceContent v-if="state.activeIndex === '5'" />
  195. <!-- 研学联建内容 -->
  196. <StudyContent v-if="state.activeIndex === '6'" />
  197. <!-- 左右装饰线 -->
  198. <div class="large-screen-left-zsline"></div>
  199. <div class="large-screen-right-zsline"></div>
  200. </div>
  201. <!-- loading动画 -->
  202. <div class="loading">
  203. <div class="loading-text">
  204. <span style="--index: 1">L</span>
  205. <span style="--index: 2">O</span>
  206. <span style="--index: 3">A</span>
  207. <span style="--index: 4">D</span>
  208. <span style="--index: 5">I</span>
  209. <span style="--index: 6">N</span>
  210. <span style="--index: 7">G</span>
  211. </div>
  212. <div class="loading-progress">
  213. <span class="value">{{ state.progress }}</span>
  214. <span class="unit">%</span>
  215. </div>
  216. </div>
  217. </div>
  218. </template>
  219. <script setup>
  220. import { shallowRef, ref, reactive, onMounted, onBeforeUnmount, nextTick, provide } from "vue"
  221. import mapScene from "./map.vue"
  222. import mHeader from "@/components/mHeader/index.vue"
  223. import mCountCard from "@/components/mCountCard/index.vue"
  224. import mMenu from "@/components/mMenu/index.vue"
  225. import mRadar from "@/components/mRadar/index.vue"
  226. import mMenuItem from "@/components/mMenuItem/index.vue"
  227. import mSvglineAnimation from "@/components/mSvglineAnimation/index.vue"
  228. import BulkCommoditySalesChart from "./components/BulkCommoditySalesChart.vue"
  229. import YearlyEconomyTrend from "./components/YearlyEconomyTrend.vue"
  230. import EconomicTrendChart from "./components/EconomicTrendChart.vue"
  231. import DistrictEconomicIncome from "./components/DistrictEconomicIncome.vue"
  232. import PurposeSpecialFunds from "./components/PurposeSpecialFunds.vue"
  233. import ProportionPopulationConsumption from "./components/ProportionPopulationConsumption.vue"
  234. import ElectricityUsage from "./components/ElectricityUsage.vue"
  235. import QuarterlyGrowthSituation from "./components/QuarterlyGrowthSituation.vue"
  236. import WaterResourceContent from "@/views/waterResource/index.vue"
  237. import WaterStationContent from "@/views/waterStation/index.vue"
  238. import HistoryContent from "@/views/history/index.vue"
  239. import ScienceContent from "@/views/science/index.vue"
  240. import StudyContent from "@/views/study/index.vue"
  241. import { Assets } from "./assets.js"
  242. import emitter from "@/utils/emitter"
  243. import gsap from "gsap"
  244. import autofit from "autofit.js"
  245. const assets = shallowRef(null)
  246. const mapSceneRef = ref(null)
  247. const historyVideoRef = ref(null)
  248. // 提供资源给子组件
  249. provide("assets", assets)
  250. const state = reactive({
  251. // 进度
  252. progress: 0,
  253. // 当前顶部导航索引
  254. activeIndex: "1",
  255. // 是否显示视频播放器
  256. showVideoPlayer: false,
  257. // 当前悬停的圩区
  258. hoverPolder: null,
  259. // 当前选中的圩区详情页(null表示显示圩区总览)
  260. selectedPolderDetail: null,
  261. // 图层开关状态
  262. showGate: true,
  263. showHydrology: false,
  264. // 当前圩区数据
  265. currentPolder: {
  266. name: '张西联圩',
  267. innerWaterLevel: '2.66m',
  268. outerWaterLevel: '2.70m',
  269. gateFlow: '0.5 m³/s',
  270. pumpFlow: '12.8 m³/s'
  271. },
  272. // 圩区数据字典
  273. polderData: {
  274. zhangxi: {
  275. name: '张西联圩',
  276. innerWaterLevel: '2.66m',
  277. outerWaterLevel: '2.70m',
  278. gateFlow: '0.5 m³/s',
  279. pumpFlow: '12.8 m³/s'
  280. },
  281. zhangdong: {
  282. name: '张东联圩',
  283. innerWaterLevel: '2.65m',
  284. outerWaterLevel: '3.05m',
  285. gateFlow: '0.4 m³/s',
  286. pumpFlow: '10.5 m³/s'
  287. },
  288. konggang: {
  289. name: '孔港联圩',
  290. innerWaterLevel: '2.58m',
  291. outerWaterLevel: '2.65m',
  292. gateFlow: '0.3 m³/s',
  293. pumpFlow: '8.5 m³/s'
  294. }
  295. },
  296. // 历史沿革视频源
  297. historyVideoSrc: new URL("@/assets/video/昆山水务水文人工.mp4", import.meta.url).href,
  298. // 历史沿革图片源
  299. historyImageSrc: new URL("@/assets/video/昆山水务水文水质监测.png", import.meta.url).href,
  300. // 是否显示历史沿革图片
  301. showHistoryImage: false,
  302. // 当前地图模式: 'kunshan' | 'suzhou',默认苏州地图
  303. currentMapMode: 'suzhou',
  304. // 研学联建背景图状态
  305. studyBgImage: 'default', // 'default' 或 'practice'
  306. // 卡片统计数据
  307. totalView: [
  308. {
  309. icon: "xiaoshoujine",
  310. zh: "区域总面积",
  311. value: 931,
  312. unit: "平方公里",
  313. },
  314. {
  315. icon: "zongxiaoliang",
  316. zh: "水文站数量",
  317. value: 57,
  318. unit: "个",
  319. },
  320. {
  321. icon: "xiaoshoujine",
  322. zh: "水厂数量",
  323. value: 8,
  324. unit: "座",
  325. },
  326. {
  327. icon: "zongxiaoliang",
  328. zh: "排水口数量",
  329. value: 156,
  330. unit: "个",
  331. },
  332. ],
  333. })
  334. // 切换研学联建背景图
  335. function switchStudyBgImage(type) {
  336. state.studyBgImage = type
  337. }
  338. onMounted(() => {
  339. // 监听地图播放完成,执行事件
  340. emitter.$on("mapPlayComplete", handleMapPlayComplete)
  341. // 监听水文站图标点击事件
  342. emitter.$on("waterStationClick", handleWaterStationClick)
  343. // 监听地图模式变化
  344. emitter.$on("mapModeChanged", handleMapModeChanged)
  345. // 监听切换到区域总览页面事件
  346. emitter.$on("switchToRegionOverview", handleSwitchToRegionOverview)
  347. // 监听研学联建背景图切换事件
  348. emitter.$on("switchStudyBgImage", switchStudyBgImage)
  349. // 自动适配
  350. assets.value = autofit.init({
  351. dh: 1080,
  352. dw: 1920,
  353. el: "#large-screen",
  354. resize: true,
  355. })
  356. // 初始化资源
  357. initAssets(async () => {
  358. // 加载地图
  359. emitter.$emit("loadMap", assets.value)
  360. // 隐藏loading
  361. await hideLoading()
  362. // 播放场景
  363. mapSceneRef.value.play()
  364. })
  365. })
  366. onBeforeUnmount(() => {
  367. emitter.$off("mapPlayComplete", handleMapPlayComplete)
  368. emitter.$off("waterStationClick", handleWaterStationClick)
  369. emitter.$off("mapModeChanged", handleMapModeChanged)
  370. emitter.$off("switchToRegionOverview", handleSwitchToRegionOverview)
  371. emitter.$off("switchStudyBgImage", switchStudyBgImage)
  372. })
  373. // 处理地图模式变化
  374. function handleMapModeChanged(mode) {
  375. state.currentMapMode = mode
  376. }
  377. // 处理切换到区域总览页面
  378. function handleSwitchToRegionOverview() {
  379. // 切换到区域总览页面(activeIndex 为 1)
  380. state.activeIndex = '1'
  381. }
  382. // 切换到苏州地图
  383. function switchToSuzhouMap() {
  384. if (state.currentMapMode === 'suzhou') return
  385. if (mapSceneRef.value) {
  386. mapSceneRef.value.switchToSuzhou()
  387. }
  388. }
  389. // 切换到昆山地图
  390. function switchToKunshanMap() {
  391. if (state.currentMapMode === 'kunshan') return
  392. if (mapSceneRef.value) {
  393. mapSceneRef.value.switchToKunshan()
  394. }
  395. }
  396. // 处理水文站图标点击事件
  397. function handleWaterStationClick(stationInfo) {
  398. console.log('播放水文站视频:', stationInfo.name)
  399. state.showVideoPlayer = true
  400. }
  401. // 关闭视频播放器
  402. function closeVideoPlayer() {
  403. state.showVideoPlayer = false
  404. // 暂停视频播放
  405. const videoPlayer = document.querySelector('.video-player')
  406. if (videoPlayer) {
  407. videoPlayer.pause()
  408. }
  409. }
  410. function showPolderInfo(polderKey) {
  411. state.hoverPolder = polderKey
  412. }
  413. function hidePolderInfo() {
  414. state.hoverPolder = null
  415. }
  416. function selectPolderDetail(polderKey) {
  417. state.selectedPolderDetail = polderKey
  418. if (polderKey === 'konggang') {
  419. state.showGate = true
  420. state.showHydrology = false
  421. }
  422. }
  423. function getKonggangImage() {
  424. if (state.showGate && state.showHydrology) {
  425. return new URL("@/assets/images/孔港联圩泵站水文站.png", import.meta.url).href
  426. } else if (state.showGate) {
  427. return new URL("@/assets/images/孔港联圩泵站.png", import.meta.url).href
  428. } else if (state.showHydrology) {
  429. return new URL("@/assets/images/孔港联圩水文站.png", import.meta.url).href
  430. } else {
  431. return new URL("@/assets/images/孔港联圩.png", import.meta.url).href
  432. }
  433. }
  434. function backToPolderOverview() {
  435. state.selectedPolderDetail = null
  436. }
  437. // 初始化加载资源
  438. function initAssets(onLoadCallback) {
  439. assets.value = new Assets()
  440. // 资源加载进度
  441. let params = {
  442. progress: 0,
  443. }
  444. assets.value.instance.on("onProgress", (path, itemsLoaded, itemsTotal) => {
  445. let p = Math.floor((itemsLoaded / itemsTotal) * 100)
  446. gsap.to(params, {
  447. progress: p,
  448. onUpdate: () => {
  449. state.progress = Math.floor(params.progress)
  450. },
  451. })
  452. })
  453. // 资源加载完成
  454. assets.value.instance.on("onLoad", () => {
  455. onLoadCallback && onLoadCallback()
  456. })
  457. }
  458. // 隐藏loading
  459. async function hideLoading() {
  460. return new Promise((resolve, reject) => {
  461. let tl = gsap.timeline()
  462. tl.to(".loading-text span", {
  463. y: "200%",
  464. opacity: 0,
  465. ease: "power4.inOut",
  466. duration: 2,
  467. stagger: 0.2,
  468. })
  469. tl.to(".loading-progress", { opacity: 0, ease: "power4.inOut", duration: 2 }, "<")
  470. tl.to(
  471. ".loading",
  472. {
  473. opacity: 0,
  474. ease: "power4.inOut",
  475. onComplete: () => {
  476. resolve()
  477. },
  478. },
  479. "-=1"
  480. )
  481. })
  482. }
  483. function handleMenuSelect(index) {
  484. state.activeIndex = index
  485. // 切换水文站图标显示
  486. if (index === "3") {
  487. // 在水文测站页面显示图标
  488. emitter.$emit("toggleWaterStations", true)
  489. // 重置圩区详情页面,返回总览
  490. state.selectedPolderDetail = null
  491. } else {
  492. // 其他页面隐藏图标
  493. emitter.$emit("toggleWaterStations", false)
  494. }
  495. nextTick(() => {
  496. if (index === "1") {
  497. gsap.to(".left-card", { x: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  498. gsap.to(".right-card", { x: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  499. gsap.to(".count-card", { y: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  500. gsap.to(".bottom-tray", { y: 0, opacity: 1, duration: 0.5 })
  501. gsap.to(".bottom-radar", { y: 0, opacity: 1, duration: 0.5 })
  502. } else if (index === "2") {
  503. gsap.to(".water-resource-content .module-card", { x: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  504. } else if (index === "3") {
  505. gsap.to(".water-station-content .water-station-card", { x: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  506. } else if (index === "4") {
  507. gsap.to(".history-content .event-card", { x: 0, opacity: 1, duration: 0.5 })
  508. gsap.to(".history-content .timeline-container", { y: 0, opacity: 1, duration: 0.5, delay: 0.2 })
  509. } else if (index === "5") {
  510. gsap.to(".science-content .science-card", { y: 0, opacity: 1, duration: 0.6, stagger: 0.15 })
  511. } else if (index === "6") {
  512. gsap.to(".study-content .left-column .module-card", { x: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  513. gsap.to(".study-content .right-column .module-card", { x: 0, opacity: 1, duration: 0.5, stagger: 0.1 })
  514. }
  515. })
  516. }
  517. function handleVideoChange(videoType) {
  518. if (videoType === "water") {
  519. state.showHistoryImage = true
  520. state.historyImageSrc = new URL("@/assets/video/昆山水务水文水质监测.png", import.meta.url).href
  521. } else {
  522. state.showHistoryImage = false
  523. if (videoType === "auto") {
  524. state.historyVideoSrc = new URL("@/assets/video/昆山水务水文自动化起步.mp4", import.meta.url).href
  525. } else if (videoType === "3d") {
  526. state.historyVideoSrc = new URL("@/assets/video/昆山水务水文三维孪生半自动.mp4", import.meta.url).href
  527. } else if (videoType === "smart") {
  528. state.historyVideoSrc = new URL("@/assets/video/昆山水务水文三维孪生智能.mp4", import.meta.url).href
  529. } else if (videoType === "wisdom") {
  530. state.historyVideoSrc = new URL("@/assets/video/智慧水务.mp4", import.meta.url).href
  531. } else {
  532. state.historyVideoSrc = new URL("@/assets/video/昆山水务水文人工.mp4", import.meta.url).href
  533. }
  534. nextTick(() => {
  535. if (historyVideoRef.value) {
  536. historyVideoRef.value.load()
  537. historyVideoRef.value.play()
  538. }
  539. })
  540. }
  541. }
  542. // 地图开始动画播放完成
  543. function handleMapPlayComplete() {
  544. let tl = gsap.timeline({ paused: false })
  545. let leftCards = gsap.utils.toArray(".left-card")
  546. let rightCards = gsap.utils.toArray(".right-card")
  547. let countCards = gsap.utils.toArray(".count-card")
  548. tl.addLabel("start", 0.5)
  549. tl.addLabel("menu", 0.5)
  550. tl.addLabel("card", 1)
  551. tl.addLabel("countCard", 3)
  552. tl.to(".m-header", { y: 0, opacity: 1, duration: 1.5, ease: "power4.out" }, "start")
  553. tl.to(".bottom-tray", { y: 0, opacity: 1, duration: 1.5, ease: "power4.out" }, "start")
  554. tl.to(
  555. ".top-menu",
  556. {
  557. y: 0,
  558. opacity: 1,
  559. duration: 1.5,
  560. ease: "power4.out",
  561. },
  562. "-=1"
  563. )
  564. tl.to(".bottom-radar", { y: 0, opacity: 1, duration: 1.5, ease: "power4.out" }, "-=2")
  565. tl.to(leftCards, { x: 0, opacity: 1, stagger: 0.2, duration: 1.5, ease: "power4.out" }, "card")
  566. tl.to(rightCards, { x: 0, opacity: 1, stagger: 0.2, duration: 1.5, ease: "power4.out" }, "card")
  567. tl.to(
  568. countCards,
  569. {
  570. y: 0,
  571. opacity: 1,
  572. stagger: 0.2,
  573. duration: 1.5,
  574. ease: "power4.out",
  575. },
  576. "card"
  577. )
  578. }
  579. </script>
  580. <style lang="scss">
  581. @use "~@/assets/style/home.scss";
  582. .vignette-overlay {
  583. position: absolute;
  584. z-index: 1;
  585. left: 0;
  586. top: 0;
  587. right: 0;
  588. bottom: 0;
  589. pointer-events: none;
  590. background: radial-gradient(
  591. ellipse 70% 70% at center,
  592. rgba(0, 20, 40, 0) 40%,
  593. rgba(0, 20, 40, 1.0) 100%
  594. );
  595. box-shadow: inset 0 0 100px rgba(0, 191, 255, 0.1);
  596. }
  597. .m-header-weather,
  598. .m-header-date {
  599. span {
  600. padding-right: 10px;
  601. color: #c4f3fe;
  602. font-size: 14px;
  603. }
  604. }
  605. .top-menu {
  606. position: absolute;
  607. left: 0px;
  608. right: 0px;
  609. top: 40px;
  610. z-index: 3;
  611. display: flex;
  612. justify-content: center;
  613. .top-menu-mid-space {
  614. width: 800px;
  615. }
  616. }
  617. .bottom-radar {
  618. position: absolute;
  619. right: 500px;
  620. bottom: 100px;
  621. z-index: 3;
  622. }
  623. .main-btn-group {
  624. display: flex;
  625. left: 50%;
  626. transform: translateX(-50%);
  627. bottom: 10px;
  628. z-index: 999;
  629. &.disabled {
  630. pointer-events: none;
  631. }
  632. .btn {
  633. margin-right: 10px;
  634. }
  635. }
  636. .bottom-svg-line-left,
  637. .bottom-svg-line-right {
  638. position: absolute;
  639. right: 50%;
  640. width: 721px;
  641. height: 57px;
  642. margin-right: -5px;
  643. bottom: -21px;
  644. }
  645. .bottom-svg-line-right {
  646. transform: scaleX(-1);
  647. left: 50%;
  648. right: inherit;
  649. margin-right: inherit;
  650. margin-left: -5px;
  651. }
  652. /* 初始化动画开始位置 */
  653. .m-header {
  654. transform: translateY(-100%);
  655. opacity: 0;
  656. }
  657. .top-menu {
  658. transform: translateY(-250%);
  659. opacity: 0;
  660. }
  661. .count-card {
  662. transform: translateY(150%);
  663. opacity: 0;
  664. }
  665. .left-card {
  666. transform: translateX(-150%);
  667. opacity: 0;
  668. }
  669. .right-card {
  670. transform: translateX(150%);
  671. opacity: 0;
  672. }
  673. .right-wrap {
  674. position: absolute;
  675. right: 32px;
  676. top: 180px;
  677. width: 398px;
  678. bottom: 50px;
  679. perspective: 500px;
  680. perspective-origin: 50% 50%;
  681. }
  682. .right-wrap-3d {
  683. position: absolute;
  684. left: 0;
  685. top: 0;
  686. right: 0;
  687. bottom: 0;
  688. display: flex;
  689. flex-direction: column;
  690. justify-content: space-between;
  691. transform: translate3d(0px, 0px, 0px) scaleX(1) scaleY(1) rotateX(0deg) rotateY(-6deg) rotateZ(0deg) skewX(0deg) skewY(0deg);
  692. z-index: 4;
  693. }
  694. .right-wrap-3d > div {
  695. margin-bottom: 16px;
  696. }
  697. .right-wrap-3d > div:last-child {
  698. margin-bottom: 0;
  699. }
  700. .bottom-tray {
  701. transform: translateY(100%);
  702. opacity: 0;
  703. }
  704. .bottom-radar {
  705. transform: translateY(100%);
  706. opacity: 0;
  707. }
  708. // 地图切换按钮样式
  709. .map-switch-btns {
  710. position: absolute;
  711. left: 50%;
  712. bottom: 100px;
  713. transform: translateX(-50%);
  714. z-index: 10;
  715. display: flex;
  716. gap: 10px;
  717. pointer-events: auto;
  718. .map-switch-btn {
  719. padding: 8px 20px;
  720. background: rgba(0, 20, 40, 0.85);
  721. border: 1px solid rgba(48, 220, 255, 0.3);
  722. border-radius: 4px;
  723. cursor: pointer;
  724. transition: all 0.3s ease;
  725. span {
  726. color: #a3dcde;
  727. font-size: 14px;
  728. }
  729. &:hover {
  730. background: rgba(48, 220, 255, 0.2);
  731. border-color: rgba(48, 220, 255, 0.6);
  732. }
  733. &.active {
  734. background: rgba(48, 220, 255, 0.3);
  735. border-color: rgba(48, 220, 255, 0.8);
  736. box-shadow: 0 0 15px rgba(48, 220, 255, 0.4);
  737. span {
  738. color: #30dcff;
  739. font-weight: bold;
  740. }
  741. }
  742. }
  743. }
  744. .fusion-bg, .business-bg, .history-bg, .culture-bg, .science-bg, .study-bg {
  745. position: absolute;
  746. z-index: 1;
  747. left: 0;
  748. top: 0;
  749. right: 0;
  750. bottom: 0;
  751. background-color: #000;
  752. img, video {
  753. width: 100%;
  754. height: 100%;
  755. object-fit: cover;
  756. }
  757. }
  758. .business-bg {
  759. .business-marker-btn {
  760. position: absolute;
  761. width: 150px;
  762. height: 150px;
  763. background: transparent;
  764. cursor: pointer;
  765. transition: all 0.3s ease;
  766. z-index: 10;
  767. &.zhangxi {
  768. left: 40%;
  769. top: 60%;
  770. transform: translate(-50%, -50%);
  771. }
  772. &.zhangdong {
  773. left: 55%;
  774. top: 60%;
  775. transform: translate(-50%, -50%);
  776. }
  777. &.konggang {
  778. left: 70%;
  779. top: 45%;
  780. transform: translate(-50%, -50%);
  781. }
  782. }
  783. .polder-popup {
  784. position: absolute;
  785. left: 170px;
  786. top: 50%;
  787. transform: translateY(-50%);
  788. width: 200px;
  789. background: rgba(0, 20, 40, 0.95);
  790. border: 2px solid #30dcff;
  791. border-radius: 8px;
  792. padding: 15px;
  793. z-index: 100;
  794. .polder-popup-title {
  795. font-size: 16px;
  796. font-weight: bold;
  797. color: #30dcff;
  798. margin-bottom: 10px;
  799. padding-bottom: 8px;
  800. border-bottom: 1px solid rgba(48, 220, 255, 0.3);
  801. }
  802. .polder-popup-item {
  803. display: flex;
  804. justify-content: space-between;
  805. padding: 5px 0;
  806. font-size: 13px;
  807. .label {
  808. color: #a3dcde;
  809. }
  810. .value {
  811. color: #30dcff;
  812. font-weight: bold;
  813. }
  814. }
  815. &.polder-popup-left {
  816. left: auto;
  817. right: 170px;
  818. }
  819. }
  820. .polder-back-btn {
  821. position: absolute;
  822. left: 50px;
  823. top: 100px;
  824. padding: 10px 25px;
  825. background: rgba(0, 20, 40, 0.85);
  826. border: 2px solid #30dcff;
  827. border-radius: 6px;
  828. cursor: pointer;
  829. transition: all 0.3s ease;
  830. z-index: 100;
  831. span {
  832. color: #30dcff;
  833. font-size: 16px;
  834. font-weight: bold;
  835. }
  836. &:hover {
  837. background: rgba(48, 220, 255, 0.2);
  838. box-shadow: 0 0 15px rgba(48, 220, 255, 0.5);
  839. }
  840. }
  841. .polder-layer-controls {
  842. position: absolute;
  843. right: 20px;
  844. top: 50%;
  845. transform: translateY(-50%);
  846. display: flex;
  847. flex-direction: column;
  848. gap: 15px;
  849. z-index: 1000;
  850. .toggle-item {
  851. display: flex;
  852. align-items: center;
  853. gap: 10px;
  854. padding: 8px 12px;
  855. background: rgba(0, 20, 40, 0.8);
  856. border-radius: 6px;
  857. .toggle-text {
  858. color: #fff;
  859. font-size: 14px;
  860. text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
  861. }
  862. .toggle-switch {
  863. width: 44px;
  864. height: 22px;
  865. background: rgba(100, 100, 100, 0.5);
  866. border-radius: 11px;
  867. cursor: pointer;
  868. transition: all 0.3s ease;
  869. position: relative;
  870. .toggle-knob {
  871. position: absolute;
  872. left: 2px;
  873. top: 2px;
  874. width: 18px;
  875. height: 18px;
  876. background: #a3dcde;
  877. border-radius: 50%;
  878. transition: all 0.3s ease;
  879. }
  880. &.active {
  881. background: rgba(48, 220, 255, 0.4);
  882. box-shadow: 0 0 10px rgba(48, 220, 255, 0.3);
  883. .toggle-knob {
  884. left: 24px;
  885. background: #30dcff;
  886. box-shadow: 0 0 8px rgba(48, 220, 255, 0.5);
  887. }
  888. }
  889. }
  890. }
  891. }
  892. }
  893. .left-column {
  894. position: absolute;
  895. z-index: 4;
  896. width: 398px;
  897. left: 32px;
  898. top: 180px;
  899. bottom: 50px;
  900. perspective: 500px;
  901. perspective-origin: 50% 50%;
  902. .left-column-3d {
  903. position: absolute;
  904. left: 0;
  905. top: 0;
  906. right: 0;
  907. bottom: 0;
  908. display: flex;
  909. flex-direction: column;
  910. justify-content: space-between;
  911. transform: translate3d(0px, 0px, 0px) scaleX(1) scaleY(1) rotateX(0deg) rotateY(6deg) rotateZ(0deg) skewX(0deg) skewY(0deg);
  912. z-index: 4;
  913. }
  914. .module-card {
  915. height: 450px;
  916. background: rgba(0, 20, 40, 0.8);
  917. border: 1px solid rgba(48, 220, 255, 0.3);
  918. border-radius: 10px;
  919. box-shadow: 0 0 20px rgba(48, 220, 255, 0.1);
  920. transform: translateX(-150%);
  921. opacity: 0;
  922. }
  923. }
  924. .right-column {
  925. position: absolute;
  926. z-index: 4;
  927. width: 398px;
  928. right: 32px;
  929. top: 150px;
  930. bottom: 50px;
  931. perspective: 800px;
  932. perspective-origin: 50% 50%;
  933. .right-column-3d {
  934. position: absolute;
  935. left: 0;
  936. top: 0;
  937. right: 0;
  938. bottom: 0;
  939. display: flex;
  940. flex-direction: column;
  941. justify-content: space-between;
  942. transform: translate3d(0px, 0px, 0px) scaleX(1) scaleY(1) rotateX(0deg) rotateY(-6deg) rotateZ(0deg) skewX(0deg) skewY(0deg);
  943. }
  944. .module-card {
  945. height: 450px;
  946. background: rgba(0, 20, 40, 0.8);
  947. border: 1px solid rgba(48, 220, 255, 0.3);
  948. border-radius: 10px;
  949. box-shadow: 0 0 20px rgba(48, 220, 255, 0.1);
  950. transform: translateX(150%);
  951. opacity: 0;
  952. }
  953. }
  954. // 视频背景样式
  955. .video-background {
  956. position: fixed;
  957. top: 0;
  958. left: 0;
  959. width: 100vw;
  960. height: 100vh;
  961. z-index: 1;
  962. .video-bg-player {
  963. width: 100%;
  964. height: 100%;
  965. object-fit: cover;
  966. }
  967. .video-close-btn {
  968. position: absolute;
  969. top: 100px;
  970. right: 40px;
  971. padding: 8px 20px;
  972. background: rgba(0, 20, 40, 0.9);
  973. border: 1px solid rgba(48, 220, 255, 0.5);
  974. border-radius: 4px;
  975. display: flex;
  976. align-items: center;
  977. justify-content: center;
  978. cursor: pointer;
  979. z-index: 10;
  980. transition: all 0.3s ease;
  981. span {
  982. color: #30dcff;
  983. font-size: 14px;
  984. font-weight: bold;
  985. }
  986. &:hover {
  987. background: rgba(48, 220, 255, 0.2);
  988. border-color: rgba(48, 220, 255, 0.8);
  989. box-shadow: 0 0 20px rgba(48, 220, 255, 0.5);
  990. span {
  991. color: #00ffff;
  992. }
  993. }
  994. }
  995. }
  996. </style>