aside.vue 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110
  1. <template>
  2. <el-container class="container">
  3. <!-- 侧边栏 --start -->
  4. <el-aside :class="asideWidth" id="el-aside">
  5. <el-menu
  6. ref="menu"
  7. :collapse="isCollapse"
  8. :uniqueOpened="true"
  9. default-active=""
  10. class="el-menu-vertical-demo"
  11. text-color="#303133"
  12. background-color="#f8f9fa"
  13. active-text-color="#409eff"
  14. :collapse-transition="false"
  15. >
  16. <!-- 展开收缩控制按钮 -->
  17. <el-menu-item index="0" @click="isCollapse=!isCollapse" id="fold">
  18. <i
  19. class="iconfont iconshouqi iconfont2"
  20. style="display:block;margin:0;text-align:center"
  21. :class="isCollapse?'rotate':'rotate2'"
  22. ></i>
  23. </el-menu-item>
  24. <!-- 加载数据 -->
  25. <el-sub-menu index="1">
  26. <template #title>
  27. <i class="iconfont iconshuju iconfont2"></i>
  28. <span>数据服务</span>
  29. </template>
  30. <el-sub-menu v-for="(data,index) in server_config" :key="data.id" :index="data.id" popper-append-to-body="false">
  31. <template #title>{{data.name}}</template>
  32. <div class="box" :index="index">
  33. <div
  34. class="imgbox"
  35. v-for="data2 in data.children"
  36. :key="data2.name"
  37. @click="addDatas(data.id,data2)"
  38. >
  39. <img :src="data2.thumbnail" alt class="img" />
  40. <img src="/img/common/cross.png" alt class="cross" v-show="data2.state != 0" />
  41. <span>{{data2.name}}</span>
  42. <el-popconfirm
  43. title="确定删除吗?"
  44. confirmButtonText="确认"
  45. cancelButtonText="取消"
  46. @confirm="DeleteDates(data.id,data2)"
  47. >
  48. <template #reference>
  49. <CircleClose
  50. v-show="data2.state != 0 && data.id !='baseLayer'"
  51. title="删除"
  52. class="circle-close-icon"
  53. />
  54. </template>
  55. </el-popconfirm>
  56. </div>
  57. </div>
  58. </el-sub-menu>
  59. <!-- 本地服务二级菜单 -->
  60. <el-sub-menu index="1-4">
  61. <template #title>自定义服务</template>
  62. <el-menu-item
  63. v-for="(service, index) in addCustomServices"
  64. :key="service.id || index"
  65. :index="'1-4-' + (service.id || index)"
  66. @dblclick="loadCustomService(service)"
  67. class="custom-service-item"
  68. >
  69. <span class="service-name" :class="{ 'service-loaded': isServiceLoaded(service) }">{{ service.name }}</span>
  70. <el-popconfirm
  71. v-if="isServiceLoaded(service)"
  72. title="确定卸载吗?"
  73. confirmButtonText="确认"
  74. cancelButtonText="取消"
  75. @confirm="unloadCustomService(service)"
  76. >
  77. <template #reference>
  78. <el-icon class="delete-service-icon" title="卸载">
  79. <CircleClose />
  80. </el-icon>
  81. </template>
  82. </el-popconfirm>
  83. </el-menu-item>
  84. </el-sub-menu>
  85. <el-menu-item index="1-5" @click="addCustomService" class="no-active-style">
  86. <el-icon @click.stop="addCustomService" class="square-plus-icon"><Plus /></el-icon>
  87. </el-menu-item>
  88. </el-sub-menu>
  89. <!-- 加载组件 -->
  90. <el-sub-menu v-for="(data,index) in config" :key="index" :index="data.id">
  91. <template #title>
  92. <i :class="data.icon" class="iconfont2"></i>
  93. <span>{{data.name}}</span>
  94. </template>
  95. <div class="box">
  96. <div
  97. class="imgbox"
  98. v-for="data2 in data.children"
  99. :key="data2.component"
  100. @click="data2.component && change(data2.component,data2.destroy)"
  101. >
  102. <img :src="data2.imgSrc" alt class="img" />
  103. <img
  104. src="/img/common/cross.png"
  105. alt
  106. class="cross"
  107. v-show="data2.component && (data2.component === view || data2.component === view2)"
  108. />
  109. <span>{{data2.name}}</span>
  110. </div>
  111. </div>
  112. </el-sub-menu>
  113. <!-- 模型菜单 -->
  114. <el-sub-menu index="model" v-if="modelConfig.id">
  115. <template #title>
  116. <i :class="modelConfig.icon" class="iconfont2"></i>
  117. <span>{{modelConfig.name}}</span>
  118. </template>
  119. <el-sub-menu
  120. v-for="(secondLevel, firstLevelKey) in groupedModels"
  121. :key="firstLevelKey"
  122. :index="'model-' + firstLevelKey"
  123. popper-append-to-body="false"
  124. >
  125. <template #title>
  126. <span>{{getFirstLevelName(firstLevelKey)}}</span>
  127. <el-tag size="small" type="info" style="margin-left: 8px;">{{getTotalCount(secondLevel)}}</el-tag>
  128. </template>
  129. <el-sub-menu
  130. v-for="(models, secondLevelKey) in secondLevel"
  131. :key="secondLevelKey"
  132. :index="'model-' + firstLevelKey + '-' + secondLevelKey"
  133. popper-append-to-body="false"
  134. >
  135. <template #title>
  136. <span>{{getSecondLevelName(secondLevelKey)}}</span>
  137. </template>
  138. <el-menu-item
  139. v-for="model in models"
  140. :key="model.id"
  141. :index="'model-' + firstLevelKey + '-' + secondLevelKey + '-' + model.id"
  142. @dblclick="loadModel(model)"
  143. class="model-menu-item"
  144. >
  145. <span class="model-name" :class="{ 'model-loaded': isModelLoaded(model.id) }">{{ model.name }}</span>
  146. <el-popconfirm
  147. v-if="isModelLoaded(model.id)"
  148. title="确定删除吗?"
  149. confirmButtonText="确认"
  150. cancelButtonText="取消"
  151. @confirm="removeModel(model)"
  152. >
  153. <template #reference>
  154. <el-icon
  155. class="delete-model-icon"
  156. title="删除模型"
  157. >
  158. <CircleClose />
  159. </el-icon>
  160. </template>
  161. </el-popconfirm>
  162. </el-menu-item>
  163. </el-sub-menu>
  164. </el-sub-menu>
  165. </el-sub-menu>
  166. <!-- 台风菜单 -->
  167. <el-sub-menu index="typhoon">
  168. <template #title>
  169. <img src="/img/typhoon.png" alt="" class="typhoon-icon" />
  170. <span>台风</span>
  171. </template>
  172. </el-sub-menu>
  173. </el-menu>
  174. </el-aside>
  175. <!-- 侧边栏部分--end -->
  176. <!-- 右侧内容展示部分 -->
  177. <el-main>
  178. <loading-bar></loading-bar>
  179. <!-- 添加自定义服务组件 -->
  180. <component
  181. v-if="addService"
  182. :is="addService"
  183. :clear-callback="addCustomService"
  184. :add-callback="addCallback"
  185. :get-layer-name="getName"
  186. ></component>
  187. <!-- viewer组件 -->
  188. <component v-if="initViewer" :is="initViewer" :after-initviewer="removeLoad" :opening-animation="true"></component>
  189. <!-- 分析功能组件 -->
  190. <keep-alive>
  191. <component
  192. v-if="view"
  193. :is="view"
  194. :key="view"
  195. :delete-callback="deleteCallback"
  196. data-url="/data/netcdf/result_100_200_9_40.nc"
  197. skylineSpatialAnalysisUrl="http://www.supermapol.com/realspace/services/spatialAnalysis-data_all/restjsr/spatialanalyst/geometry/3d/skylinesectorbody.json"
  198. ></component>
  199. </keep-alive>
  200. <!-- 地质体组件,单独展示,需要销毁 -->
  201. <component v-if="view2" :is="view2" :key="view2"></component>
  202. </el-main>
  203. </el-container>
  204. </template>
  205. <script>
  206. import config from "../../config/views_config.js"; //组件配置
  207. import server_config from "../../config/server_config.js"; //服务配置
  208. import server_flyTo_config from "../../config/server_position_config.js"; //服务坐标配置
  209. import layerManagement from "../../js/common/layerManagement.js"; //图层管理封装方法
  210. import camera from "../../js/common/camera.js"; //相机操作
  211. import loadingBar from "../../components/loading.vue"; //加载动画
  212. import { CircleClose, Plus, CirclePlus } from '@element-plus/icons-vue'; //删除图标
  213. import { listModel } from '@/api/watershed/model'; //模型API
  214. import { getDefaultMapConfig, saveMapConfig } from '@/api/cesium/mapConfig'; //地图配置API
  215. import serviceApi from '@/api/watershed/service'; //自定义服务API
  216. import { ElMessage } from 'element-plus';
  217. import { getToken } from '@/utils/auth'
  218. import axios from 'axios'
  219. export default {
  220. name: "layout-aside",
  221. components: {
  222. loadingBar,
  223. CircleClose
  224. },
  225. data() {
  226. return {
  227. isCollapse: false,
  228. asideWidth: "asideWidth1",
  229. view: "",
  230. view2: "",
  231. initViewer: "",
  232. addService: "",
  233. sceneURL:
  234. "http://www.supermapol.com/realspace/services/3D-ZF_normal/rest/realspace",
  235. config: config,
  236. server_config: server_config,
  237. baseLayerObj: server_config[1].children[0],
  238. terrainLayerObj: null,
  239. addCustomServices: [],
  240. getNames: [], //记录自定义服务名称
  241. varName: null, //临时保存当前自定义名称
  242. loadedCustomServiceIds: [], //已加载的自定义服务ID
  243. modelConfig: {}, //模型菜单配置
  244. modelData: [], //模型数据
  245. loadedModelEntities: [], //已加载的模型实体
  246. saveConfigTimer: null, //保存配置的防抖定时器
  247. lastSaveTime: 0, //上次保存时间戳
  248. };
  249. },
  250. computed: {
  251. groupedModels() {
  252. const grouped = {}
  253. this.modelData.forEach(model => {
  254. const type = model.type || '5/5-1'
  255. const parts = type.split('/')
  256. const firstLevel = parts[0] || '5'
  257. const secondLevel = parts[1] || '5-1'
  258. if (!grouped[firstLevel]) {
  259. grouped[firstLevel] = {}
  260. }
  261. if (!grouped[firstLevel][secondLevel]) {
  262. grouped[firstLevel][secondLevel] = []
  263. }
  264. grouped[firstLevel][secondLevel].push(model)
  265. })
  266. return grouped
  267. }
  268. },
  269. methods: {
  270. async fetchCustomServices() {
  271. try {
  272. console.log('开始从数据库加载自定义服务...');
  273. const response = await serviceApi.getServiceList({});
  274. console.log('获取到的自定义服务:', response);
  275. if (response && response.rows) {
  276. this.addCustomServices = response.rows.map(service => ({
  277. id: service.id || service.serviceId,
  278. name: service.name || service.serviceName,
  279. type: service.type || service.serviceType,
  280. url: service.url || service.serviceUrl,
  281. tokenRequired: service.tokenRequired || service.token_required,
  282. token: service.token || service.serviceToken,
  283. status: service.status,
  284. layers: [{
  285. type: service.type || service.serviceType,
  286. layerName: service.name || service.serviceName
  287. }]
  288. }));
  289. console.log('处理后的自定义服务列表:', this.addCustomServices);
  290. }
  291. } catch (error) {
  292. console.error('加载自定义服务失败:', error);
  293. }
  294. },
  295. getModelTypeOptions() {
  296. return [
  297. {
  298. value: '1',
  299. label: '水利工程实体',
  300. children: [
  301. { value: '1-1', label: '水库工程' },
  302. { value: '1-2', label: '水闸工程' },
  303. { value: '1-3', label: '泵站工程' },
  304. { value: '1-4', label: '灌区工程' },
  305. { value: '1-5', label: '堤防与护岸工程' }
  306. ]
  307. },
  308. {
  309. value: '2',
  310. label: '水系水利设施',
  311. children: [
  312. { value: '2-1', label: '河流' },
  313. { value: '2-2', label: '湖泊与水库水面' },
  314. { value: '2-3', label: '渠道与输水管道' },
  315. { value: '2-4', label: '河口与海岸带' }
  316. ]
  317. },
  318. {
  319. value: '3',
  320. label: '地理环境要素',
  321. children: [
  322. { value: '3-1', label: '地形地貌' },
  323. { value: '3-2', label: '行政区划' },
  324. { value: '3-3', label: '重要地物' }
  325. ]
  326. },
  327. {
  328. value: '4',
  329. label: '自然生态景观',
  330. children: [
  331. { value: '4-1', label: '湖泊湿地' },
  332. { value: '4-2', label: '森林公园' },
  333. { value: '4-3', label: '地质公园' },
  334. { value: '4-4', label: '海岸带景观' }
  335. ]
  336. },
  337. {
  338. value: '5',
  339. label: '模型集与项目',
  340. children: [
  341. { value: '5-1', label: '待分类模型' },
  342. { value: '5-2', label: 'XX市防洪排涝工程' }
  343. ]
  344. }
  345. ]
  346. },
  347. getFirstLevelName(firstLevelKey) {
  348. const options = this.getModelTypeOptions()
  349. const option = options.find(opt => opt.value === firstLevelKey)
  350. return option ? option.label : firstLevelKey
  351. },
  352. getSecondLevelName(secondLevelKey) {
  353. const options = this.getModelTypeOptions()
  354. for (const opt of options) {
  355. if (opt.children) {
  356. const child = opt.children.find(c => c.value === secondLevelKey)
  357. if (child) return child.label
  358. }
  359. }
  360. return secondLevelKey
  361. },
  362. getTotalCount(secondLevel) {
  363. let count = 0
  364. for (const key in secondLevel) {
  365. count += secondLevel[key].length
  366. }
  367. return count
  368. },
  369. getTypeDisplayName(type) {
  370. const typeMapping = {
  371. 'RESERVOIR': '水库',
  372. 'HYDROPOWER': '水电站',
  373. 'IRRIGATION': '灌溉',
  374. 'FLOOD_CONTROL': '防洪',
  375. 'CANAL': '渠道',
  376. 'PUMPING_STATION': '泵站',
  377. 'OTHER': '其他'
  378. }
  379. if (typeMapping[type]) {
  380. return typeMapping[type]
  381. }
  382. return type
  383. },
  384. async fetchModels() {
  385. try {
  386. const data = await listModel({})
  387. const modelList = data.rows || []
  388. console.log('获取到的模型数据:', modelList)
  389. this.modelData = modelList.map(model => {
  390. console.log('模型详情:', model)
  391. return {
  392. id: model.id,
  393. name: model.name,
  394. type: model.type,
  395. format: model.format,
  396. uploadUnit: model.uploadUnit || '未知单位',
  397. filePath: model.filePath || model.file_path || model.url || '',
  398. coordinates: model.coordinates,
  399. originalData: model
  400. }
  401. })
  402. console.log('处理后的模型数据:', this.modelData)
  403. if (this.modelData.length > 0) {
  404. this.modelConfig = {
  405. id: 'model',
  406. icon: 'iconfont iconmoxing',
  407. name: '模型'
  408. }
  409. }
  410. } catch (error) {
  411. console.error('获取模型数据错误:', error)
  412. }
  413. },
  414. async loadModel(model, shouldFlyTo = true) {
  415. console.log('========== 开始加载模型 ==========')
  416. console.log('模型名称:', model.name)
  417. console.log('模型路径:', model.filePath)
  418. console.log('经纬度:', model.coordinates)
  419. console.log('模型格式:', model.format)
  420. console.log('viewer状态:', viewer)
  421. if (!viewer) {
  422. ElMessage.error('Cesium viewer 未初始化')
  423. return
  424. }
  425. if (!viewer.scene) {
  426. ElMessage.error('Cesium scene 未初始化')
  427. return
  428. }
  429. console.log('检查模型是否已加载:', model.id)
  430. console.log('已加载的模型实体:', this.loadedModelEntities)
  431. console.log('isModelLoaded结果:', this.isModelLoaded(model.id))
  432. if (this.isModelLoaded(model.id)) {
  433. console.log('模型已加载,先卸载再重新加载')
  434. this.removeModel(model)
  435. }
  436. try {
  437. if (!model.filePath) {
  438. ElMessage.warning('模型文件路径不存在,请检查数据库中的filePath字段')
  439. console.error('模型路径为空,模型数据:', model)
  440. return
  441. }
  442. let modelUrl = model.filePath
  443. console.log('原始模型路径:', modelUrl)
  444. // 修复路径中的双斜杠问题
  445. modelUrl = modelUrl.replace(/\/+/g, '/')
  446. console.log('修复后的模型路径:', modelUrl)
  447. // 检查是否是本地路径或相对路径
  448. const isLocalPath = modelUrl.startsWith('/') || modelUrl.startsWith('./') || modelUrl.startsWith('../') || !modelUrl.startsWith('http')
  449. if (isLocalPath) {
  450. console.log('检测到本地路径,通过后端API获取模型文件...')
  451. const baseURL = import.meta.env.VITE_APP_BASE_API || ''
  452. const apiUrl = baseURL + "/common/download/resource?resource=" + encodeURIComponent(modelUrl)
  453. console.log('API URL:', apiUrl)
  454. try {
  455. const response = await axios({
  456. method: 'get',
  457. url: apiUrl,
  458. responseType: 'blob',
  459. headers: { 'Authorization': 'Bearer ' + getToken() }
  460. })
  461. console.log('API响应状态:', response.status)
  462. console.log('Content-Type:', response.headers['content-type'])
  463. console.log('响应数据大小:', response.data.size, 'bytes')
  464. if (response.data.size === 0) {
  465. console.error('下载的模型文件为空')
  466. ElMessage.error('模型文件为空,请检查文件是否有效')
  467. return
  468. }
  469. let mimeType = 'model/gltf-binary'
  470. const format = model.format ? model.format.toUpperCase() : 'GLB'
  471. const fileExtension = model.filePath ? model.filePath.split('.').pop().toLowerCase() : ''
  472. console.log('模型格式:', format)
  473. console.log('文件扩展名:', fileExtension)
  474. if (format === 'GLTF' || fileExtension === 'gltf') {
  475. mimeType = 'model/gltf+json'
  476. } else if (format === 'GLB' || fileExtension === 'glb') {
  477. mimeType = 'model/gltf-binary'
  478. } else if (format === 'OBJ' || fileExtension === 'obj') {
  479. mimeType = 'model/obj'
  480. }
  481. console.log('使用的MIME类型:', mimeType)
  482. const blob = new Blob([response.data], { type: mimeType })
  483. modelUrl = URL.createObjectURL(blob)
  484. console.log('创建的Blob URL:', modelUrl)
  485. // ElMessage.success('模型文件下载成功,开始加载...')
  486. } catch (apiError) {
  487. console.error('通过API获取模型文件失败:', apiError)
  488. console.error('错误详情:', apiError.response)
  489. ElMessage.error('无法获取模型文件,请检查文件路径或权限')
  490. return
  491. }
  492. } else {
  493. console.log('使用远程URL直接加载模型:', modelUrl)
  494. }
  495. let position
  496. if (model.coordinates && model.coordinates.trim() !== '' && model.coordinates !== '未设置') {
  497. try {
  498. const coords = model.coordinates.split(',').map(c => parseFloat(c.trim()))
  499. console.log('解析后的坐标数组:', coords)
  500. if (coords.length >= 2 && !isNaN(coords[0]) && !isNaN(coords[1])) {
  501. const lon = coords[0]
  502. const lat = coords[1]
  503. const height = coords[2] || 0
  504. // 获取地面高度
  505. const cartographic = Cesium.Cartographic.fromDegrees(lon, lat)
  506. const terrainHeight = viewer.scene.globe.getHeight(cartographic)
  507. const groundHeight = terrainHeight || 0
  508. // 模型高度 = 地面高度 + 用户指定的高度
  509. position = Cesium.Cartesian3.fromDegrees(lon, lat, groundHeight + height)
  510. console.log('使用经纬度定位 - 经度:', lon, '纬度:', lat, '地面高度:', groundHeight, '模型高度:', height, '总高度:', groundHeight + height)
  511. } else {
  512. console.warn('经纬度格式不正确或为NaN,使用默认位置')
  513. position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
  514. }
  515. } catch (e) {
  516. console.error('解析经纬度失败:', e)
  517. position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
  518. }
  519. } else {
  520. console.warn('经纬度为空或未设置,使用默认位置')
  521. position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
  522. }
  523. console.log('最终位置:', position)
  524. if (!position) {
  525. ElMessage.error('位置计算失败')
  526. return
  527. }
  528. const entity = viewer.entities.add({
  529. name: model.id,
  530. position: position,
  531. model: {
  532. uri: modelUrl,
  533. minimumPixelSize: 0,
  534. maximumScale: 1,
  535. scale: 1.0,
  536. show: true,
  537. distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0.0, 50000000.0),
  538. heightReference: Cesium.HeightReference.NONE
  539. }
  540. })
  541. console.log('实体已添加:', entity)
  542. console.log('模型URL:', modelUrl)
  543. console.log('模型URI:', entity.model.uri)
  544. this.loadedModelEntities.push(entity)
  545. if (shouldFlyTo) {
  546. setTimeout(() => {
  547. try {
  548. viewer.flyTo(entity, {
  549. duration: 2,
  550. offset: new Cesium.HeadingPitchRange(Cesium.Math.toRadians(0), Cesium.Math.toRadians(-30), 5)
  551. })
  552. console.log('开始飞行到模型位置')
  553. } catch (flyError) {
  554. console.error('飞行失败:', flyError)
  555. ElMessage.warning('模型已加载,但跳转失败')
  556. }
  557. }, 100)
  558. }
  559. this.saveLoadedServices()
  560. // ElMessage.success(`模型 "${model.name}" 加载成功`)
  561. console.log('========== 模型加载完成 ==========')
  562. } catch (error) {
  563. console.error('========== 加载模型错误 ==========')
  564. console.error('错误信息:', error)
  565. console.error('错误堆栈:', error.stack)
  566. ElMessage.error('加载模型失败: ' + (error.message || '未知错误'))
  567. }
  568. },
  569. isModelLoaded(modelId) {
  570. return this.loadedModelEntities.some(entity => entity.name === modelId)
  571. },
  572. removeModel(model) {
  573. const index = this.loadedModelEntities.findIndex(entity => entity.name === model.id)
  574. if (index !== -1) {
  575. const entity = this.loadedModelEntities[index]
  576. viewer.entities.remove(entity)
  577. this.loadedModelEntities.splice(index, 1)
  578. this.saveLoadedServices()
  579. // ElMessage.success(`模型 "${model.name}" 已删除`)
  580. }
  581. },
  582. // 区分地质体组件(需销毁)和其他组件
  583. change(val, val2) {
  584. if (val2) {
  585. this.view = "";
  586. if (val === this.view2) {
  587. this.view2 = "";
  588. return;
  589. }
  590. this.view2 = val;
  591. } else {
  592. this.view2 = "";
  593. if (val === this.view) {
  594. this.view = "";
  595. return;
  596. }
  597. this.view = val;
  598. }
  599. },
  600. //添加公共服务
  601. addWebServe(obj, shouldFlyTo = true) {
  602. if (obj.state === 0) {
  603. // add mvt
  604. if (obj.type === "MVT") {
  605. layerManagement.addMvtLayer(obj.proxiedUrl, obj.name, () => {
  606. obj.state = 1;
  607. window.store.actions.setChangeLayers();
  608. this.saveLoadedServices();
  609. });
  610. return;
  611. }
  612. // add scene
  613. layerManagement.addScene(
  614. obj.proxiedUrl,
  615. { autoSetView: false },
  616. layers => {
  617. obj.state = 1;
  618. window.store.actions.setChangeLayers();
  619. // 白膜添加线框
  620. if (obj.style && obj.style.fillStyle) {
  621. layers[0].style3D.lineColor = Cesium.Color.fromCssColorString(
  622. "rgb(67,67,67)"
  623. );
  624. layers[0].style3D.fillStyle =
  625. Cesium.FillStyle[obj.style.fillStyle];
  626. }
  627. this.saveLoadedServices();
  628. if (shouldFlyTo) {
  629. this.flyTo(obj.name, server_flyTo_config);
  630. }
  631. }
  632. );
  633. } else {
  634. if (shouldFlyTo) {
  635. this.flyTo(obj.name, server_flyTo_config);
  636. }
  637. }
  638. },
  639. // 添加底图
  640. addBaseLayer(obj) {
  641. if (this.baseLayerObj.type === obj.type) return;
  642. this.baseLayerObj.state = 0;
  643. obj.state = 1;
  644. this.baseLayerObj = obj;
  645. let type = obj.type;
  646. let url = obj.proxiedUrl;
  647. let imageryLayerCollection = viewer.scene.globe._imageryLayerCollection;
  648. let layer = imageryLayerCollection.get(0);
  649. let imageryProvider;
  650. if (imageryLayerCollection.get(2)) {
  651. imageryLayerCollection.remove(imageryLayerCollection.get(2));
  652. }
  653. if (imageryLayerCollection.get(1)) {
  654. imageryLayerCollection.remove(imageryLayerCollection.get(1));
  655. }
  656. switch (type) {
  657. case "BINGMAP":
  658. imageryProvider = new Cesium.BingMapsImageryProvider({
  659. url: url,
  660. key:
  661. "Aq0D7MCY5ErORA9vrwFtfE9aancUq5J6uNjw0GieF0ostaIrVuJZ8ScXxNHHvEwS"
  662. });
  663. break;
  664. case "TIANDITU":
  665. imageryProvider = new Cesium.TiandituImageryProvider({
  666. url: url,
  667. token: "3fb1e9fda20ee995dc815c8243553ce8"
  668. });
  669. break;
  670. case "IMAGE":
  671. imageryProvider = new Cesium.SingleTileImageryProvider({
  672. url: url
  673. });
  674. break;
  675. case "OSM":
  676. imageryProvider = new Cesium.createOpenStreetMapImageryProvider({
  677. url: url
  678. });
  679. break;
  680. case "MAPBOX":
  681. imageryProvider = new Cesium.MapboxImageryProvider({
  682. mapId: "mapbox.dark"
  683. });
  684. break;
  685. case "SUPERMAPDARK":
  686. imageryProvider = new Cesium.SuperMapImageryProvider({
  687. url: url
  688. });
  689. break;
  690. case "SUPERMAPLIGHT":
  691. imageryProvider = new Cesium.SuperMapImageryProvider({
  692. url: url
  693. });
  694. break;
  695. case "GRIDIMAGERY":
  696. imageryProvider = imageryProvider;
  697. break;
  698. default:
  699. imageryProvider = new Cesium.SingleTileImageryProvider({
  700. url: url
  701. });
  702. break;
  703. }
  704. if (type != "GRIDIMAGERY") {
  705. imageryLayerCollection.addImageryProvider(imageryProvider, 0);
  706. imageryLayerCollection.remove(layer);
  707. }
  708. if (type == "GRIDIMAGERY") {
  709. imageryLayerCollection.addImageryProvider(
  710. new Cesium.TileCoordinatesImageryProvider(),
  711. 1
  712. );
  713. imageryLayerCollection.addImageryProvider(
  714. new Cesium.GridImageryProvider(),
  715. 2
  716. );
  717. }
  718. this.saveLoadedServices();
  719. },
  720. // 添加在线地形
  721. addOnlineTerrain(obj) {
  722. if (this.terrainLayerObj) this.terrainLayerObj.state = 0;
  723. obj.state = 1;
  724. this.terrainLayerObj = obj;
  725. let type = obj.type;
  726. let url = obj.proxiedUrl;
  727. switch (type) {
  728. case "STKTerrain":
  729. viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
  730. url: url,
  731. isSct: false
  732. });
  733. break;
  734. case "tianDiTuTerrain":
  735. var t_Provider = new Cesium.TiandituTerrainProvider({
  736. token: "3fb1e9fda20ee995dc815c8243553ce8"
  737. });
  738. viewer.terrainProvider = t_Provider;
  739. break;
  740. case "supermapOnlineTerrain":
  741. viewer.terrainProvider = new Cesium.SCTTerrainProvider({
  742. urls: [url]
  743. });
  744. break;
  745. default:
  746. break;
  747. }
  748. this.saveLoadedServices();
  749. },
  750. // 移除地形
  751. removeTerrainLayer() {
  752. viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider({
  753. ellipsoid: viewer.scene.globe.ellipsoid
  754. });
  755. this.terrainLayerObj = null;
  756. },
  757. addDatas(id, obj, shouldFlyTo = true) {
  758. switch (id) {
  759. case "webServe":
  760. this.addWebServe(obj, shouldFlyTo);
  761. break;
  762. case "baseLayer":
  763. this.addBaseLayer(obj);
  764. break;
  765. case "onlineTerrain":
  766. this.addOnlineTerrain(obj);
  767. break;
  768. }
  769. },
  770. // 初始化viewer后的传入回调:添加底图和清除加载动画
  771. async removeLoad() {
  772. this.isRestoring = true; // 标记正在恢复,禁止保存
  773. setTimeout(() => {
  774. let loading = document.getElementById("loadingbar");
  775. if (loading) loading.remove(); //移除加载动画
  776. document.getElementById("el-aside").classList.remove("disable"); //解除禁止点击
  777. }, 1000);
  778. // 等待自定义服务加载完成后再恢复
  779. setTimeout(async () => {
  780. await this.fetchCustomServices();
  781. await this.restoreLoadedServices();
  782. this.isRestoring = false; // 恢复完成,允许保存
  783. }, 1500);
  784. },
  785. // 删除场景公共服务
  786. DeleteDates(datatype, obj) {
  787. switch (datatype) {
  788. case "webServe":
  789. obj.layers.forEach(layer => {
  790. layerManagement.layersDelete(layer.type, layer.layerName, () => {
  791. obj.state = 0;
  792. window.store.actions.setChangeLayers();
  793. this.saveLoadedServices();
  794. });
  795. });
  796. break;
  797. case "baseLayer":
  798. break;
  799. case "onlineTerrain":
  800. this.removeTerrainLayer();
  801. obj.state = 0;
  802. this.saveLoadedServices();
  803. break;
  804. }
  805. },
  806. isServiceLoaded(service) {
  807. if (!service) {
  808. return false;
  809. }
  810. const serviceId = service.id || service.serviceId;
  811. return this.loadedCustomServiceIds.includes(serviceId);
  812. },
  813. async loadCustomService(service, flyTo = true, shouldSave = true) {
  814. console.log('========== 开始加载自定义服务 ==========');
  815. console.log('服务名称:', service.name);
  816. console.log('服务类型:', service.type);
  817. console.log('服务URL:', service.url);
  818. console.log('是否跳转视角:', flyTo);
  819. console.log('viewer状态:', viewer);
  820. if (!viewer) {
  821. ElMessage.error('Cesium viewer 未初始化');
  822. return;
  823. }
  824. if (this.isServiceLoaded(service)) {
  825. console.log('服务已加载,执行定位');
  826. if (flyTo) {
  827. const type = service.type || (service.layers && service.layers[0] ? service.layers[0].type : null);
  828. if (type === 'SCENE' || type === 'S3M') {
  829. const layerNames = service.loadedLayerNames || [service.name];
  830. let foundLayer = null;
  831. for (const name of layerNames) {
  832. foundLayer = viewer.scene.layers.find(name);
  833. if (foundLayer) break;
  834. }
  835. if (foundLayer) {
  836. console.log('跳转到图层:', foundLayer.name);
  837. viewer.flyTo(foundLayer, { duration: 2 });
  838. } else if (viewer.scene.layers.layerQueue && viewer.scene.layers.layerQueue.length > 0) {
  839. viewer.flyTo(viewer.scene.layers.layerQueue[0], { duration: 2 });
  840. }
  841. } else {
  842. const layerName = service.name;
  843. camera.flyByLayerName(type, layerName);
  844. }
  845. }
  846. return;
  847. }
  848. try {
  849. const type = service.type || (service.layers && service.layers[0] ? service.layers[0].type : null);
  850. const url = service.url;
  851. const layerName = service.name;
  852. const token = service.token;
  853. const tokenRequired = service.tokenRequired;
  854. const serviceId = service.id || service.serviceId;
  855. const markAsLoaded = () => {
  856. if (!this.loadedCustomServiceIds.includes(serviceId)) {
  857. this.loadedCustomServiceIds.push(serviceId);
  858. }
  859. };
  860. switch (type) {
  861. case 'SCENE':
  862. let sceneOptions = {
  863. autoSetView: flyTo
  864. };
  865. if (tokenRequired && token) {
  866. sceneOptions.SceneToken = token;
  867. }
  868. const beforeLayersCount = viewer.scene.layers ? viewer.scene.layers.layerQueue.length : 0;
  869. console.log('加载前图层数量:', beforeLayersCount);
  870. console.log('是否自动跳转视角:', flyTo);
  871. layerManagement.addScene(url, sceneOptions, (layers) => {
  872. console.log('SCENE加载成功:', layers);
  873. markAsLoaded();
  874. const afterLayers = viewer.scene.layers ? viewer.scene.layers.layerQueue : [];
  875. const newLayers = afterLayers.slice(beforeLayersCount);
  876. console.log('新添加的图层:', newLayers);
  877. const newLayerNames = newLayers.map(l => l.name);
  878. console.log('新图层名称:', newLayerNames);
  879. service.loadedLayerNames = newLayerNames;
  880. const currentPosition = viewer.camera.positionCartographic;
  881. service.defaultCameraPosition = {
  882. longitude: currentPosition.longitude,
  883. latitude: currentPosition.latitude,
  884. height: currentPosition.height,
  885. heading: viewer.camera.heading,
  886. pitch: viewer.camera.pitch,
  887. roll: viewer.camera.roll
  888. };
  889. console.log('记录默认视口位置:', service.defaultCameraPosition);
  890. if (flyTo && newLayers.length > 0) {
  891. camera.flyByLayerName('SCENE', newLayers[0].name);
  892. }
  893. });
  894. break;
  895. case 'S3M':
  896. let scps = [{ url: url, options: { name: layerName } }];
  897. layerManagement.addS3mLayers(scps, (layers) => {
  898. console.log('S3M加载成功:', layers);
  899. markAsLoaded();
  900. if (flyTo) {
  901. camera.flyByLayerName('S3M', layerName);
  902. }
  903. });
  904. break;
  905. case 'IMG':
  906. let imgLayer = layerManagement.addImageLayer(url);
  907. console.log('IMG加载成功:', imgLayer);
  908. markAsLoaded();
  909. if (flyTo) {
  910. camera.flyByLayerName('IMG', layerName);
  911. }
  912. break;
  913. case 'TERRAIN':
  914. let terrainProvider = layerManagement.addTerrainLayer(url, false);
  915. console.log('TERRAIN加载成功:', terrainProvider);
  916. markAsLoaded();
  917. break;
  918. case 'MVT':
  919. layerManagement.addMvtLayer(url, layerName, (mvtlayer) => {
  920. console.log('MVT加载成功:', mvtlayer);
  921. markAsLoaded();
  922. });
  923. break;
  924. default:
  925. ElMessage.warning('不支持的服务类型: ' + type);
  926. return;
  927. }
  928. if (shouldSave) {
  929. ElMessage.success(`服务 "${service.name}" 加载成功`);
  930. }
  931. console.log('========== 自定义服务加载完成 ==========');
  932. if (shouldSave) {
  933. this.saveLoadedServices();
  934. }
  935. } catch (error) {
  936. console.error('加载自定义服务失败:', error);
  937. ElMessage.error('加载服务失败: ' + (error.message || '未知错误'));
  938. }
  939. },
  940. unloadCustomService(service) {
  941. console.log('开始卸载服务:', service);
  942. if (!service) {
  943. console.warn('服务对象为空');
  944. return;
  945. }
  946. const serviceId = service.id || service.serviceId;
  947. const serviceType = service.type || 'SCENE';
  948. if (serviceType === 'SCENE' && service.loadedLayerNames && service.loadedLayerNames.length > 0) {
  949. console.log('使用loadedLayerNames卸载SCENE图层:', service.loadedLayerNames);
  950. service.loadedLayerNames.forEach((layerName, index) => {
  951. console.log('卸载图层:', layerName);
  952. const isLast = index === service.loadedLayerNames.length - 1;
  953. layerManagement.layersDelete('S3M', layerName, () => {
  954. console.log('图层已卸载:', layerName);
  955. if (isLast) {
  956. const idx = this.loadedCustomServiceIds.indexOf(serviceId);
  957. if (idx > -1) {
  958. this.loadedCustomServiceIds.splice(idx, 1);
  959. console.log('已从loadedCustomServiceIds中移除服务:', serviceId);
  960. }
  961. window.store.actions.setChangeLayers();
  962. this.saveLoadedServices();
  963. }
  964. });
  965. });
  966. } else if (service.layers && service.layers.length > 0) {
  967. service.layers.forEach((layer, index) => {
  968. console.log('卸载图层:', layer.type, layer.layerName);
  969. const isLast = index === service.layers.length - 1;
  970. layerManagement.layersDelete(layer.type, layer.layerName, () => {
  971. console.log('图层已卸载:', layer.layerName);
  972. if (isLast) {
  973. const idx = this.loadedCustomServiceIds.indexOf(serviceId);
  974. if (idx > -1) {
  975. this.loadedCustomServiceIds.splice(idx, 1);
  976. console.log('已从loadedCustomServiceIds中移除服务:', serviceId);
  977. }
  978. window.store.actions.setChangeLayers();
  979. this.saveLoadedServices();
  980. }
  981. });
  982. });
  983. } else {
  984. const type = service.type || 'SCENE';
  985. const layerName = service.name;
  986. console.log('卸载参数 - serviceId:', serviceId, 'type:', type, 'layerName:', layerName);
  987. layerManagement.layersDelete(type, layerName, () => {
  988. console.log('服务已卸载:', service.name);
  989. const idx = this.loadedCustomServiceIds.indexOf(serviceId);
  990. if (idx > -1) {
  991. this.loadedCustomServiceIds.splice(idx, 1);
  992. }
  993. window.store.actions.setChangeLayers();
  994. this.saveLoadedServices();
  995. });
  996. }
  997. },
  998. // 控制自定义服务组件显隐
  999. addCustomService() {
  1000. if (this.addService === "") {
  1001. this.addService = "Sm3dCustomService";
  1002. // 重置面板位置到默认值,使用setTimeout确保组件完全渲染
  1003. setTimeout(() => {
  1004. const panel = document.getElementById('CustomService-panel');
  1005. if (panel) {
  1006. panel.style.left = '35%';
  1007. panel.style.top = '25%';
  1008. }
  1009. }, 100);
  1010. } else {
  1011. this.addService = "";
  1012. }
  1013. // 移除自定义服务菜单项的选中状态
  1014. this.$nextTick(() => {
  1015. const menu = this.$refs.menu;
  1016. if (menu) {
  1017. menu.activeIndex = "";
  1018. }
  1019. });
  1020. },
  1021. // 自定义服务添加回调函数
  1022. addCallback(layers, add_type, serviceInfo) {
  1023. console.log('接收到addCallback调用:', layers, add_type, serviceInfo);
  1024. if (!Array.isArray(layers)) {
  1025. layers = [layers];
  1026. }
  1027. let serviceOption = [];
  1028. let serviceUrl = serviceInfo ? serviceInfo.url : "";
  1029. let serviceToken = serviceInfo ? serviceInfo.token : "";
  1030. let isAddToken = serviceInfo ? serviceInfo.tokenRequired : false;
  1031. try {
  1032. for (let i = 0; i < layers.length; i++) {
  1033. if (!layers[i]) {
  1034. console.log('layers[' + i + '] 是 undefined 或 null,跳过');
  1035. continue;
  1036. }
  1037. if (layers[i].hasOwnProperty("_isS3MB")) {
  1038. let option = {
  1039. type: "S3M",
  1040. layerName: layers[i].name
  1041. ? layers[i].name
  1042. : "s3m_" + new Date().getTime().toFixed(6)
  1043. };
  1044. serviceOption.push(option);
  1045. if (!layers[i].maxVisibleAltitude)
  1046. layers[i].maxVisibleAltitude = 8000;
  1047. } else if (viewer.imageryLayers.contains(layers[i])) {
  1048. let option = {
  1049. type: "IMG",
  1050. layerName: layers[i].imageryProvider.tablename
  1051. ? layers[i].imageryProvider.tablename
  1052. : "image" + new Date().getTime().toFixed(6)
  1053. };
  1054. serviceOption.push(option);
  1055. } else if (
  1056. layers[i].imageryLayer &&
  1057. layers[i].imageryLayer._imageryProvider instanceof
  1058. Cesium.MvtProviderGL
  1059. ) {
  1060. let option = {
  1061. type: "MVT",
  1062. layerName: layers[i].name
  1063. ? layers[i].name
  1064. : "mvt" + new Date().getTime().toFixed(6)
  1065. };
  1066. serviceOption.push(option);
  1067. let index = this.addCustomServices.length;
  1068. let obj = {
  1069. name: layers[i].name ? layers[i].name : "自定义服务" + index,
  1070. layers: serviceOption
  1071. };
  1072. this.addCustomServices.push(obj);
  1073. this.addService = "";
  1074. // 存储服务到数据库
  1075. console.log('准备调用saveServiceToDatabase (MVT):', serviceInfo, add_type, obj.name);
  1076. if (serviceInfo && serviceUrl) {
  1077. console.log('调用saveServiceToDatabase (MVT)');
  1078. this.saveServiceToDatabase(serviceInfo, add_type, obj.name);
  1079. } else {
  1080. console.log('serviceInfo或serviceUrl为空 (MVT):', serviceInfo, serviceUrl);
  1081. }
  1082. return;
  1083. } else {
  1084. let option = {
  1085. type: "TERRAIN",
  1086. layerName: layers[i].tablename ? layers[i].tablename : "terrain"
  1087. };
  1088. serviceOption.push(option);
  1089. }
  1090. }
  1091. } catch (error) {
  1092. console.error('处理layers时发生错误:', error);
  1093. }
  1094. if (this.varName) this.getNames.push(this.varName);
  1095. let index = this.addCustomServices.length;
  1096. let len = this.getNames.length - 1;
  1097. let obj = {
  1098. name: this.varName ? this.varName : "自定义服务" + index,
  1099. layers: serviceOption
  1100. };
  1101. this.addCustomServices.push(obj);
  1102. this.addService = "";
  1103. this.varName = undefined;
  1104. // 存储服务到数据库
  1105. console.log('准备调用saveServiceToDatabase (非MVT):', serviceInfo, add_type, obj.name);
  1106. if (serviceInfo && serviceUrl) {
  1107. console.log('调用saveServiceToDatabase (非MVT)');
  1108. this.saveServiceToDatabase(serviceInfo, add_type, obj.name);
  1109. } else {
  1110. console.log('serviceInfo或serviceUrl为空 (非MVT):', serviceInfo, serviceUrl);
  1111. }
  1112. if ((add_type && add_type === "SCENE") || add_type === "TERRAIN") return;
  1113. camera.flyByLayerName(add_type, obj.layers[0].layerName); //定位
  1114. },
  1115. // 保存服务到数据库
  1116. saveServiceToDatabase(serviceInfo, serviceType, serviceName) {
  1117. console.log('开始保存服务到数据库:', serviceInfo, serviceType, serviceName);
  1118. const serviceData = {
  1119. name: serviceName,
  1120. type: serviceType,
  1121. url: serviceInfo.url,
  1122. tokenRequired: serviceInfo.tokenRequired ? 1 : 0,
  1123. token: serviceInfo.token,
  1124. status: 'NORMAL'
  1125. };
  1126. console.log('准备保存服务:', serviceData);
  1127. serviceApi.addService(serviceData)
  1128. .then(response => {
  1129. console.log('服务保存成功:', response);
  1130. const newServiceId = response.data || response.serviceId;
  1131. if (newServiceId) {
  1132. const service = this.addCustomServices.find(s => s.name === serviceName);
  1133. if (service) {
  1134. service.id = newServiceId;
  1135. service.url = serviceInfo.url;
  1136. service.token = serviceInfo.token;
  1137. service.tokenRequired = serviceInfo.tokenRequired;
  1138. service.type = serviceType;
  1139. }
  1140. if (!this.loadedCustomServiceIds.includes(newServiceId)) {
  1141. this.loadedCustomServiceIds.push(newServiceId);
  1142. }
  1143. }
  1144. })
  1145. .catch(error => {
  1146. console.error('服务保存失败:', error);
  1147. if (error.response) {
  1148. console.error('错误响应状态:', error.response.status);
  1149. console.error('错误响应数据:', error.response.data);
  1150. } else if (error.request) {
  1151. console.error('错误请求:', error.request);
  1152. } else {
  1153. console.error('错误信息:', error.message);
  1154. }
  1155. });
  1156. },
  1157. // 自定义服务获取名称回调
  1158. getName(name) {
  1159. this.varName = name;
  1160. },
  1161. // 添加服务飞行函数(公共服务配置好的定位)
  1162. flyTo(webName, obj) {
  1163. let cameraParam = obj[webName];
  1164. if (cameraParam) {
  1165. viewer.scene.camera.flyTo({
  1166. destination: new Cesium.Cartesian3(
  1167. cameraParam.Cartesian3.x,
  1168. cameraParam.Cartesian3.y,
  1169. cameraParam.Cartesian3.z
  1170. ),
  1171. orientation: {
  1172. heading: cameraParam.heading,
  1173. pitch: cameraParam.pitch,
  1174. roll: cameraParam.roll
  1175. },
  1176. duration: 0
  1177. });
  1178. return;
  1179. } else {
  1180. }
  1181. },
  1182. //图层管理右键删除回调
  1183. deleteCallback(node_rightClick) {
  1184. this.server_config[0].children.forEach(sceneObj => {
  1185. sceneObj.layers.forEach(layerObj => {
  1186. if (layerObj.layerName === node_rightClick.label) {
  1187. sceneObj.state = 0;
  1188. return;
  1189. }
  1190. });
  1191. });
  1192. },
  1193. // 保存已加载的服务到数据库 (带防抖)
  1194. saveLoadedServices() {
  1195. if (this.isRestoring) {
  1196. console.log('正在恢复配置,跳过保存');
  1197. return;
  1198. }
  1199. if (this.saveConfigTimer) {
  1200. clearTimeout(this.saveConfigTimer);
  1201. }
  1202. this.saveConfigTimer = setTimeout(async () => {
  1203. const now = Date.now();
  1204. if (now - this.lastSaveTime < 2000) {
  1205. console.log('距离上次保存不足2秒,跳过保存');
  1206. return;
  1207. }
  1208. try {
  1209. const loadedServices = {
  1210. baseLayer: this.baseLayerObj ? {
  1211. type: this.baseLayerObj.type,
  1212. name: this.baseLayerObj.name,
  1213. proxiedUrl: this.baseLayerObj.proxiedUrl
  1214. } : null,
  1215. terrainLayer: this.terrainLayerObj ? {
  1216. type: this.terrainLayerObj.type,
  1217. name: this.terrainLayerObj.name,
  1218. proxiedUrl: this.terrainLayerObj.proxiedUrl
  1219. } : null,
  1220. webServices: this.server_config[0].children
  1221. .filter(service => service.state === 1)
  1222. .map(service => ({
  1223. type: service.type,
  1224. name: service.name,
  1225. proxiedUrl: service.proxiedUrl,
  1226. layers: service.layers,
  1227. style: service.style
  1228. }))
  1229. };
  1230. const loadedModels = this.loadedModelEntities.map(entity => {
  1231. const model = this.modelData.find(m => m.id === entity.name);
  1232. return model ? {
  1233. id: model.id,
  1234. name: model.name,
  1235. type: model.type,
  1236. filePath: model.filePath,
  1237. coordinates: model.coordinates,
  1238. format: model.format
  1239. } : null;
  1240. }).filter(m => m !== null);
  1241. const loadedCustomServices = this.addCustomServices
  1242. .filter(service => this.loadedCustomServiceIds.includes(service.id || service.serviceId))
  1243. .map(service => ({
  1244. id: service.id || service.serviceId,
  1245. name: service.name,
  1246. type: service.type,
  1247. url: service.url,
  1248. tokenRequired: service.tokenRequired,
  1249. token: service.token,
  1250. loadedLayerNames: service.loadedLayerNames || []
  1251. }));
  1252. const configData = {
  1253. configName: '默认配置',
  1254. baseLayerType: loadedServices.baseLayer ? loadedServices.baseLayer.type : '',
  1255. baseLayerName: loadedServices.baseLayer ? loadedServices.baseLayer.name : '',
  1256. baseLayerUrl: loadedServices.baseLayer ? loadedServices.baseLayer.proxiedUrl : '',
  1257. terrainLayerType: loadedServices.terrainLayer ? loadedServices.terrainLayer.type : '',
  1258. terrainLayerName: loadedServices.terrainLayer ? loadedServices.terrainLayer.name : '',
  1259. terrainLayerUrl: loadedServices.terrainLayer ? loadedServices.terrainLayer.proxiedUrl : '',
  1260. webServices: JSON.stringify(loadedServices.webServices),
  1261. loadedModels: JSON.stringify(loadedModels),
  1262. loadedCustomServices: JSON.stringify(loadedCustomServices)
  1263. };
  1264. console.log('========== 保存地图配置 ==========');
  1265. await saveMapConfig(configData);
  1266. this.lastSaveTime = Date.now();
  1267. console.log('地图配置已保存到数据库');
  1268. } catch (error) {
  1269. console.error('保存地图配置失败:', error);
  1270. console.error('错误详情:', error.message);
  1271. if (error.response) {
  1272. console.error('响应状态:', error.response.status);
  1273. console.error('响应数据:', error.response.data);
  1274. }
  1275. }
  1276. }, 500);
  1277. },
  1278. // 从数据库恢复已加载的服务和模型
  1279. async restoreLoadedServices() {
  1280. try {
  1281. console.log('========== 开始恢复地图配置 ==========');
  1282. const response = await getDefaultMapConfig();
  1283. console.log('API响应:', response);
  1284. if (!response || !response.data) {
  1285. console.log('没有找到保存的地图配置,使用默认配置');
  1286. return;
  1287. }
  1288. const config = response.data;
  1289. console.log('保存的配置:', config);
  1290. let loadedServices = null;
  1291. let loadedModels = null;
  1292. // 保存需要恢复的自定义服务信息(不立即设置loadedCustomServiceIds,避免跳过加载步骤)
  1293. let customServicesToLoad = [];
  1294. if (config.loadedCustomServices) {
  1295. try {
  1296. const savedCustomServices = JSON.parse(config.loadedCustomServices);
  1297. console.log('解析的自定义服务配置:', savedCustomServices);
  1298. if (savedCustomServices && savedCustomServices.length > 0) {
  1299. customServicesToLoad = savedCustomServices.map(s => ({
  1300. id: s.id,
  1301. loadedLayerNames: s.loadedLayerNames || []
  1302. }));
  1303. console.log('需要恢复的自定义服务:', customServicesToLoad);
  1304. }
  1305. } catch (e) {
  1306. console.error('解析自定义服务配置失败:', e);
  1307. }
  1308. }
  1309. // 解析服务配置
  1310. if (config.webServices) {
  1311. try {
  1312. loadedServices = JSON.parse(config.webServices);
  1313. console.log('解析的服务配置:', loadedServices);
  1314. } catch (e) {
  1315. console.error('解析服务配置失败:', e);
  1316. }
  1317. }
  1318. // 解析模型配置
  1319. if (config.loadedModels) {
  1320. try {
  1321. loadedModels = JSON.parse(config.loadedModels);
  1322. console.log('解析的模型配置:', loadedModels);
  1323. } catch (e) {
  1324. console.error('解析模型配置失败:', e);
  1325. }
  1326. }
  1327. // 恢复底图
  1328. if (config.baseLayerType) {
  1329. const baseLayer = this.server_config[1].children.find(
  1330. layer => layer.type === config.baseLayerType
  1331. );
  1332. if (baseLayer) {
  1333. console.log('恢复底图:', baseLayer.name);
  1334. await new Promise(resolve => {
  1335. this.addBaseLayer(baseLayer);
  1336. setTimeout(resolve, 500);
  1337. });
  1338. }
  1339. }
  1340. // 恢复地形
  1341. if (config.terrainLayerType) {
  1342. const terrainLayer = this.server_config[2].children.find(
  1343. layer => layer.type === config.terrainLayerType
  1344. );
  1345. if (terrainLayer) {
  1346. await new Promise(resolve => {
  1347. this.addOnlineTerrain(terrainLayer);
  1348. setTimeout(resolve, 500);
  1349. });
  1350. }
  1351. }
  1352. // 恢复公共服务
  1353. if (loadedServices && loadedServices.length > 0) {
  1354. for (const webService of loadedServices) {
  1355. const service = this.server_config[0].children.find(
  1356. s => s.name === webService.name
  1357. );
  1358. if (service) {
  1359. console.log('恢复公共服务:', service.name);
  1360. await new Promise(resolve => {
  1361. service.state = 0;
  1362. this.addWebServe(service, false);
  1363. setTimeout(resolve, 500);
  1364. });
  1365. }
  1366. }
  1367. }
  1368. // 恢复模型
  1369. if (loadedModels && loadedModels.length > 0) {
  1370. for (const modelData of loadedModels) {
  1371. const model = this.modelData.find(m => m.id === modelData.id);
  1372. if (model && !this.isModelLoaded(model.id)) {
  1373. await new Promise(resolve => {
  1374. this.loadModel(model, false);
  1375. setTimeout(resolve, 1000);
  1376. });
  1377. }
  1378. }
  1379. }
  1380. // 恢复自定义服务 (初始化时不跳转视角,不保存)
  1381. if (customServicesToLoad && customServicesToLoad.length > 0) {
  1382. for (const serviceInfo of customServicesToLoad) {
  1383. const customService = this.addCustomServices.find(
  1384. s => (s.id || s.serviceId) === serviceInfo.id
  1385. );
  1386. if (customService && !this.isServiceLoaded(customService)) {
  1387. // 恢复 loadedLayerNames
  1388. if (serviceInfo.loadedLayerNames && serviceInfo.loadedLayerNames.length > 0) {
  1389. customService.loadedLayerNames = serviceInfo.loadedLayerNames;
  1390. console.log('恢复服务loadedLayerNames:', customService.name, serviceInfo.loadedLayerNames);
  1391. }
  1392. console.log('恢复自定义服务:', customService.name);
  1393. await new Promise(resolve => {
  1394. this.loadCustomService(customService, false, false);
  1395. setTimeout(resolve, 1000);
  1396. });
  1397. }
  1398. }
  1399. }
  1400. console.log('地图配置恢复完成');
  1401. } catch (error) {
  1402. console.error('恢复地图配置失败:', error);
  1403. console.error('错误详情:', error.message);
  1404. console.error('错误堆栈:', error.stack);
  1405. if (error.response) {
  1406. console.error('响应状态:', error.response.status);
  1407. console.error('响应数据:', error.response.data);
  1408. }
  1409. }
  1410. },
  1411. // 清除所有持久化数据
  1412. clearPersistence() {
  1413. localStorage.removeItem('cesium_loaded_services');
  1414. localStorage.removeItem('cesium_loaded_models');
  1415. console.log('持久化数据已清除');
  1416. }
  1417. },
  1418. watch: {
  1419. isCollapse(val) {
  1420. if (val) {
  1421. this.asideWidth = "asideWidth2";
  1422. } else {
  1423. this.asideWidth = "asideWidth1";
  1424. }
  1425. }
  1426. },
  1427. mounted() {
  1428. document.getElementById("el-aside").classList.add("disable"); //禁止点击
  1429. // 直接初始化 viewer,不使用 window.onload
  1430. this.$nextTick(() => {
  1431. this.initViewer = "Sm3dViewer";
  1432. // 获取模型数据
  1433. this.fetchModels();
  1434. // 从数据库加载自定义服务
  1435. this.fetchCustomServices();
  1436. });
  1437. }
  1438. };
  1439. </script>
  1440. <style lang="scss" scoped>
  1441. .container {
  1442. position: relative;
  1443. z-index: 1;
  1444. width: 100%;
  1445. height: 100%;
  1446. pointer-events: auto;
  1447. }
  1448. /* 侧边栏宽度控制 */
  1449. .asideWidth1 {
  1450. width: 280px !important;
  1451. transition-duration: 0.3s;
  1452. height: 100% !important;
  1453. }
  1454. .asideWidth2 {
  1455. transition-duration: 0.5s;
  1456. width: 40px !important;
  1457. height: 100% !important;
  1458. display: flex !important;
  1459. justify-content: center !important;
  1460. }
  1461. /* 确保el-container能够铺满整个容器 */
  1462. .el-container {
  1463. height: 100% !important;
  1464. width: 100% !important;
  1465. pointer-events: auto;
  1466. }
  1467. /* 确保el-aside能够正确接收点击事件 */
  1468. .el-aside {
  1469. position: relative;
  1470. z-index: 1000;
  1471. pointer-events: auto;
  1472. }
  1473. /* 确保el-main能够铺满整个容器 */
  1474. .el-main {
  1475. position: relative;
  1476. z-index: 1;
  1477. height: 100% !important;
  1478. width: 100% !important;
  1479. padding: 0 !important;
  1480. margin: 0 !important;
  1481. pointer-events: auto;
  1482. }
  1483. /* 确保cesiumContainer能够铺满整个el-main */
  1484. #cesiumContainer {
  1485. position: relative;
  1486. z-index: 10;
  1487. height: 100% !important;
  1488. width: 100% !important;
  1489. pointer-events: auto;
  1490. }
  1491. /* 确保cesium-viewer能够铺满整个cesiumContainer */
  1492. .cesium-viewer {
  1493. height: 100% !important;
  1494. width: 100% !important;
  1495. pointer-events: auto;
  1496. }
  1497. /* 确保cesium-widget能够铺满整个cesium-viewer */
  1498. .cesium-widget {
  1499. height: 100% !important;
  1500. width: 100% !important;
  1501. pointer-events: auto;
  1502. }
  1503. /* 确保canvas能够铺满整个cesium-widget */
  1504. .cesium-widget canvas {
  1505. position: relative;
  1506. z-index: 5;
  1507. height: 100% !important;
  1508. width: 100% !important;
  1509. pointer-events: auto;
  1510. }
  1511. /* 确保侧边栏菜单能够正确接收点击事件 */
  1512. .el-menu-vertical-demo {
  1513. position: relative;
  1514. z-index: 1001;
  1515. pointer-events: auto;
  1516. }
  1517. /* 确保加载动画不影响点击事件 */
  1518. #loadingbar {
  1519. position: absolute;
  1520. top: 50%;
  1521. left: 50%;
  1522. transform: translate(-50%, -50%);
  1523. z-index: 9999;
  1524. pointer-events: none;
  1525. }
  1526. /* 侧边栏样式增强 */
  1527. .el-aside {
  1528. background-color: #f8f9fa;
  1529. overflow-y: scroll;
  1530. overflow-x: hidden;
  1531. scrollbar-width: none;
  1532. /* firefox */
  1533. -ms-overflow-style: none;
  1534. height: 100% !important;
  1535. box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
  1536. .el-menu--collapse {
  1537. width: 40px;
  1538. height: 100% !important;
  1539. }
  1540. :deep(.el-menu--collapse.el-menu) {
  1541. width: 40px !important;
  1542. margin: 0 auto !important;
  1543. }
  1544. :deep(.el-menu--collapse .el-menu-item),
  1545. :deep(.el-menu--collapse .el-submenu__title),
  1546. :deep(.el-menu--collapse .el-sub-menu__title) {
  1547. padding: 0 !important;
  1548. text-align: center !important;
  1549. display: flex !important;
  1550. align-items: center !important;
  1551. justify-content: center !important;
  1552. position: relative !important;
  1553. left: 0 !important;
  1554. right: 0 !important;
  1555. width: 40px !important;
  1556. height: 45px !important;
  1557. margin: 0 auto !important;
  1558. /* 强制开启hover触发,解决折叠后无法识别子项问题 */
  1559. pointer-events: auto !important;
  1560. cursor: pointer !important;
  1561. }
  1562. :deep(.el-menu--collapse .el-sub-menu) {
  1563. width: 40px !important;
  1564. /* 二级弹框容器相对定位,让三级能基于它定位 */
  1565. position: relative !important;
  1566. }
  1567. :deep(.el-menu--collapse .el-menu-item > *),
  1568. :deep(.el-menu--collapse .el-submenu__title > *),
  1569. :deep(.el-menu--collapse .el-sub-menu__title > *) {
  1570. margin: 0 !important;
  1571. padding: 0 !important;
  1572. position: static !important;
  1573. left: auto !important;
  1574. right: auto !important;
  1575. top: auto !important;
  1576. bottom: auto !important;
  1577. transform: none !important;
  1578. float: none !important;
  1579. display: inline-block !important;
  1580. width: auto !important;
  1581. height: auto !important;
  1582. }
  1583. :deep(.el-menu--collapse .el-menu-item i.rotate),
  1584. :deep(.el-menu--collapse .el-submenu__title i.rotate),
  1585. :deep(.el-menu--collapse .el-sub-menu__title i.rotate) {
  1586. margin: 0 !important;
  1587. padding: 0 !important;
  1588. display: inline-block !important;
  1589. text-align: center !important;
  1590. width: auto !important;
  1591. height: auto !important;
  1592. position: static !important;
  1593. left: auto !important;
  1594. right: auto !important;
  1595. transform: rotate(-90deg) !important;
  1596. line-height: normal !important;
  1597. float: none !important;
  1598. }
  1599. :deep(.el-menu--collapse .el-submenu__title .iconfont2),
  1600. :deep(.el-menu--collapse .el-menu-item .iconfont2),
  1601. :deep(.el-menu--collapse .el-sub-menu__title .iconfont2) {
  1602. margin: 0 !important;
  1603. padding: 0 !important;
  1604. display: inline-block !important;
  1605. text-align: center !important;
  1606. width: auto !important;
  1607. height: auto !important;
  1608. position: static !important;
  1609. left: auto !important;
  1610. right: auto !important;
  1611. transform: none !important;
  1612. float: none !important;
  1613. }
  1614. :deep(.el-menu--collapse .el-sub-menu__title .typhoon-icon) {
  1615. margin: 0 !important;
  1616. padding: 0 !important;
  1617. display: inline-block !important;
  1618. text-align: center !important;
  1619. width: 20px !important;
  1620. height: 20px !important;
  1621. position: static !important;
  1622. left: auto !important;
  1623. right: auto !important;
  1624. transform: none !important;
  1625. float: none !important;
  1626. }
  1627. :deep(.el-menu--collapse .el-submenu__title span),
  1628. :deep(.el-menu--collapse .el-menu-item span),
  1629. :deep(.el-menu--collapse .el-sub-menu__title span) {
  1630. display: none !important;
  1631. }
  1632. :deep(.el-menu--collapse .el-submenu__icon-arrow),
  1633. :deep(.el-menu--collapse .el-sub-menu__icon-arrow) {
  1634. display: none !important;
  1635. }
  1636. .el-menu {
  1637. border-right: none;
  1638. height: 100% !important;
  1639. .el-menu-item,
  1640. .el-submenu__title {
  1641. height: 45px;
  1642. line-height: 45px;
  1643. }
  1644. :deep(.el-menu--collapse .el-menu-item),
  1645. :deep(.el-menu--collapse .el-submenu__title),
  1646. :deep(.el-menu--collapse .el-sub-menu__title) {
  1647. padding: 0 !important;
  1648. text-align: center !important;
  1649. display: flex !important;
  1650. align-items: center !important;
  1651. justify-content: center !important;
  1652. position: relative !important;
  1653. left: 0 !important;
  1654. right: 0 !important;
  1655. width: 40px !important;
  1656. height: 45px !important;
  1657. line-height: normal !important;
  1658. }
  1659. #fold {
  1660. height: 35px;
  1661. transition-duration: 1s;
  1662. color: #303133 !important;
  1663. padding: 0 !important;
  1664. text-align: center !important;
  1665. line-height: 35px !important;
  1666. display: flex !important;
  1667. align-items: center !important;
  1668. justify-content: center !important;
  1669. background-color: transparent !important;
  1670. }
  1671. #fold.is-active {
  1672. background-color: transparent !important;
  1673. color: #303133 !important;
  1674. }
  1675. #fold:hover {
  1676. background-color: rgba(64, 158, 255, 0.1) !important;
  1677. }
  1678. }
  1679. }
  1680. /* 修复Element Plus 2.x 子菜单标题样式 */
  1681. .el-submenu__title {
  1682. padding: 0 12px !important;
  1683. }
  1684. .el-menu--vertical > .el-menu-item {
  1685. padding: 0 12px !important;
  1686. }
  1687. /* 确保标题框尺寸正确 */
  1688. .el-submenu__title .el-submenu__icon-arrow {
  1689. margin-top: -4px !important;
  1690. }
  1691. /* 修复图标和文字对齐 */
  1692. .iconfont2 {
  1693. font-size: 20px !important;
  1694. margin-right: 10px;
  1695. vertical-align: middle;
  1696. color: #303133;
  1697. }
  1698. /* 台风图标样式 */
  1699. .typhoon-icon {
  1700. width: 20px;
  1701. height: 20px;
  1702. margin-right: 10px;
  1703. vertical-align: middle;
  1704. }
  1705. /* 子菜单项文字颜色 */
  1706. .el-menu--vertical .el-sub-menu .el-menu-item {
  1707. color: #303133 !important;
  1708. font-size: 0.875rem !important;
  1709. line-height: 1.5;
  1710. }
  1711. /* 本地服务菜单项文字向左移动 */
  1712. .custom-service-item {
  1713. display: flex;
  1714. justify-content: space-between;
  1715. align-items: center;
  1716. padding-left: 50px !important;
  1717. }
  1718. .custom-service-item.is-active {
  1719. background-color: transparent !important;
  1720. }
  1721. .custom-service-item .service-name {
  1722. flex: 1;
  1723. overflow: hidden;
  1724. text-overflow: ellipsis;
  1725. white-space: nowrap;
  1726. }
  1727. .custom-service-item .service-name.service-loaded {
  1728. color: #409eff;
  1729. font-weight: 500;
  1730. }
  1731. .custom-service-item .delete-service-icon {
  1732. margin-left: 8px;
  1733. cursor: pointer;
  1734. color: #909399;
  1735. transition: color 0.3s;
  1736. }
  1737. .custom-service-item .delete-service-icon:hover {
  1738. color: #f56c6c;
  1739. }
  1740. /* 自定义服务菜单项移除选中样式 */
  1741. .no-active-style {
  1742. background-color: transparent !important;
  1743. }
  1744. .no-active-style.is-active {
  1745. background-color: transparent !important;
  1746. }
  1747. /* 模型菜单项样式 */
  1748. .model-menu-item {
  1749. display: flex;
  1750. justify-content: space-between;
  1751. align-items: center;
  1752. padding-left: 50px !important;
  1753. }
  1754. .model-menu-item.is-active {
  1755. background-color: transparent !important;
  1756. }
  1757. .model-menu-item .model-name {
  1758. flex: 1;
  1759. overflow: hidden;
  1760. text-overflow: ellipsis;
  1761. white-space: nowrap;
  1762. padding-left: 10px;
  1763. }
  1764. .model-menu-item .model-name.model-loaded {
  1765. color: #409eff;
  1766. font-weight: 500;
  1767. }
  1768. .model-menu-item .delete-model-icon {
  1769. margin-left: 8px;
  1770. cursor: pointer;
  1771. color: #909399;
  1772. transition: color 0.3s;
  1773. }
  1774. .model-menu-item .delete-model-icon:hover {
  1775. color: #f56c6c;
  1776. }
  1777. /* 滚动条隐藏 */
  1778. .el-aside::-webkit-scrollbar {
  1779. display: none;
  1780. }
  1781. /* 功能模块网格布局 */
  1782. .box {
  1783. font-family: "Microsoft Yahei";
  1784. display: flex;
  1785. width: 100%;
  1786. padding: 0 10px 0 28px;
  1787. justify-content: space-between;
  1788. box-sizing: border-box;
  1789. flex-wrap: wrap;
  1790. .imgbox {
  1791. position: relative;
  1792. width: 100px;
  1793. height: 100px;
  1794. display: flex;
  1795. box-sizing: border-box;
  1796. margin-bottom: 5px;
  1797. flex-wrap: wrap;
  1798. justify-content: center;
  1799. span {
  1800. font-size: 12px;
  1801. color: #303133;
  1802. }
  1803. .img {
  1804. width: 100%;
  1805. height: 72px;
  1806. border-radius: 4px;
  1807. background-color: #585858;
  1808. border: 1px solid #e4e7ed;
  1809. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  1810. transition: all 0.3s ease;
  1811. }
  1812. .img:hover {
  1813. border: 2px solid #409eff;
  1814. box-shadow: 0 4px 8px rgba(64, 158, 255, 0.2);
  1815. transform: translateY(-2px);
  1816. }
  1817. .cross {
  1818. width: 25px;
  1819. height: 25px;
  1820. position: absolute;
  1821. top: 0;
  1822. left: 0;
  1823. }
  1824. }
  1825. }
  1826. /* 删除图标样式 */
  1827. .circle-close-icon {
  1828. position: absolute;
  1829. right: 0px;
  1830. top: 3px;
  1831. color: #ffffff;
  1832. cursor: pointer;
  1833. width: 16px;
  1834. height: 16px;
  1835. font-size: 16px;
  1836. outline: none;
  1837. }
  1838. .circle-close-icon:focus,
  1839. .circle-close-icon:active {
  1840. outline: none;
  1841. box-shadow: none;
  1842. }
  1843. /* el-menu-item 内的删除图标样式 */
  1844. .el-menu-item .circle-close-icon {
  1845. top: 50%;
  1846. transform: translateY(-50%);
  1847. color: #000000;
  1848. right: 25px;
  1849. }
  1850. /* 展开/收缩按钮动画 */
  1851. .rotate2 {
  1852. transform: rotate(0deg);
  1853. -ms-transform: rotate(0); /* IE 9 */
  1854. -moz-transform: rotate(0); /* Firefox */
  1855. -webkit-transform: rotate(0); /* Safari 和 Chrome */
  1856. -o-transform: rotate(0); /* Opera */
  1857. transition-duration: 1s;
  1858. }
  1859. .rotate {
  1860. margin-left: 8px !important;
  1861. line-height: 52px !important;
  1862. transform: rotate(-90deg);
  1863. -ms-transform: rotate(-90deg); /* IE 9 */
  1864. -moz-transform: rotate(-90deg); /* Firefox */
  1865. -webkit-transform: rotate(-90deg); /* Safari 和 Chrome */
  1866. -o-transform: rotate(-90deg); /* Opera */
  1867. transition-duration: 1s;
  1868. }
  1869. /* 禁用状态 */
  1870. .disable {
  1871. pointer-events: none;
  1872. }
  1873. /* 图标样式 */
  1874. .iconfont2 {
  1875. font-size: 20px !important;
  1876. margin-right: 10px;
  1877. }
  1878. /* 方形加号图标 */
  1879. .square-plus-icon {
  1880. display: inline-flex;
  1881. align-items: center;
  1882. justify-content: center;
  1883. width: 24px;
  1884. height: 24px;
  1885. border: 1.5px solid #909399;
  1886. border-radius: 4px;
  1887. font-size: 16px;
  1888. margin-left: 10px;
  1889. }
  1890. // ########### 关键修复:折叠状态下 二级弹框保留原位置 + 三级强制弹出 ###########
  1891. // 1. 折叠状态二级弹框:强制保留原有正确位置,不做任何改动
  1892. :deep(.el-menu--collapse .el-sub-menu > .el-sub-menu__popper) {
  1893. position: fixed !important;
  1894. margin-left: 0 !important;
  1895. transform: none !important;
  1896. z-index: 9998 !important;
  1897. }
  1898. // 2. 折叠状态二级弹框容器:hover时强制显示三级内容,核心触发逻辑
  1899. :deep(.el-menu--collapse .el-sub-menu__popper) {
  1900. pointer-events: auto !important;
  1901. &:hover .box {
  1902. display: flex !important;
  1903. }
  1904. }
  1905. // 3. 折叠状态三级内容(box):强制定位在二级弹框右侧,顶对齐
  1906. :deep(.el-menu--collapse .el-sub-menu__popper .box) {
  1907. position: absolute !important;
  1908. left: 100% !important; // 紧贴二级弹框右侧
  1909. top: 0 !important; // 和二级弹框顶对齐
  1910. z-index: 9999 !important;
  1911. width: 300px !important; // 适配你的imgbox布局
  1912. background: #f8f9fa !important;
  1913. border: 1px solid #e4e7ed !important;
  1914. border-left: none !important;
  1915. box-shadow: 2px 2px 10px rgba(0,0,0,0.05) !important;
  1916. padding: 10px !important;
  1917. margin: 0 !important;
  1918. // 强制显示,解决折叠后被隐藏问题
  1919. display: flex !important;
  1920. pointer-events: auto !important;
  1921. }
  1922. // 4. 折叠状态三级imgbox布局适配:清除多余间距,正常显示
  1923. :deep(.el-menu--collapse .el-sub-menu__popper .box .imgbox) {
  1924. margin: 0 10px 10px 0 !important;
  1925. }
  1926. // 5. 展开状态:恢复原有布局,不影响任何效果
  1927. :deep(.el-menu--not-collapse .el-sub-menu__popper .box) {
  1928. position: static !important;
  1929. width: 100% !important;
  1930. box-shadow: none !important;
  1931. border: none !important;
  1932. }
  1933. </style>