CrossingDetailPanel.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  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>
  14. <SignalTimingChart :cycleLength="cycleLength" :currentTime="currentSec" :phaseData="mockPhaseData"
  15. :showScanLine="dataReady" :autoScan="dataReady" @scan-tick="onScanTick" />
  16. </div>
  17. </div>
  18. <div class="detail-panel-right">
  19. <form class="detail-right-form" @submit.prevent>
  20. <div class="form-group">
  21. <div class="control-method">
  22. <div class="control-label-wrap">
  23. <span class="control-label">控制方式</span>
  24. <div class="control-operation">
  25. <div class="operation-btn" :class="{ 'is-active': isManualMode }"
  26. @click="toggleManualMode">
  27. {{ isManualMode ? '退出手动控制' : '手动控制' }}
  28. </div>
  29. </div>
  30. </div>
  31. </div>
  32. <div class="form-interactive-area" :class="{ 'is-disabled': !isManualMode }">
  33. <div class="control-method-content">
  34. <SegmentedRadio v-model="currentMethod" :options="controlMethodOptions" size="auto" />
  35. </div>
  36. <div class="control-scheme" :class="{ 'is-disabled': isSchemeDisabled }">
  37. <div class="control-label-wrap">
  38. <span class="control-label">控制方案</span>
  39. <DropdownSelect v-model="currentScheme" :options="schemeOptions" size="auto" />
  40. </div>
  41. <div class="current-stage">
  42. <div class="current-stage-warp">
  43. <div class="current-stage-label">当前阶段:</div>
  44. <div v-for="(item, index) in currentStageList" :key="index" class="stage-item-wrapper">
  45. <div class="phase-box" :class="{ 'is-active': item.value === currentStage }"
  46. @click="currentStage = item.value">
  47. <img :src="item.img" alt="stage" class="phase-image" />
  48. </div>
  49. <div class="bottom-controls">
  50. <div class="input-unit-wrapper">
  51. <input type="number" v-model.number="item.time" class="stage-input"
  52. :disabled="!canEditStage"
  53. :title="canEditStage ? '修改阶段时间' : '当前控制方式不可修改'" />
  54. <span class="unit">s</span>
  55. </div>
  56. <span class="percent">{{ stagePercent(item.time) }}</span>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. <!-- 方案圆饼图 -->
  62. <div class="donut-row" v-if="!showLockTime">
  63. <div class="donut-item">
  64. <div class="donut-title">实时方案(执行方案3)</div>
  65. <PlanDonutChart :chartData="realtimeDonutData"
  66. :centerValue="String(realtimeRemaining)" centerLabel="剩余时长" :showTotal="true"
  67. :totalValue="cycleLength" :scale="panelScale" />
  68. </div>
  69. <div class="donut-item">
  70. <div class="donut-title">下周期方案</div>
  71. <PlanDonutChart :chartData="nextCycleDonutData" :centerValue="String(cycleLength)"
  72. centerLabel="总时长" :showTotal="false" :scale="panelScale" />
  73. </div>
  74. </div>
  75. <transition name="fade">
  76. <div class="lock-time" v-if="showLockTime">
  77. <div class="lock-time-label-wrap glow-header">
  78. <div class="lock-time-label">锁定时间</div>
  79. <div class="lock-time-close" @click="showLockTime = false">✕</div>
  80. </div>
  81. <div class="lock-time-options">
  82. <div class="lock-time-option">
  83. <label>
  84. <input type="radio" v-model="lockTimeType" value="continuous" /> 持续放行
  85. </label>
  86. </div>
  87. <div class="lock-time-option">
  88. <label>
  89. <input type="radio" v-model="lockTimeType" value="timer" /> 放行
  90. <DropdownSelect placeholder="锁定时间" v-model="currentLocktime"
  91. :options="locktimeOptions" size="auto" @click.native.prevent />
  92. 秒解锁
  93. </label>
  94. </div>
  95. </div>
  96. </div>
  97. </transition>
  98. </div>
  99. </div>
  100. <div class="button-group" v-show="isManualMode">
  101. <div>
  102. <button type="button" class="btn btn-cancel" @click="onCancel()">取消</button>
  103. <button type="button" class="btn btn-confirm" @click="onConfirm()">确认</button>
  104. </div>
  105. </div>
  106. </div>
  107. </form>
  108. </div>
  109. </div>
  110. </template>
  111. <script>
  112. import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
  113. import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
  114. import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
  115. import DropdownSelect from '@/components/ui/DropdownSelect.vue';
  116. import PlanDonutChart from '@/components/ui/PlanDonutChart.vue';
  117. import { apiGetCrossingDetailData } from '@/api';
  118. export default {
  119. name: 'CrossingPanel',
  120. components: {
  121. SignalTimingChart,
  122. IntersectionMapVideos,
  123. SegmentedRadio,
  124. DropdownSelect,
  125. PlanDonutChart
  126. },
  127. props: {
  128. preloadedData: { type: Object, default: null },
  129. iconMode: { type: String, default: 'simple' } // 'default' | 'simple'
  130. },
  131. data() {
  132. return {
  133. // 核心状态控制
  134. isManualMode: false, // 是否处于手动控制模式
  135. showLockTime: false, // 是否显示锁定时间弹窗
  136. lockTimeType: 'continuous', // 锁定时间类型
  137. dataReady: false,
  138. followPhase: false,
  139. intersectionData: {},
  140. currentRoute: {},
  141. cycleLength: 140,
  142. currentSec: 0,
  143. phaseDiff: 0,
  144. coordTime: 0,
  145. mockPhaseData: [],
  146. panelScale: 1,
  147. // 控制方式数据
  148. controlMethodOptions: [],
  149. currentMethod: 'temp',
  150. currentScheme: 'early_peak',
  151. schemeOptions: [],
  152. // 实时方案圆饼图数据(从 phaseData 动态生成)
  153. realtimeDonutData: [],
  154. // 下周期方案圆饼图数据
  155. nextCycleDonutData: [],
  156. // 实时方案剩余时长
  157. realtimeRemaining: 0,
  158. // 各阶段基础数据(从 phaseData 解析)
  159. phaseStages: [],
  160. currentLocktime: 50,
  161. locktimeOptions: [],
  162. currentStage: '1',
  163. // 补充了 time 属性,用于双向绑定输入框的时间
  164. currentStageList: []
  165. }
  166. },
  167. computed: {
  168. // 黄闪、关灯、全红时禁用控制方案
  169. isSchemeDisabled() {
  170. return ['yellow_flash', 'lights_off', 'all_red'].includes(this.currentMethod);
  171. },
  172. // 定周期、中心控制、感应控制、临时方案可编辑当前阶段
  173. canEditStage() {
  174. return ['fixed', 'system', 'sensor', 'temp'].includes(this.currentMethod);
  175. }
  176. },
  177. watch: {
  178. // 监听控制方式切换
  179. currentMethod(newVal) {
  180. // 需求4:切换步进方案策略时,出现锁定时间弹窗
  181. if (newVal === 'step') {
  182. this.showLockTime = true;
  183. } else {
  184. this.showLockTime = false;
  185. }
  186. // 模拟需求1:根据不同模式,切换对应的控制方案数据 (Mock 逻辑)
  187. this.updateSchemeDataByMethod(newVal);
  188. }
  189. },
  190. mounted() {
  191. this.initScaleObserver();
  192. if (this.preloadedData) {
  193. this.applyData(this.preloadedData);
  194. } else {
  195. this.loadData();
  196. }
  197. },
  198. beforeDestroy() {
  199. if (this._ro) this._ro.disconnect();
  200. },
  201. methods: {
  202. stagePercent(time) {
  203. const total = this.currentStageList.reduce((s, item) => s + (item.time || 0), 0);
  204. if (!total) return '0%';
  205. return Math.round(time / total * 100) + '%';
  206. },
  207. onScanTick(activeTime) {
  208. if (!this.mockPhaseData || this.mockPhaseData.length === 0) return;
  209. // 只看第一轨道(trackIdx=0)的相位
  210. const phase = this.mockPhaseData.find(p => p[0] === 0 && activeTime >= p[1] && activeTime < p[2]);
  211. if (!phase) return;
  212. const type = phase[5]; // green/stripe/yellow/red
  213. const iconValue = phase[6]; // 如 "STRAIGHT_DOWN,STRAIGHT_UP"
  214. const direction = phase[7]; // ns/ew
  215. const phaseName = phase[3]; // P1/P2 等
  216. const endTime = phase[2];
  217. const remaining = Math.max(0, Math.round(endTime - activeTime));
  218. const nsGreen = (type === 'green' && direction === 'ns');
  219. const ewGreen = (type === 'green' && direction === 'ew');
  220. // 从图标值解析当前允许的行驶方向类型
  221. // STRAIGHT→S, TURN_*_LEFT→L, *_UTURN→U
  222. let activeArrowTypes = [];
  223. if ((nsGreen || ewGreen) && iconValue) {
  224. const icons = iconValue.split(',');
  225. icons.forEach(ic => {
  226. if (ic.includes('UTURN')) activeArrowTypes.push('U');
  227. if (ic.includes('TURN') && !ic.includes('UTURN')) activeArrowTypes.push('L');
  228. if (ic.includes('STRAIGHT')) activeArrowTypes.push('S');
  229. });
  230. // 去重
  231. activeArrowTypes = [...new Set(activeArrowTypes)];
  232. }
  233. // 人行道全红判断:只有 P1/P3 绿灯期间人行道才有绿灯,其余时段全红
  234. const pedAllRed = !(type === 'green' && (phaseName === 'P1' || phaseName === 'P3'));
  235. this.$set(this.intersectionData, 'signals', {
  236. pedAllRed,
  237. ns: {
  238. phaseName: nsGreen ? ({ P1: '南北直行', P2: '南北左转' }[phaseName] || '南北') : (this.intersectionData.signals?.ns?.phaseName || '南北'),
  239. time: remaining,
  240. isGreen: nsGreen,
  241. activeArrowTypes: nsGreen ? activeArrowTypes : []
  242. },
  243. ew: {
  244. phaseName: ewGreen ? ({ P3: '东西直行', P4: '东西左转' }[phaseName] || '东西') : (this.intersectionData.signals?.ew?.phaseName || '东西'),
  245. time: remaining,
  246. isGreen: ewGreen,
  247. activeArrowTypes: ewGreen ? activeArrowTypes : []
  248. }
  249. });
  250. // 更新实时方案圆饼图的已走时长
  251. this.updateRealtimeDonut(activeTime);
  252. },
  253. // 从 phaseData 解析4个阶段的绿灯时长,构建圆饼图数据
  254. buildDonutFromPhaseData() {
  255. const phaseData = this.mockPhaseData || [];
  256. const stageColors = ['#3b82f6', '#a855f7', '#14b8a6', '#f59e0b'];
  257. const stageLabels = ['1-南北直行', '2-南北左转', '3-东西直行', '4-东西左转'];
  258. // 提取 track 0 的绿灯相位(每阶段的第一个 green)
  259. const greenPhases = phaseData.filter(p => p[0] === 0 && p[5] === 'green');
  260. const stages = greenPhases.slice(0, 4).map((p, i) => {
  261. const total = p[8] || Math.floor(this.cycleLength / 4);
  262. return {
  263. label: stageLabels[i] || `阶段${i + 1}`,
  264. value: total, // 阶段总时长
  265. start: p[1], // 阶段开始时间(从绿灯起始)
  266. end: p[1] + total, // 阶段结束时间
  267. color: stageColors[i]
  268. };
  269. });
  270. this.phaseStages = stages;
  271. // 实时方案:已走时长 + 4个阶段
  272. this.realtimeDonutData = [
  273. { label: '已走时长', value: 0, color: '#8892a0' },
  274. ...stages.map(s => ({ label: s.label, value: s.value, color: s.color }))
  275. ];
  276. this.realtimeRemaining = this.cycleLength;
  277. // 下周期方案:4个阶段,初始数据与实时方案相同
  278. this.nextCycleDonutData = stages.map(s => ({
  279. label: s.label,
  280. value: s.value,
  281. color: s.color
  282. }));
  283. },
  284. // 根据扫描线时间更新实时方案圆饼图
  285. updateRealtimeDonut(activeTime) {
  286. if (this.phaseStages.length === 0) return;
  287. const elapsed = Math.round(activeTime);
  288. const remaining = Math.max(0, this.cycleLength - elapsed);
  289. this.realtimeRemaining = remaining;
  290. // 计算各阶段的剩余时长
  291. const stageValues = this.phaseStages.map(s => {
  292. if (activeTime >= s.end) return 0; // 阶段已完成
  293. if (activeTime < s.start) return s.value; // 阶段未开始
  294. return Math.max(0, Math.round(s.end - activeTime)); // 阶段进行中
  295. });
  296. this.realtimeDonutData = [
  297. { label: '已走时长', value: elapsed, color: '#8892a0' },
  298. ...this.phaseStages.map((s, i) => ({
  299. label: s.label,
  300. value: stageValues[i],
  301. color: s.color
  302. }))
  303. ];
  304. },
  305. initScaleObserver() {
  306. const ro = new ResizeObserver(entries => {
  307. const { width } = entries[0].contentRect;
  308. const s = Math.min(width / 1315, 1);
  309. this.$el.style.setProperty('--s', s);
  310. this.panelScale = s;
  311. });
  312. ro.observe(this.$el);
  313. this._ro = ro;
  314. },
  315. async loadData() {
  316. const nodeId = this.$attrs.id || this.id;
  317. const data = await apiGetCrossingDetailData(nodeId, { iconMode: this.iconMode });
  318. if (data) {
  319. this.applyData(data);
  320. }
  321. },
  322. applyData(data) {
  323. this.currentRoute = data.currentRoute || {};
  324. this.intersectionData = data.intersectionData || {};
  325. this.mockPhaseData = data.phaseData || [];
  326. this.cycleLength = data.cycleLength || 140;
  327. this.currentSec = data.currentTime || 0;
  328. this.phaseDiff = data.phaseDiff || 0;
  329. this.coordTime = data.coordTime || 0;
  330. this.currentStageList = data.stageList || [];
  331. this.buildDonutFromPhaseData();
  332. this.$nextTick(() => {
  333. this.dataReady = true;
  334. });
  335. this.schemeOptions = data.schemeOptions || [];
  336. if (data.currentScheme) this.currentScheme = data.currentScheme;
  337. if (data.controlMethodOptions) this.controlMethodOptions = data.controlMethodOptions;
  338. if (data.currentMethod) this.currentMethod = data.currentMethod;
  339. if (data.locktimeOptions) this.locktimeOptions = data.locktimeOptions;
  340. },
  341. // 切换手动控制模式
  342. toggleManualMode() {
  343. this.isManualMode = !this.isManualMode;
  344. if (!this.isManualMode) {
  345. // 如果退出手动模式,可选择重置表单状态
  346. this.showLockTime = false;
  347. }
  348. },
  349. // 模拟:根据控制方式改变下拉方案的数据
  350. updateSchemeDataByMethod(method) {
  351. if (method === 'system') {
  352. this.schemeOptions = [
  353. { label: '系统优化方案A', value: 'sys_a' },
  354. { label: '系统优化方案B', value: 'sys_b' }
  355. ];
  356. this.currentScheme = 'sys_a';
  357. } else {
  358. this.schemeOptions = [
  359. { label: '早高峰', value: 'early_peak' },
  360. { label: '晚高峰', value: 'evening_peak' },
  361. { label: '平峰', value: 'normal' }
  362. ];
  363. this.currentScheme = 'early_peak';
  364. }
  365. },
  366. // 取消按钮
  367. onCancel() {
  368. this.isManualMode = false;
  369. this.showLockTime = false;
  370. // 可以在此添加回滚初始数据的逻辑
  371. },
  372. // 需求5:点击确认按钮提交 + 表单验证
  373. onConfirm() {
  374. // 验证1:临时方案必须检查时间是否有效
  375. if (this.currentMethod === 'temp') {
  376. const isInvalid = this.currentStageList.some(item => !item.time || item.time <= 0);
  377. if (isInvalid) {
  378. alert('请输入有效的阶段时间 (必须大于0)!');
  379. return;
  380. }
  381. }
  382. // 验证2:步进方案必须选择锁定类型
  383. if (this.currentMethod === 'step') {
  384. if (this.lockTimeType === 'timer' && !this.currentLocktime) {
  385. alert('请选择解锁时间!');
  386. return;
  387. }
  388. }
  389. // 构造提交参数
  390. const submitData = {
  391. method: this.currentMethod,
  392. scheme: this.currentScheme,
  393. stages: this.currentMethod === 'temp' ? this.currentStageList : null,
  394. lockConfig: this.currentMethod === 'step' ? {
  395. type: this.lockTimeType,
  396. time: this.lockTimeType === 'timer' ? this.currentLocktime : null
  397. } : null
  398. };
  399. console.log('提交的数据:', submitData);
  400. // 提交完成后可根据业务决定是否退出手动模式
  401. // this.isManualMode = false;
  402. }
  403. }
  404. }
  405. </script>
  406. <style scoped>
  407. .crossing-detail-panel {
  408. --s: 1;
  409. display: flex;
  410. flex-direction: row;
  411. gap: clamp(4px, calc(var(--s) * 12px), 12px);
  412. height: 100%;
  413. min-height: 0;
  414. overflow: hidden;
  415. }
  416. /* ===== 左侧:还原原始固定 55% 占比 ===== */
  417. .detail-panel-left {
  418. display: flex;
  419. flex-direction: column;
  420. flex: 0 0 55%;
  421. min-height: 0;
  422. min-width: 0;
  423. }
  424. /* ===== 右侧:flex 列容器 + 滚动兜底 ===== */
  425. .detail-panel-right {
  426. flex: 1;
  427. min-width: 0;
  428. min-height: 0;
  429. overflow-y: auto;
  430. overflow-x: hidden;
  431. display: flex;
  432. flex-direction: column;
  433. }
  434. .intersection-video-wrap {
  435. width: 100%;
  436. min-height: 0;
  437. flex: 2;
  438. }
  439. .signal-timing-wrap {
  440. flex: 0 0 auto;
  441. min-height: 0;
  442. height: clamp(110px, calc(var(--s) * 200px), 200px);
  443. width: 100%;
  444. min-width: 0;
  445. background-color: transparent;
  446. box-sizing: border-box;
  447. position: relative;
  448. display: flex;
  449. flex-direction: column;
  450. overflow: hidden;
  451. padding: clamp(3px, calc(var(--s) * 10px), 10px);
  452. }
  453. .header {
  454. display: flex;
  455. justify-content: space-between;
  456. align-items: center;
  457. margin-bottom: clamp(2px, calc(var(--s) * 6px), 15px);
  458. color: #e0e6f1;
  459. flex-shrink: 0;
  460. }
  461. .title-area {
  462. font-size: clamp(9px, calc(var(--s) * 16px), 16px);
  463. }
  464. .main-title {
  465. font-size: clamp(10px, calc(var(--s) * 18px), 18px);
  466. font-weight: bold;
  467. margin-right: clamp(4px, calc(var(--s) * 10px), 10px);
  468. }
  469. .sub-info {
  470. font-size: clamp(8px, calc(var(--s) * 12px), 12px);
  471. opacity: 0.8;
  472. }
  473. .checkbox-area {
  474. font-size: clamp(8px, calc(var(--s) * 12px), 12px);
  475. display: flex;
  476. align-items: center;
  477. cursor: pointer;
  478. opacity: 0.7;
  479. user-select: none;
  480. }
  481. .checkbox-area:hover {
  482. opacity: 1;
  483. }
  484. .checkbox-mock {
  485. width: clamp(10px, calc(var(--s) * 14px), 14px);
  486. height: clamp(10px, calc(var(--s) * 14px), 14px);
  487. border: 1px solid rgba(255, 255, 255, 0.5);
  488. margin-right: clamp(3px, calc(var(--s) * 6px), 6px);
  489. border-radius: 2px;
  490. display: flex;
  491. align-items: center;
  492. justify-content: center;
  493. }
  494. .checkbox-mock.is-checked {
  495. background-color: #4da8ff;
  496. border-color: #4da8ff;
  497. }
  498. .chart-container {
  499. width: 100%;
  500. min-width: 0;
  501. flex: 1;
  502. min-height: 50px;
  503. overflow: hidden;
  504. }
  505. .loading-overlay {
  506. flex: 1;
  507. display: flex;
  508. align-items: center;
  509. justify-content: center;
  510. color: #758599;
  511. font-size: 14px;
  512. }
  513. /* ===== 右侧表单内层容器 ===== */
  514. .detail-right-form {
  515. flex: 1;
  516. min-height: 0;
  517. display: flex;
  518. flex-direction: column;
  519. }
  520. .form-group {
  521. flex: 1;
  522. min-height: 0;
  523. display: flex;
  524. flex-direction: column;
  525. }
  526. /** 控制方法 */
  527. .control-method {
  528. color: #ffffff;
  529. flex-shrink: 0;
  530. }
  531. .control-label-wrap {
  532. display: flex;
  533. align-items: center;
  534. margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
  535. column-gap: clamp(4px, calc(var(--s) * 10px), 20px);
  536. }
  537. .control-label {
  538. font-size: clamp(12px, calc(var(--s) * 22px), 28px);
  539. color: #ffffff;
  540. white-space: nowrap;
  541. }
  542. .control-label-wrap span {
  543. display: inline-block;
  544. }
  545. .operation-btn {
  546. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  547. cursor: pointer;
  548. user-select: none;
  549. }
  550. .operation-btn:hover {
  551. text-decoration: underline;
  552. }
  553. .operation-btn.is-active {
  554. text-decoration: underline;
  555. }
  556. .control-method .control-label-wrap {
  557. justify-content: space-between;
  558. }
  559. /* 控制方式按钮组:设置 font-size 供 SegmentedRadio size="auto" 继承 */
  560. .control-method-content {
  561. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  562. }
  563. .control-scheme {
  564. margin-top: clamp(4px, calc(var(--s) * 10px), 20px);
  565. /* 设置 font-size 供 DropdownSelect size="auto" 继承 */
  566. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  567. }
  568. .control-scheme.is-disabled {
  569. opacity: 0.4;
  570. pointer-events: none;
  571. }
  572. .lock-time {
  573. width: 80%;
  574. border-radius: 8px;
  575. box-shadow:
  576. inset 0px 0px 10px 0px rgba(88, 146, 255, 0.4),
  577. inset 20px 0px 30px -10px rgba(88, 146, 255, 0.15);
  578. }
  579. .lock-time-label-wrap {
  580. display: flex;
  581. align-items: center;
  582. justify-content: space-between;
  583. padding: clamp(4px, calc(var(--s) * 8px), 10px);
  584. border-radius: 8px 8px 0 0;
  585. color: #ffffff;
  586. }
  587. .lock-time-label {
  588. font-size: clamp(10px, calc(var(--s) * 16px), 16px);
  589. color: #ffffff;
  590. }
  591. .lock-time-options {
  592. display: flex;
  593. flex-direction: column;
  594. row-gap: clamp(4px, calc(var(--s) * 10px), 10px);
  595. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  596. padding: clamp(4px, calc(var(--s) * 10px), 10px);
  597. color: #ffffff;
  598. }
  599. .lock-time-option {
  600. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  601. }
  602. .lock-time-close {
  603. cursor: pointer;
  604. }
  605. .glow-header {
  606. background: linear-gradient(180deg,
  607. rgba(65, 115, 205, 0.6) 0%,
  608. rgba(40, 70, 130, 0.1) 100%);
  609. backdrop-filter: blur(10px);
  610. }
  611. .current-stage {
  612. background-color: rgba(65, 115, 205, 0.2);
  613. border: 1px solid #3660a5;
  614. margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
  615. display: flex;
  616. justify-content: center;
  617. }
  618. .current-stage-warp {
  619. width: 100%;
  620. display: flex;
  621. align-items: center;
  622. justify-content: space-around;
  623. flex-wrap: wrap;
  624. gap: clamp(4px, calc(var(--s) * 8px), 10px);
  625. padding: clamp(6px, calc(var(--s) * 12px), 32px);
  626. color: #ffffff;
  627. }
  628. .current-stage-label {
  629. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  630. width: auto;
  631. white-space: nowrap;
  632. }
  633. .stage-input {
  634. width: clamp(32px, calc(var(--s) * 65px), 65px);
  635. border: 1px solid rgba(161, 190, 255, 0.7);
  636. background-color: transparent;
  637. padding: clamp(2px, calc(var(--s) * 5px), 5px);
  638. color: #ffffff;
  639. text-align: center;
  640. }
  641. .phase-box {
  642. position: relative;
  643. width: clamp(30px, calc(var(--s) * 90px), 90px);
  644. height: clamp(30px, calc(var(--s) * 90px), 90px);
  645. background: #E6F0FF;
  646. border-radius: 4px;
  647. display: flex;
  648. align-items: center;
  649. justify-content: center;
  650. cursor: pointer;
  651. transition: all 0.3s ease;
  652. box-sizing: border-box;
  653. overflow: hidden;
  654. }
  655. .phase-image {
  656. width: 100%;
  657. height: 100%;
  658. object-fit: contain;
  659. display: block;
  660. }
  661. .phase-box::after {
  662. content: '';
  663. position: absolute;
  664. top: 0;
  665. left: 0;
  666. width: 100%;
  667. height: 100%;
  668. background: rgba(30, 106, 255, 0.5);
  669. opacity: 0;
  670. transition: opacity 0.3s ease;
  671. pointer-events: none;
  672. }
  673. .phase-box.is-active::after {
  674. opacity: 1;
  675. }
  676. /** 按钮 */
  677. .btn {
  678. display: inline-flex;
  679. justify-content: center;
  680. align-items: center;
  681. height: clamp(22px, calc(var(--s) * 36px), 36px);
  682. padding: 0 clamp(10px, calc(var(--s) * 32px), 32px);
  683. font-size: clamp(9px, calc(var(--s) * 14px), 14px);
  684. border-radius: 4px;
  685. cursor: pointer;
  686. user-select: none;
  687. transition: all 0.2s ease-in-out;
  688. box-sizing: border-box;
  689. }
  690. .btn-cancel {
  691. background-color: transparent;
  692. color: #d1d5db;
  693. border: 1px solid rgba(130, 150, 190, 0.4);
  694. }
  695. .btn-cancel:hover {
  696. color: #ffffff;
  697. border-color: rgba(130, 150, 190, 0.8);
  698. background-color: rgba(255, 255, 255, 0.05);
  699. }
  700. .btn-cancel:active {
  701. background-color: rgba(255, 255, 255, 0.1);
  702. }
  703. .btn-confirm {
  704. background-color: #3b74ff;
  705. color: #ffffff;
  706. border: 1px solid #3b74ff;
  707. }
  708. .btn-confirm:hover {
  709. background-color: #5a8bff;
  710. border-color: #5a8bff;
  711. box-shadow: 0 2px 8px rgba(59, 116, 255, 0.3);
  712. }
  713. .btn-confirm:active {
  714. background-color: #265bed;
  715. border-color: #265bed;
  716. box-shadow: none;
  717. }
  718. .button-group {
  719. display: flex;
  720. justify-content: flex-end;
  721. margin-top: clamp(4px, calc(var(--s) * 10px), 20px);
  722. flex-shrink: 0;
  723. }
  724. .button-group>div {
  725. display: flex;
  726. gap: clamp(4px, calc(var(--s) * 8px), 12px);
  727. }
  728. /* 禁用状态 */
  729. .form-interactive-area {
  730. transition: opacity 0.3s;
  731. flex: 1;
  732. min-height: 0;
  733. overflow-y: auto;
  734. overflow-x: hidden;
  735. }
  736. .form-interactive-area.is-disabled {
  737. opacity: 0.6;
  738. pointer-events: none;
  739. }
  740. /* 当前阶段输入框微调 */
  741. .stage-item-wrapper {
  742. display: flex;
  743. flex-direction: column;
  744. align-items: stretch;
  745. gap: clamp(2px, calc(var(--s) * 4px), 6px);
  746. position: relative;
  747. }
  748. .bottom-controls {
  749. display: flex;
  750. align-items: center;
  751. justify-content: center;
  752. gap: clamp(4px, calc(var(--s) * 6px), 8px); /* 输入框和百分比的间距 */
  753. }
  754. /* 新增包裹层的相对定位 */
  755. .input-unit-wrapper {
  756. position: relative;
  757. display: inline-block;
  758. }
  759. .stage-input {
  760. width: clamp(32px, calc(var(--s) * 65px), 65px);
  761. border: 1px solid rgba(161, 190, 255, 0.7);
  762. background-color: transparent;
  763. padding: clamp(2px, calc(var(--s) * 5px), 5px);
  764. /* 给右侧留出空间,防止数字过长被 s 挡住 */
  765. padding-right: clamp(10px, calc(var(--s) * 16px), 16px);
  766. color: #ffffff;
  767. text-align: center;
  768. border-radius: 4px;
  769. }
  770. /* 修改 s 单位的定位方式为垂直居中 */
  771. .input-unit-wrapper .unit {
  772. position: absolute;
  773. top: 50%;
  774. transform: translateY(-50%);
  775. right: clamp(3px, calc(var(--s) * 6px), 8px);
  776. color: #77A1FF;
  777. font-size: clamp(8px, calc(var(--s) * 12px), 12px);
  778. pointer-events: none;
  779. }
  780. /* 微调百分比的间距,让排版更紧凑 */
  781. .stage-item-wrapper .percent {
  782. color: rgba(255, 255, 255, 0.5);
  783. font-size: clamp(8px, calc(var(--s) * 11px), 11px);
  784. white-space: nowrap;
  785. }
  786. .stage-input {
  787. width: clamp(32px, calc(var(--s) * 65px), 65px);
  788. border: 1px solid rgba(161, 190, 255, 0.7);
  789. background-color: transparent;
  790. padding: clamp(2px, calc(var(--s) * 5px), 5px);
  791. color: #ffffff;
  792. text-align: center;
  793. border-radius: 4px;
  794. }
  795. .stage-input:disabled {
  796. border-color: rgba(255, 255, 255, 0.2);
  797. color: rgba(255, 255, 255, 0.5);
  798. background-color: rgba(0, 0, 0, 0.2);
  799. }
  800. /* 弹窗过渡动画 */
  801. .fade-enter-active,
  802. .fade-leave-active {
  803. transition: opacity 0.3s, transform 0.3s;
  804. }
  805. .fade-enter,
  806. .fade-leave-to {
  807. opacity: 0;
  808. transform: translateY(-10px);
  809. }
  810. /* lock-time 弹窗补充样式 */
  811. .lock-time {
  812. margin-top: clamp(4px, calc(var(--s) * 10px), 15px);
  813. background-color: rgba(20, 30, 50, 0.9);
  814. }
  815. /* 单选框基础对齐 */
  816. .lock-time-option label {
  817. display: flex;
  818. align-items: center;
  819. gap: clamp(3px, calc(var(--s) * 6px), 8px);
  820. cursor: pointer;
  821. }
  822. /* ===== 方案圆饼图左右布局 ===== */
  823. .donut-row {
  824. display: flex;
  825. gap: clamp(4px, calc(var(--s) * 16px), 16px);
  826. width: 100%;
  827. }
  828. .donut-item {
  829. flex: 1;
  830. min-width: 0;
  831. }
  832. .donut-title {
  833. font-size: clamp(11px, calc(var(--s) * 13px), 14px);
  834. color: #a0aec0;
  835. margin-bottom: 4px;
  836. }
  837. </style>