|
|
@@ -0,0 +1,377 @@
|
|
|
+<template>
|
|
|
+ <div class="scheme-stage-edit">
|
|
|
+ <!-- ① 时间区段(仅"修改临时配时方案"显示) -->
|
|
|
+ <div v-if="showTimeRange" class="time-range-bar">
|
|
|
+ <el-date-picker class="ssed-field"
|
|
|
+ v-model="form.timeRange.startDate" type="date" size="small" placeholder="选择日期"
|
|
|
+ value-format="yyyy-MM-dd" :clearable="false" :append-to-body="true" />
|
|
|
+ <el-time-picker class="ssed-field"
|
|
|
+ v-model="form.timeRange.startTime" size="small" placeholder="选择时间"
|
|
|
+ value-format="HH:mm:ss" :clearable="false" :append-to-body="true" />
|
|
|
+ <el-date-picker class="ssed-field"
|
|
|
+ v-model="form.timeRange.endDate" type="date" size="small" placeholder="选择日期"
|
|
|
+ value-format="yyyy-MM-dd" :clearable="false" :append-to-body="true" />
|
|
|
+ <el-time-picker class="ssed-field"
|
|
|
+ v-model="form.timeRange.endTime" size="small" placeholder="选择时间"
|
|
|
+ value-format="HH:mm:ss" :clearable="false" :append-to-body="true" />
|
|
|
+ <el-select class="ssed-field"
|
|
|
+ v-model="form.timeRange.duration" size="small" placeholder="请选择时长"
|
|
|
+ :popper-append-to-body="true">
|
|
|
+ <el-option v-for="d in durationOptions" :key="d" :label="d + '分钟'" :value="d" />
|
|
|
+ </el-select>
|
|
|
+ <el-select class="ssed-field"
|
|
|
+ v-model="form.timeRange.period" size="small" placeholder="请选择周期"
|
|
|
+ :popper-append-to-body="true">
|
|
|
+ <el-option v-for="p in 8" :key="p" :label="'周期' + p" :value="p" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ② 周期信息 -->
|
|
|
+ <div class="cycle-bar">
|
|
|
+ <el-checkbox v-model="form.isFixedCycle" class="ssed-checkbox">固定周期</el-checkbox>
|
|
|
+ <span class="cycle-total-wrap">
|
|
|
+ <span class="cycle-total-label">周期总时长(s):</span>
|
|
|
+ <template v-if="!cycleEditing">
|
|
|
+ <strong class="cycle-total-value">{{ cycleTotal }}</strong>
|
|
|
+ <a class="cycle-edit-link" @click="startCycleEdit">修改</a>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <el-input-number v-model="cycleEditValue" size="mini"
|
|
|
+ :min="cycleEditMin" :max="cycleEditMax" :step="1" controls-position="right" />
|
|
|
+ <el-button size="mini" type="primary" plain @click="applyCycleEdit">应用</el-button>
|
|
|
+ <el-button size="mini" plain @click="cancelCycleEdit">取消</el-button>
|
|
|
+ </template>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ③ 阶段表(vuedraggable 排序 + el-input-number 编辑锁定时长) -->
|
|
|
+ <div class="stage-table-wrap">
|
|
|
+ <table class="stage-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th class="col-idx">阶段号</th>
|
|
|
+ <th class="col-name">阶段名称</th>
|
|
|
+ <th class="col-ratio">绿信比</th>
|
|
|
+ <th class="col-time">锁定时长</th>
|
|
|
+ <th class="col-handle">移动</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <draggable v-model="form.stages" tag="tbody" handle=".drag-handle" :animation="200">
|
|
|
+ <tr v-for="(row, idx) in stagesWithRatio" :key="row.value">
|
|
|
+ <td class="col-idx">{{ idx + 1 }}</td>
|
|
|
+ <td class="col-name">{{ row.phaseName || `P${idx + 1}` }}</td>
|
|
|
+ <td class="col-ratio">{{ row.ratio }}</td>
|
|
|
+ <td class="col-time">
|
|
|
+ <el-input-number
|
|
|
+ v-model="form.stages[idx].time"
|
|
|
+ size="mini" :min="1" :max="180" :step="1"
|
|
|
+ controls-position="right"
|
|
|
+ @change="(cur, old) => onStageTimeChange(idx, cur, old)" />
|
|
|
+ </td>
|
|
|
+ <td class="col-handle"><i class="drag-handle">☰</i></td>
|
|
|
+ </tr>
|
|
|
+ </draggable>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ④ 底部按钮 -->
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button size="small" plain @click="handleCancel">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="handleSave">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import draggable from 'vuedraggable';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'SchemeStageEditDialog',
|
|
|
+ components: { draggable },
|
|
|
+ inject: {
|
|
|
+ // DashboardLayout 顶层 provide,组件内通过它做自关闭
|
|
|
+ dialogManager: { default: null },
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ // 自关闭用:与 dialogManager.openDialog 的 id 同值(通过 data 透传过来)
|
|
|
+ dialogId: { type: String, required: true },
|
|
|
+ // 是否展示顶部时间区段(true=修改临时配时方案 / false=修改配时方案)
|
|
|
+ showTimeRange: { type: Boolean, default: false },
|
|
|
+ // 表单数据
|
|
|
+ payload: {
|
|
|
+ type: Object,
|
|
|
+ required: true,
|
|
|
+ // { stages: [{ value, phaseName, time, img, direction, locktimeOptions }],
|
|
|
+ // timeRange: { startDate, startTime, endDate, endTime, duration, period },
|
|
|
+ // isFixedCycle: Boolean }
|
|
|
+ },
|
|
|
+ // 保存/取消回调(外部通过 data 传入)
|
|
|
+ onSave: { type: Function, default: null },
|
|
|
+ onCancel: { type: Function, default: null },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ form: null,
|
|
|
+ cycleEditing: false,
|
|
|
+ cycleEditValue: 0,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ cycleTotal() {
|
|
|
+ if (!this.form || !this.form.stages) return 0;
|
|
|
+ return this.form.stages.reduce((a, b) => a + (Number(b.time) || 0), 0);
|
|
|
+ },
|
|
|
+ stagesWithRatio() {
|
|
|
+ if (!this.form || !this.form.stages) return [];
|
|
|
+ const total = this.cycleTotal;
|
|
|
+ return this.form.stages.map((s, i) => ({
|
|
|
+ ...s,
|
|
|
+ idx: i + 1,
|
|
|
+ ratio: total > 0 ? Math.round((s.time / total) * 100) + '%' : '0%',
|
|
|
+ }));
|
|
|
+ },
|
|
|
+ durationOptions() {
|
|
|
+ const list = [];
|
|
|
+ for (let i = 30; i <= 300; i += 30) list.push(i);
|
|
|
+ return list;
|
|
|
+ },
|
|
|
+ cycleEditMin() {
|
|
|
+ // 最小 = 阶段数(每段保留至少 1s)
|
|
|
+ return Math.max(1, (this.form && this.form.stages ? this.form.stages.length : 1));
|
|
|
+ },
|
|
|
+ cycleEditMax() { return 999; },
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ // 深拷贝 payload,避免污染外部数据
|
|
|
+ this.form = JSON.parse(JSON.stringify(this.payload || {}));
|
|
|
+ if (!this.form.stages) this.form.stages = [];
|
|
|
+ if (this.showTimeRange && !this.form.timeRange) this.form.timeRange = {};
|
|
|
+ if (this.form.isFixedCycle === undefined) this.form.isFixedCycle = false;
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 自关闭:调 dialogManager.closeDialog 把自己从 activeDialogs 移除
|
|
|
+ // (DashboardLayout provide 的方法名是 closeDialog,对应 mixin 内部的 handleDialogClose)
|
|
|
+ doClose() {
|
|
|
+ if (this.dialogManager && typeof this.dialogManager.closeDialog === 'function') {
|
|
|
+ this.dialogManager.closeDialog(this.dialogId);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleSave() {
|
|
|
+ // 锁定时长合法性兜底:保 sum > 0
|
|
|
+ const total = this.cycleTotal;
|
|
|
+ if (total <= 0) return;
|
|
|
+ if (typeof this.onSave === 'function') {
|
|
|
+ // 把 form 深拷贝再回传,保险隔离
|
|
|
+ this.onSave(JSON.parse(JSON.stringify(this.form)));
|
|
|
+ }
|
|
|
+ this.doClose();
|
|
|
+ },
|
|
|
+ handleCancel() {
|
|
|
+ if (typeof this.onCancel === 'function') this.onCancel();
|
|
|
+ this.doClose();
|
|
|
+ },
|
|
|
+
|
|
|
+ startCycleEdit() {
|
|
|
+ this.cycleEditValue = this.cycleTotal;
|
|
|
+ this.cycleEditing = true;
|
|
|
+ },
|
|
|
+ cancelCycleEdit() {
|
|
|
+ this.cycleEditing = false;
|
|
|
+ },
|
|
|
+ applyCycleEdit() {
|
|
|
+ const newTotal = Number(this.cycleEditValue) || 0;
|
|
|
+ const oldTotal = this.cycleTotal;
|
|
|
+ if (newTotal <= 0 || oldTotal <= 0 || newTotal === oldTotal) {
|
|
|
+ this.cycleEditing = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const ratio = newTotal / oldTotal;
|
|
|
+ // 各阶段按比例缩放
|
|
|
+ this.form.stages.forEach((s, i) => {
|
|
|
+ this.$set(this.form.stages, i, { ...s, time: Math.max(1, Math.round(s.time * ratio)) });
|
|
|
+ });
|
|
|
+ // 处理舍入误差:把差值补到第一段
|
|
|
+ const sumNow = this.form.stages.reduce((a, b) => a + b.time, 0);
|
|
|
+ const diff = newTotal - sumNow;
|
|
|
+ if (diff !== 0 && this.form.stages.length > 0) {
|
|
|
+ this.$set(this.form.stages, 0, {
|
|
|
+ ...this.form.stages[0],
|
|
|
+ time: Math.max(1, this.form.stages[0].time + diff),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ this.cycleEditing = false;
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 固定周期模式下编辑某段:把 delta 从其它段按当前时长比例扣减/补偿,保 sum 不变 */
|
|
|
+ onStageTimeChange(idx, current, old) {
|
|
|
+ if (!this.form.isFixedCycle) return;
|
|
|
+ const cur = Number(current) || 0;
|
|
|
+ const prev = Number(old) || 0;
|
|
|
+ const delta = cur - prev;
|
|
|
+ if (delta === 0) return;
|
|
|
+
|
|
|
+ const others = this.form.stages
|
|
|
+ .map((s, i) => ({ s, i }))
|
|
|
+ .filter(o => o.i !== idx);
|
|
|
+ const otherSum = others.reduce((a, b) => a + b.s.time, 0);
|
|
|
+ if (otherSum <= 0) return;
|
|
|
+
|
|
|
+ let remaining = -delta;
|
|
|
+ others.forEach((o, k) => {
|
|
|
+ let share;
|
|
|
+ if (k === others.length - 1) {
|
|
|
+ share = remaining; // 最后一个吸收剩余以严格对账
|
|
|
+ } else {
|
|
|
+ share = Math.round(-delta * o.s.time / otherSum);
|
|
|
+ remaining -= share;
|
|
|
+ }
|
|
|
+ const newTime = Math.max(1, o.s.time + share);
|
|
|
+ this.$set(this.form.stages, o.i, { ...o.s, time: newTime });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.scheme-stage-edit {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: clamp(8px, 1.5cqw, 16px);
|
|
|
+ background: rgba(5, 22, 45, 0.92);
|
|
|
+ color: #e0e6f1;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+ container-type: inline-size;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== ① 时间区段 ===== */
|
|
|
+.time-range-bar {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: clamp(4px, 1cqw, 10px);
|
|
|
+ padding-bottom: clamp(6px, 1cqw, 12px);
|
|
|
+ border-bottom: 1px dashed rgba(127, 182, 255, 0.18);
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.ssed-field {
|
|
|
+ flex: 1 1 130px;
|
|
|
+ min-width: 110px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== ② 周期信息 ===== */
|
|
|
+.cycle-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: clamp(8px, 2cqw, 20px);
|
|
|
+ padding: clamp(6px, 1cqw, 12px) 0;
|
|
|
+ flex-shrink: 0;
|
|
|
+ font-size: clamp(11px, 2.2cqw, 13px);
|
|
|
+}
|
|
|
+.ssed-checkbox >>> .el-checkbox__label {
|
|
|
+ color: #d1d5db;
|
|
|
+ font-size: inherit;
|
|
|
+}
|
|
|
+.cycle-total-wrap {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+.cycle-total-label {
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+.cycle-total-value {
|
|
|
+ color: #f8fafc;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.cycle-edit-link {
|
|
|
+ color: #4da8ff;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 2px;
|
|
|
+}
|
|
|
+.cycle-edit-link:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== ③ 阶段表 ===== */
|
|
|
+.stage-table-wrap {
|
|
|
+ flex: 1 1 0;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: auto;
|
|
|
+ margin-top: clamp(4px, 1cqw, 8px);
|
|
|
+}
|
|
|
+.stage-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
|
+.stage-table-wrap::-webkit-scrollbar-thumb { background: rgba(30, 77, 142, 0.6); border-radius: 3px; }
|
|
|
+.stage-table-wrap::-webkit-scrollbar-track { background: transparent; }
|
|
|
+
|
|
|
+.stage-table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: separate;
|
|
|
+ border-spacing: 0;
|
|
|
+ font-size: clamp(11px, 2.2cqw, 13px);
|
|
|
+}
|
|
|
+.stage-table thead tr {
|
|
|
+ background: linear-gradient(180deg, rgba(58, 127, 209, 0.22) 0%, rgba(58, 127, 209, 0.08) 100%);
|
|
|
+}
|
|
|
+.stage-table th {
|
|
|
+ padding: 6px 8px;
|
|
|
+ text-align: center;
|
|
|
+ color: rgba(127, 182, 255, 0.95);
|
|
|
+ font-weight: 600;
|
|
|
+ border-bottom: 1px solid rgba(127, 182, 255, 0.35);
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.stage-table td {
|
|
|
+ padding: 6px 8px;
|
|
|
+ text-align: center;
|
|
|
+ color: #e0e6f1;
|
|
|
+ border-bottom: 1px dashed rgba(30, 77, 142, 0.45);
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+.stage-table tbody tr:hover {
|
|
|
+ background: rgba(58, 127, 209, 0.10);
|
|
|
+}
|
|
|
+
|
|
|
+.col-idx { width: 12%; }
|
|
|
+.col-name { width: 32%; text-align: left; padding-left: 14px; }
|
|
|
+.col-ratio { width: 16%; }
|
|
|
+.col-time { width: 28%; }
|
|
|
+.col-handle { width: 12%; }
|
|
|
+
|
|
|
+.drag-handle {
|
|
|
+ display: inline-block;
|
|
|
+ cursor: grab;
|
|
|
+ color: rgba(127, 182, 255, 0.7);
|
|
|
+ font-style: normal;
|
|
|
+ font-size: 16px;
|
|
|
+ user-select: none;
|
|
|
+ padding: 0 4px;
|
|
|
+}
|
|
|
+.drag-handle:active { cursor: grabbing; color: #4da8ff; }
|
|
|
+
|
|
|
+/* ===== ④ 底部按钮 ===== */
|
|
|
+.dialog-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ padding-top: clamp(6px, 1cqw, 12px);
|
|
|
+ border-top: 1px solid rgba(127, 182, 255, 0.15);
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== element 控件深色适配(局部覆盖默认浅色主题) ===== */
|
|
|
+.scheme-stage-edit >>> .el-input__inner,
|
|
|
+.scheme-stage-edit >>> .el-input-number .el-input__inner {
|
|
|
+ background: rgba(15, 32, 55, 0.6);
|
|
|
+ border-color: rgba(127, 182, 255, 0.30);
|
|
|
+ color: #e0e6f1;
|
|
|
+}
|
|
|
+.scheme-stage-edit >>> .el-input__inner::placeholder {
|
|
|
+ color: rgba(255, 255, 255, 0.35);
|
|
|
+}
|
|
|
+.scheme-stage-edit >>> .el-input__icon {
|
|
|
+ color: rgba(127, 182, 255, 0.7);
|
|
|
+}
|
|
|
+</style>
|