CrossingDetailPanel.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. <template>
  2. <div class="crossing-detail-panel">
  3. <div class="detail-panel-left">
  4. <div class="intersection-video-wrap">
  5. <IntersectionMapVideos :mapData="intersectionData" :videoUrls="currentRoute.cornerVideos" />
  6. </div>
  7. <div class="signal-timing-wrap">
  8. <div class="header">
  9. <div class="title-area">
  10. <span class="main-title">方案状态</span>
  11. <span class="sub-info">(周期: {{ cycleLength }} 相位差: {{ phaseDiff }} 协调时间: {{ coordTime }})</span>
  12. </div>
  13. <div class="checkbox-area">
  14. <div class="checkbox-mock" :class="{ 'is-checked': followPhase }"
  15. @click="followPhase = !followPhase">
  16. <span v-if="followPhase" style="color: #fff; font-size: 12px; margin-left: 1px;">✓</span>
  17. </div>
  18. <span>跟随相位</span>
  19. </div>
  20. </div>
  21. <SignalTimingChart :cycleLength="cycleLength" :currentTime="currentSec" :phaseData="mockPhaseData" />
  22. </div>
  23. </div>
  24. <div class="detail-panel-right">
  25. <form class="detail-right-form" @submit.prevent>
  26. <div class="form-group">
  27. <div class="control-method">
  28. <div class="control-label-wrap">
  29. <span class="control-label">控制方式</span>
  30. <div class="control-operation">
  31. <div class="operation-btn"
  32. :class="{ 'is-active': isManualMode }"
  33. @click="toggleManualMode">
  34. {{ isManualMode ? '退出手动控制' : '手动控制' }}
  35. </div>
  36. </div>
  37. </div>
  38. </div>
  39. <div class="form-interactive-area" :class="{ 'is-disabled': !isManualMode }">
  40. <div class="control-method-content">
  41. <SegmentedRadio v-model="currentMethod" :options="controlMethodOptions" size="auto" />
  42. </div>
  43. <div class="control-scheme">
  44. <div class="control-label-wrap">
  45. <span class="control-label">控制方案</span>
  46. <DropdownSelect v-model="currentScheme" :options="schemeOptions" size="auto" />
  47. </div>
  48. <div class="current-stage">
  49. <div class="current-stage-warp">
  50. <div class="current-stage-label">当前阶段:</div>
  51. <div v-for="(item, index) in currentStageList" :key="index" class="stage-item-wrapper">
  52. <div
  53. class="phase-box"
  54. :class="{ 'is-active': item.value === currentStage }"
  55. @click="currentStage = item.value"
  56. >
  57. <img :src="item.img" alt="stage" class="phase-image" />
  58. </div>
  59. <input
  60. type="number"
  61. v-model.number="item.time"
  62. class="stage-input"
  63. :disabled="currentMethod !== 'temp'"
  64. :title="currentMethod !== 'temp' ? '仅临时方案可修改' : '修改阶段时间'"
  65. />
  66. <span class="unit">s</span>
  67. </div>
  68. </div>
  69. </div>
  70. <transition name="fade">
  71. <div class="lock-time" v-if="showLockTime">
  72. <div class="lock-time-label-wrap glow-header">
  73. <div class="lock-time-label">锁定时间</div>
  74. <div class="lock-time-close" @click="showLockTime = false">✕</div>
  75. </div>
  76. <div class="lock-time-options">
  77. <div class="lock-time-option">
  78. <label>
  79. <input type="radio" v-model="lockTimeType" value="continuous" /> 持续放行
  80. </label>
  81. </div>
  82. <div class="lock-time-option">
  83. <label>
  84. <input type="radio" v-model="lockTimeType" value="timer" /> 放行
  85. <DropdownSelect placeholder="锁定时间" v-model="currentLocktime" :options="locktimeOptions" size="auto"
  86. @click.native.prevent />
  87. 秒解锁
  88. </label>
  89. </div>
  90. </div>
  91. </div>
  92. </transition>
  93. </div>
  94. </div>
  95. <div class="button-group" v-show="isManualMode">
  96. <div>
  97. <button type="button" class="btn btn-cancel" @click="onCancel()">取消</button>
  98. <button type="button" class="btn btn-confirm" @click="onConfirm()">确认</button>
  99. </div>
  100. </div>
  101. </div>
  102. </form>
  103. </div>
  104. </div>
  105. </template>
  106. <script>
  107. import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
  108. import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
  109. import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
  110. import DropdownSelect from '@/components/ui/DropdownSelect.vue';
  111. import { apiGetCrossingDetailData } from '@/api';
  112. export default {
  113. name: 'CrossingPanel',
  114. components: {
  115. SignalTimingChart,
  116. IntersectionMapVideos,
  117. SegmentedRadio,
  118. DropdownSelect
  119. },
  120. props: {
  121. preloadedData: { type: Object, default: null }
  122. },
  123. data() {
  124. return {
  125. // 核心状态控制
  126. isManualMode: false, // 是否处于手动控制模式
  127. showLockTime: false, // 是否显示锁定时间弹窗
  128. lockTimeType: 'continuous', // 锁定时间类型
  129. followPhase: false,
  130. intersectionData: {},
  131. currentRoute: {},
  132. cycleLength: 140,
  133. currentSec: 15,
  134. phaseDiff: 0,
  135. coordTime: 0,
  136. mockPhaseData: [],
  137. // 控制方式数据
  138. controlMethodOptions: [],
  139. currentMethod: 'temp',
  140. currentScheme: 'early_peak',
  141. schemeOptions: [],
  142. currentLocktime: 50,
  143. locktimeOptions: [],
  144. currentStage: '1',
  145. // 补充了 time 属性,用于双向绑定输入框的时间
  146. currentStageList: []
  147. }
  148. },
  149. watch: {
  150. // 监听控制方式切换
  151. currentMethod(newVal) {
  152. // 需求4:切换步进方案策略时,出现锁定时间弹窗
  153. if (newVal === 'step') {
  154. this.showLockTime = true;
  155. } else {
  156. this.showLockTime = false;
  157. }
  158. // 模拟需求1:根据不同模式,切换对应的控制方案数据 (Mock 逻辑)
  159. this.updateSchemeDataByMethod(newVal);
  160. }
  161. },
  162. mounted() {
  163. this.initScaleObserver();
  164. if (this.preloadedData) {
  165. this.applyData(this.preloadedData);
  166. } else {
  167. this.loadData();
  168. }
  169. },
  170. beforeDestroy() {
  171. if (this._ro) this._ro.disconnect();
  172. },
  173. methods: {
  174. initScaleObserver() {
  175. const ro = new ResizeObserver(entries => {
  176. const { width } = entries[0].contentRect;
  177. const s = Math.min(width / 1315, 1);
  178. this.$el.style.setProperty('--s', s);
  179. });
  180. ro.observe(this.$el);
  181. this._ro = ro;
  182. },
  183. async loadData() {
  184. const nodeId = this.$attrs.id || this.id;
  185. const data = await apiGetCrossingDetailData(nodeId);
  186. if (data) {
  187. this.applyData(data);
  188. }
  189. },
  190. applyData(data) {
  191. this.currentRoute = data.currentRoute || {};
  192. this.intersectionData = data.intersectionData || {};
  193. this.mockPhaseData = data.phaseData || [];
  194. this.cycleLength = data.cycleLength || 140;
  195. this.currentSec = data.currentTime || 0;
  196. this.phaseDiff = data.phaseDiff || 0;
  197. this.coordTime = data.coordTime || 0;
  198. this.currentStageList = data.stageList || [];
  199. this.schemeOptions = data.schemeOptions || [];
  200. if (data.currentScheme) this.currentScheme = data.currentScheme;
  201. if (data.controlMethodOptions) this.controlMethodOptions = data.controlMethodOptions;
  202. if (data.currentMethod) this.currentMethod = data.currentMethod;
  203. if (data.locktimeOptions) this.locktimeOptions = data.locktimeOptions;
  204. },
  205. // 切换手动控制模式
  206. toggleManualMode() {
  207. this.isManualMode = !this.isManualMode;
  208. if (!this.isManualMode) {
  209. // 如果退出手动模式,可选择重置表单状态
  210. this.showLockTime = false;
  211. }
  212. },
  213. // 模拟:根据控制方式改变下拉方案的数据
  214. updateSchemeDataByMethod(method) {
  215. if (method === 'system') {
  216. this.schemeOptions = [
  217. { label: '系统优化方案A', value: 'sys_a' },
  218. { label: '系统优化方案B', value: 'sys_b' }
  219. ];
  220. this.currentScheme = 'sys_a';
  221. } else {
  222. this.schemeOptions = [
  223. { label: '早高峰', value: 'early_peak' },
  224. { label: '晚高峰', value: 'evening_peak' },
  225. { label: '平峰', value: 'normal' }
  226. ];
  227. this.currentScheme = 'early_peak';
  228. }
  229. },
  230. // 取消按钮
  231. onCancel() {
  232. this.isManualMode = false;
  233. this.showLockTime = false;
  234. // 可以在此添加回滚初始数据的逻辑
  235. },
  236. // 需求5:点击确认按钮提交 + 表单验证
  237. onConfirm() {
  238. // 验证1:临时方案必须检查时间是否有效
  239. if (this.currentMethod === 'temp') {
  240. const isInvalid = this.currentStageList.some(item => !item.time || item.time <= 0);
  241. if (isInvalid) {
  242. alert('请输入有效的阶段时间 (必须大于0)!');
  243. return;
  244. }
  245. }
  246. // 验证2:步进方案必须选择锁定类型
  247. if (this.currentMethod === 'step') {
  248. if (this.lockTimeType === 'timer' && !this.currentLocktime) {
  249. alert('请选择解锁时间!');
  250. return;
  251. }
  252. }
  253. // 构造提交参数
  254. const submitData = {
  255. method: this.currentMethod,
  256. scheme: this.currentScheme,
  257. stages: this.currentMethod === 'temp' ? this.currentStageList : null,
  258. lockConfig: this.currentMethod === 'step' ? {
  259. type: this.lockTimeType,
  260. time: this.lockTimeType === 'timer' ? this.currentLocktime : null
  261. } : null
  262. };
  263. console.log('提交的数据:', submitData);
  264. // 提交完成后可根据业务决定是否退出手动模式
  265. // this.isManualMode = false;
  266. }
  267. }
  268. }
  269. </script>
  270. <style scoped>
  271. .crossing-detail-panel {
  272. --s: 1;
  273. display: flex;
  274. flex-direction: row;
  275. gap: clamp(4px, calc(var(--s) * 12px), 12px);
  276. height: 100%;
  277. min-height: 0;
  278. overflow: hidden;
  279. }
  280. /* ===== 左侧:还原原始固定 55% 占比 ===== */
  281. .detail-panel-left {
  282. display: flex;
  283. flex-direction: column;
  284. flex: 0 0 55%;
  285. min-height: 0;
  286. min-width: 0;
  287. }
  288. /* ===== 右侧:flex 列容器 + 滚动兜底 ===== */
  289. .detail-panel-right {
  290. flex: 1;
  291. min-width: 0;
  292. min-height: 0;
  293. overflow-y: auto;
  294. overflow-x: hidden;
  295. display: flex;
  296. flex-direction: column;
  297. }
  298. .intersection-video-wrap {
  299. width: 100%;
  300. min-height: 0;
  301. flex: 2;
  302. }
  303. .signal-timing-wrap {
  304. flex: 1;
  305. min-height: 0;
  306. height: 120px;
  307. flex-shrink: 0;
  308. width: 100%;
  309. min-width: 0;
  310. background-color: transparent;
  311. box-sizing: border-box;
  312. position: relative;
  313. display: flex;
  314. flex-direction: column;
  315. overflow: hidden;
  316. padding: clamp(3px, calc(var(--s) * 10px), 10px);
  317. }
  318. .header {
  319. display: flex;
  320. justify-content: space-between;
  321. align-items: center;
  322. margin-bottom: clamp(2px, calc(var(--s) * 6px), 15px);
  323. color: #e0e6f1;
  324. flex-shrink: 0;
  325. }
  326. .title-area {
  327. font-size: clamp(9px, calc(var(--s) * 16px), 16px);
  328. }
  329. .main-title {
  330. font-size: clamp(10px, calc(var(--s) * 18px), 18px);
  331. font-weight: bold;
  332. margin-right: clamp(4px, calc(var(--s) * 10px), 10px);
  333. }
  334. .sub-info {
  335. font-size: clamp(8px, calc(var(--s) * 12px), 12px);
  336. opacity: 0.8;
  337. }
  338. .checkbox-area {
  339. font-size: clamp(8px, calc(var(--s) * 12px), 12px);
  340. display: flex;
  341. align-items: center;
  342. cursor: pointer;
  343. opacity: 0.7;
  344. user-select: none;
  345. }
  346. .checkbox-area:hover {
  347. opacity: 1;
  348. }
  349. .checkbox-mock {
  350. width: clamp(10px, calc(var(--s) * 14px), 14px);
  351. height: clamp(10px, calc(var(--s) * 14px), 14px);
  352. border: 1px solid rgba(255, 255, 255, 0.5);
  353. margin-right: clamp(3px, calc(var(--s) * 6px), 6px);
  354. border-radius: 2px;
  355. display: flex;
  356. align-items: center;
  357. justify-content: center;
  358. }
  359. .checkbox-mock.is-checked {
  360. background-color: #4da8ff;
  361. border-color: #4da8ff;
  362. }
  363. .chart-container {
  364. width: 100%;
  365. min-width: 0;
  366. flex: 1;
  367. min-height: 50px;
  368. overflow: hidden;
  369. }
  370. .loading-overlay {
  371. flex: 1;
  372. display: flex;
  373. align-items: center;
  374. justify-content: center;
  375. color: #758599;
  376. font-size: 14px;
  377. }
  378. /* ===== 右侧表单内层容器 ===== */
  379. .detail-right-form {
  380. flex: 1;
  381. min-height: 0;
  382. display: flex;
  383. flex-direction: column;
  384. }
  385. .form-group {
  386. flex: 1;
  387. min-height: 0;
  388. display: flex;
  389. flex-direction: column;
  390. }
  391. /** 控制方法 */
  392. .control-method {
  393. color: #ffffff;
  394. flex-shrink: 0;
  395. }
  396. .control-label-wrap {
  397. display: flex;
  398. align-items: center;
  399. margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
  400. column-gap: clamp(4px, calc(var(--s) * 10px), 20px);
  401. }
  402. .control-label {
  403. font-size: clamp(12px, calc(var(--s) * 22px), 28px);
  404. color: #ffffff;
  405. white-space: nowrap;
  406. }
  407. .control-label-wrap span {
  408. display: inline-block;
  409. }
  410. .operation-btn {
  411. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  412. cursor: pointer;
  413. user-select: none;
  414. }
  415. .operation-btn:hover {
  416. text-decoration: underline;
  417. }
  418. .operation-btn.is-active {
  419. text-decoration: underline;
  420. }
  421. .control-method .control-label-wrap {
  422. justify-content: space-between;
  423. }
  424. /* 控制方式按钮组:设置 font-size 供 SegmentedRadio size="auto" 继承 */
  425. .control-method-content {
  426. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  427. }
  428. .control-scheme {
  429. margin-top: clamp(4px, calc(var(--s) * 10px), 20px);
  430. /* 设置 font-size 供 DropdownSelect size="auto" 继承 */
  431. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  432. }
  433. .lock-time {
  434. width: 80%;
  435. border-radius: 8px;
  436. box-shadow:
  437. inset 0px 0px 10px 0px rgba(88, 146, 255, 0.4),
  438. inset 20px 0px 30px -10px rgba(88, 146, 255, 0.15);
  439. }
  440. .lock-time-label-wrap {
  441. display: flex;
  442. align-items: center;
  443. justify-content: space-between;
  444. padding: clamp(4px, calc(var(--s) * 8px), 10px);
  445. border-radius: 8px 8px 0 0;
  446. color: #ffffff;
  447. }
  448. .lock-time-label {
  449. font-size: clamp(10px, calc(var(--s) * 16px), 16px);
  450. color: #ffffff;
  451. }
  452. .lock-time-options {
  453. display: flex;
  454. flex-direction: column;
  455. row-gap: clamp(4px, calc(var(--s) * 10px), 10px);
  456. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  457. padding: clamp(4px, calc(var(--s) * 10px), 10px);
  458. color: #ffffff;
  459. }
  460. .lock-time-option {
  461. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  462. }
  463. .lock-time-close {
  464. cursor: pointer;
  465. }
  466. .glow-header {
  467. background: linear-gradient(180deg,
  468. rgba(65, 115, 205, 0.6) 0%,
  469. rgba(40, 70, 130, 0.1) 100%);
  470. backdrop-filter: blur(10px);
  471. }
  472. .current-stage {
  473. background-color: rgba(65, 115, 205, 0.2);
  474. border: 1px solid #3660a5;
  475. margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
  476. display: flex;
  477. justify-content: center;
  478. }
  479. .current-stage-warp {
  480. display: flex;
  481. align-items: center;
  482. justify-content: center;
  483. flex-wrap: wrap;
  484. gap: clamp(4px, calc(var(--s) * 8px), 10px);
  485. padding: clamp(6px, calc(var(--s) * 12px), 32px);
  486. color: #ffffff;
  487. }
  488. .current-stage-label {
  489. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  490. width: auto;
  491. white-space: nowrap;
  492. }
  493. .stage-input {
  494. width: clamp(32px, calc(var(--s) * 65px), 65px);
  495. border: 1px solid rgba(161,190,255,0.7);
  496. background-color: transparent;
  497. padding: clamp(2px, calc(var(--s) * 5px), 5px);
  498. color: #ffffff;
  499. text-align: center;
  500. }
  501. .phase-box {
  502. position: relative;
  503. width: clamp(30px, calc(var(--s) * 65px), 65px);
  504. height: clamp(30px, calc(var(--s) * 65px), 65px);
  505. background: #E6F0FF;
  506. border-radius: 4px;
  507. display: flex;
  508. align-items: center;
  509. justify-content: center;
  510. cursor: pointer;
  511. transition: all 0.3s ease;
  512. box-sizing: border-box;
  513. overflow: hidden;
  514. }
  515. .phase-image {
  516. width: 100%;
  517. height: 100%;
  518. object-fit: contain;
  519. display: block;
  520. }
  521. .phase-box::after {
  522. content: '';
  523. position: absolute;
  524. top: 0;
  525. left: 0;
  526. width: 100%;
  527. height: 100%;
  528. background: rgba(30, 106, 255, 0.5);
  529. opacity: 0;
  530. transition: opacity 0.3s ease;
  531. pointer-events: none;
  532. }
  533. .phase-box.is-active::after {
  534. opacity: 1;
  535. }
  536. /** 按钮 */
  537. .btn {
  538. display: inline-flex;
  539. justify-content: center;
  540. align-items: center;
  541. height: clamp(22px, calc(var(--s) * 36px), 36px);
  542. padding: 0 clamp(10px, calc(var(--s) * 32px), 32px);
  543. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  544. border-radius: 4px;
  545. cursor: pointer;
  546. user-select: none;
  547. transition: all 0.2s ease-in-out;
  548. box-sizing: border-box;
  549. }
  550. .btn-cancel {
  551. background-color: transparent;
  552. color: #d1d5db;
  553. border: 1px solid rgba(130, 150, 190, 0.4);
  554. }
  555. .btn-cancel:hover {
  556. color: #ffffff;
  557. border-color: rgba(130, 150, 190, 0.8);
  558. background-color: rgba(255, 255, 255, 0.05);
  559. }
  560. .btn-cancel:active {
  561. background-color: rgba(255, 255, 255, 0.1);
  562. }
  563. .btn-confirm {
  564. background-color: #3b74ff;
  565. color: #ffffff;
  566. border: 1px solid #3b74ff;
  567. }
  568. .btn-confirm:hover {
  569. background-color: #5a8bff;
  570. border-color: #5a8bff;
  571. box-shadow: 0 2px 8px rgba(59, 116, 255, 0.3);
  572. }
  573. .btn-confirm:active {
  574. background-color: #265bed;
  575. border-color: #265bed;
  576. box-shadow: none;
  577. }
  578. .button-group {
  579. display: flex;
  580. justify-content: flex-end;
  581. margin-top: clamp(4px, calc(var(--s) * 10px), 20px);
  582. flex-shrink: 0;
  583. }
  584. .button-group>div {
  585. display: flex;
  586. gap: clamp(4px, calc(var(--s) * 8px), 12px);
  587. }
  588. /* 禁用状态 */
  589. .form-interactive-area {
  590. transition: opacity 0.3s;
  591. flex: 1;
  592. min-height: 0;
  593. overflow-y: auto;
  594. overflow-x: hidden;
  595. }
  596. .form-interactive-area.is-disabled {
  597. opacity: 0.6;
  598. pointer-events: none;
  599. }
  600. /* 当前阶段输入框微调 */
  601. .stage-item-wrapper {
  602. display: flex;
  603. flex-direction: column;
  604. align-items: center;
  605. gap: clamp(2px, calc(var(--s) * 4px), 6px);
  606. position: relative;
  607. }
  608. .stage-input {
  609. width: clamp(32px, calc(var(--s) * 65px), 65px);
  610. border: 1px solid rgba(161,190,255,0.7);
  611. background-color: transparent;
  612. padding: clamp(2px, calc(var(--s) * 5px), 5px);
  613. color: #ffffff;
  614. text-align: center;
  615. border-radius: 4px;
  616. }
  617. .stage-input:disabled {
  618. border-color: rgba(255, 255, 255, 0.2);
  619. color: rgba(255, 255, 255, 0.5);
  620. background-color: rgba(0, 0, 0, 0.2);
  621. }
  622. .stage-item-wrapper .unit {
  623. position: absolute;
  624. bottom: clamp(2px, calc(var(--s) * 6px), 6px);
  625. right: clamp(3px, calc(var(--s) * 6px), 8px);
  626. color: #77A1FF;
  627. font-size: clamp(8px, calc(var(--s) * 12px), 12px);
  628. pointer-events: none;
  629. }
  630. /* 弹窗过渡动画 */
  631. .fade-enter-active, .fade-leave-active {
  632. transition: opacity 0.3s, transform 0.3s;
  633. }
  634. .fade-enter, .fade-leave-to {
  635. opacity: 0;
  636. transform: translateY(-10px);
  637. }
  638. /* lock-time 弹窗补充样式 */
  639. .lock-time {
  640. margin-top: clamp(4px, calc(var(--s) * 10px), 15px);
  641. background-color: rgba(20, 30, 50, 0.9);
  642. }
  643. /* 单选框基础对齐 */
  644. .lock-time-option label {
  645. display: flex;
  646. align-items: center;
  647. gap: clamp(3px, calc(var(--s) * 6px), 8px);
  648. cursor: pointer;
  649. }
  650. </style>