calibration.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. <template>
  2. <div class="calibration-container">
  3. <!-- 地图舞台容器 -->
  4. <div class="map-stage" ref="mapStage">
  5. <!-- 地图底层 -->
  6. <div class="map-canvas-wrapper" ref="mapWrapper">
  7. <OlMap
  8. ref="olmap"
  9. :width="olWidth + 'px'"
  10. :height="olHeight + 'px'"
  11. backgroundColor="#F5F5F5"
  12. :robotPoseData="laserPositionData"
  13. :poseCalibrationIndex="nowCalibId"
  14. :showDefaultControls="false"
  15. :mapName="mapName"
  16. />
  17. </div>
  18. <MqttComp ref="mqtt" :topics="topics" @message-received="onMessage" />
  19. <!-- 地图工具条 -->
  20. <MapToolbar
  21. class="map-toolbar"
  22. :canAddCalibration="canAddCalibration"
  23. :hasRobotPosition="hasRobotPosition"
  24. :isFullscreen="isFullscreen"
  25. @zoom-in="handleZoomIn"
  26. @zoom-out="handleZoomOut"
  27. @center-to-robot="handleCenterToRobot"
  28. @toggle-fullscreen="handleToggleFullscreen"
  29. @add-calibration-point="addCalibration"
  30. />
  31. <!-- 右侧信息面板 (宽屏模式,浮层) -->
  32. <RightPanel
  33. v-if="!isMobileMode"
  34. class="right-panel"
  35. :class="{ 'panel-collapsed': isPanelCollapsed }"
  36. :style="{ width: isPanelCollapsed ? '0px' : rightPanelWidth + 'px' }"
  37. :laserPositionData="laserPositionData"
  38. :gnssPositionData="gnssPositionData"
  39. :calibrationList="calibrationList"
  40. :currentMap="currentMap"
  41. :panelWidth="rightPanelWidth"
  42. :mapName="mapName"
  43. @panel-toggle="handlePanelToggle"
  44. @panel-resize="handlePanelResize"
  45. @add-calibration="addCalibration"
  46. @remove-calibration="removeCalibration"
  47. @execute-calibration="executeCalibration"
  48. />
  49. <!-- 面板展开按钮 (面板收起时显示) -->
  50. <div
  51. class="panel-reopen-btn"
  52. v-if="shouldShowExpandButton"
  53. @click="expandPanel"
  54. title="展开面板"
  55. >
  56. <i class="el-icon-arrow-left"></i>
  57. </div>
  58. <!-- 右上角信息按钮 (窄屏模式) -->
  59. <div class="info-toggle-btn" v-if="isMobileMode" @click="showInfoDrawer">
  60. <el-button type="primary" icon="el-icon-info" circle size="small" />
  61. </div>
  62. </div>
  63. <!-- 抽屉模式 (窄屏) -->
  64. <el-drawer
  65. :visible.sync="drawerVisible"
  66. :direction="'rtl'"
  67. :modal="false"
  68. :size="'360px'"
  69. :with-header="false"
  70. custom-class="info-drawer"
  71. :wrapperClosable="false"
  72. v-if="isMobileMode"
  73. >
  74. <div class="drawer-content">
  75. <div class="drawer-header">
  76. <div class="drawer-title">
  77. <h3>实时标定信息</h3>
  78. <span class="map-name">(当前地图: {{ currentMap.name }})</span>
  79. </div>
  80. <el-button
  81. @click="drawerVisible = false"
  82. type="text"
  83. size="mini"
  84. icon="el-icon-close"
  85. />
  86. </div>
  87. <div class="drawer-body">
  88. <RightPanel
  89. :laserPositionData="laserPositionData"
  90. :gnssPositionData="gnssPositionData"
  91. :calibrationList="calibrationList"
  92. :currentMap="currentMap"
  93. :isDrawerMode="true"
  94. @add-calibration="addCalibration"
  95. @remove-calibration="removeCalibration"
  96. @execute-calibration="executeCalibration"
  97. class="drawer-panel"
  98. />
  99. </div>
  100. </div>
  101. </el-drawer>
  102. </div>
  103. </template>
  104. <script>
  105. import OlMap from "@/components/OlMap";
  106. import RightPanel from "./components/RightPanel.vue";
  107. import MapToolbar from "./components/MapToolbar.vue";
  108. import MqttComp from "@/components/Mqtt/mqttComp.vue";
  109. export default {
  110. name: "Calibration",
  111. components: {
  112. OlMap,
  113. RightPanel,
  114. MapToolbar,
  115. MqttComp
  116. },
  117. data() {
  118. return {
  119. topics:[this.$mqttPrefix + '/localization/pose'],
  120. // 弹出层标题
  121. title: "",
  122. calibrationList: [],
  123. // 激光定位数据
  124. laserPositionData: {
  125. x: 0,
  126. y: 0,
  127. angle: 0
  128. },
  129. // gnss定位数据
  130. gnssPositionData: {
  131. status: '0/0',
  132. longitude: 0,
  133. latitude: 0,
  134. angle: 0
  135. },
  136. // 当前地图
  137. currentMap: {
  138. id: 1,
  139. name: 'sh02'
  140. },
  141. olWidth: 0, // 用于存储宽度的变量
  142. olHeight: 0,
  143. nowCalibId: 0, // 当前标定点id
  144. // 布局相关状态
  145. rightPanelWidth: 360, // 右侧面板宽度
  146. isPanelCollapsed: false, // 面板是否折叠
  147. windowWidth: window.innerWidth, // 窗口宽度
  148. drawerVisible: false, // 抽屉是否可见
  149. isFullscreen: false, // 是否全屏状态
  150. mapName: this.$route.params.mapName || 'Unknown', // 当前地图名称
  151. isRobotFollow: false // 是否跟随机器人
  152. };
  153. },
  154. computed: {
  155. // 是否为移动模式(窄屏)
  156. isMobileMode() {
  157. return this.windowWidth < 1440;
  158. },
  159. // 是否可以添加标定点
  160. canAddCalibration() {
  161. return this.laserPositionData.x !== 0 || this.laserPositionData.y !== 0;
  162. },
  163. // 是否有机器人位置数据
  164. hasRobotPosition() {
  165. return this.laserPositionData.x !== 0 || this.laserPositionData.y !== 0;
  166. },
  167. // 是否显示展开按钮
  168. shouldShowExpandButton() {
  169. return !this.isMobileMode && this.isPanelCollapsed;
  170. }
  171. },
  172. created() {
  173. // 从 localStorage 恢复状态
  174. const savedPanelWidth = localStorage.getItem('calibration-panel-width');
  175. const savedPanelCollapsed = localStorage.getItem('calibration-panel-collapsed');
  176. if (savedPanelWidth) {
  177. this.rightPanelWidth = Math.max(320, Math.min(420, parseInt(savedPanelWidth)));
  178. }
  179. if (savedPanelCollapsed) {
  180. this.isPanelCollapsed = savedPanelCollapsed === 'true';
  181. }
  182. },
  183. mounted() {
  184. // const mapId = this.$route.params.mapId;
  185. // this.updateOlCss();
  186. // 监听窗口大小变化
  187. window.addEventListener('resize', this.handleWindowResize);
  188. // 监听全屏状态变化
  189. document.addEventListener('fullscreenchange', this.handleFullscreenChange);
  190. document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange);
  191. document.addEventListener('mozfullscreenchange', this.handleFullscreenChange);
  192. document.addEventListener('MSFullscreenChange', this.handleFullscreenChange);
  193. // 初始设置面板宽度
  194. this.updateRightPanelWidth();
  195. // 页面初始化完成
  196. },
  197. beforeDestroy() {
  198. window.removeEventListener('resize', this.handleWindowResize);
  199. document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
  200. document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange);
  201. document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange);
  202. document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange);
  203. },
  204. methods: {
  205. onMessage({ topic, message }) {
  206. // console.log("收到消息:");
  207. try {
  208. const data = message.args[0];
  209. const {xyz, rpy, blh, heading} = data.pose;
  210. // 激光定位实时数据
  211. this.laserPositionData.x = xyz[0];
  212. this.laserPositionData.y = xyz[1];
  213. this.laserPositionData.angle = rpy[2];
  214. // GNSS定位实时数据
  215. this.gnssPositionData.longitude = blh[1]; // 经度
  216. this.gnssPositionData.latitude = blh[0]; // 纬度
  217. this.gnssPositionData.angle = heading; // 航向角
  218. this.gnssPositionData.status = data.rtk.star+ '/' + data.rtk.status; // RTK状态
  219. } catch (e) {
  220. console.error("解析失败:", e);
  221. }
  222. },
  223. updateOlCss() {
  224. this.$nextTick(() => {
  225. const mapStage = this.$refs.mapStage;
  226. if (mapStage) {
  227. this.olWidth = mapStage.offsetWidth;
  228. this.olHeight = mapStage.offsetHeight;
  229. // 触发地图重绘
  230. if (this.$refs.olmap && this.$refs.olmap.map) {
  231. this.$refs.olmap.map.updateSize();
  232. }
  233. }
  234. });
  235. },
  236. // 处理窗口大小变化
  237. handleWindowResize() {
  238. this.windowWidth = window.innerWidth;
  239. this.updateOlCss();
  240. },
  241. // 处理全屏状态变化
  242. handleFullscreenChange() {
  243. this.isFullscreen = !!(
  244. document.fullscreenElement ||
  245. document.webkitFullscreenElement ||
  246. document.mozFullScreenElement ||
  247. document.msFullscreenElement
  248. );
  249. // 全屏状态变化后触发地图重绘
  250. setTimeout(() => {
  251. this.updateOlCss();
  252. }, 100);
  253. },
  254. // 更新右侧面板宽度
  255. updateRightPanelWidth() {
  256. if (this.isMobileMode) {
  257. this.rightPanelWidth = 0;
  258. } else {
  259. const savedWidth = localStorage.getItem('calibration-panel-width');
  260. const savedCollapsed = localStorage.getItem('calibration-panel-collapsed');
  261. if (savedCollapsed === 'true') {
  262. this.isPanelCollapsed = true;
  263. this.rightPanelWidth = 360; // 保持原宽度,但面板是折叠状态
  264. } else if (savedWidth) {
  265. this.rightPanelWidth = Math.max(320, Math.min(420, parseInt(savedWidth)));
  266. this.isPanelCollapsed = false;
  267. } else {
  268. this.rightPanelWidth = 360;
  269. this.isPanelCollapsed = false;
  270. }
  271. }
  272. this.updateOlCss();
  273. },
  274. // 处理面板切换
  275. handlePanelToggle(isCollapsed) {
  276. this.isPanelCollapsed = isCollapsed;
  277. localStorage.setItem('calibration-panel-collapsed', String(isCollapsed));
  278. // 面板宽度不变,只是改变显示状态
  279. this.updateOlCss();
  280. },
  281. // 展开面板
  282. expandPanel() {
  283. this.isPanelCollapsed = false;
  284. localStorage.setItem('calibration-panel-collapsed', 'false');
  285. this.updateOlCss();
  286. },
  287. // 处理面板调整大小
  288. handlePanelResize(newWidth) {
  289. this.rightPanelWidth = newWidth;
  290. // 浮层模式下不需要重算地图尺寸
  291. },
  292. // 显示信息抽屉
  293. showInfoDrawer() {
  294. this.drawerVisible = true;
  295. },
  296. // 地图API适配器
  297. getMapInstance() {
  298. return this.$refs.olmap && this.$refs.olmap.map ? this.$refs.olmap.map : null;
  299. },
  300. // 地图工具条事件处理
  301. handleZoomIn() {
  302. try {
  303. const map = this.getMapInstance();
  304. if (map) {
  305. // OpenLayers API
  306. const view = map.getView();
  307. const currentZoom = view.getZoom();
  308. view.animate({
  309. zoom: currentZoom + 1,
  310. duration: 250
  311. });
  312. } else if (this.$refs.olmap && this.$refs.olmap.zoomIn) {
  313. // 备用方法
  314. this.$refs.olmap.zoomIn();
  315. }
  316. } catch (error) {
  317. console.warn('地图放大失败:', error);
  318. this.$message.warning('地图放大失败');
  319. }
  320. },
  321. handleZoomOut() {
  322. try {
  323. const map = this.getMapInstance();
  324. if (map) {
  325. // OpenLayers API
  326. const view = map.getView();
  327. const currentZoom = view.getZoom();
  328. view.animate({
  329. zoom: Math.max(currentZoom - 1, 1),
  330. duration: 250
  331. });
  332. } else if (this.$refs.olmap && this.$refs.olmap.zoomOut) {
  333. // 备用方法
  334. this.$refs.olmap.zoomOut();
  335. }
  336. } catch (error) {
  337. console.warn('地图缩小失败:', error);
  338. this.$message.warning('地图缩小失败');
  339. }
  340. },
  341. handleCenterToRobot() {
  342. try {
  343. if (!this.hasRobotPosition) {
  344. this.$message.warning('暂无机器人定位数据');
  345. return;
  346. }
  347. const map = this.getMapInstance();
  348. if (map) {
  349. // OpenLayers API
  350. const view = map.getView();
  351. view.animate({
  352. center: [this.laserPositionData.x, this.laserPositionData.y],
  353. zoom: Math.max(view.getZoom(), 15),
  354. duration: 500
  355. });
  356. this.$message.success('已定位到机器人位置');
  357. } else if (this.$refs.olmap && this.$refs.olmap.centerToRobot) {
  358. // 备用方法
  359. this.$refs.olmap.centerToRobot();
  360. }
  361. } catch (error) {
  362. console.warn('定位机器人失败:', error);
  363. this.$message.warning('定位机器人失败');
  364. }
  365. },
  366. handleToggleFullscreen() {
  367. try {
  368. const mapStage = this.$refs.mapStage;
  369. if (!mapStage) return;
  370. if (!this.isFullscreen) {
  371. // 进入全屏
  372. if (mapStage.requestFullscreen) {
  373. mapStage.requestFullscreen();
  374. } else if (mapStage.webkitRequestFullscreen) {
  375. mapStage.webkitRequestFullscreen();
  376. } else if (mapStage.mozRequestFullScreen) {
  377. mapStage.mozRequestFullScreen();
  378. } else if (mapStage.msRequestFullscreen) {
  379. mapStage.msRequestFullscreen();
  380. }
  381. this.isFullscreen = true;
  382. } else {
  383. // 退出全屏
  384. if (document.exitFullscreen) {
  385. document.exitFullscreen();
  386. } else if (document.webkitExitFullscreen) {
  387. document.webkitExitFullscreen();
  388. } else if (document.mozCancelFullScreen) {
  389. document.mozCancelFullScreen();
  390. } else if (document.msExitFullscreen) {
  391. document.msExitFullscreen();
  392. }
  393. this.isFullscreen = false;
  394. }
  395. // 延迟触发地图重绘
  396. setTimeout(() => {
  397. this.updateOlCss();
  398. }, 100);
  399. } catch (error) {
  400. console.warn('全屏切换失败:', error);
  401. this.$message.warning('全屏功能不可用');
  402. }
  403. },
  404. // 添加标定
  405. addCalibration() {
  406. // 每次添加前,先根据现有列表重新排一次 id,保证连续
  407. this.calibrationList = this.calibrationList.map((item, index) => {
  408. return { ...item, id: index + 1 };
  409. });
  410. // 新的 id 就是当前长度+1
  411. const newId = this.calibrationList.length + 1;
  412. const coordinate = `${this.laserPositionData.x},${this.laserPositionData.y}`;
  413. const data = { id: newId, coordinate, angle: this.laserPositionData.angle };
  414. this.calibrationList.push(data);
  415. this.nowCalibId = newId;
  416. },
  417. // 移除标定
  418. removeCalibration(id) {
  419. if (this.calibrationList.length < 1) return;
  420. // 过滤掉要删除的
  421. this.calibrationList = this.calibrationList.filter(item => item.id !== id);
  422. // 删除后重新编号,保证 id 连续
  423. this.calibrationList = this.calibrationList.map((item, index) => {
  424. return { ...item, id: index + 1 };
  425. });
  426. this.nowCalibId = -1; // 标记无选中
  427. },
  428. /* // 添加标定
  429. addCalibration() {
  430. // 需要获取无人车实时坐标, 现在先写死
  431. this.nowCalibId = this.calibrationList.length > 0 ? this.calibrationList[this.calibrationList.length - 1].id + 1 : 1;
  432. let coordinate = `${this.laserPositionData.x},${this.laserPositionData.y}`
  433. let data = { id: this.nowCalibId, coordinate: coordinate, angle: this.laserPositionData.angle }
  434. this.calibrationList.push(data)
  435. },
  436. // 移除标定
  437. removeCalibration(id) {
  438. if (this.calibrationList.length < 1) {
  439. return;
  440. }
  441. this.calibrationList = this.calibrationList.filter(item => item.id !== id);
  442. // 删除图标
  443. this.nowCalibId = - id;
  444. }, */
  445. // 一键标定
  446. executeCalibration() {
  447. if (this.calibrationList.length < 1) {
  448. this.$message('请添加标定点!');
  449. return;
  450. }
  451. this.$modal.msgSuccess("标定执行成功");
  452. }
  453. },
  454. watch: {
  455. // 监听窗口宽度变化,自动切换模式
  456. windowWidth(newWidth) {
  457. if (newWidth < 1440 && !this.isMobileMode) {
  458. // 切换到移动模式时关闭抽屉
  459. this.drawerVisible = false;
  460. }
  461. this.updateRightPanelWidth();
  462. },
  463. // 监听抽屉开关状态,确保地图重绘
  464. drawerVisible() {
  465. this.$nextTick(() => {
  466. this.updateOlCss();
  467. });
  468. }
  469. }
  470. };
  471. </script>
  472. <style lang="scss" scoped>
  473. .calibration-container {
  474. width: 100%;
  475. height: calc(100vh - 84px);
  476. min-height: 600px;
  477. background: var(--color-bg-secondary);
  478. position: relative;
  479. overflow: hidden;
  480. .map-stage {
  481. position: relative;
  482. width: 100%;
  483. height: 100%;
  484. background: var(--color-bg-card);
  485. border-radius: var(--radius-lg);
  486. overflow: hidden;
  487. box-shadow: var(--shadow-card);
  488. .map-canvas-wrapper {
  489. position: absolute;
  490. top: 0;
  491. left: 0;
  492. right: 0;
  493. bottom: 0;
  494. z-index: 1;
  495. }
  496. .map-toolbar {
  497. position: absolute;
  498. left: 16px;
  499. top: 16px;
  500. z-index: 11;
  501. pointer-events: auto;
  502. }
  503. .right-panel {
  504. position: absolute;
  505. right: 16px;
  506. top: 16px;
  507. height: calc(100% - 32px);
  508. z-index: 10;
  509. border-radius: var(--radius-lg);
  510. box-shadow: var(--shadow-xl);
  511. background: var(--color-bg-card);
  512. display: flex;
  513. flex-direction: column;
  514. pointer-events: auto;
  515. transition: all var(--duration-200) var(--ease-out);
  516. &.panel-collapsed {
  517. width: 0 !important;
  518. opacity: 0;
  519. pointer-events: none;
  520. overflow: hidden;
  521. }
  522. }
  523. .panel-reopen-btn {
  524. position: absolute;
  525. right: 0;
  526. top: 50%;
  527. transform: translateY(-50%);
  528. z-index: 12;
  529. width: 36px;
  530. height: 72px;
  531. background: var(--color-bg-card);
  532. border: 1px solid var(--color-border-primary);
  533. border-radius: 8px 0 0 8px;
  534. box-shadow: var(--shadow-lg);
  535. cursor: pointer;
  536. display: flex;
  537. align-items: center;
  538. justify-content: center;
  539. color: var(--color-text-secondary);
  540. transition: all var(--duration-200) var(--ease-out);
  541. pointer-events: auto;
  542. &:hover {
  543. background: var(--color-primary);
  544. color: var(--color-text-inverse);
  545. border-color: var(--color-primary);
  546. transform: translateY(-50%) translateX(-2px);
  547. box-shadow: var(--shadow-xl);
  548. }
  549. i {
  550. font-size: var(--font-size-lg);
  551. font-weight: bold;
  552. }
  553. }
  554. .info-toggle-btn {
  555. position: absolute;
  556. top: 16px;
  557. right: 16px;
  558. z-index: 10;
  559. pointer-events: auto;
  560. .el-button {
  561. box-shadow: var(--shadow-lg);
  562. }
  563. }
  564. }
  565. }
  566. /* 抽屉样式 */
  567. .drawer-content {
  568. height: 100%;
  569. display: flex;
  570. flex-direction: column;
  571. background: var(--color-bg-card);
  572. .drawer-header {
  573. display: flex;
  574. align-items: center;
  575. justify-content: space-between;
  576. padding: var(--spacing-4);
  577. border-bottom: 1px solid var(--color-border-secondary);
  578. background: var(--color-bg-tertiary);
  579. .drawer-title {
  580. h3 {
  581. margin: 0;
  582. font-size: var(--font-size-lg);
  583. font-weight: var(--font-weight-semibold);
  584. color: var(--color-text-primary);
  585. line-height: var(--line-height-tight);
  586. }
  587. .map-name {
  588. font-size: var(--font-size-xs);
  589. color: var(--color-danger);
  590. margin-left: var(--spacing-2);
  591. }
  592. }
  593. }
  594. .drawer-body {
  595. flex: 1;
  596. overflow: hidden;
  597. .drawer-panel {
  598. height: 100%;
  599. ::v-deep .panel-header {
  600. display: none; /* 隐藏面板头部,使用抽屉头部 */
  601. }
  602. ::v-deep .panel-content {
  603. height: 100%;
  604. padding: var(--spacing-4);
  605. }
  606. ::v-deep .resize-handle {
  607. display: none; /* 抽屉模式下隐藏调整手柄 */
  608. }
  609. }
  610. }
  611. }
  612. /* 响应式设计 */
  613. @media (max-width: 1439px) {
  614. .calibration-container {
  615. .map-stage {
  616. .right-panel {
  617. display: none; /* 窄屏时隐藏固定面板 */
  618. }
  619. }
  620. }
  621. }
  622. @media (max-width: 768px) {
  623. .calibration-container {
  624. height: calc(100vh - 60px);
  625. .map-stage {
  626. border-radius: 0;
  627. .map-toolbar {
  628. left: 12px;
  629. top: 12px;
  630. }
  631. .panel-reopen-btn {
  632. width: 32px;
  633. height: 60px;
  634. }
  635. .info-toggle-btn {
  636. top: 12px;
  637. right: 12px;
  638. }
  639. }
  640. }
  641. }
  642. /* 动画效果 */
  643. @keyframes slideInRight {
  644. from {
  645. opacity: 0;
  646. transform: translateX(20px);
  647. }
  648. to {
  649. opacity: 1;
  650. transform: translateX(0);
  651. }
  652. }
  653. @keyframes slideInUp {
  654. from {
  655. opacity: 0;
  656. transform: translateY(20px);
  657. }
  658. to {
  659. opacity: 1;
  660. transform: translateY(0);
  661. }
  662. }
  663. .calibration-container {
  664. animation: slideInUp 0.3s ease-out;
  665. }
  666. .info-toggle-btn {
  667. animation: slideInRight 0.3s ease-out 0.2s both;
  668. }
  669. /* 暗色主题适配 */
  670. html.dark {
  671. .calibration-container {
  672. .map-stage {
  673. background: var(--color-bg-tertiary);
  674. box-shadow: var(--shadow-card);
  675. .right-panel {
  676. background: var(--color-bg-tertiary);
  677. border-color: var(--color-border-tertiary);
  678. box-shadow: var(--shadow-xl);
  679. }
  680. .panel-reopen-btn {
  681. background: var(--color-bg-tertiary);
  682. border-color: var(--color-border-tertiary);
  683. &:hover {
  684. background: var(--color-primary);
  685. border-color: var(--color-primary);
  686. }
  687. }
  688. }
  689. }
  690. .drawer-content {
  691. background: var(--color-bg-tertiary);
  692. .drawer-header {
  693. background: var(--color-bg-quaternary);
  694. border-bottom-color: var(--color-border-tertiary);
  695. }
  696. }
  697. }
  698. </style>
  699. <style>
  700. /* 全局抽屉样式 */
  701. .info-drawer {
  702. background-color: var(--color-bg-card);
  703. box-shadow: var(--shadow-xl);
  704. border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  705. }
  706. .info-drawer .el-drawer__body {
  707. padding: 0;
  708. height: 100%;
  709. overflow: hidden;
  710. }
  711. /* 暗色主题抽屉样式 */
  712. html.dark .info-drawer {
  713. background-color: var(--color-bg-tertiary);
  714. box-shadow: var(--shadow-xl);
  715. }
  716. </style>