| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178 |
- <template>
- <div class="crossing-detail-panel">
- <div class="detail-panel-left">
- <div class="intersection-video-wrap">
- <IntersectionMapVideos :mapData="intersectionData" :videoUrls="currentRoute.cornerVideos" />
- </div>
- <div class="signal-timing-wrap">
- <div class="header">
- <div class="title-area">
- <span class="main-title">方案状态</span>
- <span class="sub-info">(周期: {{ cycleLength }} 相位差: {{ phaseDiff }} 协调时间: {{ coordTime }})</span>
- </div>
- </div>
- <SignalTimingChart :cycleLength="cycleLength" :currentTime="currentSec" :phaseData="mockPhaseData"
- :showScanLine="dataReady" :autoScan="dataReady" @scan-tick="onScanTick" />
- </div>
- </div>
- <div class="detail-panel-right">
- <form class="detail-right-form" @submit.prevent>
- <div class="form-group">
- <div class="control-method">
- <div class="control-label-wrap">
- <span class="control-label">控制方式</span>
- <div class="control-operation">
- <div class="operation-btn" :class="{ 'is-active': isManualMode }"
- @click="toggleManualMode">
- {{ isManualMode ? '退出手动控制' : '手动控制' }}
- </div>
- </div>
- </div>
- </div>
- <div class="form-interactive-area">
- <div class="form-editable-area" :class="{ 'is-disabled': !isManualMode }">
- <div class="control-method-content">
- <SegmentedRadio v-model="currentMethod" :options="controlMethodOptions" size="auto" />
- </div>
- <div class="control-scheme">
- <div class="control-label-wrap">
- <span class="control-label">控制方案</span>
- <DropdownSelect v-model="currentScheme" :options="schemeOptions" size="auto" :class="{'DropdownSelect-is-disabled': !isManualMode }"/>
- </div>
- <div class="time-form-bar" v-if="currentMethod === 'temp'">
- <el-date-picker v-model="startDate" type="date" placeholder="选择日期"
- value-format="yyyy-MM-dd" size="small" :append-to-body="true"
- :popper-options="{ boundariesPadding: 0, gpuAcceleration: false }">
- </el-date-picker>
- <el-time-picker v-model="startTime" placeholder="选择时间"
- value-format="HH:mm:ss" size="small" :append-to-body="true"
- :popper-options="{ boundariesPadding: 0, gpuAcceleration: false }">
- </el-time-picker>
- <el-date-picker v-model="endDate" type="date" placeholder="选择日期"
- value-format="yyyy-MM-dd" size="small" :append-to-body="true"
- :popper-options="{ boundariesPadding: 0, gpuAcceleration: false }">
- </el-date-picker>
- <el-time-picker v-model="endTime" placeholder="选择时间"
- value-format="HH:mm:ss" size="small" :append-to-body="true"
- :popper-options="{ boundariesPadding: 0, gpuAcceleration: false }">
- </el-time-picker>
- <div class="form-item-labeled">
- <span class="form-label">时长</span>
- <el-select v-model="duration" placeholder="请选择时长" size="small"
- :popper-append-to-body="true">
- <el-option v-for="d in durationOptions" :key="d" :label="d"
- :value="d"></el-option>
- </el-select>
- </div>
- <div class="form-item-labeled">
- <span class="form-label">周期</span>
- <el-select v-model="period" placeholder="请选择周期" size="small"
- :popper-append-to-body="true">
- <el-option v-for="p in 8" :key="p" :label="'周期' + p"
- :value="p"></el-option>
- </el-select>
- </div>
- </div>
- <div class="current-stage">
- <div class="current-stage-warp">
- <div class="current-stage-label">当前阶段:</div>
- <div v-for="(item, index) in currentStageList" :key="index"
- class="stage-item-wrapper">
- <div class="phase-box" :class="{ 'is-active': item.value === currentStage }"
- @click="onStageClick(item.value)">
- <img :src="item.img" alt="stage" class="phase-image" />
- </div>
- <div class="bottom-controls">
- <div class="input-unit-wrapper">
- <input type="number" v-model.number="item.time" class="stage-input"
- :disabled="!canEditStage"
- :title="canEditStage ? '修改阶段时间' : '当前控制方式不可修改'" />
- <span class="unit">s</span>
- </div>
- <span class="percent">{{ stagePercent(item.time) }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 方案圆饼图 -->
- <div class="donut-row">
- <div class="donut-item">
- <div class="donut-title">实时方案(执行方案3)</div>
- <PlanDonutChart :chartData="realtimeDonutData"
- :centerValue="String(realtimeRemaining)" centerLabel="剩余时长" :showTotal="true"
- :totalValue="cycleLength" :scale="panelScale" />
- </div>
- <div class="donut-item">
- <div class="donut-title">下周期方案</div>
- <PlanDonutChart :chartData="nextCycleDonutData" :centerValue="String(cycleLength)"
- centerLabel="总时长" :showTotal="false" :scale="panelScale" />
- </div>
- </div>
- </div>
- <div class="button-group" v-show="isManualMode && currentMethod !== 'step'">
- <div>
- <button type="button" class="btn btn-cancel" @click="onCancel()">取消</button>
- <button type="button" class="btn btn-confirm" @click="onConfirm()">确认</button>
- </div>
- </div>
- </div>
- </form>
- </div>
- <!-- 步进锁定时间弹窗 -->
- <transition name="fade">
- <div class="lock-time-overlay" v-if="showLockTime" @click.self="showLockTime = false">
- <div class="lock-time-dialog">
- <div class="lock-time-header">
- <span class="lock-time-title">锁定时间</span>
- <span class="lock-time-close" @click="showLockTime = false">✕</span>
- </div>
- <div class="lock-time-divider"></div>
- <div class="lock-time-body">
- <div class="lock-time-options">
- <div class="lock-time-option">
- <label>
- <input type="radio" v-model="lockTimeType" value="continuous" /> 持续放行
- </label>
- </div>
- <div class="lock-time-option">
- <label>
- <input type="radio" v-model="lockTimeType" value="timer" /> 放行
- <DropdownSelect placeholder="锁定时间" v-model="currentLocktime"
- :options="locktimeOptions" size="auto" @click.native.prevent />
- 秒解锁
- </label>
- </div>
- </div>
- <div class="lock-time-actions">
- <button type="button" class="btn btn-cancel"
- @click="onLockTimeCancel">取消</button>
- <button type="button" class="btn btn-confirm"
- @click="onLockTimeConfirm">确认</button>
- </div>
- </div>
- </div>
- </div>
- </transition>
- </div>
- </template>
- <script>
- import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
- import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
- import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
- import DropdownSelect from '@/components/ui/DropdownSelect.vue';
- import PlanDonutChart from '@/components/ui/PlanDonutChart.vue';
- import { apiGetCrossingDetailData } from '@/api';
- export default {
- name: 'CrossingPanel',
- components: {
- SignalTimingChart,
- IntersectionMapVideos,
- SegmentedRadio,
- DropdownSelect,
- PlanDonutChart
- },
- props: {
- preloadedData: { type: Object, default: null },
- iconMode: { type: String, default: 'simple' } // 'default' | 'simple'
- },
- data() {
- return {
- startDate: '2026-04-13',
- startTime: '14:03:06',
- endDate: '2026-04-13',
- endTime: '15:03:06',
- duration: null,
- period: null,
- // 核心状态控制
- isManualMode: false, // 是否处于手动控制模式
- showLockTime: false, // 是否显示锁定时间弹窗
- lockTimeType: 'continuous', // 锁定时间类型
- dataReady: false,
- followPhase: false,
- intersectionData: {},
- currentRoute: {},
- cycleLength: 140,
- currentSec: 0,
- phaseDiff: 0,
- coordTime: 0,
- mockPhaseData: [],
- panelScale: 1,
- // 控制方式数据
- controlMethodOptions: [],
- currentMethod: 'temp',
- currentScheme: 'early_peak',
- schemeOptions: [],
- // 实时方案圆饼图数据(从 phaseData 动态生成)
- realtimeDonutData: [],
- // 下周期方案圆饼图数据
- nextCycleDonutData: [],
- // 实时方案剩余时长
- realtimeRemaining: 0,
- // 各阶段基础数据(从 phaseData 解析)
- phaseStages: [],
- currentLocktime: 50,
- locktimeOptions: [],
- currentStage: '1',
- // 补充了 time 属性,用于双向绑定输入框的时间
- currentStageList: []
- }
- },
- computed: {
- // 黄闪、关灯、全红时禁用控制方案
- isSchemeDisabled() {
- return ['yellow_flash', 'lights_off', 'all_red'].includes(this.currentMethod);
- },
- // 定周期、中心计划、感应控制、临时方案可编辑当前阶段
- canEditStage() {
- return ['fixed', 'system', 'sensor', 'temp'].includes(this.currentMethod);
- },
- // 时长选项:30, 60, 90 ... 300
- durationOptions() {
- const list = [];
- for (let i = 30; i <= 300; i += 30) list.push(i);
- return list;
- }
- },
- watch: {
- // 监听控制方式切换
- currentMethod(newVal) {
- // 切换到步进时不自动弹出锁定时间,等用户点击阶段再弹
- this.showLockTime = false;
- if (newVal === 'step') {
- this.syncLocktimeByStage();
- }
- // 模拟需求1:根据不同模式,切换对应的控制方案数据 (Mock 逻辑)
- this.updateSchemeDataByMethod(newVal);
- }
- },
- mounted() {
- this.initScaleObserver();
- if (this.preloadedData) {
- this.applyData(this.preloadedData);
- } else {
- this.loadData();
- }
- },
- beforeDestroy() {
- if (this._ro) this._ro.disconnect();
- },
- methods: {
- // 点击阶段:切换选中,步进模式下同时弹出锁定时间
- onStageClick(value) {
- this.currentStage = value;
- if (this.currentMethod === 'step') {
- this.showLockTime = true;
- this.syncLocktimeByStage();
- }
- },
- // 根据当前选中阶段同步锁定时间选项
- syncLocktimeByStage() {
- const stage = this.currentStageList.find(s => s.value === this.currentStage);
- if (stage && stage.locktimeOptions) {
- this.locktimeOptions = stage.locktimeOptions;
- // 如果当前值不在新选项中,重置为第一个
- const hasValue = this.locktimeOptions.some(o => o.value === this.currentLocktime);
- if (!hasValue) {
- this.currentLocktime = this.locktimeOptions[0]?.value || null;
- }
- }
- },
- stagePercent(time) {
- const total = this.currentStageList.reduce((s, item) => s + (item.time || 0), 0);
- if (!total) return '0%';
- return Math.round(time / total * 100) + '%';
- },
- onScanTick(activeTime) {
- if (!this.mockPhaseData || this.mockPhaseData.length === 0) return;
- // 只看第一轨道(trackIdx=0)的相位
- const phase = this.mockPhaseData.find(p => p[0] === 0 && activeTime >= p[1] && activeTime < p[2]);
- if (!phase) return;
- const type = phase[5]; // green/stripe/yellow/red
- const iconValue = phase[6]; // 如 "STRAIGHT_DOWN,STRAIGHT_UP"
- const direction = phase[7]; // ns/ew
- const phaseName = phase[3]; // P1/P2 等
- const endTime = phase[2];
- const remaining = Math.max(0, Math.round(endTime - activeTime));
- const nsGreen = (type === 'green' && direction === 'ns');
- const ewGreen = (type === 'green' && direction === 'ew');
- // 从图标值解析当前允许的行驶方向类型
- // STRAIGHT→S, TURN_*_LEFT→L, *_UTURN→U
- let activeArrowTypes = [];
- if ((nsGreen || ewGreen) && iconValue) {
- const icons = iconValue.split(',');
- icons.forEach(ic => {
- if (ic.includes('UTURN')) activeArrowTypes.push('U');
- if (ic.includes('TURN') && !ic.includes('UTURN')) activeArrowTypes.push('L');
- if (ic.includes('STRAIGHT')) activeArrowTypes.push('S');
- });
- // 去重
- activeArrowTypes = [...new Set(activeArrowTypes)];
- }
- // 人行道全红判断:只有 P1/P3 绿灯期间人行道才有绿灯,其余时段全红
- const pedAllRed = !(type === 'green' && (phaseName === 'P1' || phaseName === 'P3'));
- this.$set(this.intersectionData, 'signals', {
- pedAllRed,
- ns: {
- phaseName: nsGreen ? ({ P1: '南北直行', P2: '南北左转' }[phaseName] || '南北') : (this.intersectionData.signals?.ns?.phaseName || '南北'),
- time: remaining,
- isGreen: nsGreen,
- activeArrowTypes: nsGreen ? activeArrowTypes : []
- },
- ew: {
- phaseName: ewGreen ? ({ P3: '东西直行', P4: '东西左转' }[phaseName] || '东西') : (this.intersectionData.signals?.ew?.phaseName || '东西'),
- time: remaining,
- isGreen: ewGreen,
- activeArrowTypes: ewGreen ? activeArrowTypes : []
- }
- });
- // 更新实时方案圆饼图的已走时长
- this.updateRealtimeDonut(activeTime);
- },
- // 从 phaseData 解析4个阶段的绿灯时长,构建圆饼图数据
- buildDonutFromPhaseData() {
- const phaseData = this.mockPhaseData || [];
- const stageColors = ['#3b82f6', '#a855f7', '#14b8a6', '#f59e0b'];
- const stageLabels = ['1-南北直行', '2-南北左转', '3-东西直行', '4-东西左转'];
- // 提取 track 0 的绿灯相位(每阶段的第一个 green)
- const greenPhases = phaseData.filter(p => p[0] === 0 && p[5] === 'green');
- const stages = greenPhases.slice(0, 4).map((p, i) => {
- const total = p[8] || Math.floor(this.cycleLength / 4);
- return {
- label: stageLabels[i] || `阶段${i + 1}`,
- value: total, // 阶段总时长
- start: p[1], // 阶段开始时间(从绿灯起始)
- end: p[1] + total, // 阶段结束时间
- color: stageColors[i]
- };
- });
- this.phaseStages = stages;
- // 实时方案:已走时长 + 4个阶段
- this.realtimeDonutData = [
- { label: '已走时长', value: 0, color: '#8892a0' },
- ...stages.map(s => ({ label: s.label, value: s.value, color: s.color }))
- ];
- this.realtimeRemaining = this.cycleLength;
- // 下周期方案:4个阶段,初始数据与实时方案相同
- this.nextCycleDonutData = stages.map(s => ({
- label: s.label,
- value: s.value,
- color: s.color
- }));
- },
- // 根据扫描线时间更新实时方案圆饼图
- updateRealtimeDonut(activeTime) {
- if (this.phaseStages.length === 0) return;
- const elapsed = Math.round(activeTime);
- const remaining = Math.max(0, this.cycleLength - elapsed);
- this.realtimeRemaining = remaining;
- // 计算各阶段的剩余时长
- const stageValues = this.phaseStages.map(s => {
- if (activeTime >= s.end) return 0; // 阶段已完成
- if (activeTime < s.start) return s.value; // 阶段未开始
- return Math.max(0, Math.round(s.end - activeTime)); // 阶段进行中
- });
- this.realtimeDonutData = [
- { label: '已走时长', value: elapsed, color: '#8892a0' },
- ...this.phaseStages.map((s, i) => ({
- label: s.label,
- value: stageValues[i],
- color: s.color
- }))
- ];
- },
- initScaleObserver() {
- const ro = new ResizeObserver(entries => {
- const { width } = entries[0].contentRect;
- const s = Math.min(width / 1315, 1);
- this.$el.style.setProperty('--s', s);
- this.panelScale = s;
- });
- ro.observe(this.$el);
- this._ro = ro;
- },
- async loadData() {
- const nodeId = this.$attrs.id || this.id;
- const data = await apiGetCrossingDetailData(nodeId, { iconMode: this.iconMode });
- if (data) {
- this.applyData(data);
- }
- },
- applyData(data) {
- this.currentRoute = data.currentRoute || {};
- this.intersectionData = data.intersectionData || {};
- this.mockPhaseData = data.phaseData || [];
- this.cycleLength = data.cycleLength || 140;
- this.currentSec = data.currentTime || 0;
- this.phaseDiff = data.phaseDiff || 0;
- this.coordTime = data.coordTime || 0;
- this.currentStageList = data.stageList || [];
- this.buildDonutFromPhaseData();
- this.$nextTick(() => {
- this.dataReady = true;
- });
- this.schemeOptions = data.schemeOptions || [];
- if (data.currentScheme) this.currentScheme = data.currentScheme;
- if (data.controlMethodOptions) this.controlMethodOptions = data.controlMethodOptions;
- if (data.currentMethod) this.currentMethod = data.currentMethod;
- if (data.locktimeOptions) this.locktimeOptions = data.locktimeOptions;
- if (data.startDate) this.startDate = data.startDate;
- if (data.startTime) this.startTime = data.startTime;
- if (data.endDate) this.endDate = data.endDate;
- if (data.endTime) this.endTime = data.endTime;
- if (data.duration) this.duration = data.duration;
- if (data.period) this.period = data.period;
- },
- // 切换手动控制模式
- toggleManualMode() {
- this.isManualMode = !this.isManualMode;
- if (!this.isManualMode) {
- // 如果退出手动模式,可选择重置表单状态
- this.showLockTime = false;
- }
- },
- // 模拟:根据控制方式改变下拉方案的数据
- updateSchemeDataByMethod(method) {
- if (method === 'system') {
- this.schemeOptions = [
- { label: '系统优化方案A', value: 'sys_a' },
- { label: '系统优化方案B', value: 'sys_b' }
- ];
- this.currentScheme = 'sys_a';
- } else {
- this.schemeOptions = [
- { label: '早高峰', value: 'early_peak' },
- { label: '晚高峰', value: 'evening_peak' },
- { label: '平峰', value: 'normal' }
- ];
- this.currentScheme = 'early_peak';
- }
- },
- // 取消按钮(退出手动控制)
- onCancel() {
- this.isManualMode = false;
- this.showLockTime = false;
- },
- // 步进锁定时间弹窗:取消(不退出手动控制)
- onLockTimeCancel() {
- this.showLockTime = false;
- },
- // 步进锁定时间弹窗:确认(不退出手动控制)
- onLockTimeConfirm() {
- this.showLockTime = false;
- const submitData = {
- method: 'step',
- stage: this.currentStage,
- lockTimeType: this.lockTimeType,
- locktime: this.lockTimeType === 'timer' ? this.currentLocktime : null,
- };
- console.log('步进指令:', submitData);
- this.$emit('confirm', submitData);
- },
- // 需求5:点击确认按钮提交 + 表单验证
- onConfirm() {
- // 验证1:临时方案必须检查时间是否有效
- if (this.currentMethod === 'temp') {
- const isInvalid = this.currentStageList.some(item => !item.time || item.time <= 0);
- if (isInvalid) {
- alert('请输入有效的阶段时间 (必须大于0)!');
- return;
- }
- }
- // 验证2:步进方案必须选择锁定类型
- if (this.currentMethod === 'step') {
- if (this.lockTimeType === 'timer' && !this.currentLocktime) {
- alert('请选择解锁时间!');
- return;
- }
- }
- // 构造提交参数
- const submitData = {
- method: this.currentMethod,
- scheme: this.currentScheme,
- stages: this.currentMethod === 'temp' ? this.currentStageList : null,
- lockConfig: this.currentMethod === 'step' ? {
- stage: this.currentStage,
- type: this.lockTimeType,
- time: this.lockTimeType === 'timer' ? this.currentLocktime : null
- } : null
- };
- console.log('提交的数据:', submitData);
- // 提交完成后可根据业务决定是否退出手动模式
- // this.isManualMode = false;
- }
- }
- }
- </script>
- <style scoped>
- .crossing-detail-panel {
- --s: 1;
- position: relative;
- display: flex;
- flex-direction: row;
- gap: clamp(4px, calc(var(--s) * 12px), 12px);
- height: 100%;
- min-height: 0;
- overflow: hidden;
- }
- /* ===== 左侧:还原原始固定 55% 占比 ===== */
- .detail-panel-left {
- display: flex;
- flex-direction: column;
- flex: 0 0 55%;
- min-height: 0;
- min-width: 0;
- }
- /* ===== 右侧:flex 列容器 + 滚动兜底 ===== */
- .detail-panel-right {
- flex: 1;
- min-width: 0;
- min-height: 0;
- overflow-y: auto;
- overflow-x: hidden;
- display: flex;
- flex-direction: column;
- }
- .intersection-video-wrap {
- width: 100%;
- min-height: 0;
- flex: 2;
- }
- .signal-timing-wrap {
- flex: 0 0 auto;
- min-height: 0;
- height: clamp(95px, calc(var(--s) * 166px), 166px);
- width: 100%;
- min-width: 0;
- background-color: transparent;
- box-sizing: border-box;
- position: relative;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- padding: clamp(3px, calc(var(--s) * 10px), 10px);
- }
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: clamp(2px, calc(var(--s) * 6px), 15px);
- color: #e0e6f1;
- flex-shrink: 0;
- }
- .title-area {
- font-size: clamp(9px, calc(var(--s) * 16px), 16px);
- }
- .main-title {
- font-size: clamp(10px, calc(var(--s) * 18px), 18px);
- font-weight: bold;
- margin-right: clamp(4px, calc(var(--s) * 10px), 10px);
- }
- .sub-info {
- font-size: clamp(8px, calc(var(--s) * 12px), 12px);
- opacity: 0.8;
- }
- .checkbox-area {
- font-size: clamp(8px, calc(var(--s) * 12px), 12px);
- display: flex;
- align-items: center;
- cursor: pointer;
- opacity: 0.7;
- user-select: none;
- }
- .checkbox-area:hover {
- opacity: 1;
- }
- .checkbox-mock {
- width: clamp(10px, calc(var(--s) * 14px), 14px);
- height: clamp(10px, calc(var(--s) * 14px), 14px);
- border: 1px solid rgba(255, 255, 255, 0.5);
- margin-right: clamp(3px, calc(var(--s) * 6px), 6px);
- border-radius: 2px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .checkbox-mock.is-checked {
- background-color: #4da8ff;
- border-color: #4da8ff;
- }
- .chart-container {
- width: 100%;
- min-width: 0;
- flex: 1;
- min-height: 50px;
- overflow: hidden;
- }
- .loading-overlay {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #758599;
- font-size: 14px;
- }
- /* ===== 右侧表单内层容器 ===== */
- .detail-right-form {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- }
- .form-group {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- }
- /** 控制方法 */
- .control-method {
- color: #ffffff;
- flex-shrink: 0;
- }
- .control-label-wrap {
- display: flex;
- align-items: center;
- margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
- column-gap: clamp(4px, calc(var(--s) * 10px), 20px);
- margin-top: clamp(4px, calc(var(--s) * 20px), 20px);
- }
- .control-label {
- font-size: clamp(12px, calc(var(--s) * 22px), 28px);
- color: #ffffff;
- white-space: nowrap;
- }
- .control-label-wrap span {
- display: inline-block;
- }
- .operation-btn {
- font-size: clamp(10px, calc(var(--s) * 16px), 16px);
- cursor: pointer;
- user-select: none;
- }
- .operation-btn:hover {
- text-decoration: underline;
- }
- .operation-btn.is-active {
- text-decoration: underline;
- }
- .control-method .control-label-wrap {
- justify-content: space-between;
- }
- /* 控制方式按钮组:设置 font-size 供 SegmentedRadio size="auto" 继承 */
- .control-method-content {
- font-size: clamp(10px, calc(var(--s) * 15px), 15px);
- }
- .control-scheme {
- margin-top: clamp(4px, calc(var(--s) * 10px), 20px);
- /* 设置 font-size 供 DropdownSelect size="auto" 继承 */
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- }
- .control-scheme.is-disabled {
- opacity: 0.4;
- pointer-events: none;
- }
- .lock-time {
- width: 80%;
- border-radius: 8px;
- box-shadow:
- inset 0px 0px 10px 0px rgba(88, 146, 255, 0.4),
- inset 20px 0px 30px -10px rgba(88, 146, 255, 0.15);
- }
- /* 步进锁定时间弹窗 - 遮罩 */
- .lock-time-overlay {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(0, 0, 0, 0.45);
- z-index: 200;
- border-radius: inherit;
- }
- /* 步进锁定时间弹窗 */
- .lock-time-dialog {
- background: linear-gradient(135deg, rgba(10, 25, 60, 0.97) 0%, rgba(20, 40, 90, 0.97) 100%);
- border: 1px solid rgba(161, 190, 255, 0.3);
- border-radius: 6px;
- min-width: 220px;
- max-width: 320px;
- width: 70%;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
- }
- .lock-time-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: clamp(4px, calc(var(--s) * 8px), 10px);
- border-radius: 6px 6px 0 0;
- color: #ffffff;
- background: linear-gradient(180deg,
- rgba(65, 115, 205, 0.6) 0%,
- rgba(40, 70, 130, 0.1) 100%);
- backdrop-filter: blur(10px);
- }
- .lock-time-title {
- font-size: clamp(10px, calc(var(--s) * 16px), 16px);
- color: #ffffff;
- }
- .lock-time-close {
- cursor: pointer;
- color: #ffffff;
- }
- .lock-time-body {
- padding: 0;
- }
- .lock-time-options {
- display: flex;
- flex-direction: column;
- row-gap: clamp(4px, calc(var(--s) * 10px), 10px);
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- padding: clamp(4px, calc(var(--s) * 10px), 10px);
- color: #ffffff;
- }
- .lock-time-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding: 8px 12px;
- border-top: 1px solid rgba(161, 190, 255, 0.15);
- }
- .lock-time-option {
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- }
- /* 过渡动画 */
- .fade-enter-active,
- .fade-leave-active {
- transition: opacity 0.25s;
- }
- .fade-enter,
- .fade-leave-to {
- opacity: 0;
- }
- .current-stage {
- background-color: rgba(65, 115, 205, 0.2);
- border: 1px solid #3660a5;
- margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
- display: flex;
- justify-content: center;
- }
- .current-stage-warp {
- width: 100%;
- display: flex;
- align-items: center;
- flex-wrap: nowrap;
- gap: clamp(8px, calc(var(--s) * 18px), 24px);
- padding: clamp(4px, calc(var(--s) * 8px), 16px);
- color: #ffffff;
- }
- .current-stage-label {
- font-size: clamp(10px, calc(var(--s) * 16px), 16px);
- white-space: nowrap;
- flex-shrink: 0;
- }
- .stage-input {
- width: 100%;
- min-width: 0;
- border: 1px solid rgba(161, 190, 255, 0.7);
- background-color: transparent;
- padding: clamp(2px, calc(var(--s) * 5px), 5px);
- color: #ffffff;
- text-align: center;
- }
- .phase-box {
- position: relative;
- width: 100%;
- aspect-ratio: 1 / 1;
- background: #E6F0FF;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.3s ease;
- box-sizing: border-box;
- overflow: hidden;
- }
- .phase-image {
- width: 100%;
- height: 100%;
- object-fit: contain;
- display: block;
- }
- .phase-box::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(30, 106, 255, 0.5);
- opacity: 0;
- transition: opacity 0.3s ease;
- pointer-events: none;
- }
- .phase-box.is-active::after {
- opacity: 1;
- }
- /** 按钮 */
- .btn {
- display: inline-flex;
- justify-content: center;
- align-items: center;
- height: clamp(22px, calc(var(--s) * 36px), 36px);
- padding: 0 clamp(10px, calc(var(--s) * 32px), 32px);
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- border-radius: 4px;
- cursor: pointer;
- user-select: none;
- transition: all 0.2s ease-in-out;
- box-sizing: border-box;
- }
- .btn-cancel {
- background-color: transparent;
- color: #d1d5db;
- border: 1px solid rgba(130, 150, 190, 0.4);
- }
- .btn-cancel:hover {
- color: #ffffff;
- border-color: rgba(130, 150, 190, 0.8);
- background-color: rgba(255, 255, 255, 0.05);
- }
- .btn-cancel:active {
- background-color: rgba(255, 255, 255, 0.1);
- }
- .btn-confirm {
- background-color: #3b74ff;
- color: #ffffff;
- border: 1px solid #3b74ff;
- }
- .btn-confirm:hover {
- background-color: #5a8bff;
- border-color: #5a8bff;
- box-shadow: 0 2px 8px rgba(59, 116, 255, 0.3);
- }
- .btn-confirm:active {
- background-color: #265bed;
- border-color: #265bed;
- box-shadow: none;
- }
- .button-group {
- display: flex;
- justify-content: flex-end;
- margin-top: clamp(4px, calc(var(--s) * 10px), 20px);
- flex-shrink: 0;
- }
- .button-group>div {
- display: flex;
- gap: clamp(4px, calc(var(--s) * 8px), 12px);
- }
- /* 禁用状态 */
- .form-interactive-area {
- transition: opacity 0.3s;
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- overflow-x: hidden;
- }
- .form-editable-area.is-disabled {
- opacity: 0.6;
- pointer-events: none;
- }
- .DropdownSelect-is-disabled{
- opacity: 0.6;
- }
- /* 当前阶段输入框微调 */
- .stage-item-wrapper {
- flex: 1 1 0;
- min-width: 0;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- gap: clamp(2px, calc(var(--s) * 4px), 6px);
- position: relative;
- }
- .bottom-controls {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: clamp(4px, calc(var(--s) * 6px), 8px);
- /* 输入框和百分比的间距 */
- }
- /* 新增包裹层的相对定位 */
- .input-unit-wrapper {
- position: relative;
- display: inline-block;
- }
- .stage-input {
- width: 100%;
- min-width: 0;
- border: 1px solid rgba(161, 190, 255, 0.7);
- background-color: transparent;
- padding: clamp(2px, calc(var(--s) * 5px), 5px);
- /* 给右侧留出空间,防止数字过长被 s 挡住 */
- padding-right: clamp(10px, calc(var(--s) * 16px), 16px);
- color: #ffffff;
- text-align: center;
- border-radius: 4px;
- font-size: clamp(9px, calc(var(--s) * 13px), 13px);
- }
- /* 修改 s 单位的定位方式为垂直居中 */
- .input-unit-wrapper .unit {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- right: clamp(3px, calc(var(--s) * 6px), 8px);
- color: #77A1FF;
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- pointer-events: none;
- }
- /* 微调百分比的间距,让排版更紧凑 */
- .stage-item-wrapper .percent {
- color: rgba(255, 255, 255, 0.5);
- font-size: clamp(9px, calc(var(--s) * 13px), 13px);
- white-space: nowrap;
- }
- .stage-input {
- width: 100%;
- min-width: 0;
- border: 1px solid rgba(161, 190, 255, 0.7);
- background-color: transparent;
- padding: clamp(2px, calc(var(--s) * 5px), 5px);
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- color: #ffffff;
- text-align: center;
- border-radius: 4px;
- -moz-appearance: textfield;
- }
- .stage-input::-webkit-outer-spin-button,
- .stage-input::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
- .stage-input:disabled {
- border-color: rgba(255, 255, 255, 0.2);
- color: rgba(255, 255, 255, 0.5);
- background-color: rgba(0, 0, 0, 0.2);
- }
- /* 弹窗过渡动画 */
- /* lock-time 弹窗补充样式 */
- .lock-time {
- margin-top: clamp(4px, calc(var(--s) * 10px), 15px);
- background-color: rgba(20, 30, 50, 0.9);
- }
- /* 单选框基础对齐 */
- .lock-time-option label {
- display: flex;
- align-items: center;
- gap: clamp(3px, calc(var(--s) * 6px), 8px);
- cursor: pointer;
- }
- /* ===== 方案圆饼图左右布局 ===== */
- .donut-row {
- display: flex;
- gap: clamp(4px, calc(var(--s) * 16px), 16px);
- width: 100%;
- margin-top: clamp(4px, calc(var(--s) * 20px), 20px);
- }
- .donut-item {
- flex: 1;
- min-width: 0;
- }
- .donut-title {
- font-size: clamp(12px, calc(var(--s) * 22px), 28px);
- color: #ffffff;
- margin-bottom: 4px;
- }
- /* ===== 时间表单栏布局 ===== */
- .time-form-bar {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: clamp(3px, calc(var(--s) * 5px), 6px);
- margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
- }
- /* 第二行:带标签的项各占 2 列(共 4 列,两项铺满一行) */
- .time-form-bar .form-item-labeled {
- grid-column: span 2;
- display: flex;
- align-items: center;
- gap: clamp(3px, calc(var(--s) * 6px), 8px);
- }
- .time-form-bar .form-label {
- white-space: nowrap;
- flex-shrink: 0;
- color: rgba(200, 220, 255, 0.65);
- font-size: clamp(9px, calc(var(--s) * 12px), 13px);
- }
- .time-form-bar .form-item-labeled .el-select {
- flex: 1;
- min-width: 0;
- }
- /* ===== ElementUI 深色主题适配 ===== */
- /* 覆盖 el-date-editor 固定宽度,让其跟随 grid 布局 */
- .time-form-bar>>>.el-date-editor.el-input,
- .time-form-bar>>>.el-date-editor.el-input__inner,
- .time-form-bar>>>.el-select {
- width: 100%;
- }
- /* 输入框样式 */
- .time-form-bar>>>.el-input__inner {
- background-color: rgba(255, 255, 255, 0.06);
- border: 1px solid rgba(161, 190, 255, 0.35);
- color: #e0e6f1;
- font-size: clamp(9px, calc(var(--s) * 11px), 11px);
- height: clamp(22px, calc(var(--s) * 28px), 28px);
- line-height: clamp(22px, calc(var(--s) * 28px), 28px);
- padding-left: clamp(22px, calc(var(--s) * 26px), 26px);
- padding-right: clamp(4px, calc(var(--s) * 6px), 6px);
- }
- .time-form-bar>>>.el-input__inner::placeholder {
- color: rgba(255, 255, 255, 0.3);
- }
- .time-form-bar>>>.el-input__inner:hover {
- border-color: rgba(161, 190, 255, 0.6);
- }
- .time-form-bar>>>.el-input__inner:focus {
- border-color: #3b74ff;
- }
- /* 图标颜色、尺寸跟随缩放 */
- .time-form-bar>>>.el-input__prefix,
- .time-form-bar>>>.el-input__suffix {
- color: rgba(255, 255, 255, 0.4);
- }
- .time-form-bar>>>.el-input__icon {
- font-size: clamp(9px, calc(var(--s) * 12px), 12px);
- line-height: clamp(22px, calc(var(--s) * 28px), 28px);
- width: clamp(18px, calc(var(--s) * 22px), 22px);
- }
- </style>
|