aside.vue 149 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350
  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. <!-- 基础地理实体使用文字列表方式 -->
  33. <template v-if="data.id === 'vectorData'">
  34. <el-menu-item
  35. v-for="(data2, idx) in data.children"
  36. :key="data2.name"
  37. :index="data.id + '-' + idx"
  38. @dblclick="addDatas(data.id, data2)"
  39. class="vector-data-item"
  40. >
  41. <span class="vector-data-name" :class="{ 'vector-data-loaded': data2.state !== 0 }">{{ data2.name }}</span>
  42. <el-popconfirm
  43. v-if="data2.state !== 0"
  44. title="确定卸载吗?"
  45. confirmButtonText="确认"
  46. cancelButtonText="取消"
  47. @confirm="DeleteDates(data.id, data2)"
  48. >
  49. <template #reference>
  50. <el-icon class="delete-vector-data-icon" title="卸载">
  51. <CircleClose />
  52. </el-icon>
  53. </template>
  54. </el-popconfirm>
  55. </el-menu-item>
  56. </template>
  57. <!-- 其他数据服务使用图片方式 -->
  58. <div v-else class="box" :index="index">
  59. <div
  60. class="imgbox"
  61. v-for="data2 in data.children"
  62. :key="data2.name"
  63. @click="addDatas(data.id,data2)"
  64. >
  65. <img :src="data2.thumbnail" alt class="img" />
  66. <img src="/img/common/cross.png" alt class="cross" v-show="data2.state != 0" />
  67. <span>{{data2.name}}</span>
  68. <el-popconfirm
  69. title="确定删除吗?"
  70. confirmButtonText="确认"
  71. cancelButtonText="取消"
  72. @confirm="DeleteDates(data.id,data2)"
  73. >
  74. <template #reference>
  75. <CircleClose
  76. v-show="data2.state != 0 && data.id !='baseLayer'"
  77. title="删除"
  78. class="circle-close-icon"
  79. />
  80. </template>
  81. </el-popconfirm>
  82. </div>
  83. </div>
  84. </el-sub-menu>
  85. <!-- 本地服务二级菜单 -->
  86. <el-sub-menu index="1-4">
  87. <template #title>自定义服务</template>
  88. <el-menu-item
  89. v-for="(service, index) in addCustomServices"
  90. :key="service.id || index"
  91. :index="'1-4-' + (service.id || index)"
  92. @dblclick="loadCustomService(service)"
  93. class="custom-service-item"
  94. >
  95. <span class="service-name" :class="{ 'service-loaded': isServiceLoaded(service) }">{{ service.name }}</span>
  96. <el-popconfirm
  97. v-if="isServiceLoaded(service)"
  98. title="确定卸载吗?"
  99. confirmButtonText="确认"
  100. cancelButtonText="取消"
  101. @confirm="unloadCustomService(service)"
  102. >
  103. <template #reference>
  104. <el-icon class="delete-service-icon" title="卸载">
  105. <CircleClose />
  106. </el-icon>
  107. </template>
  108. </el-popconfirm>
  109. </el-menu-item>
  110. </el-sub-menu>
  111. <el-menu-item index="1-5" @click="addCustomService" class="no-active-style">
  112. <el-icon @click.stop="addCustomService" class="square-plus-icon"><Plus /></el-icon>
  113. </el-menu-item>
  114. </el-sub-menu>
  115. <!-- 加载组件 -->
  116. <el-sub-menu v-for="(data,index) in config" :key="index" :index="data.id">
  117. <template #title>
  118. <i :class="data.icon" class="iconfont2"></i>
  119. <span>{{data.name}}</span>
  120. </template>
  121. <div class="box">
  122. <div
  123. class="imgbox"
  124. v-for="data2 in data.children"
  125. :key="data2.component"
  126. @click="data2.component && change(data2.component,data2.destroy)"
  127. >
  128. <img :src="data2.imgSrc" alt class="img" />
  129. <img
  130. src="/img/common/cross.png"
  131. alt
  132. class="cross"
  133. v-show="data2.component && (data2.component === view || data2.component === view2)"
  134. />
  135. <span>{{data2.name}}</span>
  136. </div>
  137. </div>
  138. </el-sub-menu>
  139. <!-- 模型菜单 -->
  140. <el-sub-menu index="model" v-if="modelConfig.id">
  141. <template #title>
  142. <i :class="modelConfig.icon" class="iconfont2"></i>
  143. <span>{{modelConfig.name}}</span>
  144. </template>
  145. <el-sub-menu
  146. v-for="(secondLevel, firstLevelKey) in groupedModels"
  147. :key="firstLevelKey"
  148. :index="'model-' + firstLevelKey"
  149. popper-append-to-body="false"
  150. >
  151. <template #title>
  152. <span>{{getFirstLevelName(firstLevelKey)}}</span>
  153. <el-tag size="small" type="info" style="margin-left: 8px;">{{getTotalCount(secondLevel)}}</el-tag>
  154. </template>
  155. <el-sub-menu
  156. v-for="(models, secondLevelKey) in secondLevel"
  157. :key="secondLevelKey"
  158. :index="'model-' + firstLevelKey + '-' + secondLevelKey"
  159. popper-append-to-body="false"
  160. >
  161. <template #title>
  162. <span>{{getSecondLevelName(secondLevelKey)}}</span>
  163. </template>
  164. <el-menu-item
  165. v-for="model in models"
  166. :key="model.id"
  167. :index="'model-' + firstLevelKey + '-' + secondLevelKey + '-' + model.id"
  168. @dblclick="loadModel(model)"
  169. class="model-menu-item"
  170. >
  171. <span class="model-name" :class="{ 'model-loaded': isModelLoaded(model.id) }">{{ model.name }}</span>
  172. <el-popconfirm
  173. v-if="isModelLoaded(model.id)"
  174. title="确定删除吗?"
  175. confirmButtonText="确认"
  176. cancelButtonText="取消"
  177. @confirm="removeModel(model)"
  178. >
  179. <template #reference>
  180. <el-icon
  181. class="delete-model-icon"
  182. title="删除模型"
  183. >
  184. <CircleClose />
  185. </el-icon>
  186. </template>
  187. </el-popconfirm>
  188. </el-menu-item>
  189. </el-sub-menu>
  190. </el-sub-menu>
  191. </el-sub-menu>
  192. <!-- 台风菜单 -->
  193. <el-sub-menu index="typhoon">
  194. <template #title>
  195. <img src="/img/typhoon.png" alt="" class="typhoon-icon" />
  196. <span>台风</span>
  197. </template>
  198. <!-- 实时台风菜单项 -->
  199. <el-menu-item index="typhoon-realtime" class="model-menu-item" @click="!showRealtimeTyphoon && loadRealtimeTyphoon()">
  200. <span class="typhoon-name" :class="{ 'typhoon-loaded': showRealtimeTyphoon }">实时台风</span>
  201. <el-popconfirm
  202. v-if="showRealtimeTyphoon"
  203. title="确定卸载吗?"
  204. confirmButtonText="确认"
  205. cancelButtonText="取消"
  206. @confirm="removeRealtimeTyphoon"
  207. >
  208. <template #reference>
  209. <el-icon
  210. class="delete-typhoon-icon"
  211. title="卸载台风"
  212. >
  213. <CircleClose />
  214. </el-icon>
  215. </template>
  216. </el-popconfirm>
  217. </el-menu-item>
  218. <!-- 风场菜单项 -->
  219. <el-menu-item index="typhoon-windfield" class="model-menu-item" @click="!showWindField && loadWindField()">
  220. <span class="typhoon-name" :class="{ 'typhoon-loaded': showWindField }">风场</span>
  221. <el-popconfirm
  222. v-if="showWindField"
  223. title="确定卸载吗?"
  224. confirmButtonText="确认"
  225. cancelButtonText="取消"
  226. @confirm="removeWindField"
  227. >
  228. <template #reference>
  229. <el-icon
  230. class="delete-typhoon-icon"
  231. title="卸载风场"
  232. >
  233. <CircleClose />
  234. </el-icon>
  235. </template>
  236. </el-popconfirm>
  237. </el-menu-item>
  238. <!-- 降雨菜单项 -->
  239. <el-menu-item index="typhoon-rainfall" class="model-menu-item" @click="!showRainfall && loadRainfall()">
  240. <span class="typhoon-name" :class="{ 'typhoon-loaded': showRainfall }">降雨</span>
  241. <el-popconfirm
  242. v-if="showRainfall"
  243. title="确定卸载吗?"
  244. confirmButtonText="确认"
  245. cancelButtonText="取消"
  246. @confirm="removeRainfall"
  247. >
  248. <template #reference>
  249. <el-icon
  250. class="delete-typhoon-icon"
  251. title="卸载降雨"
  252. >
  253. <CircleClose />
  254. </el-icon>
  255. </template>
  256. </el-popconfirm>
  257. </el-menu-item>
  258. <!-- 云图菜单项 -->
  259. <el-menu-item index="typhoon-cloud" class="model-menu-item" @click="!showCloudMap && loadCloudMap()">
  260. <span class="typhoon-name" :class="{ 'typhoon-loaded': showCloudMap }">云图</span>
  261. <el-popconfirm
  262. v-if="showCloudMap"
  263. title="确定卸载吗?"
  264. confirmButtonText="确认"
  265. cancelButtonText="取消"
  266. @confirm="removeCloudMap"
  267. >
  268. <template #reference>
  269. <el-icon
  270. class="delete-typhoon-icon"
  271. title="卸载云图"
  272. >
  273. <CircleClose />
  274. </el-icon>
  275. </template>
  276. </el-popconfirm>
  277. </el-menu-item>
  278. <!-- 雷达菜单项 -->
  279. <el-menu-item index="typhoon-radar" class="model-menu-item" @click="!showRadar && loadRadar()">
  280. <span class="typhoon-name" :class="{ 'typhoon-loaded': showRadar }">雷达</span>
  281. <el-popconfirm
  282. v-if="showRadar"
  283. title="确定卸载吗?"
  284. confirmButtonText="确认"
  285. cancelButtonText="取消"
  286. @confirm="removeRadar"
  287. >
  288. <template #reference>
  289. <el-icon
  290. class="delete-typhoon-icon"
  291. title="卸载雷达"
  292. >
  293. <CircleClose />
  294. </el-icon>
  295. </template>
  296. </el-popconfirm>
  297. </el-menu-item>
  298. </el-sub-menu>
  299. <!-- 业务场景菜单 -->
  300. <el-sub-menu index="business-scenario">
  301. <template #title>
  302. <i class="iconfont iconxingcheng iconfont2"></i>
  303. <span>业务场景</span>
  304. </template>
  305. <!-- 固定场景 -->
  306. <template v-for="scene in fixedScenes" :key="scene.sceneId">
  307. <el-menu-item
  308. :index="'business-scenario-' + scene.sceneId"
  309. class="model-menu-item"
  310. @click="loadFixedScene(scene)"
  311. >
  312. <span class="scene-name" :class="{ 'scene-loaded': isSceneLoaded(scene.sceneId) }">{{ scene.sceneName }}</span>
  313. <el-popconfirm
  314. v-if="isSceneLoaded(scene.sceneId)"
  315. title="确定卸载吗?"
  316. confirmButtonText="确认"
  317. cancelButtonText="取消"
  318. @confirm="unloadScene(scene)"
  319. >
  320. <template #reference>
  321. <el-icon
  322. class="delete-scene-icon"
  323. title="卸载场景"
  324. >
  325. <CircleClose />
  326. </el-icon>
  327. </template>
  328. </el-popconfirm>
  329. </el-menu-item>
  330. </template>
  331. <!-- 数据库场景 -->
  332. <template v-for="scene in businessScenes" :key="scene.sceneId">
  333. <el-menu-item
  334. :index="'business-scenario-' + scene.sceneId"
  335. class="model-menu-item"
  336. @click="loadBusinessScenarioFromDB(scene)"
  337. >
  338. <span class="scene-name" :class="{ 'scene-loaded': isSceneLoaded(scene.sceneId) }">{{ scene.sceneName }}</span>
  339. <el-popconfirm
  340. v-if="isSceneLoaded(scene.sceneId)"
  341. title="确定卸载吗?"
  342. confirmButtonText="确认"
  343. cancelButtonText="取消"
  344. @confirm="unloadScene(scene)"
  345. >
  346. <template #reference>
  347. <el-icon
  348. class="delete-scene-icon"
  349. title="卸载场景"
  350. >
  351. <CircleClose />
  352. </el-icon>
  353. </template>
  354. </el-popconfirm>
  355. <el-popconfirm
  356. v-else
  357. title="确定删除吗?"
  358. confirmButtonText="确认"
  359. cancelButtonText="取消"
  360. @confirm="handleDeleteBusinessScene(scene.sceneId)"
  361. >
  362. <template #reference>
  363. <el-icon
  364. class="delete-scene-icon"
  365. title="删除场景"
  366. >
  367. <CircleClose />
  368. </el-icon>
  369. </template>
  370. </el-popconfirm>
  371. </el-menu-item>
  372. </template>
  373. <el-menu-item index="business-scenario-create" @click="handleCreateScenario" class="no-active-style">
  374. <el-icon @click.stop="handleCreateScenario" class="square-plus-icon"><Plus /></el-icon>
  375. <span>创建场景</span>
  376. </el-menu-item>
  377. </el-sub-menu>
  378. </el-menu>
  379. </el-aside>
  380. <!-- 侧边栏部分--end -->
  381. <!-- 右侧内容展示部分 -->
  382. <el-main>
  383. <loading-bar></loading-bar>
  384. <!-- viewer组件 -->
  385. <component v-if="initViewer" :is="initViewer" :after-initviewer="removeLoad" :opening-animation="true"></component>
  386. <!-- 台风可视化组件 -->
  387. <TyphoonVisualization
  388. v-if="showTyphoon"
  389. :viewer="viewer"
  390. :visible="showTyphoon"
  391. :data-url="typhoonDataUrl"
  392. ></TyphoonVisualization>
  393. <!-- 实时台风可视化组件 -->
  394. <RealtimeTyphoon
  395. v-if="showRealtimeTyphoon"
  396. :viewer="viewer"
  397. :visible="showRealtimeTyphoon"
  398. ></RealtimeTyphoon>
  399. <!-- 风场可视化组件 -->
  400. <WindFieldVisualization
  401. v-if="showWindField"
  402. :viewer="viewer"
  403. :visible="showWindField"
  404. :data-url="`${PROXY_SLT}/Api/LastWind`"
  405. ></WindFieldVisualization>
  406. <!-- 降雨可视化组件 -->
  407. <RainfallVisualization
  408. v-if="showRainfall"
  409. :viewer="viewer"
  410. :visible="showRainfall"
  411. :default-hours="24"
  412. ></RainfallVisualization>
  413. <!-- 云图可视化组件 -->
  414. <CloudMapVisualization
  415. v-if="showCloudMap"
  416. :viewer="viewer"
  417. :visible="showCloudMap"
  418. :default-type="30"
  419. ></CloudMapVisualization>
  420. <!-- 雷达可视化组件 -->
  421. <RadarVisualization
  422. v-if="showRadar"
  423. :viewer="viewer"
  424. :visible="showRadar"
  425. ></RadarVisualization>
  426. <!-- 土地利用图例 -->
  427. <div class="land-use-legend" v-if="showLandUseLegend">
  428. <div class="land-use-legend-header">
  429. <span>土地利用图例</span>
  430. <span class="land-use-legend-toggle" @click="showLandUseLegend = false">−</span>
  431. </div>
  432. <div class="land-use-legend-body">
  433. <div class="land-use-legend-item" v-for="item in landUseLegendItems" :key="item.value">
  434. <span class="land-use-legend-color" :style="{ backgroundColor: item.color }"></span>
  435. <span class="land-use-legend-label">{{ item.label }}</span>
  436. </div>
  437. </div>
  438. </div>
  439. <!-- 添加自定义服务组件 -->
  440. <component
  441. v-if="addService"
  442. :is="addService"
  443. :clear-callback="addCustomService"
  444. :add-callback="addCallback"
  445. :get-layer-name="getName"
  446. ></component>
  447. <!-- 分析功能组件 -->
  448. <keep-alive>
  449. <component
  450. v-if="view"
  451. :is="view"
  452. :key="view"
  453. :delete-callback="deleteCallback"
  454. data-url="/data/netcdf/result_100_200_9_40.nc"
  455. skylineSpatialAnalysisUrl="http://www.supermapol.com/realspace/services/spatialAnalysis-data_all/restjsr/spatialanalyst/geometry/3d/skylinesectorbody.json"
  456. ></component>
  457. </keep-alive>
  458. <!-- 地质体组件,单独展示,需要销毁 -->
  459. <component v-if="view2" :is="view2" :key="view2"></component>
  460. <!-- 模型拆解弹框 -->
  461. <ModelDisassemblyDialog
  462. ref="modelDisassemblyDialog"
  463. :visible="showModelDialog"
  464. :model-path="'/models/启闭机.glb'"
  465. :model-name="'启闭机模型'"
  466. :icon-config="modelIconConfig"
  467. @close="showModelDialog = false"
  468. />
  469. <!-- 鹰眼组件 -->
  470. <Sm3dOverviewMap
  471. v-if="view === 'Sm3dOverviewMap'"
  472. :visible="view === 'Sm3dOverviewMap'"
  473. @close="view = ''"
  474. ></Sm3dOverviewMap>
  475. <!-- MVT属性弹窗组件 -->
  476. <MvtAttributePopup
  477. :visible="mvtPopupVisible"
  478. :position="mvtPopupPosition"
  479. :title="mvtPopupTitle"
  480. :attributes="mvtPopupAttributes"
  481. @close="mvtPopupVisible = false"
  482. />
  483. <!-- 闸门启闭控制组件(非莒口水闸场景使用) -->
  484. <GateControl
  485. v-if="!showJuKouScenePage"
  486. ref="gateControl"
  487. :visible="showGateControl"
  488. :initial-position="gatePosition"
  489. @close="showGateControl = false"
  490. @position-change="handleGatePositionChange"
  491. />
  492. <!-- 莒口水闸业务场景独立页面组件 -->
  493. <JuKouShuiZhaPage
  494. v-if="showJuKouScenePage"
  495. :viewer="viewer"
  496. :visible="showJuKouScenePage"
  497. @scene-loaded="onJuKouSceneLoaded"
  498. @scene-unloaded="onJuKouSceneUnloaded"
  499. />
  500. </el-main>
  501. </el-container>
  502. </template>
  503. <script>
  504. import config from "../../config/views_config.js"; //组件配置
  505. import server_config, { PROXY_TIANDITU_TILE, PROXY_SLT, TIANDITU_KEY } from "../../config/server_config.js"; //服务配置
  506. import server_flyTo_config from "../../config/server_position_config.js"; //服务坐标配置
  507. import layerManagement from "../../js/common/layerManagement.js"; //图层管理封装方法
  508. import waterLayer from "../../js/common/waterLayer.js"; //水面图层
  509. import camera from "../../js/common/camera.js"; //相机操作
  510. import loadingBar from "../../components/loading.vue"; //加载动画
  511. import TyphoonVisualization from "../../components/typhoon-visualization/typhoon-visualization.vue"; //台风可视化组件
  512. import RealtimeTyphoon from "../../components/realtime-typhoon/realtime-typhoon.vue"; //实时台风可视化组件
  513. import WindFieldVisualization from "../../components/wind-field-visualization/wind-field-visualization.vue"; //风场可视化组件
  514. import RainfallVisualization from "../../components/rainfall-visualization/rainfall-visualization.vue"; //降雨可视化组件
  515. import CloudMapVisualization from "../../components/cloud-map-visualization/cloud-map-visualization.vue"; //云图可视化组件
  516. import RadarVisualization from "../../components/radar-visualization/radar-visualization.vue"; //雷达可视化组件
  517. import CesiumThreeFusion from "@/components/ThreeCesiumIntegration/CesiumThreeFusion.vue"; //Three.js与Cesium融合组件
  518. import ModelDisassemblyDialog from "../../components/ModelDisassemblyDialog.vue"; //模型拆解弹框组件
  519. import { CircleClose, Plus, CirclePlus, Setting, Delete } from '@element-plus/icons-vue'; //删除图标
  520. import { listModel, getModel } from '@/api/watershed/model'; //模型API
  521. import { getDefaultMapConfig, saveMapConfig } from '@/api/cesium/mapConfig'; //地图配置API
  522. import serviceApi from '@/api/watershed/service'; //自定义服务API
  523. import { listBusinessScene, addBusinessScene, delBusinessScene } from '@/api/cesium/businessScene'; //业务场景API
  524. import { ElMessage, ElMessageBox } from 'element-plus';
  525. import { getToken } from '@/utils/auth'
  526. import axios from 'axios'
  527. import { onMounted, watch, computed } from 'vue'
  528. import ModelEditor from "../../components/draw/draw-solid-figure/ModelEditor"; //模型编辑器
  529. import MvtAttributePopup from "../../components/MvtAttributePopup.vue"; //MVT属性弹窗组件
  530. import MvtClickHandler from "../../js/common/mvtClickHandler.js"; //MVT点击处理器
  531. import GateControl from "../../components/gate-control/GateControl.vue"; //闸门启闭控制组件
  532. import JuKouShuiZhaPage from "../../components/ju-kou-scene/JuKouShuiZhaPage.vue"; //莒口水闸业务场景页面组件
  533. import { getFixedScenes, isFixedScene } from "../../config/fixedScenes.js"; //固定场景配置
  534. import { loadJuKouScene, unloadJuKouScene, getController, JuKouSceneConfig } from "../../business-scenes/JuKouShuiZhaScene.js"; //莒口水闸业务场景模块
  535. export default {
  536. name: "layout-aside",
  537. components: {
  538. loadingBar,
  539. CircleClose,
  540. TyphoonVisualization,
  541. RealtimeTyphoon,
  542. WindFieldVisualization,
  543. RainfallVisualization,
  544. CloudMapVisualization,
  545. RadarVisualization,
  546. CesiumThreeFusion,
  547. ModelDisassemblyDialog,
  548. MvtAttributePopup,
  549. GateControl,
  550. JuKouShuiZhaPage
  551. },
  552. setup() {
  553. // 使用 computed 来获取 window.viewer
  554. const viewer = computed(() => window.viewer);
  555. return {
  556. viewer
  557. }
  558. },
  559. data() {
  560. return {
  561. isCollapse: false,
  562. asideWidth: "asideWidth1",
  563. view: "",
  564. view2: "",
  565. initViewer: "",
  566. addService: "",
  567. sceneURL:
  568. "http://www.supermapol.com/realspace/services/3D-ZF_normal/rest/realspace",
  569. config: config,
  570. server_config: server_config,
  571. // 暴露给模板使用:Options API 模板只能访问 data/computed/methods,
  572. // 不能直接访问 import 的变量,否则模板里 PROXY_SLT 为 undefined,
  573. // 导致 :data-url="`${PROXY_SLT}/Api/LastWind`" 变成 "undefined/Api/LastWind"
  574. PROXY_SLT: PROXY_SLT,
  575. baseLayerObj: server_config[1].children[0],
  576. terrainLayerObj: null,
  577. addCustomServices: [],
  578. getNames: [], //记录自定义服务名称
  579. varName: null, //临时保存当前自定义名称
  580. loadedCustomServiceIds: [], //已加载的自定义服务ID
  581. modelConfig: {}, //模型菜单配置
  582. modelData: [], //模型数据
  583. loadedModelEntities: [], //已加载的模型实体
  584. currentModelEditor: null, //当前模型编辑器
  585. lastHighlightedModel: null, //最后高亮的模型
  586. saveConfigTimer: null, //保存配置的防抖定时器
  587. lastSaveTime: 0, //上次保存时间戳
  588. showTyphoon: false, //是否显示台风可视化
  589. typhoonDataUrl: '', //台风数据URL
  590. loadedTyphoonId: null, //已加载的台风ID
  591. showRealtimeTyphoon: false, //是否显示实时台风可视化
  592. showWindField: false, //是否显示风场可视化
  593. showRainfall: false, //是否显示降雨可视化
  594. showCloudMap: false, //是否显示云图可视化
  595. showRadar: false, //是否显示雷达可视化
  596. showLandUseLegend: false, //是否显示土地利用图例
  597. landUseLegendItems: [
  598. { value: 0, color: '#000000', label: '背景/海域无效像元' },
  599. { value: 1, color: '#FFF8DC', label: '耕地' },
  600. { value: 2, color: '#006400', label: '有林地' },
  601. { value: 3, color: '#2E8B57', label: '灌木林地' },
  602. { value: 4, color: '#98FB98', label: '草地' },
  603. { value: 5, color: '#00008B', label: '河流水面、湖泊、水库坑塘' },
  604. { value: 6, color: '#87CEEB', label: '冰川及永久积雪' },
  605. { value: 7, color: '#D2B48C', label: '裸地、戈壁、盐碱地' },
  606. { value: 8, color: '#FF00FF', label: '城乡建设用地' },
  607. { value: 9, color: '#00BFFF', label: '沼泽、滩涂湿地' },
  608. { value: 10, color: '#000000', label: '园地(果园、茶园等)' },
  609. { value: 11, color: '#000000', label: '荒漠、重度盐碱地' },
  610. { value: 12, color: '#000000', label: '独立工矿、公路铁路用地' },
  611. { value: 13, color: '#000000', label: '其他未利用土地' },
  612. ],
  613. showModelDialog: false, //是否显示模型弹框
  614. businessScenes: [], //业务场景列表(从数据库读取)
  615. fixedScenes: [], //固定场景列表
  616. currentSceneId: null, //当前加载的场景ID
  617. // 闸门控制相关
  618. showGateControl: false, //是否显示闸门控制面板
  619. gatePosition: 0, //闸门当前开度(0-100)
  620. // 莒口水闸场景页面显示
  621. showJuKouScenePage: false, //是否显示莒口水闸场景页面组件
  622. // MVT属性弹窗相关
  623. mvtPopupVisible: false, //弹窗是否可见
  624. mvtPopupPosition: { x: 0, y: 0 }, //弹窗位置
  625. mvtPopupTitle: '属性信息', //弹窗标题
  626. mvtPopupAttributes: {}, //属性数据
  627. // MVT点击处理器
  628. mvtClickHandler: null, //MVT点击处理器实例
  629. _hasMvtLayer: false, //是否有MVT图层加载
  630. // 模型拆解图标配置
  631. modelIconConfig: null, //场景模型拆解图标配置
  632. };
  633. },
  634. computed: {
  635. groupedModels() {
  636. const grouped = {}
  637. this.modelData.forEach(model => {
  638. const type = model.type || '5/5-1'
  639. const parts = type.split('/')
  640. const firstLevel = parts[0] || '5'
  641. const secondLevel = parts[1] || '5-1'
  642. if (!grouped[firstLevel]) {
  643. grouped[firstLevel] = {}
  644. }
  645. if (!grouped[firstLevel][secondLevel]) {
  646. grouped[firstLevel][secondLevel] = []
  647. }
  648. grouped[firstLevel][secondLevel].push(model)
  649. })
  650. return grouped
  651. }
  652. },
  653. methods: {
  654. async fetchCustomServices() {
  655. try {
  656. console.log('开始从数据库加载自定义服务...');
  657. const response = await serviceApi.getServiceList({});
  658. console.log('获取到的自定义服务:', response);
  659. if (response && response.rows) {
  660. this.addCustomServices = response.rows.map(service => ({
  661. id: service.id || service.serviceId,
  662. name: service.name || service.serviceName,
  663. type: service.type || service.serviceType,
  664. url: service.url || service.serviceUrl,
  665. tokenRequired: service.tokenRequired || service.token_required,
  666. token: service.token || service.serviceToken,
  667. status: service.status,
  668. layers: [{
  669. type: service.type || service.serviceType,
  670. layerName: service.name || service.serviceName
  671. }]
  672. }));
  673. console.log('处理后的自定义服务列表:', this.addCustomServices);
  674. }
  675. } catch (error) {
  676. console.error('加载自定义服务失败:', error);
  677. }
  678. },
  679. getModelTypeOptions() {
  680. return [
  681. {
  682. value: '1',
  683. label: '水利工程实体',
  684. children: [
  685. { value: '1-1', label: '水库工程' },
  686. { value: '1-2', label: '水闸工程' },
  687. { value: '1-3', label: '泵站工程' },
  688. { value: '1-4', label: '灌区工程' },
  689. { value: '1-5', label: '堤防与护岸工程' }
  690. ]
  691. },
  692. {
  693. value: '2',
  694. label: '水系水利设施',
  695. children: [
  696. { value: '2-1', label: '河流' },
  697. { value: '2-2', label: '湖泊与水库水面' },
  698. { value: '2-3', label: '渠道与输水管道' },
  699. { value: '2-4', label: '河口与海岸带' }
  700. ]
  701. },
  702. {
  703. value: '3',
  704. label: '地理环境要素',
  705. children: [
  706. { value: '3-1', label: '地形地貌' },
  707. { value: '3-2', label: '行政区划' },
  708. { value: '3-3', label: '重要地物' }
  709. ]
  710. },
  711. {
  712. value: '4',
  713. label: '自然生态景观',
  714. children: [
  715. { value: '4-1', label: '湖泊湿地' },
  716. { value: '4-2', label: '森林公园' },
  717. { value: '4-3', label: '地质公园' },
  718. { value: '4-4', label: '海岸带景观' }
  719. ]
  720. },
  721. {
  722. value: '5',
  723. label: '模型集与项目',
  724. children: [
  725. { value: '5-1', label: '待分类模型' },
  726. { value: '5-2', label: 'XX市防洪排涝工程' }
  727. ]
  728. }
  729. ]
  730. },
  731. getFirstLevelName(firstLevelKey) {
  732. const options = this.getModelTypeOptions()
  733. const option = options.find(opt => opt.value === firstLevelKey)
  734. return option ? option.label : firstLevelKey
  735. },
  736. getSecondLevelName(secondLevelKey) {
  737. const options = this.getModelTypeOptions()
  738. for (const opt of options) {
  739. if (opt.children) {
  740. const child = opt.children.find(c => c.value === secondLevelKey)
  741. if (child) return child.label
  742. }
  743. }
  744. return secondLevelKey
  745. },
  746. getTotalCount(secondLevel) {
  747. let count = 0
  748. for (const key in secondLevel) {
  749. count += secondLevel[key].length
  750. }
  751. return count
  752. },
  753. getTypeDisplayName(type) {
  754. const typeMapping = {
  755. 'RESERVOIR': '水库',
  756. 'HYDROPOWER': '水电站',
  757. 'IRRIGATION': '灌溉',
  758. 'FLOOD_CONTROL': '防洪',
  759. 'CANAL': '渠道',
  760. 'PUMPING_STATION': '泵站',
  761. 'OTHER': '其他'
  762. }
  763. if (typeMapping[type]) {
  764. return typeMapping[type]
  765. }
  766. return type
  767. },
  768. async fetchModels() {
  769. try {
  770. const data = await listModel({})
  771. const modelList = data.rows || []
  772. console.log('获取到的模型数据:', modelList)
  773. this.modelData = modelList.map(model => {
  774. console.log('模型详情:', model)
  775. return {
  776. id: model.id,
  777. name: model.name,
  778. type: model.type,
  779. format: model.format,
  780. uploadUnit: model.uploadUnit || '未知单位',
  781. filePath: model.filePath || model.file_path || model.url || '',
  782. coordinates: model.modelCoordinates || model.coordinates || '',
  783. rotationX: model.rotationX != null ? model.rotationX : (model.rotation_x != null ? model.rotation_x : 0),
  784. rotationY: model.rotationY != null ? model.rotationY : (model.rotation_y != null ? model.rotation_y : 0),
  785. rotationZ: model.rotationZ != null ? model.rotationZ : (model.rotation_z != null ? model.rotation_z : 0),
  786. scaleX: model.scaleX != null ? model.scaleX : (model.scale_x != null ? model.scale_x : 1),
  787. scaleY: model.scaleY != null ? model.scaleY : (model.scale_y != null ? model.scale_y : 1),
  788. scaleZ: model.scaleZ != null ? model.scaleZ : (model.scale_z != null ? model.scale_z : 1),
  789. originalData: model
  790. }
  791. })
  792. console.log('处理后的模型数据:', this.modelData)
  793. if (this.modelData.length > 0) {
  794. this.modelConfig = {
  795. id: 'model',
  796. icon: 'iconfont iconmoxing',
  797. name: '模型'
  798. }
  799. }
  800. } catch (error) {
  801. console.error('获取模型数据错误:', error)
  802. }
  803. },
  804. async loadModel(model, shouldFlyTo = true) {
  805. if (!viewer) {
  806. ElMessage.error('Cesium viewer 未初始化')
  807. return
  808. }
  809. if (!viewer.scene) {
  810. ElMessage.error('Cesium scene 未初始化')
  811. return
  812. }
  813. try {
  814. const latestData = await getModel(model.id)
  815. if (latestData && latestData.data) {
  816. const latest = latestData.data
  817. model.coordinates = latest.modelCoordinates || latest.coordinates || model.coordinates
  818. model.rotationX = latest.rotationX != null ? latest.rotationX : (model.rotationX || 0)
  819. model.rotationY = latest.rotationY != null ? latest.rotationY : (model.rotationY || 0)
  820. model.rotationZ = latest.rotationZ != null ? latest.rotationZ : (model.rotationZ || 0)
  821. model.scaleX = latest.scaleX != null ? latest.scaleX : (model.scaleX || 1)
  822. model.scaleY = latest.scaleY != null ? latest.scaleY : (model.scaleY || 1)
  823. model.scaleZ = latest.scaleZ != null ? latest.scaleZ : (model.scaleZ || 1)
  824. }
  825. } catch (fetchError) {
  826. console.warn('获取最新模型数据失败,使用缓存数据:', fetchError.message)
  827. }
  828. if (this.currentModelEditor) {
  829. if (this.currentModelEditor.destroy) {
  830. this.currentModelEditor.destroy();
  831. }
  832. this.currentModelEditor = null;
  833. }
  834. if (this.lastHighlightedModel) {
  835. this.restoreModelStyle(this.lastHighlightedModel);
  836. this.lastHighlightedModel = null;
  837. }
  838. if (this.isModelLoaded(model.id)) {
  839. this.removeModel(model)
  840. }
  841. try {
  842. if (!model.filePath) {
  843. ElMessage.warning('模型文件路径不存在,请检查数据库中的filePath字段')
  844. return
  845. }
  846. let modelUrl = model.filePath.replace(/\/+/g, '/')
  847. const isLocalPath = modelUrl.startsWith('/') || modelUrl.startsWith('./') || modelUrl.startsWith('../') || !modelUrl.startsWith('http')
  848. if (isLocalPath) {
  849. const baseURL = import.meta.env.VITE_APP_BASE_API || ''
  850. const apiUrl = baseURL + "/common/download/resource?resource=" + encodeURIComponent(modelUrl)
  851. try {
  852. const response = await axios({
  853. method: 'get',
  854. url: apiUrl,
  855. responseType: 'blob',
  856. headers: { 'Authorization': 'Bearer ' + getToken() }
  857. })
  858. if (response.data.size === 0) {
  859. ElMessage.error('模型文件为空,请检查文件是否有效')
  860. return
  861. }
  862. let mimeType = 'model/gltf-binary'
  863. const format = model.format ? model.format.toUpperCase() : 'GLB'
  864. const fileExtension = model.filePath ? model.filePath.split('.').pop().toLowerCase() : ''
  865. if (format === 'GLTF' || fileExtension === 'gltf') {
  866. mimeType = 'model/gltf+json'
  867. } else if (format === 'GLB' || fileExtension === 'glb') {
  868. mimeType = 'model/gltf-binary'
  869. } else if (format === 'OBJ' || fileExtension === 'obj') {
  870. mimeType = 'model/obj'
  871. }
  872. const blob = new Blob([response.data], { type: mimeType })
  873. modelUrl = URL.createObjectURL(blob)
  874. } catch (apiError) {
  875. console.error('通过API获取模型文件失败:', apiError)
  876. ElMessage.error('无法获取模型文件,请检查文件路径或权限')
  877. return
  878. }
  879. }
  880. let position
  881. if (model.coordinates && model.coordinates.trim() !== '' && model.coordinates !== '未设置') {
  882. try {
  883. const coords = model.coordinates.split(',').map(c => parseFloat(c.trim()))
  884. if (coords.length >= 2 && !isNaN(coords[0]) && !isNaN(coords[1])) {
  885. const lon = coords[0]
  886. const lat = coords[1]
  887. const modelHeight = coords[2] !== undefined && !isNaN(coords[2]) ? coords[2] : 0
  888. const cartographic = Cesium.Cartographic.fromDegrees(lon, lat)
  889. if (viewer.scene.globe && viewer.terrainProvider) {
  890. try {
  891. const updatedPositions = await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [cartographic])
  892. const terrainHeight = updatedPositions[0].height || 0
  893. position = Cesium.Cartesian3.fromDegrees(lon, lat, modelHeight)
  894. } catch (terrainError) {
  895. position = Cesium.Cartesian3.fromDegrees(lon, lat, modelHeight)
  896. }
  897. } else {
  898. position = Cesium.Cartesian3.fromDegrees(lon, lat, modelHeight)
  899. }
  900. } else {
  901. position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
  902. }
  903. } catch (e) {
  904. console.error('解析经纬度失败:', e)
  905. position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
  906. }
  907. } else {
  908. position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
  909. }
  910. if (!position) {
  911. ElMessage.error('位置计算失败')
  912. return
  913. }
  914. const entity = viewer.entities.add({
  915. name: model.id,
  916. position: position,
  917. model: {
  918. uri: modelUrl,
  919. minimumPixelSize: 0,
  920. maximumScale: 1,
  921. scale: 1.0,
  922. show: true,
  923. distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0.0, 50000000.0),
  924. heightReference: Cesium.HeightReference.NONE
  925. },
  926. _rotation: { x: model.rotationX || 0, y: model.rotationY || 0, z: model.rotationZ || 0 },
  927. _scale: { x: model.scaleX || 1, y: model.scaleY || 1, z: model.scaleZ || 1 }
  928. })
  929. if (model.rotationX !== undefined || model.rotationY !== undefined || model.rotationZ !== undefined) {
  930. const heading = Cesium.Math.toRadians(model.rotationZ || 0);
  931. const pitch = Cesium.Math.toRadians(model.rotationX || 0);
  932. const roll = Cesium.Math.toRadians(model.rotationY || 0);
  933. const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
  934. entity.orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
  935. }
  936. if (model.scaleX !== undefined || model.scaleY !== undefined || model.scaleZ !== undefined) {
  937. const scaleX = model.scaleX || 1;
  938. const scaleY = model.scaleY || 1;
  939. const scaleZ = model.scaleZ || 1;
  940. entity.model.scale = (scaleX + scaleY + scaleZ) / 3;
  941. entity._modelScale = { x: scaleX, y: scaleY, z: scaleZ };
  942. }
  943. this.loadedModelEntities.push(entity)
  944. // requestRenderMode 下,模型资源(glb)异步加载,注册短时渲染需求确保加载完成后立即显示
  945. if (window.__requestContinuousRender) {
  946. window.__requestContinuousRender('model-loading');
  947. clearTimeout(window.__modelLoadTimer);
  948. window.__modelLoadTimer = setTimeout(() => {
  949. window.__cancelContinuousRender && window.__cancelContinuousRender('model-loading');
  950. }, 5000);
  951. }
  952. if (shouldFlyTo) {
  953. setTimeout(() => {
  954. const calculateFlyDistance = () => {
  955. const tryGetModelSize = (attempts = 0) => {
  956. if (attempts > 15) {
  957. viewer.flyTo(entity, {
  958. duration: 2,
  959. offset: new Cesium.HeadingPitchRange(Cesium.Math.toRadians(0), Cesium.Math.toRadians(-30), 100)
  960. });
  961. return;
  962. }
  963. const entityPosition = entity.position.getValue(viewer.clock.currentTime);
  964. if (!entityPosition) {
  965. setTimeout(() => tryGetModelSize(attempts + 1), 300);
  966. return;
  967. }
  968. let bestModel = null;
  969. let bestDistance = Infinity;
  970. for (let i = 0; i < viewer.scene.primitives.length; i++) {
  971. const primitive = viewer.scene.primitives.get(i);
  972. if (primitive instanceof Cesium.Model) {
  973. const modelPosition = primitive.modelMatrix ? primitive.modelMatrix.clone() : null;
  974. if (modelPosition) {
  975. const modelCartesian = Cesium.Matrix4.getTranslation(modelPosition, new Cesium.Cartesian3());
  976. const distance = Cesium.Cartesian3.distance(entityPosition, modelCartesian);
  977. if (distance < bestDistance) {
  978. bestDistance = distance;
  979. bestModel = primitive;
  980. }
  981. }
  982. }
  983. }
  984. if (bestModel && bestModel.boundingSphere && bestModel.boundingSphere.radius > 0) {
  985. const modelRadius = bestModel.boundingSphere.radius;
  986. const flyDistance = modelRadius * 3;
  987. viewer.flyTo(entity, {
  988. duration: 2,
  989. offset: new Cesium.HeadingPitchRange(Cesium.Math.toRadians(0), Cesium.Math.toRadians(-30), flyDistance)
  990. });
  991. return;
  992. }
  993. setTimeout(() => tryGetModelSize(attempts + 1), 300);
  994. };
  995. tryGetModelSize();
  996. };
  997. calculateFlyDistance();
  998. }, 1500)
  999. }
  1000. this.saveLoadedServices()
  1001. this.addModelClickHandler(entity)
  1002. } catch (error) {
  1003. console.error('加载模型失败:', error)
  1004. ElMessage.error('加载模型失败: ' + (error.message || '未知错误'))
  1005. }
  1006. },
  1007. isModelLoaded(modelId) {
  1008. return this.loadedModelEntities.some(entity => entity.name === modelId)
  1009. },
  1010. isSceneLoaded(sceneId) {
  1011. return this.currentSceneId === sceneId
  1012. },
  1013. // 加载场景配置的模型
  1014. async loadSceneModel(model) {
  1015. const viewer = window.viewer
  1016. if (!viewer || !viewer.scene) {
  1017. console.error('Cesium viewer 未初始化')
  1018. return
  1019. }
  1020. if (this.isModelLoaded(model.id)) {
  1021. return
  1022. }
  1023. try {
  1024. const Cesium = window.Cesium
  1025. const coords = model.coordinates.split(',').map(c => parseFloat(c.trim()))
  1026. if (coords.length < 2) {
  1027. console.error('坐标格式不正确:', model.coordinates)
  1028. return
  1029. }
  1030. const lon = coords[0]
  1031. const lat = coords[1]
  1032. const height = coords.length > 2 ? coords[2] : 0
  1033. const position = Cesium.Cartesian3.fromDegrees(lon, lat, height)
  1034. const rotation = model.rotation ? model.rotation.split(',').map(r => parseFloat(r.trim())) : [0, 0, 0]
  1035. const heading = Cesium.Math.toRadians(rotation[2] || 0)
  1036. const pitch = Cesium.Math.toRadians(rotation[0] || 0)
  1037. const roll = Cesium.Math.toRadians(rotation[1] || 0)
  1038. const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll)
  1039. const entity = viewer.entities.add({
  1040. id: model.id,
  1041. name: model.name,
  1042. position: position,
  1043. orientation: Cesium.Transforms.headingPitchRollQuaternion(position, hpr),
  1044. model: {
  1045. uri: model.filePath,
  1046. minimumPixelSize: 128,
  1047. maximumScale: 20000,
  1048. distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0.0, 50000000.0),
  1049. heightReference: Cesium.HeightReference.NONE,
  1050. clampAnimations: true
  1051. },
  1052. disableTransform: model.disableTransform || false,
  1053. gateNodePrefix: model.gateNodePrefix || '',
  1054. gateNodeNames: model.gateNodeNames || []
  1055. })
  1056. this.loadedModelEntities.push(entity)
  1057. const modelFileName = model.filePath.split('/').pop()
  1058. const isModel = (obj) => {
  1059. if (!obj) return false
  1060. const hasModelProps = obj._gltf !== undefined ||
  1061. obj.readyPromise !== undefined ||
  1062. obj._url !== undefined ||
  1063. obj.constructor?.name === 'Model' ||
  1064. (obj._runtimeNodes && Array.isArray(obj._runtimeNodes)) ||
  1065. obj._uri !== undefined
  1066. return hasModelProps
  1067. }
  1068. const findModelRecursively = (collection) => {
  1069. if (!collection || typeof collection.get !== 'function') {
  1070. return null
  1071. }
  1072. let foundModels = []
  1073. for (let i = 0; i < collection.length; i++) {
  1074. const primitive = collection.get(i)
  1075. if (isModel(primitive)) {
  1076. const modelUrl = primitive._url || primitive._uri
  1077. if (modelUrl && modelUrl.includes(modelFileName)) {
  1078. return primitive
  1079. }
  1080. foundModels.push(primitive)
  1081. }
  1082. if (primitive && primitive.length !== undefined && typeof primitive.get === 'function') {
  1083. const found = findModelRecursively(primitive)
  1084. if (found) return found
  1085. }
  1086. }
  1087. if (foundModels.length > 0) {
  1088. return foundModels[0]
  1089. }
  1090. return null
  1091. }
  1092. const processModel = (modelPrimitive) => {
  1093. // 查找 GLTF 数据的辅助函数
  1094. const findGltfData = (obj) => {
  1095. if (!obj || typeof obj !== 'object') return null;
  1096. // 尝试从多个位置获取 GLTF 数据
  1097. if (obj._gltf && obj._gltf.nodes) {
  1098. return obj._gltf;
  1099. }
  1100. if (obj._model && obj._model._gltf && obj._model._gltf.nodes) {
  1101. return obj._model._gltf;
  1102. }
  1103. if (obj._sourceChapters) {
  1104. for (const chapter of obj._sourceChapters) {
  1105. if (chapter._gltf && chapter._gltf.nodes) {
  1106. return chapter._gltf;
  1107. }
  1108. }
  1109. }
  1110. // 深度搜索
  1111. const searchDeep = (currentObj, depth = 0) => {
  1112. if (depth > 3) return null;
  1113. for (const key of Object.keys(currentObj)) {
  1114. if (key.startsWith('_') && typeof currentObj[key] === 'object') {
  1115. if (currentObj[key]._gltf && currentObj[key]._gltf.nodes) {
  1116. return currentObj[key]._gltf;
  1117. }
  1118. const found = searchDeep(currentObj[key], depth + 1);
  1119. if (found) return found;
  1120. }
  1121. }
  1122. return null;
  1123. };
  1124. return searchDeep(obj);
  1125. };
  1126. const waitForGltf = (callback) => {
  1127. let waitCount = 0
  1128. const maxWaitCount = 30 // 减少等待次数,从50次降到30次
  1129. const checkGltf = () => {
  1130. waitCount++
  1131. // 使用增强的查找函数
  1132. const gltfData = findGltfData(modelPrimitive);
  1133. const hasGltfData = gltfData !== null;
  1134. console.log(`⏳ 等待 GLTF [${waitCount}/${maxWaitCount}]: ready=${modelPrimitive.ready}, 找到GLTF=${hasGltfData}`)
  1135. if (hasGltfData) {
  1136. console.log('✅ GLTF 数据完整,开始提取节点...')
  1137. // 将找到的 GLTF 数据挂载到 modelPrimitive 上
  1138. if (!modelPrimitive._gltf) {
  1139. modelPrimitive._gltf = gltfData;
  1140. }
  1141. callback()
  1142. } else if (waitCount < maxWaitCount) {
  1143. setTimeout(checkGltf, 100) // 减少等待间隔,从200ms降到100ms
  1144. } else {
  1145. console.warn('⚠️ 等待超时,尝试提取现有数据...')
  1146. callback()
  1147. }
  1148. }
  1149. checkGltf()
  1150. }
  1151. const handleReadyModel = () => {
  1152. console.log('🔧 handleReadyModel 被调用');
  1153. this.applyTransparentMaterial(modelPrimitive, model.transparency)
  1154. this.enableAntialiasing(modelPrimitive)
  1155. const gateNames = entity.gateNodeNames
  1156. const hasGateNames = gateNames && (Array.isArray(gateNames) || Array.isArray(gateNames.valueOf?.())) && gateNames.length > 0
  1157. console.log('========== 莒口水闸场景处理 ==========');
  1158. console.log('模型URL:', model.url);
  1159. console.log('gateNodeNames:', gateNames);
  1160. console.log('hasGateNames:', hasGateNames);
  1161. console.log('entity:', entity);
  1162. console.log('model:', model);
  1163. // 对于莒口水闸场景,使用控制器初始化闸门
  1164. const modelPath = model.url || model.filePath;
  1165. const isJuKouScene = modelPath && modelPath.includes('莒口水闸')
  1166. if (hasGateNames || isJuKouScene) {
  1167. console.log('🔄 莒口水闸场景,使用控制器初始化闸门...');
  1168. const that = this
  1169. // 使用控制器初始化场景
  1170. createController(entity, {
  1171. initialPosition: that.gatePosition,
  1172. onGateNodesReady: (gateNames) => {
  1173. console.log(`✅ 闸门节点就绪,共 ${gateNames.length} 个闸门`);
  1174. // 显示闸门控制面板
  1175. that.showGateControl = true;
  1176. // 更新 GateControl 组件的闸门列表
  1177. that.$nextTick(() => {
  1178. if (that.$refs.gateControl) {
  1179. that.$refs.gateControl.updateGateList(gateNames);
  1180. console.log('✅ 闸门列表已更新');
  1181. } else if (window.gateControlInstance) {
  1182. window.gateControlInstance.updateGateList(gateNames);
  1183. console.log('✅ 通过全局实例更新闸门列表');
  1184. }
  1185. });
  1186. }
  1187. });
  1188. }
  1189. }
  1190. if (modelPrimitive.ready === true) {
  1191. handleReadyModel()
  1192. } else if (modelPrimitive.readyPromise && typeof modelPrimitive.readyPromise.then === 'function') {
  1193. try {
  1194. const promise = modelPrimitive.readyPromise.then(handleReadyModel)
  1195. if (promise && typeof promise.catch === 'function') {
  1196. promise.catch((error) => {
  1197. console.error('模型加载失败:', error)
  1198. })
  1199. }
  1200. } catch (e) {
  1201. console.error('处理 readyPromise 失败:', e)
  1202. handleReadyModel()
  1203. }
  1204. } else {
  1205. setTimeout(() => {
  1206. handleReadyModel()
  1207. }, 500)
  1208. }
  1209. }
  1210. const findModelInPrimitives = () => {
  1211. if (!window.viewer || !window.viewer.scene) {
  1212. return null
  1213. }
  1214. const primitives = window.viewer.scene.primitives
  1215. return findModelRecursively(primitives)
  1216. }
  1217. let targetModel = findModelInPrimitives()
  1218. if (targetModel) {
  1219. processModel(targetModel)
  1220. } else {
  1221. let pollCount = 0
  1222. const maxPollCount = 50
  1223. const pollForModel = () => {
  1224. pollCount++
  1225. targetModel = findModelInPrimitives()
  1226. if (targetModel) {
  1227. processModel(targetModel)
  1228. } else if (pollCount < maxPollCount) {
  1229. setTimeout(pollForModel, 200)
  1230. }
  1231. }
  1232. pollForModel()
  1233. return
  1234. }
  1235. } catch (error) {
  1236. console.error('加载场景模型失败:', error)
  1237. }
  1238. },
  1239. // 应用半透明材质
  1240. applyTransparentMaterial(modelPrimitive, transparency) {
  1241. try {
  1242. const Cesium = window.Cesium
  1243. const alpha = transparency !== undefined ? transparency : 0.7
  1244. if (modelPrimitive._sourceChapter && modelPrimitive._sourceChapter._programs) {
  1245. const chapters = modelPrimitive._sourceChapter
  1246. if (chapters) {
  1247. modelPrimitive.style = new Cesium.ModelStyle({
  1248. color: Cesium.Color.fromAlpha(Cesium.Color.WHITE, alpha)
  1249. })
  1250. }
  1251. }
  1252. modelPrimitive.color = Cesium.Color.fromAlpha(Cesium.Color.WHITE, alpha)
  1253. } catch (error) {
  1254. console.error('应用半透明材质失败:', error)
  1255. }
  1256. },
  1257. // 启用抗锯齿
  1258. enableAntialiasing(modelPrimitive) {
  1259. try {
  1260. const Cesium = window.Cesium
  1261. modelPrimitive.enableAntiAliasing = true
  1262. if (modelPrimitive.silhouette) {
  1263. modelPrimitive.silhouetteEnabled = true
  1264. }
  1265. } catch (error) {
  1266. console.error('启用抗锯齿失败:', error)
  1267. }
  1268. },
  1269. removeTyphoon() {
  1270. console.log('卸载台风')
  1271. this.showTyphoon = false
  1272. this.typhoonDataUrl = ''
  1273. this.loadedTyphoonId = null
  1274. ElMessage.success('台风路径已卸载')
  1275. },
  1276. // 加载风场
  1277. loadWindField() {
  1278. console.log('加载风场')
  1279. if (!window.viewer) {
  1280. ElMessage.error('Cesium viewer 未初始化')
  1281. return
  1282. }
  1283. this.showWindField = true
  1284. ElMessage.success('风场加载成功')
  1285. },
  1286. // 卸载风场
  1287. removeWindField() {
  1288. console.log('卸载风场')
  1289. this.showWindField = false
  1290. ElMessage.success('风场已卸载')
  1291. },
  1292. // 加载降雨
  1293. loadRainfall() {
  1294. console.log('加载降雨')
  1295. if (!window.viewer) {
  1296. ElMessage.error('Cesium viewer 未初始化')
  1297. return
  1298. }
  1299. this.showRainfall = true
  1300. ElMessage.success('降雨加载成功')
  1301. },
  1302. // 卸载降雨
  1303. removeRainfall() {
  1304. console.log('卸载降雨')
  1305. this.showRainfall = false
  1306. ElMessage.success('降雨已卸载')
  1307. },
  1308. // 加载云图
  1309. loadCloudMap() {
  1310. console.log('加载云图')
  1311. if (!window.viewer) {
  1312. ElMessage.error('Cesium viewer 未初始化')
  1313. return
  1314. }
  1315. this.showCloudMap = true
  1316. ElMessage.success('云图加载成功')
  1317. },
  1318. // 卸载云图
  1319. removeCloudMap() {
  1320. console.log('卸载云图')
  1321. this.showCloudMap = false
  1322. ElMessage.success('云图已卸载')
  1323. },
  1324. // 加载雷达
  1325. loadRadar() {
  1326. console.log('加载雷达')
  1327. if (!window.viewer) {
  1328. ElMessage.error('Cesium viewer 未初始化')
  1329. return
  1330. }
  1331. this.showRadar = true
  1332. ElMessage.success('雷达加载成功')
  1333. },
  1334. // 卸载雷达
  1335. removeRadar() {
  1336. console.log('卸载雷达')
  1337. this.showRadar = false
  1338. ElMessage.success('雷达已卸载')
  1339. },
  1340. removeModel(model) {
  1341. const index = this.loadedModelEntities.findIndex(entity => entity.name === model.id)
  1342. if (index !== -1) {
  1343. const entity = this.loadedModelEntities[index]
  1344. // 恢复模型样式
  1345. this.restoreModelStyle(entity);
  1346. // 清理点击事件 handler
  1347. if (entity._clickHandler) {
  1348. entity._clickHandler.destroy();
  1349. entity._clickHandler = null;
  1350. }
  1351. viewer.entities.remove(entity)
  1352. this.loadedModelEntities.splice(index, 1)
  1353. this.saveLoadedServices()
  1354. // 如果当前编辑器是这个模型,清理编辑器
  1355. if (this.currentModelEditor && this.currentModelEditor.entity === entity) {
  1356. if (this.currentModelEditor.destroy) {
  1357. this.currentModelEditor.destroy();
  1358. }
  1359. this.currentModelEditor = null;
  1360. }
  1361. // 如果最后高亮的模型是这个模型,清除引用
  1362. if (this.lastHighlightedModel === entity) {
  1363. this.lastHighlightedModel = null;
  1364. }
  1365. // ElMessage.success(`模型 "${model.name}" 已删除`)
  1366. }
  1367. },
  1368. // 加载实时台风(API获取逻辑由 RealtimeTyphoon 组件内部处理)
  1369. loadRealtimeTyphoon() {
  1370. console.log('加载实时台风')
  1371. if (!window.viewer) {
  1372. ElMessage.error('Cesium viewer 未初始化')
  1373. return
  1374. }
  1375. // 如果已加载历史台风,先卸载避免冲突
  1376. if (this.showTyphoon) {
  1377. this.removeTyphoon()
  1378. }
  1379. this.showRealtimeTyphoon = true
  1380. },
  1381. // 卸载实时台风
  1382. removeRealtimeTyphoon() {
  1383. console.log('卸载实时台风')
  1384. this.showRealtimeTyphoon = false
  1385. ElMessage.success('实时台风已卸载')
  1386. },
  1387. // 加载业务场景
  1388. loadBusinessScenario(scenarioType) {
  1389. console.log('加载业务场景:', scenarioType)
  1390. if (!window.viewer) {
  1391. ElMessage.error('Cesium viewer 未初始化')
  1392. return
  1393. }
  1394. const scenarioConfig = {
  1395. 'flood-control': {
  1396. title: '防洪排涝',
  1397. description: '加载防洪排涝业务场景相关数据'
  1398. },
  1399. 'water-resource': {
  1400. title: '水资源管理',
  1401. description: '加载水资源管理业务场景相关数据'
  1402. },
  1403. 'project-supervision': {
  1404. title: '水利工程监管',
  1405. description: '加载水利工程监管业务场景相关数据'
  1406. },
  1407. 'eco-environment': {
  1408. title: '生态环境保护',
  1409. description: '加载生态环境保护业务场景相关数据'
  1410. }
  1411. }
  1412. const config = scenarioConfig[scenarioType]
  1413. if (config) {
  1414. ElMessage.info(`${config.title}:${config.description}`)
  1415. console.log(config.description)
  1416. } else {
  1417. ElMessage.warning('未知的业务场景类型')
  1418. }
  1419. },
  1420. // 创建业务场景 - 通知父组件打开弹窗
  1421. handleCreateScenario() {
  1422. console.log('触发创建业务场景事件')
  1423. this.$emit('open-create-scenario')
  1424. },
  1425. // 从数据库刷新业务场景列表
  1426. refreshBusinessScenes() {
  1427. console.log('从数据库加载业务场景列表...')
  1428. listBusinessScene({}).then(response => {
  1429. if (response.code === 200) {
  1430. this.businessScenes = response.rows || []
  1431. console.log('业务场景列表加载成功:', this.businessScenes)
  1432. } else {
  1433. console.error('加载业务场景列表失败:', response.msg)
  1434. ElMessage.error('加载业务场景列表失败')
  1435. }
  1436. }).catch(error => {
  1437. console.error('加载业务场景列表出错:', error)
  1438. ElMessage.error('加载业务场景列表出错')
  1439. })
  1440. },
  1441. // 加载固定场景列表
  1442. loadFixedScenes() {
  1443. this.fixedScenes = getFixedScenes()
  1444. console.log('固定场景列表加载成功:', this.fixedScenes)
  1445. },
  1446. // 加载固定场景
  1447. async loadFixedScene(scene) {
  1448. console.log('加载固定场景:', scene)
  1449. if (!window.viewer) {
  1450. ElMessage.error('Cesium viewer 未初始化')
  1451. return
  1452. }
  1453. if (!window.Cesium) {
  1454. ElMessage.error('Cesium 库未加载')
  1455. return
  1456. }
  1457. try {
  1458. // 设置当前场景ID
  1459. this.currentSceneId = scene.sceneId
  1460. // 加载第三人称相机位置(视口跳转)
  1461. if (scene.thirdPersonCameraPos) {
  1462. const cameraPos = JSON.parse(scene.thirdPersonCameraPos)
  1463. if (!Array.isArray(cameraPos) || cameraPos.length !== 3) {
  1464. console.error('相机位置格式不正确:', cameraPos)
  1465. ElMessage.error('相机位置数据格式错误')
  1466. return
  1467. }
  1468. const Cesium = window.Cesium
  1469. const posCartesian = Cesium.Cartesian3.fromArray(cameraPos)
  1470. if (!posCartesian) {
  1471. console.error('Cartesian3.fromArray 返回 undefined')
  1472. ElMessage.error('坐标转换失败')
  1473. return
  1474. }
  1475. // 优先使用方向向量和上向量设置相机
  1476. if (scene.cameraDirection && scene.cameraUp) {
  1477. const direction = Cesium.Cartesian3.fromArray(JSON.parse(scene.cameraDirection))
  1478. const up = Cesium.Cartesian3.fromArray(JSON.parse(scene.cameraUp))
  1479. if (direction && up) {
  1480. console.log('使用方向向量和上向量设置相机')
  1481. window.viewer.camera.setView({
  1482. destination: posCartesian,
  1483. orientation: {
  1484. direction: direction,
  1485. up: up
  1486. }
  1487. })
  1488. } else {
  1489. // 回退到HPR角度设置
  1490. const heading = scene.thirdPersonCameraHeading || 0
  1491. const pitch = scene.thirdPersonCameraPitch || Cesium.Math.toRadians(-90)
  1492. const roll = scene.thirdPersonCameraRoll || 0
  1493. window.viewer.camera.setView({
  1494. destination: posCartesian,
  1495. orientation: {
  1496. heading: heading,
  1497. pitch: pitch,
  1498. roll: roll
  1499. }
  1500. })
  1501. }
  1502. } else {
  1503. // 使用目标位置计算方向向量
  1504. if (scene.thirdPersonCameraTarget) {
  1505. const cameraTarget = JSON.parse(scene.thirdPersonCameraTarget)
  1506. if (Array.isArray(cameraTarget) && cameraTarget.length === 3) {
  1507. const targetCartesian = Cesium.Cartesian3.fromArray(cameraTarget)
  1508. if (targetCartesian) {
  1509. const direction = Cesium.Cartesian3.subtract(posCartesian, targetCartesian, new Cesium.Cartesian3())
  1510. Cesium.Cartesian3.normalize(direction, direction)
  1511. window.viewer.camera.setView({
  1512. destination: posCartesian,
  1513. orientation: {
  1514. direction: direction,
  1515. up: new Cesium.Cartesian3(0, 0, 1)
  1516. }
  1517. })
  1518. }
  1519. }
  1520. } else {
  1521. // 使用HPR角度设置
  1522. const heading = scene.thirdPersonCameraHeading || 0
  1523. const pitch = scene.thirdPersonCameraPitch || Cesium.Math.toRadians(-90)
  1524. const roll = scene.thirdPersonCameraRoll || 0
  1525. window.viewer.camera.setView({
  1526. destination: posCartesian,
  1527. orientation: {
  1528. heading: heading,
  1529. pitch: pitch,
  1530. roll: roll
  1531. }
  1532. })
  1533. }
  1534. }
  1535. }
  1536. // 莒口水闸场景使用独立页面组件
  1537. if (scene.sceneId === JuKouSceneConfig.sceneId) {
  1538. console.log('📌 使用莒口水闸独立页面组件')
  1539. // 显示莒口水闸页面组件
  1540. this.showJuKouScenePage = true
  1541. this.gatePosition = scene.initialGatePosition || 0
  1542. // 等待组件加载场景
  1543. // 场景加载逻辑由 JuKouShuiZhaPage 组件内部处理
  1544. } else {
  1545. // 其他场景使用通用加载方法
  1546. if (scene.loadedModels) {
  1547. try {
  1548. const models = JSON.parse(scene.loadedModels)
  1549. console.log('需要加载的模型:', models)
  1550. for (const model of models) {
  1551. const modelObj = {
  1552. id: model.name,
  1553. name: model.name,
  1554. filePath: model.filePath,
  1555. coordinates: model.coordinates,
  1556. rotation: model.rotation || '0,0,0',
  1557. transparency: model.transparency,
  1558. disableTransform: scene.disableModelTransform || false,
  1559. gateNodePrefix: scene.gateNodePrefix || '',
  1560. gateNodeNames: scene.gateNodeNames || []
  1561. }
  1562. this.loadSceneModel(modelObj)
  1563. }
  1564. } catch (e) {
  1565. console.error('解析模型配置失败:', e)
  1566. }
  1567. }
  1568. if (scene.enableGateControl) {
  1569. this.gatePosition = scene.initialGatePosition || 0
  1570. console.log('📌 场景启用闸门控制,等待模型加载...')
  1571. }
  1572. }
  1573. // 加载模型拆解图标
  1574. if (scene.modelDisassemblyIcon) {
  1575. this.modelIconConfig = scene.modelDisassemblyIcon
  1576. }
  1577. ElMessage.success(`已加载固定场景:${scene.sceneName}`)
  1578. } catch (error) {
  1579. console.error('加载固定场景失败:', error)
  1580. ElMessage.error('加载固定场景失败')
  1581. }
  1582. },
  1583. // 卸载场景
  1584. unloadScene(scene) {
  1585. console.log('卸载场景:', scene)
  1586. // 莒口水闸场景使用独立卸载方法
  1587. if (scene.sceneId === JuKouSceneConfig.sceneId) {
  1588. console.log('📌 使用莒口水闸独立场景卸载方法')
  1589. unloadJuKouScene()
  1590. } else {
  1591. // 其他场景使用通用卸载方法
  1592. if (scene.loadedModels) {
  1593. try {
  1594. const models = JSON.parse(scene.loadedModels)
  1595. console.log('需要卸载的模型:', models)
  1596. for (const model of models) {
  1597. this.unloadSceneModel(model.name)
  1598. }
  1599. } catch (e) {
  1600. console.error('解析模型配置失败:', e)
  1601. }
  1602. }
  1603. }
  1604. // 关闭闸门控制面板
  1605. this.showGateControl = false
  1606. this.gatePosition = 0
  1607. // 关闭莒口水闸页面组件
  1608. this.showJuKouScenePage = false
  1609. // 清除模型拆解图标配置
  1610. this.modelIconConfig = null
  1611. // 清除当前场景ID
  1612. this.currentSceneId = null
  1613. ElMessage.success(`已卸载场景:${scene.sceneName}`)
  1614. },
  1615. // 卸载场景模型
  1616. unloadSceneModel(modelId) {
  1617. const viewer = window.viewer
  1618. if (!viewer) {
  1619. console.error('Cesium viewer 未初始化')
  1620. return
  1621. }
  1622. // 查找并移除模型实体
  1623. const entityIndex = this.loadedModelEntities.findIndex(entity => entity.name === modelId)
  1624. if (entityIndex !== -1) {
  1625. const entity = this.loadedModelEntities[entityIndex]
  1626. viewer.entities.remove(entity)
  1627. this.loadedModelEntities.splice(entityIndex, 1)
  1628. console.log('场景模型卸载完成:', modelId)
  1629. } else {
  1630. console.log('未找到需要卸载的模型:', modelId)
  1631. }
  1632. },
  1633. // 显示删除确认对话框
  1634. showDeleteConfirm(scene) {
  1635. console.log('显示删除确认:', scene)
  1636. // 检查是否为固定场景
  1637. if (isFixedScene(scene.sceneId)) {
  1638. ElMessage.warning('固定场景不能删除,只能卸载')
  1639. return
  1640. }
  1641. ElMessageBox.confirm(
  1642. '确定要删除场景 "' + scene.sceneName + '" 吗?',
  1643. '提示',
  1644. {
  1645. confirmButtonText: '确定',
  1646. cancelButtonText: '取消',
  1647. type: 'warning'
  1648. }
  1649. ).then(() => {
  1650. this.handleDeleteBusinessScene(scene.sceneId)
  1651. }).catch(() => {
  1652. ElMessage.info('已取消删除')
  1653. })
  1654. },
  1655. // 删除业务场景
  1656. handleDeleteBusinessScene(sceneId) {
  1657. console.log('删除业务场景:', sceneId)
  1658. delBusinessScene(sceneId).then(response => {
  1659. if (response.code === 200) {
  1660. ElMessage.success('业务场景删除成功')
  1661. // 刷新场景列表
  1662. this.refreshBusinessScenes()
  1663. } else {
  1664. ElMessage.error('删除业务场景失败: ' + response.msg)
  1665. }
  1666. }).catch(error => {
  1667. console.error('删除业务场景出错:', error)
  1668. ElMessage.error('删除业务场景出错')
  1669. })
  1670. },
  1671. // 从数据库加载业务场景
  1672. loadBusinessScenarioFromDB(scene) {
  1673. console.log('从数据库加载业务场景:', scene)
  1674. if (!window.viewer) {
  1675. ElMessage.error('Cesium viewer 未初始化')
  1676. return
  1677. }
  1678. if (!window.Cesium) {
  1679. ElMessage.error('Cesium 库未加载')
  1680. return
  1681. }
  1682. try {
  1683. // 设置当前场景ID
  1684. this.currentSceneId = scene.sceneId
  1685. // 加载第三人称相机位置(视口跳转)
  1686. if (scene.thirdPersonCameraPos && scene.thirdPersonCameraTarget) {
  1687. console.log('thirdPersonCameraPos:', scene.thirdPersonCameraPos)
  1688. console.log('thirdPersonCameraTarget:', scene.thirdPersonCameraTarget)
  1689. const cameraPos = JSON.parse(scene.thirdPersonCameraPos)
  1690. const cameraTarget = JSON.parse(scene.thirdPersonCameraTarget)
  1691. console.log('解析后的相机位置:', cameraPos, '类型:', Array.isArray(cameraPos))
  1692. console.log('解析后的目标位置:', cameraTarget, '类型:', Array.isArray(cameraTarget))
  1693. // 检查数组长度是否正确
  1694. if (!Array.isArray(cameraPos) || cameraPos.length !== 3) {
  1695. console.error('相机位置格式不正确:', cameraPos)
  1696. ElMessage.error('相机位置数据格式错误')
  1697. return
  1698. }
  1699. if (!Array.isArray(cameraTarget) || cameraTarget.length !== 3) {
  1700. console.error('目标位置格式不正确:', cameraTarget)
  1701. ElMessage.error('目标位置数据格式错误')
  1702. return
  1703. }
  1704. // 使用 Cesium 的 flyTo 方法跳转到指定视角
  1705. const Cesium = window.Cesium
  1706. console.log('Cesium.Cartesian3:', Cesium.Cartesian3)
  1707. const posCartesian = Cesium.Cartesian3.fromArray(cameraPos)
  1708. console.log('posCartesian:', posCartesian)
  1709. const targetCartesian = Cesium.Cartesian3.fromArray(cameraTarget)
  1710. console.log('targetCartesian:', targetCartesian)
  1711. if (!posCartesian || !targetCartesian) {
  1712. console.error('Cartesian3.fromArray 返回 undefined')
  1713. ElMessage.error('坐标转换失败')
  1714. return
  1715. }
  1716. const direction = Cesium.Cartesian3.subtract(
  1717. posCartesian,
  1718. targetCartesian,
  1719. new Cesium.Cartesian3()
  1720. )
  1721. console.log('direction before normalize:', direction)
  1722. Cesium.Cartesian3.normalize(direction, direction)
  1723. console.log('direction after normalize:', direction)
  1724. // 优先使用方向向量和上向量精确恢复相机状态
  1725. if (scene.thirdPersonCameraDirection && scene.thirdPersonCameraUp) {
  1726. try {
  1727. const directionArray = JSON.parse(scene.thirdPersonCameraDirection)
  1728. const upArray = JSON.parse(scene.thirdPersonCameraUp)
  1729. if (directionArray && directionArray.length === 3 && upArray && upArray.length === 3) {
  1730. const direction = new Cesium.Cartesian3(
  1731. directionArray[0],
  1732. directionArray[1],
  1733. directionArray[2]
  1734. )
  1735. const up = new Cesium.Cartesian3(
  1736. upArray[0],
  1737. upArray[1],
  1738. upArray[2]
  1739. )
  1740. console.log('使用方向向量和上向量恢复相机状态')
  1741. console.log('方向向量:', direction)
  1742. console.log('上向量:', up)
  1743. // 使用 setView 方法精确恢复相机状态
  1744. window.viewer.camera.setView({
  1745. destination: posCartesian,
  1746. orientation: {
  1747. direction: direction,
  1748. up: up
  1749. }
  1750. })
  1751. console.log('使用方向向量和上向量恢复相机完成')
  1752. return // 使用方向向量恢复后继续后续逻辑
  1753. }
  1754. } catch (e) {
  1755. console.error('解析方向向量或上向量失败:', e)
  1756. }
  1757. }
  1758. // 如果没有方向向量,使用 HPR 角度(数据库中已存储为弧度值)
  1759. const heading = scene.thirdPersonCameraHeading || 0
  1760. const pitch = scene.thirdPersonCameraPitch || Cesium.Math.toRadians(-90) // 默认俯视90度
  1761. const roll = scene.thirdPersonCameraRoll || 0
  1762. console.log('保存的相机朝向(弧度):', heading, pitch, roll)
  1763. console.log('保存的相机朝向(角度):', Cesium.Math.toDegrees(heading), Cesium.Math.toDegrees(pitch), Cesium.Math.toDegrees(roll))
  1764. // 使用 setView 方法设置相机位置和朝向
  1765. // 注意:数据库中保存的已经是弧度值,不需要再转换
  1766. window.viewer.camera.setView({
  1767. destination: posCartesian,
  1768. orientation: {
  1769. heading: heading,
  1770. pitch: pitch,
  1771. roll: roll
  1772. }
  1773. })
  1774. console.log('使用HPR角度恢复相机完成')
  1775. }
  1776. // 加载第一人称相机设置
  1777. if (scene.firstPersonCameraPos && scene.firstPersonCameraTarget) {
  1778. const firstPersonPos = JSON.parse(scene.firstPersonCameraPos)
  1779. const firstPersonTarget = JSON.parse(scene.firstPersonCameraTarget)
  1780. // 可以根据需要应用第一人称相机设置
  1781. console.log('第一人称相机位置:', firstPersonPos, firstPersonTarget)
  1782. }
  1783. // 加载模型
  1784. if (scene.loadedModels) {
  1785. const models = JSON.parse(scene.loadedModels)
  1786. console.log('需要加载的模型:', models)
  1787. // 可以根据需要加载模型
  1788. }
  1789. // 加载数据服务
  1790. if (scene.dataServices) {
  1791. const services = JSON.parse(scene.dataServices)
  1792. console.log('需要加载的数据服务:', services)
  1793. // 可以根据需要加载数据服务
  1794. }
  1795. // 加载POI点
  1796. if (scene.poiPoints) {
  1797. const poiPoints = JSON.parse(scene.poiPoints)
  1798. console.log('需要加载的POI点:', poiPoints)
  1799. // 可以根据需要加载POI点
  1800. }
  1801. // 数据库场景不显示闸门控制框
  1802. this.showGateControl = false
  1803. ElMessage.success(`已加载业务场景:${scene.sceneName}`)
  1804. } catch (error) {
  1805. console.error('加载业务场景失败:', error)
  1806. ElMessage.error('加载业务场景失败')
  1807. }
  1808. },
  1809. // 处理闸门位置变化(支持单独控制每个闸门)
  1810. handleGatePositionChange(data) {
  1811. console.log('🔧 handleGatePositionChange 被调用:', data)
  1812. // 获取控制器实例
  1813. const controller = getController()
  1814. if (!controller || !controller.isInitialized) {
  1815. console.warn('⚠️ 莒口水闸场景控制器未初始化')
  1816. return
  1817. }
  1818. // 使用控制器设置闸门位置
  1819. if (typeof data === 'number') {
  1820. // 设置所有闸门位置
  1821. this.gatePosition = data
  1822. controller.setAllGatesPosition(data)
  1823. } else if (data && data.gateName && typeof data.position === 'number') {
  1824. // 设置单个闸门位置
  1825. controller.setSingleGatePosition(data.gateName, data.position)
  1826. }
  1827. },
  1828. // 莒口水闸场景加载完成回调
  1829. onJuKouSceneLoaded() {
  1830. console.log('✅ 莒口水闸场景页面组件加载完成')
  1831. },
  1832. // 莒口水闸场景卸载完成回调
  1833. onJuKouSceneUnloaded() {
  1834. console.log('✅ 莒口水闸场景页面组件已卸载')
  1835. this.showJuKouScenePage = false
  1836. },
  1837. // 区分地质体组件(需销毁)和其他组件
  1838. change(val, val2) {
  1839. if (val2) {
  1840. this.view = "";
  1841. if (val === this.view2) {
  1842. this.view2 = "";
  1843. return;
  1844. }
  1845. this.view2 = val;
  1846. } else {
  1847. this.view2 = "";
  1848. if (val === this.view) {
  1849. this.view = "";
  1850. return;
  1851. }
  1852. this.view = val;
  1853. }
  1854. },
  1855. //添加公共服务
  1856. addWebServe(obj, shouldFlyTo = true) {
  1857. console.log('===== addWebServe 开始 =====');
  1858. console.log('服务名称:', obj.name);
  1859. console.log('服务类型:', obj.type);
  1860. console.log('服务状态:', obj.state);
  1861. console.log('是否有transparentColor:', obj.transparentColor);
  1862. if (obj.state === 0) {
  1863. // add mvt
  1864. if (obj.type === "MVT") {
  1865. console.log('MVT类型,直接返回');
  1866. layerManagement.addMvtLayer(obj.proxiedUrl, obj.name, () => {
  1867. obj.state = 1;
  1868. window.store.actions.setChangeLayers();
  1869. this.saveLoadedServices();
  1870. });
  1871. return;
  1872. }
  1873. // add scene
  1874. console.log('REALSPACE类型,准备加载场景');
  1875. // useServerView 为 true 时使用超图服务端默认视口,否则使用本地配置的视口
  1876. const useServerView = obj.useServerView || false;
  1877. // 构建场景选项,包含透明色配置
  1878. const sceneOptions = {
  1879. autoSetView: useServerView
  1880. };
  1881. if (obj.transparentColor) {
  1882. sceneOptions.transparentColor = obj.transparentColor;
  1883. sceneOptions.transparentColorTolerance = obj.transparentColorTolerance || 0.01;
  1884. console.log('透明色配置:', sceneOptions);
  1885. } else {
  1886. console.log('没有配置透明色');
  1887. }
  1888. layerManagement.addScene(
  1889. obj.proxiedUrl,
  1890. sceneOptions,
  1891. layers => {
  1892. console.log('场景加载完成回调');
  1893. obj.state = 1;
  1894. window.store.actions.setChangeLayers();
  1895. // 白膜添加线框
  1896. if (obj.style && obj.style.fillStyle) {
  1897. layers[0].style3D.lineColor = Cesium.Color.fromCssColorString(
  1898. "rgb(67,67,67)"
  1899. );
  1900. layers[0].style3D.fillStyle =
  1901. Cesium.FillStyle[obj.style.fillStyle];
  1902. }
  1903. this.saveLoadedServices();
  1904. // 如果使用服务端默认视口,则不需要调用本地flyTo
  1905. if (shouldFlyTo && !useServerView) {
  1906. this.flyTo(obj.name, server_flyTo_config);
  1907. }
  1908. }
  1909. );
  1910. } else {
  1911. console.log('服务已加载,仅执行飞行');
  1912. if (shouldFlyTo) {
  1913. this.flyTo(obj.name, server_flyTo_config);
  1914. }
  1915. }
  1916. },
  1917. // 添加底图
  1918. addBaseLayer(obj) {
  1919. if (this.baseLayerObj.type === obj.type) return;
  1920. this.baseLayerObj.state = 0;
  1921. obj.state = 1;
  1922. this.baseLayerObj = obj;
  1923. let type = obj.type;
  1924. let url = obj.proxiedUrl;
  1925. let imageryLayerCollection = viewer.scene.globe._imageryLayerCollection;
  1926. let layer = imageryLayerCollection.get(0);
  1927. let imageryProvider;
  1928. // 保存注记图层(如果存在)
  1929. let labelLayer = null;
  1930. for (let i = 0; i < imageryLayerCollection.length; i++) {
  1931. const currentLayer = imageryLayerCollection.get(i);
  1932. if (currentLayer && currentLayer.imageryProvider && currentLayer.imageryProvider.url && currentLayer.imageryProvider.url.includes('cia_c')) {
  1933. labelLayer = currentLayer;
  1934. imageryLayerCollection.remove(currentLayer);
  1935. break;
  1936. }
  1937. }
  1938. if (imageryLayerCollection.get(2)) {
  1939. imageryLayerCollection.remove(imageryLayerCollection.get(2));
  1940. }
  1941. if (imageryLayerCollection.get(1)) {
  1942. imageryLayerCollection.remove(imageryLayerCollection.get(1));
  1943. }
  1944. switch (type) {
  1945. case "BINGMAP":
  1946. imageryProvider = new Cesium.BingMapsImageryProvider({
  1947. url: url,
  1948. key:
  1949. "Aq0D7MCY5ErORA9vrwFtfE9aancUq5J6uNjw0GieF0ostaIrVuJZ8ScXxNHHvEwS"
  1950. });
  1951. break;
  1952. case "TIANDITU":
  1953. // 使用天地图影像底图(使用HTTPS)
  1954. imageryProvider = new Cesium.WebMapTileServiceImageryProvider({
  1955. url: `${PROXY_TIANDITU_TILE}/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
  1956. layer: 'img',
  1957. style: 'default',
  1958. format: 'image/jpeg',
  1959. tileMatrixSetID: 'c',
  1960. subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
  1961. tilingScheme: new Cesium.GeographicTilingScheme(),
  1962. tileMatrixLabels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
  1963. maximumLevel: 15,
  1964. show: true
  1965. });
  1966. break;
  1967. case "IMAGE":
  1968. imageryProvider = new Cesium.SingleTileImageryProvider({
  1969. url: url
  1970. });
  1971. break;
  1972. case "OSM":
  1973. imageryProvider = new Cesium.createOpenStreetMapImageryProvider({
  1974. url: url
  1975. });
  1976. break;
  1977. case "MAPBOX":
  1978. imageryProvider = new Cesium.MapboxImageryProvider({
  1979. mapId: "mapbox.dark"
  1980. });
  1981. break;
  1982. case "SUPERMAPDARK":
  1983. imageryProvider = new Cesium.SuperMapImageryProvider({
  1984. url: url
  1985. });
  1986. break;
  1987. case "SUPERMAPLIGHT":
  1988. imageryProvider = new Cesium.SuperMapImageryProvider({
  1989. url: url
  1990. });
  1991. break;
  1992. case "GRIDIMAGERY":
  1993. imageryProvider = imageryProvider;
  1994. break;
  1995. default:
  1996. imageryProvider = new Cesium.SingleTileImageryProvider({
  1997. url: url
  1998. });
  1999. break;
  2000. }
  2001. if (type != "GRIDIMAGERY") {
  2002. imageryLayerCollection.addImageryProvider(imageryProvider, 0);
  2003. imageryLayerCollection.remove(layer);
  2004. // 重新添加注记图层
  2005. if (type === "TIANDITU") {
  2006. // 当天地图作为底图时,总是添加影像注记图层
  2007. // tk 来自外部配置 supermap.config.js
  2008. labelLayer = imageryLayerCollection.addImageryProvider(
  2009. new Cesium.WebMapTileServiceImageryProvider({
  2010. url: `${PROXY_TIANDITU_TILE}/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
  2011. layer: 'cia',
  2012. style: 'default',
  2013. format: 'image/png',
  2014. tileMatrixSetID: 'c',
  2015. subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
  2016. tilingScheme: new Cesium.GeographicTilingScheme(),
  2017. tileMatrixLabels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
  2018. maximumLevel: 15,
  2019. show: true
  2020. })
  2021. );
  2022. console.log('为天地图添加影像注记图层');
  2023. } else if (!labelLayer) {
  2024. // 其他底图时,如果没有注记图层,则添加
  2025. // tk 来自外部配置 supermap.config.js
  2026. labelLayer = imageryLayerCollection.addImageryProvider(
  2027. new Cesium.WebMapTileServiceImageryProvider({
  2028. url: `${PROXY_TIANDITU_TILE}/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
  2029. layer: 'cia',
  2030. style: 'default',
  2031. format: 'image/png',
  2032. tileMatrixSetID: 'c',
  2033. subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
  2034. tilingScheme: new Cesium.GeographicTilingScheme(),
  2035. tileMatrixLabels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
  2036. maximumLevel: 15,
  2037. show: true
  2038. })
  2039. );
  2040. console.log('添加天地图影像注记图层');
  2041. } else {
  2042. imageryLayerCollection.addImageryProvider(labelLayer);
  2043. console.log('恢复天地图影像注记图层');
  2044. }
  2045. }
  2046. if (type == "GRIDIMAGERY") {
  2047. imageryLayerCollection.addImageryProvider(
  2048. new Cesium.TileCoordinatesImageryProvider(),
  2049. 1
  2050. );
  2051. imageryLayerCollection.addImageryProvider(
  2052. new Cesium.GridImageryProvider(),
  2053. 2
  2054. );
  2055. }
  2056. this.saveLoadedServices();
  2057. },
  2058. // 添加在线地形
  2059. addOnlineTerrain(obj) {
  2060. if (this.terrainLayerObj) this.terrainLayerObj.state = 0;
  2061. obj.state = 1;
  2062. this.terrainLayerObj = obj;
  2063. let type = obj.type;
  2064. let url = obj.proxiedUrl;
  2065. switch (type) {
  2066. case "STKTerrain":
  2067. viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
  2068. url: url,
  2069. isSct: false
  2070. });
  2071. break;
  2072. case "tianDiTuTerrain":
  2073. console.log('加载 Cesium 官方地形...');
  2074. const cesiumIonToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiZGYwNWZmMi0wYTJjLTQ3MDgtODk3Mi1mNTg4YjljZTMzYWUiLCJpZCI6MjA0NTU5LCJpYXQiOjE3MTE0NDc4NDh9.xU-eWEQxv-FeADvsH_uM35EKRN6brqmXl1AQm6phnEM";
  2075. Cesium.Ion.defaultAccessToken = cesiumIonToken;
  2076. // 低性能机器关闭水面掩码和顶点法线,减少地形数据量和光照计算负担
  2077. const perfLow = window.__perfTier === 'low';
  2078. try {
  2079. viewer.terrainProvider = Cesium.createWorldTerrain({
  2080. requestWaterMask: !perfLow,
  2081. requestVertexNormals: !perfLow
  2082. });
  2083. console.log('Cesium 地形加载成功', perfLow ? '(低性能模式:已关闭水面掩码/顶点法线)' : '');
  2084. } catch (e) {
  2085. console.error('Cesium 地形加载失败:', e);
  2086. viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
  2087. }
  2088. break;
  2089. case "supermapOnlineTerrain":
  2090. viewer.terrainProvider = new Cesium.SCTTerrainProvider({
  2091. urls: [url]
  2092. });
  2093. break;
  2094. default:
  2095. break;
  2096. }
  2097. this.saveLoadedServices();
  2098. },
  2099. // 移除地形
  2100. removeTerrainLayer() {
  2101. viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider({
  2102. ellipsoid: viewer.scene.globe.ellipsoid
  2103. });
  2104. this.terrainLayerObj = null;
  2105. },
  2106. addDatas(id, obj, shouldFlyTo = true) {
  2107. switch (id) {
  2108. case "webServe":
  2109. this.addWebServe(obj, shouldFlyTo);
  2110. break;
  2111. case "baseLayer":
  2112. this.addBaseLayer(obj);
  2113. break;
  2114. case "onlineTerrain":
  2115. this.addOnlineTerrain(obj);
  2116. break;
  2117. case "vectorData":
  2118. this.addVectorData(obj, shouldFlyTo);
  2119. break;
  2120. }
  2121. },
  2122. // 添加矢量数据服务 - 根据类型选择加载方式
  2123. addVectorData(obj, shouldFlyTo = true) {
  2124. console.log('===== addVectorData 开始 =====');
  2125. console.log('服务名称:', obj.name);
  2126. console.log('服务URL:', obj.proxiedUrl);
  2127. console.log('服务类型:', obj.type);
  2128. console.log('服务状态:', obj.state);
  2129. if (!viewer) {
  2130. ElMessage.error('Cesium viewer 未初始化');
  2131. return;
  2132. }
  2133. // 检查URL是否为空
  2134. if (!obj.proxiedUrl || obj.proxiedUrl.trim() === '') {
  2135. ElMessage.warning(`${obj.name} 暂无数据`);
  2136. return;
  2137. }
  2138. if (obj.state === 0) {
  2139. // 根据数据类型选择加载方式
  2140. if (obj.type === 'MVT') {
  2141. // MVT 类型使用矢量瓦片加载
  2142. console.log('使用MVT方式加载');
  2143. layerManagement.addMvtLayer(obj.proxiedUrl, obj.name, obj.style, (mvtLayer) => {
  2144. if (mvtLayer) {
  2145. obj.state = 1;
  2146. window.store.actions.setChangeLayers();
  2147. this.saveLoadedServices();
  2148. console.log(`${obj.name} MVT加载成功`);
  2149. ElMessage.success(`${obj.name} 加载成功`);
  2150. // 添加MVT图层点击事件处理,传入当前图层的瓦片URL
  2151. this.addMvtClickHandler(obj.proxiedUrl);
  2152. } else {
  2153. ElMessage.error(`加载 ${obj.name} 失败`);
  2154. }
  2155. });
  2156. } else if (obj.type === 'RESTFEATURE') {
  2157. // RESTFEATURE 类型使用GeoJsonDataSource加载REST数据服务
  2158. console.log('使用RESTFEATURE方式加载');
  2159. this.addRestFeatureLayer(obj, shouldFlyTo);
  2160. } else if (obj.type === 'RESTIMAGE') {
  2161. // RESTIMAGE 类型使用 SuperMapImageryProvider 加载栅格地图服务
  2162. console.log('使用RESTIMAGE方式加载');
  2163. const imgLayer = layerManagement.addImageLayer(obj.proxiedUrl, obj.name, { skipOutputPrj: obj.skipOutputPrj });
  2164. if (imgLayer) {
  2165. obj.state = 1;
  2166. window.store.actions.setChangeLayers();
  2167. this.saveLoadedServices();
  2168. console.log(`${obj.name} 栅格图加载成功`);
  2169. ElMessage.success(`${obj.name} 加载成功`);
  2170. // 土地利用加载时显示图例
  2171. if (obj.name === '土地利用') {
  2172. this.showLandUseLegend = true;
  2173. }
  2174. } else {
  2175. ElMessage.error(`加载 ${obj.name} 失败`);
  2176. }
  2177. } else {
  2178. ElMessage.warning(`不支持的数据类型: ${obj.type}`);
  2179. }
  2180. } else {
  2181. console.log('服务已加载');
  2182. if (shouldFlyTo) {
  2183. this.flyToVectorData(obj.name);
  2184. }
  2185. }
  2186. },
  2187. // 使用GeoJsonDataSource加载REST要素服务(参考SHP转GeoJson的加载方式)
  2188. addRestFeatureLayer(obj, shouldFlyTo = true) {
  2189. console.log('===== 使用GeoJsonDataSource加载矢量数据 =====');
  2190. console.log('服务名称:', obj.name);
  2191. console.log('服务URL:', obj.proxiedUrl);
  2192. // 获取样式配置
  2193. const style = obj.style || {
  2194. fillColor: '#00FF00',
  2195. fillOpacity: 0.4,
  2196. strokeColor: '#0055FF',
  2197. strokeWidth: 2
  2198. };
  2199. // 参考SHP转GeoJson的加载方式,使用Cesium.GeoJsonDataSource.load
  2200. layerManagement.addGeoJsonLayerWithStyle(obj.proxiedUrl, obj.name, style, (geoJsonLayer) => {
  2201. if (geoJsonLayer) {
  2202. obj.state = 1;
  2203. window.store.actions.setChangeLayers();
  2204. this.saveLoadedServices();
  2205. console.log(`${obj.name} 矢量数据加载成功`);
  2206. if (shouldFlyTo) {
  2207. viewer.flyTo(geoJsonLayer, { duration: 2 });
  2208. }
  2209. // 为矢量数据添加点击弹窗事件
  2210. this.addVectorDataClickHandler(obj.name);
  2211. ElMessage.success(`${obj.name} 加载成功`);
  2212. } else {
  2213. ElMessage.error(`加载 ${obj.name} 失败`);
  2214. }
  2215. });
  2216. },
  2217. // 定位到矢量数据
  2218. flyToVectorData(name) {
  2219. if (!viewer || !viewer.dataSources) return;
  2220. const dataSources = viewer.dataSources._dataSources;
  2221. for (const ds of dataSources) {
  2222. if (ds.name === name || (ds.name && ds.name.includes(name))) {
  2223. viewer.flyTo(ds, { duration: 2 });
  2224. break;
  2225. }
  2226. }
  2227. },
  2228. // 为矢量数据添加点击弹窗事件
  2229. addVectorDataClickHandler(layerName) {
  2230. if (!viewer) return;
  2231. // 确保只添加一次点击事件
  2232. if (viewer._vectorDataClickHandler) {
  2233. viewer._vectorDataClickHandler.destroy();
  2234. }
  2235. viewer._vectorDataClickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
  2236. viewer._vectorDataClickHandler.setInputAction((movement) => {
  2237. const picked = viewer.scene.pick(movement.position);
  2238. if (Cesium.defined(picked) && picked.id && picked.id._dataSource) {
  2239. const dataSource = picked.id._dataSource;
  2240. const entity = picked.id;
  2241. // 检查是否是当前加载的矢量数据层
  2242. if (dataSource.name === layerName || (dataSource.name && dataSource.name.includes(layerName))) {
  2243. this.showVectorDataPopup(entity, movement.position);
  2244. }
  2245. } else if (Cesium.defined(picked) && picked.id && picked.id._properties) {
  2246. // 处理通过GeoJsonDataSource加载的实体
  2247. const entity = picked.id;
  2248. this.showVectorDataPopup(entity, movement.position);
  2249. }
  2250. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  2251. },
  2252. // 显示矢量数据弹窗
  2253. showVectorDataPopup(entity, position) {
  2254. console.log('点击矢量数据:', entity);
  2255. // 获取实体属性
  2256. let properties = {};
  2257. if (entity._properties) {
  2258. properties = entity._properties.getValue(Cesium.JulianDate.now());
  2259. } else if (entity.properties) {
  2260. properties = entity.properties.getValue(Cesium.JulianDate.now()) || entity.properties;
  2261. }
  2262. // 构建弹窗内容
  2263. let content = '<div style="padding: 10px; max-width: 300px;">';
  2264. content += `<h3 style="margin: 0 0 10px 0; font-size: 14px; color: #333;">${entity.name || '属性信息'}</h3>`;
  2265. if (properties && typeof properties === 'object') {
  2266. for (const key in properties) {
  2267. if (properties.hasOwnProperty(key)) {
  2268. let value = properties[key];
  2269. if (value === null || value === undefined) value = '';
  2270. if (typeof value === 'object') value = JSON.stringify(value);
  2271. content += `<div style="margin: 5px 0; font-size: 12px;">
  2272. <span style="color: #666;">${key}:</span>
  2273. <span style="margin-left: 5px; color: #333;">${value}</span>
  2274. </div>`;
  2275. }
  2276. }
  2277. } else {
  2278. content += '<div style="color: #999; font-size: 12px;">暂无属性信息</div>';
  2279. }
  2280. content += '</div>';
  2281. // 使用tooltip显示弹窗(tooltip2.js的API是 showAt(message, top))
  2282. if (window.tooltip) {
  2283. const topPosition = position.y + 'px';
  2284. window.tooltip.showAt(content, topPosition);
  2285. }
  2286. },
  2287. // 添加MVT图层点击事件处理
  2288. addMvtClickHandler(tileBaseUrl) {
  2289. console.log('aside.vue addMvtClickHandler 被调用', tileBaseUrl);
  2290. // 初始化 MVT 图层列表(支持多图层)
  2291. if (!this._mvtTileUrls) {
  2292. this._mvtTileUrls = [];
  2293. }
  2294. if (tileBaseUrl) {
  2295. const tileUrl = tileBaseUrl.endsWith('/')
  2296. ? tileBaseUrl + 'tiles/{z}/{x}/{y}.mvt'
  2297. : tileBaseUrl + '/tiles/{z}/{x}/{y}.mvt';
  2298. // 避免重复添加相同的瓦片URL
  2299. if (!this._mvtTileUrls.includes(tileUrl)) {
  2300. this._mvtTileUrls.push(tileUrl);
  2301. }
  2302. }
  2303. // 初始化点击处理器(单例)
  2304. if (!this.mvtClickHandler) {
  2305. this.mvtClickHandler = new MvtClickHandler(this);
  2306. }
  2307. // 标记已加载 MVT 图层,全局点击处理器会进行检测
  2308. this._hasMvtLayer = true;
  2309. },
  2310. // 初始化viewer后的传入回调:添加底图和清除加载动画
  2311. async removeLoad() {
  2312. this.isRestoring = true; // 标记正在恢复,禁止保存
  2313. setTimeout(() => {
  2314. let loading = document.getElementById("loadingbar");
  2315. if (loading) loading.remove(); //移除加载动画
  2316. document.getElementById("el-aside").classList.remove("disable"); //解除禁止点击
  2317. }, 1000);
  2318. // 等待自定义服务加载完成后再恢复
  2319. setTimeout(async () => {
  2320. await this.fetchCustomServices();
  2321. await this.restoreLoadedServices();
  2322. this.isRestoring = false; // 恢复完成,允许保存
  2323. // 加载水面图层
  2324. this.loadWaterLayers();
  2325. }, 1500);
  2326. },
  2327. // 加载水面图层
  2328. loadWaterLayers() {
  2329. console.log('========== loadWaterLayers 开始执行 ==========');
  2330. console.log('window.viewer:', window.viewer);
  2331. if (!window.viewer) {
  2332. console.error('window.viewer 不存在,无法加载水面图层');
  2333. return;
  2334. }
  2335. console.log('开始加载莒口水闸河道范围水面...');
  2336. // 加载莒口水闸河道范围水面
  2337. waterLayer.loadWaterLayer(
  2338. '/js/莒口水闸河道范围.geojson',
  2339. '莒口水闸河道',
  2340. (dataSource) => {
  2341. console.log('水面图层加载完成', dataSource);
  2342. }
  2343. );
  2344. },
  2345. // 删除场景公共服务
  2346. DeleteDates(datatype, obj) {
  2347. switch (datatype) {
  2348. case "webServe":
  2349. obj.layers.forEach(layer => {
  2350. layerManagement.layersDelete(layer.type, layer.layerName, () => {
  2351. obj.state = 0;
  2352. window.store.actions.setChangeLayers();
  2353. this.saveLoadedServices();
  2354. });
  2355. });
  2356. break;
  2357. case "baseLayer":
  2358. break;
  2359. case "onlineTerrain":
  2360. this.removeTerrainLayer();
  2361. obj.state = 0;
  2362. this.saveLoadedServices();
  2363. break;
  2364. case "vectorData":
  2365. // 根据矢量数据类型选择卸载方式:MVT用removeVectorTilesMap,RESTIMAGE用imageryLayers,RESTFEATURE用dataSources
  2366. let vectorDeleteType;
  2367. if (obj.type === 'MVT') {
  2368. vectorDeleteType = 'MVT';
  2369. } else if (obj.type === 'RESTIMAGE') {
  2370. vectorDeleteType = 'IMG';
  2371. } else {
  2372. vectorDeleteType = 'GEOJSON';
  2373. }
  2374. layerManagement.layersDelete(vectorDeleteType, obj.name, () => {
  2375. obj.state = 0;
  2376. window.store.actions.setChangeLayers();
  2377. this.saveLoadedServices();
  2378. // 土地利用卸载时隐藏图例
  2379. if (obj.name === '土地利用') {
  2380. this.showLandUseLegend = false;
  2381. }
  2382. });
  2383. // MVT 卸载后从图层列表中移除,并更新标记
  2384. if (obj.type === 'MVT') {
  2385. // 从 _mvtTileUrls 中移除当前图层的 URL
  2386. if (this._mvtTileUrls && obj.proxiedUrl) {
  2387. const tileUrl = obj.proxiedUrl.endsWith('/')
  2388. ? obj.proxiedUrl + 'tiles/{z}/{x}/{y}.mvt'
  2389. : obj.proxiedUrl + '/tiles/{z}/{x}/{y}.mvt';
  2390. const index = this._mvtTileUrls.indexOf(tileUrl);
  2391. if (index > -1) {
  2392. this._mvtTileUrls.splice(index, 1);
  2393. }
  2394. }
  2395. // 只有当所有 MVT 图层都卸载时,才清除标记
  2396. this._hasMvtLayer = this._mvtTileUrls && this._mvtTileUrls.length > 0;
  2397. this.mvtPopupVisible = false;
  2398. }
  2399. break;
  2400. }
  2401. },
  2402. isServiceLoaded(service) {
  2403. if (!service) {
  2404. return false;
  2405. }
  2406. const serviceId = service.id || service.serviceId;
  2407. return this.loadedCustomServiceIds.includes(serviceId);
  2408. },
  2409. async loadCustomService(service, flyTo = true, shouldSave = true) {
  2410. console.log('========== 开始加载自定义服务 ==========');
  2411. console.log('服务名称:', service.name);
  2412. console.log('服务类型:', service.type);
  2413. console.log('服务URL:', service.url);
  2414. console.log('是否跳转视角:', flyTo);
  2415. console.log('viewer状态:', viewer);
  2416. if (!viewer) {
  2417. ElMessage.error('Cesium viewer 未初始化');
  2418. return;
  2419. }
  2420. if (this.isServiceLoaded(service)) {
  2421. console.log('服务已加载,执行定位');
  2422. if (flyTo) {
  2423. const type = service.type || (service.layers && service.layers[0] ? service.layers[0].type : null);
  2424. if (type === 'SCENE' || type === 'S3M') {
  2425. const layerNames = service.loadedLayerNames || [service.name];
  2426. let foundLayer = null;
  2427. for (const name of layerNames) {
  2428. foundLayer = viewer.scene.layers.find(name);
  2429. if (foundLayer) break;
  2430. }
  2431. if (foundLayer) {
  2432. console.log('跳转到图层:', foundLayer.name);
  2433. viewer.flyTo(foundLayer, { duration: 2 });
  2434. } else if (viewer.scene.layers.layerQueue && viewer.scene.layers.layerQueue.length > 0) {
  2435. viewer.flyTo(viewer.scene.layers.layerQueue[0], { duration: 2 });
  2436. }
  2437. } else {
  2438. const layerName = service.name;
  2439. camera.flyByLayerName(type, layerName);
  2440. }
  2441. }
  2442. return;
  2443. }
  2444. try {
  2445. const type = service.type || (service.layers && service.layers[0] ? service.layers[0].type : null);
  2446. const url = service.url;
  2447. const layerName = service.name;
  2448. const token = service.token;
  2449. const tokenRequired = service.tokenRequired;
  2450. const serviceId = service.id || service.serviceId;
  2451. const markAsLoaded = () => {
  2452. if (!this.loadedCustomServiceIds.includes(serviceId)) {
  2453. this.loadedCustomServiceIds.push(serviceId);
  2454. }
  2455. };
  2456. switch (type) {
  2457. case 'SCENE':
  2458. let sceneOptions = {
  2459. autoSetView: flyTo
  2460. };
  2461. if (tokenRequired && token) {
  2462. sceneOptions.SceneToken = token;
  2463. }
  2464. const beforeLayersCount = viewer.scene.layers ? viewer.scene.layers.layerQueue.length : 0;
  2465. console.log('加载前图层数量:', beforeLayersCount);
  2466. console.log('是否自动跳转视角:', flyTo);
  2467. layerManagement.addScene(url, sceneOptions, (layers) => {
  2468. console.log('SCENE加载成功:', layers);
  2469. markAsLoaded();
  2470. const afterLayers = viewer.scene.layers ? viewer.scene.layers.layerQueue : [];
  2471. const newLayers = afterLayers.slice(beforeLayersCount);
  2472. console.log('新添加的图层:', newLayers);
  2473. const newLayerNames = newLayers.map(l => l.name);
  2474. console.log('新图层名称:', newLayerNames);
  2475. service.loadedLayerNames = newLayerNames;
  2476. const currentPosition = viewer.camera.positionCartographic;
  2477. service.defaultCameraPosition = {
  2478. longitude: currentPosition.longitude,
  2479. latitude: currentPosition.latitude,
  2480. height: currentPosition.height,
  2481. heading: viewer.camera.heading,
  2482. pitch: viewer.camera.pitch,
  2483. roll: viewer.camera.roll
  2484. };
  2485. console.log('记录默认视口位置:', service.defaultCameraPosition);
  2486. if (flyTo && newLayers.length > 0) {
  2487. camera.flyByLayerName('SCENE', newLayers[0].name);
  2488. }
  2489. });
  2490. break;
  2491. case 'S3M':
  2492. let scps = [{ url: url, options: { name: layerName } }];
  2493. layerManagement.addS3mLayers(scps, (layers) => {
  2494. console.log('S3M加载成功:', layers);
  2495. markAsLoaded();
  2496. if (flyTo) {
  2497. camera.flyByLayerName('S3M', layerName);
  2498. }
  2499. });
  2500. break;
  2501. case 'IMG':
  2502. case 'IMAGE':
  2503. let imgLayer = layerManagement.addImageLayer(url, layerName);
  2504. console.log('IMG/IMAGE加载成功:', imgLayer);
  2505. markAsLoaded();
  2506. if (flyTo) {
  2507. camera.flyByLayerName('IMG', layerName);
  2508. }
  2509. break;
  2510. case 'TERRAIN':
  2511. let terrainProvider = layerManagement.addTerrainLayer(url, false);
  2512. console.log('TERRAIN加载成功:', terrainProvider);
  2513. markAsLoaded();
  2514. break;
  2515. case 'MVT':
  2516. layerManagement.addMvtLayer(url, layerName, (mvtlayer) => {
  2517. console.log('MVT加载成功:', mvtlayer);
  2518. markAsLoaded();
  2519. if (flyTo) {
  2520. viewer.flyTo(mvtlayer, { duration: 2 });
  2521. }
  2522. // 添加MVT图层点击事件处理,传入当前图层的瓦片URL
  2523. this.addMvtClickHandler(url);
  2524. });
  2525. break;
  2526. case 'SHP':
  2527. case 'GEOJSON':
  2528. case 'VECTOR_TILE':
  2529. layerManagement.addGeoJsonLayer(url, layerName, (geoJsonLayer) => {
  2530. console.log('SHP/GEOJSON/VECTOR_TILE加载成功:', geoJsonLayer);
  2531. markAsLoaded();
  2532. if (flyTo) {
  2533. viewer.flyTo(geoJsonLayer, { duration: 2 });
  2534. }
  2535. });
  2536. break;
  2537. default:
  2538. ElMessage.warning('不支持的服务类型: ' + type);
  2539. return;
  2540. }
  2541. if (shouldSave) {
  2542. ElMessage.success(`服务 "${service.name}" 加载成功`);
  2543. }
  2544. console.log('========== 自定义服务加载完成 ==========');
  2545. if (shouldSave) {
  2546. this.saveLoadedServices();
  2547. }
  2548. } catch (error) {
  2549. console.error('加载自定义服务失败:', error);
  2550. ElMessage.error('加载服务失败: ' + (error.message || '未知错误'));
  2551. }
  2552. },
  2553. unloadCustomService(service) {
  2554. console.log('开始卸载服务:', service);
  2555. if (!service) {
  2556. console.warn('服务对象为空');
  2557. return;
  2558. }
  2559. const serviceId = service.id || service.serviceId;
  2560. const serviceType = service.type || 'SCENE';
  2561. const finishUnload = () => {
  2562. const idx = this.loadedCustomServiceIds.indexOf(serviceId);
  2563. if (idx > -1) {
  2564. this.loadedCustomServiceIds.splice(idx, 1);
  2565. console.log('已从loadedCustomServiceIds中移除服务:', serviceId);
  2566. }
  2567. window.store.actions.setChangeLayers();
  2568. this.saveLoadedServices();
  2569. this.$forceUpdate();
  2570. };
  2571. if (serviceType === 'SCENE' && service.loadedLayerNames && service.loadedLayerNames.length > 0) {
  2572. console.log('使用loadedLayerNames卸载SCENE图层:', service.loadedLayerNames);
  2573. let unloadedCount = 0;
  2574. service.loadedLayerNames.forEach((layerName, index) => {
  2575. console.log('卸载图层:', layerName);
  2576. layerManagement.layersDelete('S3M', layerName, () => {
  2577. console.log('图层已卸载:', layerName);
  2578. unloadedCount++;
  2579. if (unloadedCount === service.loadedLayerNames.length) {
  2580. finishUnload();
  2581. }
  2582. });
  2583. });
  2584. } else if (service.layers && service.layers.length > 0) {
  2585. let unloadedCount = 0;
  2586. service.layers.forEach((layer, index) => {
  2587. console.log('卸载图层:', layer.type, layer.layerName);
  2588. layerManagement.layersDelete(layer.type, layer.layerName, () => {
  2589. console.log('图层已卸载:', layer.layerName);
  2590. unloadedCount++;
  2591. if (unloadedCount === service.layers.length) {
  2592. finishUnload();
  2593. }
  2594. });
  2595. });
  2596. } else {
  2597. const type = service.type || 'SCENE';
  2598. const layerName = service.name;
  2599. console.log('卸载参数 - serviceId:', serviceId, 'type:', type, 'layerName:', layerName);
  2600. layerManagement.layersDelete(type, layerName, () => {
  2601. console.log('服务已卸载:', service.name);
  2602. finishUnload();
  2603. });
  2604. }
  2605. },
  2606. // 控制自定义服务组件显隐
  2607. addCustomService() {
  2608. if (this.addService === "") {
  2609. this.addService = "Sm3dCustomService";
  2610. // 重置面板位置到默认值,使用setTimeout确保组件完全渲染
  2611. setTimeout(() => {
  2612. const panel = document.getElementById('CustomService-panel');
  2613. if (panel) {
  2614. panel.style.left = '35%';
  2615. panel.style.top = '25%';
  2616. }
  2617. }, 100);
  2618. } else {
  2619. this.addService = "";
  2620. }
  2621. // 移除自定义服务菜单项的选中状态
  2622. this.$nextTick(() => {
  2623. const menu = this.$refs.menu;
  2624. if (menu) {
  2625. menu.activeIndex = "";
  2626. }
  2627. });
  2628. },
  2629. // 自定义服务添加回调函数
  2630. addCallback(layers, add_type, serviceInfo) {
  2631. console.log('接收到addCallback调用:', layers, add_type, serviceInfo);
  2632. if (!Array.isArray(layers)) {
  2633. layers = [layers];
  2634. }
  2635. let serviceOption = [];
  2636. let serviceUrl = serviceInfo ? serviceInfo.url : "";
  2637. let serviceToken = serviceInfo ? serviceInfo.token : "";
  2638. let isAddToken = serviceInfo ? serviceInfo.tokenRequired : false;
  2639. try {
  2640. for (let i = 0; i < layers.length; i++) {
  2641. if (!layers[i]) {
  2642. console.log('layers[' + i + '] 是 undefined 或 null,跳过');
  2643. continue;
  2644. }
  2645. if (layers[i].hasOwnProperty("_isS3MB")) {
  2646. let option = {
  2647. type: "S3M",
  2648. layerName: layers[i].name
  2649. ? layers[i].name
  2650. : "s3m_" + new Date().getTime().toFixed(6)
  2651. };
  2652. serviceOption.push(option);
  2653. if (!layers[i].maxVisibleAltitude)
  2654. layers[i].maxVisibleAltitude = 8000;
  2655. } else if (viewer.imageryLayers.contains(layers[i])) {
  2656. let option = {
  2657. type: "IMG",
  2658. layerName: layers[i].imageryProvider.tablename
  2659. ? layers[i].imageryProvider.tablename
  2660. : "image" + new Date().getTime().toFixed(6)
  2661. };
  2662. serviceOption.push(option);
  2663. } else if (
  2664. layers[i].imageryLayer &&
  2665. layers[i].imageryLayer._imageryProvider instanceof
  2666. Cesium.MvtProviderGL
  2667. ) {
  2668. let option = {
  2669. type: "MVT",
  2670. layerName: layers[i].name
  2671. ? layers[i].name
  2672. : "mvt" + new Date().getTime().toFixed(6)
  2673. };
  2674. serviceOption.push(option);
  2675. let index = this.addCustomServices.length;
  2676. let obj = {
  2677. name: layers[i].name ? layers[i].name : "自定义服务" + index,
  2678. layers: serviceOption
  2679. };
  2680. this.addCustomServices.push(obj);
  2681. this.addService = "";
  2682. // 存储服务到数据库
  2683. console.log('准备调用saveServiceToDatabase (MVT):', serviceInfo, add_type, obj.name);
  2684. if (serviceInfo && serviceUrl) {
  2685. console.log('调用saveServiceToDatabase (MVT)');
  2686. this.saveServiceToDatabase(serviceInfo, add_type, obj.name);
  2687. } else {
  2688. console.log('serviceInfo或serviceUrl为空 (MVT):', serviceInfo, serviceUrl);
  2689. }
  2690. return;
  2691. } else {
  2692. let option = {
  2693. type: "TERRAIN",
  2694. layerName: layers[i].tablename ? layers[i].tablename : "terrain"
  2695. };
  2696. serviceOption.push(option);
  2697. }
  2698. }
  2699. } catch (error) {
  2700. console.error('处理layers时发生错误:', error);
  2701. }
  2702. if (this.varName) this.getNames.push(this.varName);
  2703. let index = this.addCustomServices.length;
  2704. let len = this.getNames.length - 1;
  2705. let obj = {
  2706. name: this.varName ? this.varName : "自定义服务" + index,
  2707. layers: serviceOption
  2708. };
  2709. this.addCustomServices.push(obj);
  2710. this.addService = "";
  2711. this.varName = undefined;
  2712. // 存储服务到数据库
  2713. // SHP上传后端已自动保存,跳过前端重复保存
  2714. const isShpUpload = serviceUrl && serviceUrl.startsWith('/profile/geojson/');
  2715. if (!isShpUpload) {
  2716. console.log('准备调用saveServiceToDatabase (非MVT):', serviceInfo, add_type, obj.name);
  2717. if (serviceInfo && serviceUrl) {
  2718. console.log('调用saveServiceToDatabase (非MVT)');
  2719. this.saveServiceToDatabase(serviceInfo, add_type, obj.name);
  2720. } else {
  2721. console.log('serviceInfo或serviceUrl为空 (非MVT):', serviceInfo, serviceUrl);
  2722. }
  2723. } else {
  2724. console.log('SHP上传后端已自动保存,直接标记为已加载');
  2725. this.addService = "";
  2726. this.varName = undefined;
  2727. // 使用上传响应中返回的id更新服务对象并标记为已加载
  2728. const serviceId = serviceInfo && serviceInfo.id;
  2729. if (serviceId) {
  2730. // 更新已在列表中的obj,使isServiceLoaded能匹配
  2731. obj.id = serviceId;
  2732. obj.url = serviceUrl;
  2733. obj.type = add_type || 'GEOJSON';
  2734. obj.layers = [{ type: 'GEOJSON', layerName: serviceInfo.layerName || obj.name }];
  2735. if (!this.loadedCustomServiceIds.includes(serviceId)) {
  2736. this.loadedCustomServiceIds.push(serviceId);
  2737. }
  2738. }
  2739. return;
  2740. }
  2741. if ((add_type && add_type === "SCENE") || add_type === "TERRAIN") return;
  2742. camera.flyByLayerName(add_type, obj.layers[0].layerName); //定位
  2743. },
  2744. // 保存服务到数据库
  2745. saveServiceToDatabase(serviceInfo, serviceType, serviceName) {
  2746. console.log('开始保存服务到数据库:', serviceInfo, serviceType, serviceName);
  2747. const serviceData = {
  2748. name: serviceName,
  2749. type: serviceType,
  2750. url: serviceInfo.url,
  2751. tokenRequired: serviceInfo.tokenRequired ? 1 : 0,
  2752. token: serviceInfo.token,
  2753. status: 'NORMAL'
  2754. };
  2755. console.log('准备保存服务:', serviceData);
  2756. serviceApi.addService(serviceData)
  2757. .then(response => {
  2758. console.log('服务保存成功:', response);
  2759. const newServiceId = response.data || response.serviceId;
  2760. if (newServiceId) {
  2761. const service = this.addCustomServices.find(s => s.name === serviceName);
  2762. if (service) {
  2763. service.id = newServiceId;
  2764. service.url = serviceInfo.url;
  2765. service.token = serviceInfo.token;
  2766. service.tokenRequired = serviceInfo.tokenRequired;
  2767. service.type = serviceType;
  2768. }
  2769. if (!this.loadedCustomServiceIds.includes(newServiceId)) {
  2770. this.loadedCustomServiceIds.push(newServiceId);
  2771. }
  2772. }
  2773. })
  2774. .catch(error => {
  2775. console.error('服务保存失败:', error);
  2776. if (error.response) {
  2777. console.error('错误响应状态:', error.response.status);
  2778. console.error('错误响应数据:', error.response.data);
  2779. } else if (error.request) {
  2780. console.error('错误请求:', error.request);
  2781. } else {
  2782. console.error('错误信息:', error.message);
  2783. }
  2784. });
  2785. },
  2786. // 自定义服务获取名称回调
  2787. getName(name) {
  2788. this.varName = name;
  2789. },
  2790. // 添加服务飞行函数(公共服务配置好的定位)
  2791. flyTo(webName, obj) {
  2792. let cameraParam = obj[webName];
  2793. if (cameraParam) {
  2794. let destination;
  2795. // 支持两种格式:经纬度格式和笛卡尔坐标格式
  2796. if (cameraParam.longitude !== undefined && cameraParam.latitude !== undefined) {
  2797. destination = Cesium.Cartesian3.fromDegrees(
  2798. cameraParam.longitude,
  2799. cameraParam.latitude,
  2800. cameraParam.height || 50000
  2801. );
  2802. } else if (cameraParam.Cartesian3) {
  2803. destination = new Cesium.Cartesian3(
  2804. cameraParam.Cartesian3.x,
  2805. cameraParam.Cartesian3.y,
  2806. cameraParam.Cartesian3.z
  2807. );
  2808. }
  2809. viewer.scene.camera.flyTo({
  2810. destination: destination,
  2811. orientation: {
  2812. heading: cameraParam.heading,
  2813. pitch: cameraParam.pitch,
  2814. roll: cameraParam.roll
  2815. },
  2816. duration: 2
  2817. });
  2818. return;
  2819. } else {
  2820. }
  2821. },
  2822. //图层管理右键删除回调
  2823. deleteCallback(node_rightClick) {
  2824. this.server_config[0].children.forEach(sceneObj => {
  2825. sceneObj.layers.forEach(layerObj => {
  2826. if (layerObj.layerName === node_rightClick.label) {
  2827. sceneObj.state = 0;
  2828. return;
  2829. }
  2830. });
  2831. });
  2832. },
  2833. // 保存已加载的服务到数据库 (带防抖)
  2834. saveLoadedServices() {
  2835. if (this.isRestoring) {
  2836. console.log('正在恢复配置,跳过保存');
  2837. return;
  2838. }
  2839. if (this.saveConfigTimer) {
  2840. clearTimeout(this.saveConfigTimer);
  2841. }
  2842. this.saveConfigTimer = setTimeout(async () => {
  2843. const now = Date.now();
  2844. if (now - this.lastSaveTime < 2000) {
  2845. console.log('距离上次保存不足2秒,跳过保存');
  2846. return;
  2847. }
  2848. try {
  2849. const loadedServices = {
  2850. baseLayer: this.baseLayerObj ? {
  2851. type: this.baseLayerObj.type,
  2852. name: this.baseLayerObj.name,
  2853. proxiedUrl: this.baseLayerObj.proxiedUrl
  2854. } : null,
  2855. terrainLayer: this.terrainLayerObj ? {
  2856. type: this.terrainLayerObj.type,
  2857. name: this.terrainLayerObj.name,
  2858. proxiedUrl: this.terrainLayerObj.proxiedUrl
  2859. } : null,
  2860. webServices: this.server_config[0].children
  2861. .filter(service => service.state === 1)
  2862. .map(service => ({
  2863. type: service.type,
  2864. name: service.name,
  2865. proxiedUrl: service.proxiedUrl,
  2866. layers: service.layers,
  2867. style: service.style
  2868. }))
  2869. };
  2870. const loadedModels = this.loadedModelEntities.map(entity => {
  2871. const model = this.modelData.find(m => m.id === entity.name);
  2872. return model ? {
  2873. id: model.id,
  2874. name: model.name,
  2875. type: model.type,
  2876. filePath: model.filePath,
  2877. coordinates: model.coordinates,
  2878. format: model.format,
  2879. rotationX: model.rotationX,
  2880. rotationY: model.rotationY,
  2881. rotationZ: model.rotationZ,
  2882. scaleX: model.scaleX,
  2883. scaleY: model.scaleY,
  2884. scaleZ: model.scaleZ
  2885. } : null;
  2886. }).filter(m => m !== null);
  2887. const loadedCustomServices = this.addCustomServices
  2888. .filter(service => this.loadedCustomServiceIds.includes(service.id || service.serviceId))
  2889. .map(service => ({
  2890. id: service.id || service.serviceId,
  2891. name: service.name,
  2892. type: service.type,
  2893. url: service.url,
  2894. tokenRequired: service.tokenRequired,
  2895. token: service.token,
  2896. loadedLayerNames: service.loadedLayerNames || []
  2897. }));
  2898. const configData = {
  2899. configName: '默认配置',
  2900. baseLayerType: loadedServices.baseLayer ? loadedServices.baseLayer.type : '',
  2901. baseLayerName: loadedServices.baseLayer ? loadedServices.baseLayer.name : '',
  2902. baseLayerUrl: loadedServices.baseLayer ? loadedServices.baseLayer.proxiedUrl : '',
  2903. terrainLayerType: loadedServices.terrainLayer ? loadedServices.terrainLayer.type : '',
  2904. terrainLayerName: loadedServices.terrainLayer ? loadedServices.terrainLayer.name : '',
  2905. terrainLayerUrl: loadedServices.terrainLayer ? loadedServices.terrainLayer.proxiedUrl : '',
  2906. webServices: JSON.stringify(loadedServices.webServices),
  2907. loadedModels: JSON.stringify(loadedModels),
  2908. loadedCustomServices: JSON.stringify(loadedCustomServices)
  2909. };
  2910. console.log('========== 保存地图配置 ==========');
  2911. await saveMapConfig(configData);
  2912. this.lastSaveTime = Date.now();
  2913. console.log('地图配置已保存到数据库');
  2914. } catch (error) {
  2915. console.error('保存地图配置失败:', error);
  2916. console.error('错误详情:', error.message);
  2917. if (error.response) {
  2918. console.error('响应状态:', error.response.status);
  2919. console.error('响应数据:', error.response.data);
  2920. }
  2921. }
  2922. }, 500);
  2923. },
  2924. // 从数据库恢复已加载的服务和模型
  2925. async restoreLoadedServices() {
  2926. try {
  2927. console.log('========== 开始恢复地图配置 ==========');
  2928. const response = await getDefaultMapConfig();
  2929. console.log('API响应:', response);
  2930. if (!response || !response.data) {
  2931. console.log('没有找到保存的地图配置,使用默认配置');
  2932. return;
  2933. }
  2934. const config = response.data;
  2935. console.log('保存的配置:', config);
  2936. let loadedServices = null;
  2937. let loadedModels = null;
  2938. // 保存需要恢复的自定义服务信息(不立即设置loadedCustomServiceIds,避免跳过加载步骤)
  2939. let customServicesToLoad = [];
  2940. if (config.loadedCustomServices) {
  2941. try {
  2942. const savedCustomServices = JSON.parse(config.loadedCustomServices);
  2943. console.log('解析的自定义服务配置:', savedCustomServices);
  2944. if (savedCustomServices && savedCustomServices.length > 0) {
  2945. customServicesToLoad = savedCustomServices.map(s => ({
  2946. id: s.id,
  2947. loadedLayerNames: s.loadedLayerNames || []
  2948. }));
  2949. console.log('需要恢复的自定义服务:', customServicesToLoad);
  2950. }
  2951. } catch (e) {
  2952. console.error('解析自定义服务配置失败:', e);
  2953. }
  2954. }
  2955. // 解析服务配置
  2956. if (config.webServices) {
  2957. try {
  2958. loadedServices = JSON.parse(config.webServices);
  2959. console.log('解析的服务配置:', loadedServices);
  2960. } catch (e) {
  2961. console.error('解析服务配置失败:', e);
  2962. }
  2963. }
  2964. // 解析模型配置
  2965. if (config.loadedModels) {
  2966. try {
  2967. loadedModels = JSON.parse(config.loadedModels);
  2968. console.log('解析的模型配置:', loadedModels);
  2969. } catch (e) {
  2970. console.error('解析模型配置失败:', e);
  2971. }
  2972. }
  2973. // 恢复底图 (server_config[2] 是在线底图)
  2974. if (config.baseLayerType) {
  2975. const baseLayer = this.server_config[2].children.find(
  2976. layer => layer.type === config.baseLayerType
  2977. );
  2978. if (baseLayer) {
  2979. console.log('恢复底图:', baseLayer.name);
  2980. await new Promise(resolve => {
  2981. this.addBaseLayer(baseLayer);
  2982. setTimeout(resolve, 500);
  2983. });
  2984. }
  2985. }
  2986. // 恢复地形 (server_config[3] 是在线地形)
  2987. if (config.terrainLayerType) {
  2988. const terrainLayer = this.server_config[3].children.find(
  2989. layer => layer.type === config.terrainLayerType
  2990. );
  2991. if (terrainLayer) {
  2992. console.log('恢复地形:', terrainLayer.name);
  2993. await new Promise(resolve => {
  2994. this.addOnlineTerrain(terrainLayer);
  2995. setTimeout(resolve, 500);
  2996. });
  2997. }
  2998. }
  2999. // 恢复公共服务 (server_config[1] 是公共服务)
  3000. if (loadedServices && loadedServices.length > 0) {
  3001. for (const webService of loadedServices) {
  3002. const service = this.server_config[1].children.find(
  3003. s => s.name === webService.name
  3004. );
  3005. if (service) {
  3006. console.log('恢复公共服务:', service.name);
  3007. await new Promise(resolve => {
  3008. service.state = 0;
  3009. this.addWebServe(service, false);
  3010. setTimeout(resolve, 500);
  3011. });
  3012. }
  3013. }
  3014. }
  3015. // 恢复模型 —— 默认初始化时不加载模型,由用户手动加载
  3016. // if (loadedModels && loadedModels.length > 0) {
  3017. // for (const modelData of loadedModels) {
  3018. // const model = this.modelData.find(m => m.id === modelData.id);
  3019. // if (model && !this.isModelLoaded(model.id)) {
  3020. // await new Promise(resolve => {
  3021. // this.loadModel(model, false);
  3022. // setTimeout(resolve, 1000);
  3023. // });
  3024. // }
  3025. // }
  3026. // }
  3027. // 恢复自定义服务 (初始化时不跳转视角,不保存)
  3028. if (customServicesToLoad && customServicesToLoad.length > 0) {
  3029. for (const serviceInfo of customServicesToLoad) {
  3030. const customService = this.addCustomServices.find(
  3031. s => (s.id || s.serviceId) === serviceInfo.id
  3032. );
  3033. if (customService && !this.isServiceLoaded(customService)) {
  3034. // 恢复 loadedLayerNames
  3035. if (serviceInfo.loadedLayerNames && serviceInfo.loadedLayerNames.length > 0) {
  3036. customService.loadedLayerNames = serviceInfo.loadedLayerNames;
  3037. console.log('恢复服务loadedLayerNames:', customService.name, serviceInfo.loadedLayerNames);
  3038. }
  3039. console.log('恢复自定义服务:', customService.name);
  3040. await new Promise(resolve => {
  3041. this.loadCustomService(customService, false, false);
  3042. setTimeout(resolve, 1000);
  3043. });
  3044. }
  3045. }
  3046. }
  3047. console.log('地图配置恢复完成');
  3048. } catch (error) {
  3049. console.error('恢复地图配置失败:', error);
  3050. console.error('错误详情:', error.message);
  3051. console.error('错误堆栈:', error.stack);
  3052. if (error.response) {
  3053. console.error('响应状态:', error.response.status);
  3054. console.error('响应数据:', error.response.data);
  3055. }
  3056. }
  3057. },
  3058. // 清除所有持久化数据
  3059. clearPersistence() {
  3060. localStorage.removeItem('cesium_loaded_services');
  3061. localStorage.removeItem('cesium_loaded_models');
  3062. console.log('持久化数据已清除');
  3063. },
  3064. // 为模型添加点击事件,激活编辑器
  3065. addModelClickHandler(entity) {
  3066. if (!entity || !window.viewer) return;
  3067. const handler = new Cesium.ScreenSpaceEventHandler(window.viewer.canvas);
  3068. handler.setInputAction((movement) => {
  3069. const picked = window.viewer.scene.pick(movement.position);
  3070. console.log('pick结果:', picked);
  3071. console.log('picked.id:', picked ? picked.id : null);
  3072. console.log('picked.id === entity:', picked ? (picked.id === entity) : false);
  3073. if (Cesium.defined(picked) && picked.id === entity) {
  3074. console.log('点击了模型:', entity);
  3075. console.log('entity.model:', entity.model);
  3076. console.log('entity类型:', entity.constructor.name);
  3077. // 先恢复上一个模型的样式
  3078. if (this.lastHighlightedModel && this.lastHighlightedModel !== entity) {
  3079. this.restoreModelStyle(this.lastHighlightedModel);
  3080. }
  3081. // 高亮显示模型
  3082. this.highlightModel(entity);
  3083. this.lastHighlightedModel = entity;
  3084. // 激活编辑器 - 已禁用,不再显示模型控制器
  3085. // this.activateModelEditor(entity);
  3086. // 标记已选中模型
  3087. this._isModelSelected = true;
  3088. }
  3089. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  3090. // 保存 handler,以便后续清理
  3091. if (!entity._clickHandler) {
  3092. entity._clickHandler = handler;
  3093. }
  3094. },
  3095. // 全局点击处理器 - 检测空白区域点击取消选中
  3096. initGlobalClickHandler() {
  3097. if (!window.viewer || this._globalClickHandlerInitialized) return;
  3098. const handler = new Cesium.ScreenSpaceEventHandler(window.viewer.canvas);
  3099. handler.setInputAction(async (movement) => {
  3100. const picked = window.viewer.scene.pick(movement.position);
  3101. console.log('全局点击 - picked:', picked);
  3102. // ========== MVT 矢量瓦片点击检测 ==========
  3103. // MVT 无法被 scene.pick() 拾取,只在未拾取到任何实体时才尝试 MVT 查询,
  3104. // 避免与 GeoJSON 要素、模型等其他实体的点击事件冲突
  3105. if (this._hasMvtLayer && this.mvtClickHandler && !Cesium.defined(picked)) {
  3106. // 获取点击位置经纬度
  3107. const ray = window.viewer.camera.getPickRay(movement.position);
  3108. const cartesian = window.viewer.scene.globe.pick(ray, window.viewer.scene);
  3109. if (cartesian) {
  3110. const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
  3111. const lon = Cesium.Math.toDegrees(cartographic.longitude);
  3112. const lat = Cesium.Math.toDegrees(cartographic.latitude);
  3113. // 计算当前相机 zoom:画面能渲染说明该级别有瓦片,优先查该级别命中最快
  3114. // (兼容只在特定比例尺发布的服务,如水资源分区仅 z=6/7 有瓦片)
  3115. const camHeight = window.viewer.camera.positionCartographic.height;
  3116. const currentZoom = Math.round(Math.log2((2 * Math.PI * 6378137) / camHeight));
  3117. // 遍历所有已加载的 MVT 图层进行查询
  3118. await this.mvtClickHandler.handleClick(lon, lat, {
  3119. x: movement.position.x,
  3120. y: movement.position.y
  3121. }, this._mvtTileUrls || [], currentZoom);
  3122. }
  3123. }
  3124. // 如果点击的是GeoJSON点,不处理(让model-transform-panel.vue处理)
  3125. if (picked && picked.id && picked.id.isGeoJsonPoint) {
  3126. console.log('点击了GeoJSON点,不处理');
  3127. return;
  3128. }
  3129. // 检查是否点击了空白区域(不是模型)
  3130. // 如果没有 pick 到任何东西,或者 pick 到的东西不是当前选中的模型
  3131. let isClickingOnSelectedModel = false;
  3132. if (picked && picked.id && this.lastHighlightedModel) {
  3133. // 检查是否点击的是当前选中的模型
  3134. if (picked.id === this.lastHighlightedModel ||
  3135. (typeof picked.id === 'string' && picked.id === this.lastHighlightedModel._id)) {
  3136. isClickingOnSelectedModel = true;
  3137. }
  3138. }
  3139. // 如果点击的不是当前选中的模型,且有选中的模型,则取消选中
  3140. if (!isClickingOnSelectedModel && this.lastHighlightedModel) {
  3141. console.log('取消选中模型');
  3142. this.restoreModelStyle(this.lastHighlightedModel);
  3143. // 销毁编辑器
  3144. if (this.currentModelEditor) {
  3145. this.currentModelEditor.destroy();
  3146. this.currentModelEditor = null;
  3147. }
  3148. this.lastHighlightedModel = null;
  3149. this._isModelSelected = false;
  3150. }
  3151. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  3152. this._globalClickHandlerInitialized = true;
  3153. console.log('全局点击处理器已初始化');
  3154. },
  3155. // 恢复模型样式
  3156. restoreModelStyle(entity) {
  3157. if (!entity) return;
  3158. if (entity._originalShow !== undefined) {
  3159. entity.show = entity._originalShow;
  3160. }
  3161. if (entity.model) {
  3162. entity.model.color = entity._originalColor;
  3163. entity.model.colorBlendMode = entity._originalColorBlendMode;
  3164. entity.model.colorBlendAmount = entity._originalColorBlendAmount;
  3165. }
  3166. },
  3167. // 高亮显示模型
  3168. highlightModel(entity) {
  3169. if (!entity) return;
  3170. // 保存原始状态
  3171. if (!entity._originalShow) {
  3172. entity._originalShow = entity.show;
  3173. }
  3174. if (!entity._originalColor) {
  3175. entity._originalColor = entity.model ? entity.model.color : undefined;
  3176. }
  3177. if (!entity._originalColorBlendMode) {
  3178. entity._originalColorBlendMode = entity.model ? entity.model.colorBlendMode : undefined;
  3179. }
  3180. if (!entity._originalColorBlendAmount) {
  3181. entity._originalColorBlendAmount = entity.model ? entity.model.colorBlendAmount : undefined;
  3182. }
  3183. // 高亮显示模型 - 使用透明模式
  3184. if (entity.model) {
  3185. console.log('模型存在,设置透明');
  3186. // 尝试直接设置颜色
  3187. entity.model.color = Cesium.Color.YELLOW;
  3188. } else {
  3189. console.log('模型不存在,实体结构:', entity);
  3190. }
  3191. },
  3192. // 激活模型编辑器
  3193. activateModelEditor(entity) {
  3194. if (!entity || !window.viewer) return;
  3195. // 清除之前的编辑器
  3196. if (this.currentModelEditor) {
  3197. if (this.currentModelEditor.destroy) {
  3198. this.currentModelEditor.destroy();
  3199. }
  3200. this.currentModelEditor = null;
  3201. }
  3202. // 创建新的编辑器
  3203. try {
  3204. console.log('创建 ModelEditor for entity:', entity);
  3205. console.log('window.viewer:', window.viewer);
  3206. console.log('window.viewer.primitives:', window.viewer ? window.viewer.primitives : undefined);
  3207. this.currentModelEditor = new ModelEditor(window.viewer, entity);
  3208. // 等待模型加载完成
  3209. if (this.currentModelEditor.waitForModelLoad) {
  3210. this.currentModelEditor.waitForModelLoad().then(() => {
  3211. console.log('模型加载完成,编辑器初始化成功');
  3212. }).catch((error) => {
  3213. console.error('模型加载或编辑器初始化失败:', error);
  3214. });
  3215. }
  3216. console.log('ModelEditor 创建成功');
  3217. } catch (error) {
  3218. console.error('创建 ModelEditor 失败:', error);
  3219. this.currentModelEditor = null;
  3220. }
  3221. }
  3222. },
  3223. watch: {
  3224. isCollapse(val) {
  3225. if (val) {
  3226. this.asideWidth = "asideWidth2";
  3227. } else {
  3228. this.asideWidth = "asideWidth1";
  3229. }
  3230. }
  3231. },
  3232. mounted() {
  3233. document.getElementById("el-aside").classList.add("disable"); //禁止点击
  3234. // 直接初始化 viewer,不使用 window.onload
  3235. this.$nextTick(() => {
  3236. this.initViewer = "Sm3dViewer";
  3237. // 获取模型数据
  3238. this.fetchModels();
  3239. // 从数据库加载自定义服务
  3240. this.fetchCustomServices();
  3241. // 从数据库加载业务场景列表
  3242. this.refreshBusinessScenes();
  3243. // 加载固定场景列表
  3244. this.loadFixedScenes();
  3245. });
  3246. // 延迟初始化全局点击处理器,等待 viewer 准备好
  3247. setTimeout(() => {
  3248. console.log('延迟初始化全局点击处理器');
  3249. if (window.viewer) {
  3250. this.viewer = window.viewer;
  3251. this.initGlobalClickHandler();
  3252. }
  3253. }, 3000);
  3254. // 监听创建场景成功事件,刷新业务场景列表
  3255. this.refreshScenesHandler = () => {
  3256. this.refreshBusinessScenes()
  3257. }
  3258. document.addEventListener('refreshBusinessScenes', this.refreshScenesHandler)
  3259. },
  3260. beforeUnmount() {
  3261. // 移除事件监听
  3262. document.removeEventListener('refreshBusinessScenes', this.refreshScenesHandler)
  3263. }
  3264. };
  3265. </script>
  3266. <style lang="scss" scoped>
  3267. .container {
  3268. position: relative;
  3269. z-index: 1;
  3270. width: 100%;
  3271. height: 100%;
  3272. pointer-events: auto;
  3273. }
  3274. /* 侧边栏宽度控制 */
  3275. .asideWidth1 {
  3276. width: 280px !important;
  3277. transition-duration: 0.3s;
  3278. height: 100% !important;
  3279. }
  3280. .asideWidth2 {
  3281. transition-duration: 0.5s;
  3282. width: 40px !important;
  3283. height: 100% !important;
  3284. display: flex !important;
  3285. justify-content: center !important;
  3286. }
  3287. /* 确保el-container能够铺满整个容器 */
  3288. .el-container {
  3289. height: 100% !important;
  3290. width: 100% !important;
  3291. pointer-events: auto;
  3292. }
  3293. /* 确保el-aside能够正确接收点击事件 */
  3294. .el-aside {
  3295. position: relative;
  3296. z-index: 1000;
  3297. pointer-events: auto;
  3298. }
  3299. /* 确保el-main能够铺满整个容器 */
  3300. .el-main {
  3301. position: relative;
  3302. z-index: 1;
  3303. height: 100% !important;
  3304. width: 100% !important;
  3305. padding: 0 !important;
  3306. margin: 0 !important;
  3307. pointer-events: auto;
  3308. }
  3309. /* 确保cesiumContainer能够铺满整个el-main */
  3310. #cesiumContainer {
  3311. position: relative;
  3312. z-index: 10;
  3313. height: 100% !important;
  3314. width: 100% !important;
  3315. pointer-events: auto;
  3316. }
  3317. /* 确保cesium-viewer能够铺满整个cesiumContainer */
  3318. .cesium-viewer {
  3319. height: 100% !important;
  3320. width: 100% !important;
  3321. pointer-events: auto;
  3322. }
  3323. /* 确保cesium-widget能够铺满整个cesium-viewer */
  3324. .cesium-widget {
  3325. height: 100% !important;
  3326. width: 100% !important;
  3327. pointer-events: auto;
  3328. }
  3329. /* 确保canvas能够铺满整个cesium-widget */
  3330. .cesium-widget canvas {
  3331. position: relative;
  3332. z-index: 5;
  3333. height: 100% !important;
  3334. width: 100% !important;
  3335. pointer-events: auto;
  3336. }
  3337. /* 确保侧边栏菜单能够正确接收点击事件 */
  3338. .el-menu-vertical-demo {
  3339. position: relative;
  3340. z-index: 1001;
  3341. pointer-events: auto;
  3342. }
  3343. /* 确保加载动画不影响点击事件 */
  3344. #loadingbar {
  3345. position: absolute;
  3346. top: 50%;
  3347. left: 50%;
  3348. transform: translate(-50%, -50%);
  3349. z-index: 9999;
  3350. pointer-events: none;
  3351. }
  3352. /* 侧边栏样式增强 */
  3353. .el-aside {
  3354. background-color: #f8f9fa;
  3355. overflow-y: scroll;
  3356. overflow-x: hidden;
  3357. scrollbar-width: none;
  3358. /* firefox */
  3359. -ms-overflow-style: none;
  3360. height: 100% !important;
  3361. box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
  3362. .el-menu--collapse {
  3363. width: 40px;
  3364. height: 100% !important;
  3365. }
  3366. :deep(.el-menu--collapse.el-menu) {
  3367. width: 40px !important;
  3368. margin: 0 auto !important;
  3369. }
  3370. :deep(.el-menu--collapse .el-menu-item),
  3371. :deep(.el-menu--collapse .el-submenu__title),
  3372. :deep(.el-menu--collapse .el-sub-menu__title) {
  3373. padding: 0 !important;
  3374. text-align: center !important;
  3375. display: flex !important;
  3376. align-items: center !important;
  3377. justify-content: center !important;
  3378. position: relative !important;
  3379. left: 0 !important;
  3380. right: 0 !important;
  3381. width: 40px !important;
  3382. height: 45px !important;
  3383. margin: 0 auto !important;
  3384. /* 强制开启hover触发,解决折叠后无法识别子项问题 */
  3385. pointer-events: auto !important;
  3386. cursor: pointer !important;
  3387. }
  3388. :deep(.el-menu--collapse .el-sub-menu) {
  3389. width: 40px !important;
  3390. /* 二级弹框容器相对定位,让三级能基于它定位 */
  3391. position: relative !important;
  3392. }
  3393. :deep(.el-menu--collapse .el-menu-item > *),
  3394. :deep(.el-menu--collapse .el-submenu__title > *),
  3395. :deep(.el-menu--collapse .el-sub-menu__title > *) {
  3396. margin: 0 !important;
  3397. padding: 0 !important;
  3398. position: static !important;
  3399. left: auto !important;
  3400. right: auto !important;
  3401. top: auto !important;
  3402. bottom: auto !important;
  3403. transform: none !important;
  3404. float: none !important;
  3405. display: inline-block !important;
  3406. width: auto !important;
  3407. height: auto !important;
  3408. }
  3409. :deep(.el-menu--collapse .el-menu-item i.rotate),
  3410. :deep(.el-menu--collapse .el-submenu__title i.rotate),
  3411. :deep(.el-menu--collapse .el-sub-menu__title i.rotate) {
  3412. margin: 0 !important;
  3413. padding: 0 !important;
  3414. display: inline-block !important;
  3415. text-align: center !important;
  3416. width: auto !important;
  3417. height: auto !important;
  3418. position: static !important;
  3419. left: auto !important;
  3420. right: auto !important;
  3421. transform: rotate(-90deg) !important;
  3422. line-height: normal !important;
  3423. float: none !important;
  3424. }
  3425. :deep(.el-menu--collapse .el-submenu__title .iconfont2),
  3426. :deep(.el-menu--collapse .el-menu-item .iconfont2),
  3427. :deep(.el-menu--collapse .el-sub-menu__title .iconfont2) {
  3428. margin: 0 !important;
  3429. padding: 0 !important;
  3430. display: inline-block !important;
  3431. text-align: center !important;
  3432. width: auto !important;
  3433. height: auto !important;
  3434. position: static !important;
  3435. left: auto !important;
  3436. right: auto !important;
  3437. transform: none !important;
  3438. float: none !important;
  3439. }
  3440. :deep(.el-menu--collapse .el-sub-menu__title .typhoon-icon) {
  3441. margin: 0 !important;
  3442. padding: 0 !important;
  3443. display: inline-block !important;
  3444. text-align: center !important;
  3445. width: 20px !important;
  3446. height: 20px !important;
  3447. position: static !important;
  3448. left: auto !important;
  3449. right: auto !important;
  3450. transform: none !important;
  3451. float: none !important;
  3452. }
  3453. :deep(.el-menu--collapse .el-submenu__title span),
  3454. :deep(.el-menu--collapse .el-menu-item span),
  3455. :deep(.el-menu--collapse .el-sub-menu__title span) {
  3456. display: none !important;
  3457. }
  3458. :deep(.el-menu--collapse .el-submenu__icon-arrow),
  3459. :deep(.el-menu--collapse .el-sub-menu__icon-arrow) {
  3460. display: none !important;
  3461. }
  3462. .el-menu {
  3463. border-right: none;
  3464. height: 100% !important;
  3465. .el-menu-item,
  3466. .el-submenu__title {
  3467. height: 45px;
  3468. line-height: 45px;
  3469. }
  3470. :deep(.el-menu--collapse .el-menu-item),
  3471. :deep(.el-menu--collapse .el-submenu__title),
  3472. :deep(.el-menu--collapse .el-sub-menu__title) {
  3473. padding: 0 !important;
  3474. text-align: center !important;
  3475. display: flex !important;
  3476. align-items: center !important;
  3477. justify-content: center !important;
  3478. position: relative !important;
  3479. left: 0 !important;
  3480. right: 0 !important;
  3481. width: 40px !important;
  3482. height: 45px !important;
  3483. line-height: normal !important;
  3484. }
  3485. #fold {
  3486. height: 35px;
  3487. transition-duration: 1s;
  3488. color: #303133 !important;
  3489. padding: 0 !important;
  3490. text-align: center !important;
  3491. line-height: 35px !important;
  3492. display: flex !important;
  3493. align-items: center !important;
  3494. justify-content: center !important;
  3495. background-color: transparent !important;
  3496. }
  3497. #fold.is-active {
  3498. background-color: transparent !important;
  3499. color: #303133 !important;
  3500. }
  3501. #fold:hover {
  3502. background-color: rgba(64, 158, 255, 0.1) !important;
  3503. }
  3504. }
  3505. }
  3506. /* 修复Element Plus 2.x 子菜单标题样式 */
  3507. .el-submenu__title {
  3508. padding: 0 12px !important;
  3509. }
  3510. .el-menu--vertical > .el-menu-item {
  3511. padding: 0 12px !important;
  3512. }
  3513. /* 确保标题框尺寸正确 */
  3514. .el-submenu__title .el-submenu__icon-arrow {
  3515. margin-top: -4px !important;
  3516. }
  3517. /* 修复图标和文字对齐 */
  3518. .iconfont2 {
  3519. font-size: 20px !important;
  3520. margin-right: 10px;
  3521. vertical-align: middle;
  3522. color: #303133;
  3523. }
  3524. /* 台风图标样式 */
  3525. .typhoon-icon {
  3526. width: 20px;
  3527. height: 20px;
  3528. margin-right: 10px;
  3529. vertical-align: middle;
  3530. }
  3531. /* 子菜单项文字颜色 */
  3532. .el-menu--vertical .el-sub-menu .el-menu-item {
  3533. color: #303133 !important;
  3534. font-size: 0.875rem !important;
  3535. line-height: 1.5;
  3536. }
  3537. /* 本地服务菜单项文字向左移动 */
  3538. .custom-service-item {
  3539. display: flex;
  3540. justify-content: space-between;
  3541. align-items: center;
  3542. padding-left: 50px !important;
  3543. }
  3544. .custom-service-item.is-active {
  3545. background-color: transparent !important;
  3546. }
  3547. .custom-service-item .service-name {
  3548. flex: 1;
  3549. overflow: hidden;
  3550. text-overflow: ellipsis;
  3551. white-space: nowrap;
  3552. }
  3553. .custom-service-item .service-name.service-loaded {
  3554. color: #409eff;
  3555. font-weight: 500;
  3556. }
  3557. .custom-service-item .delete-service-icon {
  3558. margin-left: 8px;
  3559. cursor: pointer;
  3560. color: #909399;
  3561. transition: color 0.3s;
  3562. }
  3563. .custom-service-item .delete-service-icon:hover {
  3564. color: #f56c6c;
  3565. }
  3566. /* 自定义服务菜单项移除选中样式 */
  3567. .no-active-style {
  3568. background-color: transparent !important;
  3569. }
  3570. .no-active-style.is-active {
  3571. background-color: transparent !important;
  3572. }
  3573. /* 基础地理实体菜单项样式 */
  3574. .vector-data-item {
  3575. display: flex;
  3576. justify-content: space-between;
  3577. align-items: center;
  3578. padding-left: 50px !important;
  3579. }
  3580. .vector-data-item.is-active {
  3581. background-color: transparent !important;
  3582. }
  3583. .vector-data-item .vector-data-name {
  3584. flex: 1;
  3585. overflow: hidden;
  3586. text-overflow: ellipsis;
  3587. white-space: nowrap;
  3588. }
  3589. .vector-data-item .vector-data-name.vector-data-loaded {
  3590. color: #409eff;
  3591. font-weight: 500;
  3592. }
  3593. .vector-data-item .delete-vector-data-icon {
  3594. margin-left: 8px;
  3595. cursor: pointer;
  3596. color: #909399;
  3597. transition: color 0.3s;
  3598. }
  3599. .vector-data-item .delete-vector-data-icon:hover {
  3600. color: #f56c6c;
  3601. }
  3602. /* 模型菜单项样式 */
  3603. .model-menu-item {
  3604. display: flex;
  3605. justify-content: space-between;
  3606. align-items: center;
  3607. padding-left: 50px !important;
  3608. }
  3609. .model-menu-item.is-active {
  3610. background-color: transparent !important;
  3611. }
  3612. .model-menu-item .model-name {
  3613. flex: 1;
  3614. overflow: hidden;
  3615. text-overflow: ellipsis;
  3616. white-space: nowrap;
  3617. padding-left: 10px;
  3618. }
  3619. .model-menu-item .model-name.model-loaded {
  3620. color: #409eff;
  3621. font-weight: 500;
  3622. }
  3623. .model-menu-item .delete-model-icon {
  3624. margin-left: 8px;
  3625. cursor: pointer;
  3626. color: #909399;
  3627. transition: color 0.3s;
  3628. }
  3629. .model-menu-item .delete-model-icon:hover {
  3630. color: #f56c6c;
  3631. }
  3632. /* 台风加载状态 */
  3633. .el-menu-item .typhoon-name.typhoon-loaded {
  3634. color: #409eff;
  3635. font-weight: 500;
  3636. }
  3637. .el-menu-item .delete-typhoon-icon {
  3638. margin-left: 8px;
  3639. cursor: pointer;
  3640. color: #909399;
  3641. transition: color 0.3s;
  3642. }
  3643. /* 场景加载状态 */
  3644. .model-menu-item .scene-name {
  3645. flex: 1;
  3646. overflow: hidden;
  3647. text-overflow: ellipsis;
  3648. white-space: nowrap;
  3649. cursor: pointer;
  3650. }
  3651. .model-menu-item .scene-name.scene-loaded {
  3652. color: #409eff;
  3653. font-weight: 500;
  3654. }
  3655. .model-menu-item .delete-scene-icon {
  3656. margin-left: 8px;
  3657. cursor: pointer;
  3658. color: #909399;
  3659. transition: color 0.3s;
  3660. }
  3661. .model-menu-item .delete-scene-icon:hover {
  3662. color: #f56c6c;
  3663. }
  3664. .el-menu-item .delete-typhoon-icon:hover {
  3665. color: #f56c6c;
  3666. }
  3667. /* 滚动条隐藏 */
  3668. .el-aside::-webkit-scrollbar {
  3669. display: none;
  3670. }
  3671. /* 功能模块网格布局 */
  3672. .box {
  3673. font-family: "Microsoft Yahei";
  3674. display: flex;
  3675. width: 100%;
  3676. padding: 0 10px 0 28px;
  3677. justify-content: space-between;
  3678. box-sizing: border-box;
  3679. flex-wrap: wrap;
  3680. .imgbox {
  3681. position: relative;
  3682. width: 100px;
  3683. height: 100px;
  3684. display: flex;
  3685. box-sizing: border-box;
  3686. margin-bottom: 5px;
  3687. flex-wrap: wrap;
  3688. justify-content: center;
  3689. span {
  3690. font-size: 12px;
  3691. color: #303133;
  3692. }
  3693. .img {
  3694. width: 100%;
  3695. height: 72px;
  3696. border-radius: 4px;
  3697. background-color: #585858;
  3698. border: 1px solid #e4e7ed;
  3699. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  3700. transition: all 0.3s ease;
  3701. }
  3702. .img:hover {
  3703. border: 2px solid #409eff;
  3704. box-shadow: 0 4px 8px rgba(64, 158, 255, 0.2);
  3705. transform: translateY(-2px);
  3706. }
  3707. .cross {
  3708. width: 25px;
  3709. height: 25px;
  3710. position: absolute;
  3711. top: 0;
  3712. left: 0;
  3713. }
  3714. }
  3715. }
  3716. /* 删除图标样式 */
  3717. .circle-close-icon {
  3718. position: absolute;
  3719. right: 0px;
  3720. top: 3px;
  3721. color: #ffffff;
  3722. cursor: pointer;
  3723. width: 16px;
  3724. height: 16px;
  3725. font-size: 16px;
  3726. outline: none;
  3727. }
  3728. .circle-close-icon:focus,
  3729. .circle-close-icon:active {
  3730. outline: none;
  3731. box-shadow: none;
  3732. }
  3733. /* el-menu-item 内的删除图标样式 */
  3734. .el-menu-item .circle-close-icon {
  3735. top: 50%;
  3736. transform: translateY(-50%);
  3737. color: #000000;
  3738. right: 25px;
  3739. }
  3740. /* 展开/收缩按钮动画 */
  3741. .rotate2 {
  3742. transform: rotate(0deg);
  3743. -ms-transform: rotate(0); /* IE 9 */
  3744. -moz-transform: rotate(0); /* Firefox */
  3745. -webkit-transform: rotate(0); /* Safari 和 Chrome */
  3746. -o-transform: rotate(0); /* Opera */
  3747. transition-duration: 1s;
  3748. }
  3749. .rotate {
  3750. margin-left: 8px !important;
  3751. line-height: 52px !important;
  3752. transform: rotate(-90deg);
  3753. -ms-transform: rotate(-90deg); /* IE 9 */
  3754. -moz-transform: rotate(-90deg); /* Firefox */
  3755. -webkit-transform: rotate(-90deg); /* Safari 和 Chrome */
  3756. -o-transform: rotate(-90deg); /* Opera */
  3757. transition-duration: 1s;
  3758. }
  3759. /* 禁用状态 */
  3760. .disable {
  3761. pointer-events: none;
  3762. }
  3763. /* 图标样式 */
  3764. .iconfont2 {
  3765. font-size: 20px !important;
  3766. margin-right: 10px;
  3767. }
  3768. /* 方形加号图标 */
  3769. .square-plus-icon {
  3770. display: inline-flex;
  3771. align-items: center;
  3772. justify-content: center;
  3773. width: 24px;
  3774. height: 24px;
  3775. border: 1.5px solid #909399;
  3776. border-radius: 4px;
  3777. font-size: 16px;
  3778. margin-left: 10px;
  3779. }
  3780. /* 场景名称 */
  3781. .scene-name {
  3782. flex: 1;
  3783. overflow: hidden;
  3784. text-overflow: ellipsis;
  3785. white-space: nowrap;
  3786. }
  3787. /* 删除图标 */
  3788. .delete-icon {
  3789. display: inline-flex;
  3790. align-items: center;
  3791. justify-content: center;
  3792. width: 24px;
  3793. height: 24px;
  3794. font-size: 16px;
  3795. margin-left: 8px;
  3796. cursor: pointer;
  3797. opacity: 0.6;
  3798. transition: opacity 0.2s;
  3799. flex-shrink: 0;
  3800. }
  3801. .delete-icon:hover {
  3802. opacity: 1;
  3803. color: #f56c6c;
  3804. }
  3805. // ########### 关键修复:折叠状态下 二级弹框保留原位置 + 三级强制弹出 ###########
  3806. // 1. 折叠状态二级弹框:强制保留原有正确位置,不做任何改动
  3807. :deep(.el-menu--collapse .el-sub-menu > .el-sub-menu__popper) {
  3808. position: fixed !important;
  3809. margin-left: 0 !important;
  3810. transform: none !important;
  3811. z-index: 9998 !important;
  3812. }
  3813. // 2. 折叠状态二级弹框容器:hover时强制显示三级内容,核心触发逻辑
  3814. :deep(.el-menu--collapse .el-sub-menu__popper) {
  3815. pointer-events: auto !important;
  3816. &:hover .box {
  3817. display: flex !important;
  3818. }
  3819. }
  3820. // 3. 折叠状态三级内容(box):强制定位在二级弹框右侧,顶对齐
  3821. :deep(.el-menu--collapse .el-sub-menu__popper .box) {
  3822. position: absolute !important;
  3823. left: 100% !important; // 紧贴二级弹框右侧
  3824. top: 0 !important; // 和二级弹框顶对齐
  3825. z-index: 9999 !important;
  3826. width: 300px !important; // 适配你的imgbox布局
  3827. background: #f8f9fa !important;
  3828. border: 1px solid #e4e7ed !important;
  3829. border-left: none !important;
  3830. box-shadow: 2px 2px 10px rgba(0,0,0,0.05) !important;
  3831. padding: 10px !important;
  3832. margin: 0 !important;
  3833. // 强制显示,解决折叠后被隐藏问题
  3834. display: flex !important;
  3835. pointer-events: auto !important;
  3836. }
  3837. // 4. 折叠状态三级imgbox布局适配:清除多余间距,正常显示
  3838. :deep(.el-menu--collapse .el-sub-menu__popper .box .imgbox) {
  3839. margin: 0 10px 10px 0 !important;
  3840. }
  3841. // 5. 展开状态:恢复原有布局,不影响任何效果
  3842. :deep(.el-menu--not-collapse .el-sub-menu__popper .box) {
  3843. position: static !important;
  3844. width: 100% !important;
  3845. box-shadow: none !important;
  3846. border: none !important;
  3847. }
  3848. // 土地利用图例样式
  3849. .land-use-legend {
  3850. position: absolute;
  3851. bottom: 20px;
  3852. right: 20px;
  3853. z-index: 1000;
  3854. background: rgba(20, 25, 40, 0.85);
  3855. border: 1px solid rgba(100, 180, 255, 0.4);
  3856. border-radius: 6px;
  3857. padding: 10px 12px;
  3858. max-height: 60vh;
  3859. overflow-y: auto;
  3860. user-select: none;
  3861. backdrop-filter: blur(4px);
  3862. .land-use-legend-header {
  3863. display: flex;
  3864. justify-content: space-between;
  3865. align-items: center;
  3866. margin-bottom: 8px;
  3867. font-size: 14px;
  3868. font-weight: bold;
  3869. color: #e0e0e0;
  3870. border-bottom: 1px solid rgba(100, 180, 255, 0.2);
  3871. padding-bottom: 6px;
  3872. .land-use-legend-toggle {
  3873. cursor: pointer;
  3874. color: #aaa;
  3875. font-size: 16px;
  3876. line-height: 1;
  3877. padding: 0 4px;
  3878. &:hover { color: #fff; }
  3879. }
  3880. }
  3881. .land-use-legend-body {
  3882. display: flex;
  3883. flex-direction: column;
  3884. gap: 4px;
  3885. }
  3886. .land-use-legend-item {
  3887. display: flex;
  3888. align-items: center;
  3889. gap: 8px;
  3890. font-size: 12px;
  3891. color: #ccc;
  3892. }
  3893. .land-use-legend-color {
  3894. display: inline-block;
  3895. width: 16px;
  3896. height: 12px;
  3897. border: 1px solid rgba(255, 255, 255, 0.3);
  3898. border-radius: 2px;
  3899. flex-shrink: 0;
  3900. }
  3901. .land-use-legend-label {
  3902. white-space: nowrap;
  3903. }
  3904. }
  3905. </style>