pPowerManger 燃料电池页操控表单防误操作改造:滑块改为步进数字输入,提交前弹窗确认并高亮危险指令(停机/紧急停机/关机等)
build-test-deploy / ci (push) Successful in 30m21s

This commit is contained in:
zjk
2026-09-02 20:24:02 +08:00
parent 873a1a9b09
commit b33312c547
+308 -65
View File
@@ -278,34 +278,192 @@ body {
margin-bottom: 12px;
}
.slider-container {
/* 数值步进控件(替代滑块,防误拖) */
.stepper-container {
display: flex;
align-items: center;
gap: 8px;
}
.stepper-btn {
width: 40px;
height: 40px;
border: 1px solid #d1d5db;
border-radius: 4px;
background-color: #f9fafb;
color: #1a1a1a;
font-size: 1.25rem;
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.stepper-btn:hover {
background-color: #e5e7eb;
}
.stepper-btn:active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.stepper-input {
width: 90px;
height: 40px;
padding: 0 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
text-align: center;
font-family: 'SF Mono', 'Consolas', monospace;
font-size: 1rem;
font-weight: 600;
}
.stepper-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
}
.stepper-unit {
min-width: 84px;
color: #4b5563;
font-size: 0.9rem;
font-family: 'SF Mono', 'Consolas', monospace;
}
/* 下拉框危险选项标记 */
.select-control option.danger {
color: #dc2626;
font-weight: 700;
}
/* 指令确认弹窗 */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(17, 24, 39, 0.55);
z-index: 100;
align-items: center;
justify-content: center;
padding: 16px;
}
.modal-overlay.visible {
display: flex;
}
.modal {
background: white;
border-radius: 8px;
width: 100%;
max-width: 560px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
.modal-title {
padding: 16px 20px;
font-size: 1.1rem;
font-weight: 600;
border-bottom: 1px solid #e5e7eb;
}
.modal-body {
padding: 16px 20px;
overflow-y: auto;
}
.modal-hint {
font-size: 0.85rem;
color: #6b7280;
margin-bottom: 12px;
}
.modal-summary {
border: 1px solid #e5e7eb;
border-radius: 6px;
overflow: hidden;
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f3f4f6;
font-size: 0.9rem;
}
.summary-row:last-child {
border-bottom: none;
}
.summary-label {
color: #4b5563;
}
.summary-value {
font-weight: 600;
font-family: 'SF Mono', 'Consolas', monospace;
text-align: right;
}
.summary-row.danger {
background-color: #fef2f2;
}
.summary-row.danger .summary-label,
.summary-row.danger .summary-value {
color: #b91c1c;
}
.summary-danger-mark {
margin-right: 6px;
font-weight: 700;
}
.modal-note {
margin-top: 12px;
padding: 10px 12px;
border-radius: 4px;
background-color: #fef2f2;
color: #b91c1c;
font-size: 0.85rem;
font-weight: 600;
display: none;
}
.modal-note.visible {
display: block;
}
.modal-actions {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.slider {
flex-grow: 1;
height: 6px;
-webkit-appearance: none;
background: #d1d5db;
border-radius: 3px;
outline: none;
.modal-actions .button {
flex: 1;
padding: 12px;
font-size: 1rem;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: var(--primary-color);
border-radius: 50%;
cursor: pointer;
.button-confirm {
background-color: var(--primary-color);
color: white;
}
.slider-value {
min-width: 60px;
text-align: right;
font-family: 'SF Mono', 'Consolas', monospace;
.button-confirm:hover {
background-color: var(--primary-hover);
}
.form-submit {
@@ -521,22 +679,24 @@ body {
<option value="00H">无效</option>
<option value="01H">自检</option>
<option value="02H">启动</option>
<option value="03H">停机</option>
<option value="04H">复位</option>
<option value="05H">紧急停机</option>
<option value="06H">补给</option>
<option value="07H">制氢预热</option>
<option value="08H">停机维护</option>
<option value="0AH">开机</option>
<option value="0BH">关机</option>
<option value="03H" class="danger">⚠ 停机(危险)</option>
<option value="05H" class="danger">⚠ 紧急停机(危险)</option>
<option value="08H" class="danger">⚠ 停机维护(危险)</option>
<option value="0BH" class="danger">⚠ 关机(危险)</option>
</select>
</div>
<div class="control-group">
<label class="control-label">输出功率指令 (0~250,分辨率0.1kW)</label>
<div class="slider-container">
<input type="range" class="slider" name="fcPower" min="0" max="250" value="0" step="1">
<span class="slider-value">0 (0.0 kW)</span>
<div class="stepper-container">
<button type="button" class="stepper-btn" data-target="fcPower" data-step="-1">−</button>
<input type="number" class="stepper-input" id="fcPower" name="fcPower" min="0" max="250" step="1" value="0">
<button type="button" class="stepper-btn" data-target="fcPower" data-step="1">+</button>
<span class="stepper-unit" id="fcPowerUnit">0 (0.0 kW)</span>
</div>
</div>
@@ -567,9 +727,11 @@ body {
<div class="control-group">
<label class="control-label">潜深深度 (m)</label>
<div class="slider-container">
<input type="range" class="slider" name="depth" min="0" max="1000" value="0" step="1">
<span class="slider-value">0 m</span>
<div class="stepper-container">
<button type="button" class="stepper-btn" data-target="depth" data-step="-1">−</button>
<input type="number" class="stepper-input" id="depth" name="depth" min="0" max="1000" step="1" value="0">
<button type="button" class="stepper-btn" data-target="depth" data-step="1">+</button>
<span class="stepper-unit" id="depthUnit">0 m</span>
</div>
</div>
@@ -612,9 +774,11 @@ body {
<div class="control-group">
<label class="control-label">动力锂电池功率配置值 (0~400 kW)</label>
<div class="slider-container">
<input type="range" class="slider" name="powerBatPower" min="0" max="400" value="0" step="10">
<span class="slider-value">0 kW</span>
<div class="stepper-container">
<button type="button" class="stepper-btn" data-target="powerBatPower" data-step="-10">−</button>
<input type="number" class="stepper-input" id="powerBatPower" name="powerBatPower" min="0" max="400" step="10" value="0">
<button type="button" class="stepper-btn" data-target="powerBatPower" data-step="10">+</button>
<span class="stepper-unit" id="powerBatPowerUnit">0 kW</span>
</div>
</div>
@@ -622,18 +786,34 @@ body {
<label class="control-label">主机状态</label>
<div class="radio-group">
<label class="radio-option"><input type="radio" name="hostStatus" value="AAH" checked><span>运行</span></label>
<label class="radio-option"><input type="radio" name="hostStatus" value="FFH"><span>关机</span></label>
<label class="radio-option"><input type="radio" name="hostStatus" value="55H"><span>故障</span></label>
<label class="radio-option"><input type="radio" name="hostStatus" value="FFH"><span>⚠ 关机(危险)</span></label>
<label class="radio-option"><input type="radio" name="hostStatus" value="55H"><span>⚠ 故障(危险)</span></label>
</div>
</div>
<button type="submit" class="button form-submit">提交操控指令</button>
<button type="submit" class="button form-submit">预览并提交操控指令</button>
<div id="statusMessage" class="status-message"></div>
</form>
</div>
</div>
</div>
<!-- 指令确认弹窗(防误操作,提交前拦截) -->
<div class="modal-overlay" id="confirmOverlay">
<div class="modal">
<div class="modal-title">⚠ 指令确认</div>
<div class="modal-body">
<div class="modal-hint">请逐项核对下方指令摘要,红色条目为高风险指令。点击“确认执行”后指令将立即下发。</div>
<div class="modal-summary" id="modalSummary"></div>
<div class="modal-note" id="modalDangerNote">本次包含高风险指令(停机/紧急停机/停机维护/关机等),请再次确认!</div>
</div>
<div class="modal-actions">
<button type="button" class="button button-secondary" id="modalCancelBtn">取消</button>
<button type="button" class="button button-confirm" id="modalConfirmBtn">确认执行</button>
</div>
</div>
</div>
<script>
let ws = null;
@@ -793,32 +973,42 @@ function initWebSocket() {
};
}
// 更新滑块值显示
document.querySelectorAll('.slider').forEach(slider => {
const valueDisplay = slider.nextElementSibling;
if (slider.name === 'fcPower') {
valueDisplay.textContent = slider.value + ' (0.0 kW)';
} else if (slider.name === 'depth') {
valueDisplay.textContent = slider.value + ' m';
} else if (slider.name === 'powerBatPower') {
valueDisplay.textContent = slider.value + ' kW';
} else {
valueDisplay.textContent = slider.value;
}
// 数值步进控件(替代滑块,防误拖)
const stepperUnitText = {
fcPower: v => v + ' (' + (v * 0.1).toFixed(1) + ' kW)',
depth: v => v + ' m',
powerBatPower: v => v + ' kW'
};
slider.addEventListener('input', () => {
if (slider.name === 'fcPower') {
valueDisplay.textContent = slider.value + ' (' + (slider.value * 0.1).toFixed(1) + ' kW)';
} else if (slider.name === 'depth') {
valueDisplay.textContent = slider.value + ' m';
} else if (slider.name === 'powerBatPower') {
valueDisplay.textContent = slider.value + ' kW';
} else {
valueDisplay.textContent = slider.value;
}
function clampStepper(input) {
const min = parseInt(input.min, 10);
const max = parseInt(input.max, 10);
let v = parseInt(input.value, 10);
if (isNaN(v)) v = isNaN(min) ? 0 : min;
if (!isNaN(min)) v = Math.max(v, min);
if (!isNaN(max)) v = Math.min(v, max);
input.value = v;
const unitFn = stepperUnitText[input.id];
if (unitFn) {
const unitEl = document.getElementById(input.id + 'Unit');
if (unitEl) unitEl.textContent = unitFn(v);
}
}
document.querySelectorAll('.stepper-btn').forEach(btn => {
btn.addEventListener('click', () => {
const input = document.getElementById(btn.dataset.target);
if (!input) return;
input.value = (parseInt(input.value, 10) || 0) + parseInt(btn.dataset.step, 10);
clampStepper(input);
});
});
document.querySelectorAll('.stepper-input').forEach(input => {
input.addEventListener('input', () => clampStepper(input));
input.addEventListener('blur', () => clampStepper(input));
});
// 处理按钮组选择
document.querySelectorAll('.button-group button').forEach(button => {
button.addEventListener('click', () => {
@@ -838,16 +1028,71 @@ document.querySelectorAll('.button-group button').forEach(button => {
});
});
// 处理表单提交
document.getElementById('fcsForm').addEventListener('submit', async (e) => {
// 指令文本映射(用于确认弹窗摘要)
const FC_MODE_TEXT = { '00H': '无效', '01H': '调试', '02H': '自动', '03H': '补给/排放' };
const FC_CMD_TEXT = { '00H': '无效', '01H': '自检', '02H': '启动', '03H': '停机', '04H': '复位', '05H': '紧急停机', '06H': '补给', '07H': '制氢预热', '08H': '停机维护', '0AH': '开机', '0BH': '关机' };
const EMERGENCY_TEXT = { '00H': '无', '01H': '允许降载', '02H': '允许排气', '03H': '降载+排气' };
const SUPPLY_TEXT = { '00H': '无效', '01H': '液氧加注', '02H': '甲醇加注', '03H': '液氧排空', '04H': '甲醇排空', '05H': 'CO2排空', '07H': '合金加注', '08H': '氮气加注', '09H': '纯水加注', '0AH': '备用', '0BH': '备用' };
const BAT_CMD_TEXT = { '00H': '无效', '10H': '启动', '20H': '关闭' };
const HOST_STATUS_TEXT = { 'AAH': '运行', 'FFH': '关机', '55H': '故障' };
const DANGER_FC_CMDS = ['03H', '05H', '08H', '0BH'];
const DANGER_HOST_STATUS = ['FFH', '55H'];
// 生成指令摘要并打开确认弹窗(提交前拦截,防误操作)
function openConfirmModal() {
const form = document.getElementById('fcsForm');
const fv = name => form.elements[name].value;
const rows = [
{ label: '模式设定', value: FC_MODE_TEXT[fv('fcMode')] || fv('fcMode') },
{ label: '操控指令', value: FC_CMD_TEXT[fv('fcCommand')] || fv('fcCommand'), danger: DANGER_FC_CMDS.includes(fv('fcCommand')) },
{ label: '输出功率指令', value: fv('fcPower') + ' (' + (fv('fcPower') * 0.1).toFixed(1) + ' kW)' },
{ label: '纵倾姿态数据1', value: fv('pitch') },
{ label: '横倾姿态数据1', value: fv('roll') },
{ label: '应急允许', value: EMERGENCY_TEXT[fv('emergencyAllow')] || fv('emergencyAllow') },
{ label: '潜深深度', value: fv('depth') + ' m' },
{ label: '补给/排放指令', value: SUPPLY_TEXT[fv('supplyExhaustCmd')] || fv('supplyExhaustCmd') },
{ label: '仪表锂电池启停指令', value: BAT_CMD_TEXT[fv('insBatCmd')] || fv('insBatCmd') },
{ label: '动力锂电池启停指令', value: BAT_CMD_TEXT[fv('powerBatCmd')] || fv('powerBatCmd') },
{ label: '动力锂电池功率配置值', value: fv('powerBatPower') + ' kW' },
{ label: '主机状态', value: HOST_STATUS_TEXT[fv('hostStatus')] || fv('hostStatus'), danger: DANGER_HOST_STATUS.includes(fv('hostStatus')) }
];
document.getElementById('modalSummary').innerHTML = rows.map(r =>
'<div class="summary-row' + (r.danger ? ' danger' : '') + '">' +
'<span class="summary-label">' + (r.danger ? '<span class="summary-danger-mark">⚠</span>' : '') + r.label + '</span>' +
'<span class="summary-value">' + r.value + '</span>' +
'</div>'
).join('');
const hasDanger = rows.some(r => r.danger);
document.getElementById('modalDangerNote').className = 'modal-note' + (hasDanger ? ' visible' : '');
document.getElementById('confirmOverlay').classList.add('visible');
}
function closeConfirmModal() {
document.getElementById('confirmOverlay').classList.remove('visible');
}
// 提交按钮:先弹出确认框,不直接发送
document.getElementById('fcsForm').addEventListener('submit', (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
openConfirmModal();
});
// 取消:关闭弹窗,不做任何提交
document.getElementById('modalCancelBtn').addEventListener('click', closeConfirmModal);
// 确认执行:真正下发指令
document.getElementById('modalConfirmBtn').addEventListener('click', async () => {
const form = document.getElementById('fcsForm');
const statusMessage = document.getElementById('statusMessage');
closeConfirmModal();
try {
const params = new URLSearchParams();
for (const [key, value] of formData.entries()) {
for (const [key, value] of new FormData(form).entries()) {
params.append(key, value);
}
@@ -864,18 +1109,16 @@ document.getElementById('fcsForm').addEventListener('submit', async (e) => {
if (result.success) {
statusMessage.textContent = '操控指令提交成功';
statusMessage.className = 'status-message';
statusMessage.style.display = 'block';
} else {
statusMessage.textContent = result.message || '操控指令提交失败';
statusMessage.className = 'status-message error';
statusMessage.style.display = 'block';
}
} catch (error) {
statusMessage.textContent = '网络错误: ' + error.message;
statusMessage.className = 'status-message error';
statusMessage.style.display = 'block';
}
statusMessage.style.display = 'block';
setTimeout(() => {
statusMessage.style.display = 'none';
}, 5000);