| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- <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 class="checkbox-area">
- <div class="checkbox-mock" :class="{ 'is-checked': followPhase }"
- @click="followPhase = !followPhase">
- <span v-if="followPhase" style="color: #fff; font-size: 12px; margin-left: 1px;">✓</span>
- </div>
- <span>跟随相位</span>
- </div>
- </div>
- <SignalTimingChart :cycleLength="cycleLength" :currentTime="currentSec" :phaseData="mockPhaseData" />
- </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" :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" />
- </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="currentStage = item.value"
- >
- <img :src="item.img" alt="stage" class="phase-image" />
- </div>
-
- <input
- type="number"
- v-model.number="item.time"
- class="stage-input"
- :disabled="currentMethod !== 'temp'"
- :title="currentMethod !== 'temp' ? '仅临时方案可修改' : '修改阶段时间'"
- />
- <span class="unit">s</span>
- </div>
- </div>
- </div>
- <transition name="fade">
- <div class="lock-time" v-if="showLockTime">
- <div class="lock-time-label-wrap glow-header">
- <div class="lock-time-label">锁定时间</div>
- <div class="lock-time-close" @click="showLockTime = false">✕</div>
- </div>
- <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>
- </transition>
- </div>
- </div>
- <div class="button-group" v-show="isManualMode">
- <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>
- </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 { apiGetCrossingDetailData } from '@/api';
- export default {
- name: 'CrossingPanel',
- components: {
- SignalTimingChart,
- IntersectionMapVideos,
- SegmentedRadio,
- DropdownSelect
- },
- data() {
- return {
- // 核心状态控制
- isManualMode: false, // 是否处于手动控制模式
- showLockTime: false, // 是否显示锁定时间弹窗
- lockTimeType: 'continuous', // 锁定时间类型
- followPhase: false,
- intersectionData: {},
- currentRoute: {},
- cycleLength: 140,
- currentSec: 15,
- phaseDiff: 0,
- coordTime: 0,
- mockPhaseData: [],
-
- // 控制方式数据
- controlMethodOptions: [],
- currentMethod: 'temp',
- currentScheme: 'early_peak',
- schemeOptions: [],
- currentLocktime: 50,
- locktimeOptions: [],
- currentStage: '1',
- // 补充了 time 属性,用于双向绑定输入框的时间
- currentStageList: []
- }
- },
- watch: {
- // 监听控制方式切换
- currentMethod(newVal) {
- // 需求4:切换步进方案策略时,出现锁定时间弹窗
- if (newVal === 'step') {
- this.showLockTime = true;
- } else {
- this.showLockTime = false;
- }
- // 模拟需求1:根据不同模式,切换对应的控制方案数据 (Mock 逻辑)
- this.updateSchemeDataByMethod(newVal);
- }
- },
- mounted() {
- this.initScaleObserver();
- this.loadData();
- },
- beforeDestroy() {
- if (this._ro) this._ro.disconnect();
- },
- methods: {
- initScaleObserver() {
- const ro = new ResizeObserver(entries => {
- const { width } = entries[0].contentRect;
- const s = Math.min(width / 1315, 1);
- this.$el.style.setProperty('--s', s);
- });
- ro.observe(this.$el);
- this._ro = ro;
- },
- async loadData() {
- const nodeId = this.$attrs.id || this.id;
- const data = await apiGetCrossingDetailData(nodeId);
- if (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.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;
- }
- },
- // 切换手动控制模式
- 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;
- // 可以在此添加回滚初始数据的逻辑
- },
- // 需求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' ? {
- 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;
- 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: 1;
- min-height: 0;
- height: 120px;
- flex-shrink: 0;
- 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);
- }
- .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(9px, calc(var(--s) * 14px), 14px);
- 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(9px, calc(var(--s) * 14px), 14px);
- }
- .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);
- }
- .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-label-wrap {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: clamp(4px, calc(var(--s) * 8px), 10px);
- border-radius: 8px 8px 0 0;
- color: #ffffff;
- }
- .lock-time-label {
- font-size: clamp(10px, calc(var(--s) * 16px), 16px);
- color: #ffffff;
- }
- .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-option {
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- }
- .lock-time-close {
- cursor: pointer;
- }
- .glow-header {
- background: linear-gradient(180deg,
- rgba(65, 115, 205, 0.6) 0%,
- rgba(40, 70, 130, 0.1) 100%);
- backdrop-filter: blur(10px);
- }
- .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 {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-wrap: wrap;
- gap: clamp(4px, calc(var(--s) * 8px), 10px);
- padding: clamp(6px, calc(var(--s) * 12px), 32px);
- color: #ffffff;
- }
- .current-stage-label {
- font-size: clamp(9px, calc(var(--s) * 14px), 14px);
- width: auto;
- white-space: nowrap;
- }
- .stage-input {
- width: clamp(32px, calc(var(--s) * 65px), 65px);
- 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: clamp(30px, calc(var(--s) * 65px), 65px);
- height: clamp(30px, calc(var(--s) * 65px), 65px);
- 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-interactive-area.is-disabled {
- opacity: 0.6;
- pointer-events: none;
- }
- /* 当前阶段输入框微调 */
- .stage-item-wrapper {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: clamp(2px, calc(var(--s) * 4px), 6px);
- position: relative;
- }
- .stage-input {
- width: clamp(32px, calc(var(--s) * 65px), 65px);
- 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;
- border-radius: 4px;
- }
- .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);
- }
- .stage-item-wrapper .unit {
- position: absolute;
- bottom: clamp(2px, calc(var(--s) * 6px), 6px);
- right: clamp(3px, calc(var(--s) * 6px), 8px);
- color: #77A1FF;
- font-size: clamp(8px, calc(var(--s) * 12px), 12px);
- pointer-events: none;
- }
- /* 弹窗过渡动画 */
- .fade-enter-active, .fade-leave-active {
- transition: opacity 0.3s, transform 0.3s;
- }
- .fade-enter, .fade-leave-to {
- opacity: 0;
- transform: translateY(-10px);
- }
- /* 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;
- }
- </style>
|