初始化

This commit is contained in:
Jay Huang 2024-10-18 21:21:40 +08:00
commit 221c6fc239
12 changed files with 1192 additions and 0 deletions

177
app.py Normal file
View File

@ -0,0 +1,177 @@
from flask import Flask, render_template, jsonify, request
import serial
import serial.tools.list_ports
import threading
import time
import subprocess
import sys
import logging
app = Flask(__name__)
# 初始化灯光状态
lights = {
'common': False,
'alarm': False,
'up': False,
'down': False,
'emergency_stop': False
}
# 全局变量用于存储串口对象
ser = None
# 用于存储所有接收到的信号
all_signals = []
# 线程控制变量
thread_running = False
def reset_lights():
global lights
for key in lights:
lights[key] = False
def analyze_signals():
global lights, all_signals
reset_lights()
# 首先检查急停条件
if len(all_signals) >= 3 and (all_signals[-3:] == ['09', '0b', '08'] or all_signals[-3:] == ['09', '0d', '08']):
lights['emergency_stop'] = True
print("急停灯亮")
lights['common'] = False
lights['alarm'] = False
lights['up'] = False
lights['down'] = False
return # 如果急停条件满足,立即返回
# 然后检查 '08' 信号
if all_signals and all_signals[-1] == '08':
print("收到08信号, 所有灯熄灭")
reset_lights() # 重置所有灯的状态
return
if '09' in all_signals:
lights['common'] = True
if len(all_signals) >= 3 and all_signals[-3:] == ['09', '08', '09']:
lights['alarm'] = True
if all_signals[-2:] != ['08', '09']:
lights['alarm'] = False
if '0b' in all_signals or (len(all_signals) >= 2 and all_signals[-2:] == ['09', '0b']):
lights['up'] = True
if all_signals[-1:] != ['0b']:
lights['up'] = False
if '0d' in all_signals or (len(all_signals) >= 2 and all_signals[-2:] == ['09', '0d']):
lights['down'] = True
if all_signals[-1:] != ['0d']:
lights['down'] = False
def read_serial():
global all_signals, thread_running
while thread_running:
try:
if ser and ser.is_open and ser.in_waiting:
data = ser.read().hex()
print(f"接收到数据: {data}")
all_signals.append(data)
analyze_signals()
if len(all_signals) > 100:
all_signals = all_signals[-100:]
print(f"当前灯光状态: {lights}")
print(f"当前信号数组: {all_signals}")
else:
time.sleep(0.05)
except serial.SerialException as e:
print(f"串口错误: {e}")
time.sleep(1)
# 设置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/get_lights_status')
def get_lights_status():
return jsonify(lights)
@app.route('/get_signals')
def get_signals():
return jsonify(all_signals)
@app.route('/get_ports')
def get_ports():
try:
ports = []
if sys.platform.startswith('win'):
result = subprocess.check_output(['wmic', 'path', 'Win32_PnPEntity', 'Get', 'DeviceID,Caption'], universal_newlines=True)
for line in result.split('\n'):
if '(COM' in line:
com = line.split('(')[1].split(')')[0]
ports.append({'device': com, 'description': line.strip()})
elif sys.platform.startswith('linux') or sys.platform.startswith('darwin'):
cmd = 'ls /dev/tty*' if sys.platform.startswith('linux') else 'ls /dev/tty.*'
result = subprocess.check_output(cmd, shell=True, universal_newlines=True)
for line in result.split('\n'):
if line:
ports.append({'device': line, 'description': line})
else:
raise OSError(f"Unsupported operating system: {sys.platform}")
logger.debug(f"Found ports: {ports}")
return jsonify(ports)
except Exception as e:
logger.error(f"Error getting ports: {str(e)}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/set_port', methods=['POST'])
def set_port():
global ser, thread_running
data = request.json
port = data.get('port')
if not port:
return jsonify({'success': False, 'message': '未提供串口地址'})
try:
if ser and ser.is_open:
thread_running = False
time.sleep(0.5)
ser.close()
ser = serial.Serial(port, 9600, timeout=1)
thread_running = True
threading.Thread(target=read_serial, daemon=True).start()
return jsonify({'success': True, 'message': '串口设置成功'})
except serial.SerialException as e:
return jsonify({'success': False, 'message': f'串口打开失败: {str(e)}'})
except Exception as e:
return jsonify({'success': False, 'message': f'未知错误: {str(e)}'})
@app.route('/test_port', methods=['POST'])
def test_port():
global ser
if not ser or not ser.is_open:
return jsonify({'success': False, 'message': '串口未打开'})
try:
ser.write(b'TEST\r\n')
response = ser.readline().decode().strip()
if response:
return jsonify({'success': True, 'message': f'收到响应: {response}'})
else:
return jsonify({'success': False, 'message': '未收到响应'})
except Exception as e:
return jsonify({'success': False, 'message': f'测试失败: {str(e)}'})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5888)

5
static/css/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

13
static/css/flatpickr.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
static/js/flatpickr.js Normal file

File diff suppressed because one or more lines are too long

6
static/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

23
static/js/xlsx.full.min.js vendored Normal file

File diff suppressed because one or more lines are too long

68
static/js/zh.js Normal file
View File

@ -0,0 +1,68 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.zh = {}));
}(this, (function (exports) { 'use strict';
var fp = typeof window !== "undefined" && window.flatpickr !== undefined
? window.flatpickr
: {
l10ns: {},
};
var Mandarin = {
weekdays: {
shorthand: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
longhand: [
"星期日",
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
],
},
months: {
shorthand: [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
],
longhand: [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
],
},
rangeSeparator: " 至 ",
weekAbbreviation: "周",
scrollTitle: "滚动切换",
toggleTitle: "点击切换 12/24 小时时制",
};
fp.l10ns.zh = Mandarin;
var zh = fp.l10ns;
exports.Mandarin = Mandarin;
exports.default = zh;
Object.defineProperty(exports, '__esModule', { value: true });
})));

357
static/main.js Normal file
View File

@ -0,0 +1,357 @@
console.log('main.js 文件开始执行');
let lightHistory = {
common: false,
alarm: false,
up: false,
down: false,
emergency_stop: false
};
let allTestsPassed = false;
let currentDeviceType = '5'; // 默认为五孔设备
function updateLights() {
fetch('/get_lights_status')
.then(response => response.json())
.then(data => {
for (let light in data) {
let element5 = document.getElementById(light + '-5');
let element4 = document.getElementById(light + '-4');
if (element5) {
updateLightStatus(element5, data[light]);
}
if (element4) {
updateLightStatus(element4, data[light]);
}
// 更新灯光历史和检查列表
if (data[light] && !lightHistory[light]) {
lightHistory[light] = true;
updateCheckList(light);
}
}
checkAllTestsPassed();
});
}
function updateLightStatus(element, isOn) {
if (isOn) {
element.classList.add('on');
element.classList.remove('off');
} else {
element.classList.add('off');
element.classList.remove('on');
}
}
function updateCheckList(light) {
const listItem = document.querySelector(`#statusCheckList${currentDeviceType} li[data-light="${light}"]`);
if (listItem) {
const checkMark = listItem.querySelector('.badge');
checkMark.style.display = 'inline-block';
}
}
function checkAllTestsPassed() {
allTestsPassed = Array.from(document.querySelectorAll(`#statusCheckList${currentDeviceType} .badge`))
.every(badge => badge.style.display === 'inline-block');
// 更新测试结果
updateTestResult();
}
function resetCheckList() {
lightHistory = {
common: false,
alarm: false,
up: false,
down: false,
emergency_stop: false
};
allTestsPassed = false;
const deviceType = document.getElementById('deviceType').value;
const checkList = document.getElementById(`statusCheckList${deviceType}`);
if (checkList) {
const badges = checkList.querySelectorAll('.badge');
badges.forEach(badge => {
badge.style.display = 'none';
});
}
updateTestResult();
}
function updateTestResult() {
const rows = document.querySelectorAll('#resultTable tbody tr');
rows.forEach(row => {
const checkbox = row.querySelector('.form-check-input');
const label = row.querySelector('.form-check-label');
const pendingSpan = label.querySelector('.pending');
const passSpan = label.querySelector('.pass');
const failSpan = label.querySelector('.fail');
if (allTestsPassed) {
checkbox.disabled = false;
checkbox.checked = true;
pendingSpan.style.display = 'none';
passSpan.style.display = 'inline';
failSpan.style.display = 'none';
row.classList.remove('pending-row');
row.classList.add('pass-row');
row.classList.remove('fail-row');
} else {
checkbox.disabled = true;
checkbox.checked = false;
pendingSpan.style.display = 'inline';
passSpan.style.display = 'none';
failSpan.style.display = 'none';
row.classList.add('pending-row');
row.classList.remove('pass-row');
row.classList.remove('fail-row');
}
});
}
function addNewRow(button) {
const table = document.getElementById('resultTable').getElementsByTagName('tbody')[0];
const currentRow = button.closest('tr');
const serialNumberInput = currentRow.querySelector('.serial-number');
if (serialNumberInput.value.trim() === '') {
alert('请输入设备序列号');
return;
}
serialNumberInput.disabled = true;
button.style.display = 'none';
const newRow = table.insertRow(table.rows.length);
const rowIndex = table.rows.length - 1;
const cell1 = newRow.insertCell(0);
const cell2 = newRow.insertCell(1);
const cell3 = newRow.insertCell(2);
cell1.innerHTML = '<input type="text" class="form-control serial-number" placeholder="输入设备序列号">';
cell2.innerHTML = `
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="result-${rowIndex}" ${allTestsPassed ? 'checked' : 'disabled'}>
<label class="form-check-label" for="result-${rowIndex}">
<span class="pending" ${allTestsPassed ? 'style="display: none;"' : ''}>待测试</span>
<span class="pass" ${allTestsPassed ? '' : 'style="display: none;"'}>合格</span>
<span class="fail" style="display: none;">不合格</span>
</label>
</div>`;
cell3.innerHTML = '<button class="btn btn-primary add-row-btn" onclick="addNewRow(this)">增加下一条</button>';
updateTestResult();
}
function updateResultStyle(row) {
const checkbox = row.cells[1].querySelector('.form-check-input');
const serialNumberInput = row.cells[0].querySelector('.serial-number');
const label = checkbox.nextElementSibling;
const pendingSpan = label.querySelector('.pending');
const passSpan = label.querySelector('.pass');
const failSpan = label.querySelector('.fail');
checkbox.addEventListener('change', function() {
if (this.checked) {
row.classList.remove('fail-row');
row.classList.add('pass-row');
pendingSpan.style.display = 'none';
passSpan.style.display = 'inline';
failSpan.style.display = 'none';
} else {
row.classList.remove('pass-row');
row.classList.add('fail-row');
pendingSpan.style.display = 'none';
passSpan.style.display = 'none';
failSpan.style.display = 'inline';
}
});
}
function exportToExcel() {
const table = document.getElementById('resultTable');
const deviceType = document.getElementById('deviceType').value;
const tester = document.getElementById('tester').value;
const orderNumber = document.getElementById('orderNumber').value;
const testDate = document.getElementById('testDate').value;
const data = [];
// 添加测试信息
data.push(['设备型号', deviceType === '5' ? '五孔' : '四孔']);
data.push(['检测人员', tester]);
data.push(['订单编号', orderNumber]);
data.push(['测试日期', testDate]);
data.push([]); // 空行
const headers = ['设备序列号', '测试结果'];
data.push(headers);
for (let i = 1; i < table.rows.length; i++) {
const row = table.rows[i];
const serialNumber = row.cells[0].querySelector('input').value;
const testResult = row.cells[1].querySelector('input').checked ? '合格' : '不合格';
data.push([serialNumber, testResult]);
}
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(data);
const colWidth = [{ wch: 20 }, { wch: 20 }];
ws['!cols'] = colWidth;
XLSX.utils.book_append_sheet(wb, ws, "测试结果");
XLSX.writeFile(wb, `测试结果_${orderNumber}_${testDate}.xlsx`);
}
function startTest() {
disableInfoInputs();
const deviceTypeSelect = document.getElementById('deviceType');
if (deviceTypeSelect) {
deviceTypeSelect.disabled = false;
}
const firstSerialInput = document.getElementById('firstSerialNumber');
if (firstSerialInput) {
firstSerialInput.focus();
firstSerialInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function disableInfoInputs() {
const infoInputs = document.querySelectorAll('.info-input:not(#deviceType)');
infoInputs.forEach(input => {
input.disabled = true;
});
const startTestBtn = document.getElementById('startTestBtn');
if (startTestBtn) {
startTestBtn.disabled = true;
}
}
function setupDeviceTypeListener() {
const deviceTypeSelect = document.getElementById('deviceType');
if (deviceTypeSelect) {
deviceTypeSelect.addEventListener('change', function() {
currentDeviceType = this.value;
updateDeviceTypeDisplay();
});
}
}
function updateDeviceTypeDisplay() {
const deviceType = document.getElementById('deviceType').value;
const fiveHoleLights = document.getElementById('fiveHoleLights');
const fourHoleLights = document.getElementById('fourHoleLights');
const statusCheckList5 = document.getElementById('statusCheckList5');
const statusCheckList4 = document.getElementById('statusCheckList4');
if (deviceType === '5') {
fiveHoleLights.style.display = 'flex';
fourHoleLights.style.display = 'none';
statusCheckList5.style.display = 'block';
statusCheckList4.style.display = 'none';
} else if (deviceType === '4') {
fiveHoleLights.style.display = 'none';
fourHoleLights.style.display = 'flex';
statusCheckList5.style.display = 'none';
statusCheckList4.style.display = 'block';
} else {
fiveHoleLights.style.display = 'none';
fourHoleLights.style.display = 'none';
statusCheckList5.style.display = 'none';
statusCheckList4.style.display = 'none';
}
resetCheckList();
}
function setupPortSelection() {
const selectPortBtn = document.getElementById('selectPortBtn');
const confirmPortBtn = document.getElementById('confirmPortBtn');
const portSelect = document.getElementById('portSelect');
selectPortBtn.addEventListener('click', async () => {
try {
const response = await fetch('/get_ports');
const ports = await response.json();
portSelect.innerHTML = '';
if (ports.length === 0) {
const option = document.createElement('option');
option.textContent = '没有找到可用的串口';
portSelect.appendChild(option);
confirmPortBtn.disabled = true;
} else {
ports.forEach(port => {
const option = document.createElement('option');
option.value = port.device;
option.textContent = `${port.device} - ${port.description}`;
portSelect.appendChild(option);
});
confirmPortBtn.disabled = false;
}
const portModal = new bootstrap.Modal(document.getElementById('portModal'));
portModal.show();
} catch (error) {
console.error('获取串口列表失败:', error);
alert('获取串口列表失败,请重试。');
}
});
confirmPortBtn.addEventListener('click', async () => {
const selectedPort = portSelect.value;
if (selectedPort) {
try {
const response = await fetch('/set_port', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ port: selectedPort }),
});
const result = await response.json();
if (result.success) {
alert('串口设置成功');
bootstrap.Modal.getInstance(document.getElementById('portModal')).hide();
} else {
alert('串口设置失败: ' + result.message);
}
} catch (error) {
console.error('设置串口失败:', error);
alert('设置串口失败,请重试。');
}
} else {
alert('请选择一个串口');
}
});
}
document.addEventListener('DOMContentLoaded', function() {
setupDeviceTypeListener();
setupPortSelection();
const startTestBtn = document.getElementById('startTestBtn');
if (startTestBtn) {
startTestBtn.addEventListener('click', function(event) {
event.preventDefault();
startTest();
});
}
// 初始化设备类型显示
updateDeviceTypeDisplay();
// 每100毫秒更新一次灯光状态
setInterval(updateLights, 100);
});

283
static/styles.css Normal file
View File

@ -0,0 +1,283 @@
body {
background-color: #f8f9fa;
}
.container {
max-width: 1000px;
}
.card {
height: 100%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-body {
display: flex;
flex-direction: column;
}
.form-label {
font-weight: bold;
color: #495057;
}
.form-select, .form-control {
border-radius: 5px;
}
.light-container {
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
gap: 20px;
flex-grow: 1;
}
.light-item {
text-align: center;
margin: 10px;
}
.light {
width: 80px;
height: 80px;
border-radius: 50%;
display: inline-block;
transition: all 0.3s ease;
border: 2px solid #ccc; /* 统一边框颜色 */
}
.light.off {
background-color: #e0e0e0; /* 淡灰色 */
box-shadow: none;
}
.light.common.on {
background-color: #007bff; /* 蓝色 */
box-shadow: 0 0 20px 5px rgba(0, 123, 255, 0.5);
}
.light.alarm.on {
background-color: #007bff; /* 蓝色 */
box-shadow: 0 0 20px 5px rgba(0, 123, 255, 0.5);
}
.light.emergency_stop.on {
background-color: #007bff; /* 蓝色 */
box-shadow: 0 0 20px 5px rgba(0, 123, 255, 0.5);
}
.light.up.on {
background-color: #007bff; /* 蓝色 */
box-shadow: 0 0 20px 5px rgba(0, 123, 255, 0.5);
}
.light.down.on {
background-color: #007bff; /* 蓝色 */
box-shadow: 0 0 20px 5px rgba(0, 123, 255, 0.5);
}
/* Flatpickr 样式覆盖 */
.flatpickr-input {
background-color: #fff !important;
}
.flatpickr-day.selected {
background: #007bff;
border-color: #007bff;
}
.flatpickr-day.selected:hover {
background: #0056b3;
border-color: #0056b3;
}
/* 测试结果表格样式 */
#resultTable {
border-collapse: separate;
border-spacing: 0 12px;
}
#resultTable thead th {
border: none;
background-color: #f8f9fa;
color: #495057;
font-weight: bold;
padding: 12px 15px;
}
#resultTable tbody tr {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
#resultTable tbody tr:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
#resultTable td {
vertical-align: middle;
border: none;
background-color: #ffffff;
padding: 15px;
}
#resultTable .form-control,
#resultTable .form-select {
border: 1px solid #ced4da;
}
/* 开关样式 */
.form-check-input {
width: 3em !important;
height: 1.5em !important;
margin-top: 0.25em;
vertical-align: top;
background-color: rgba(0, 0, 0, 0.25);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.85%29'/%3e%3c/svg%3e");
background-position: left center;
border-radius: 2em;
transition: background-position .15s ease-in-out;
}
.form-check-input:checked {
background-color: #198754;
background-position: right center;
}
.form-check-label {
padding-left: 10px;
font-weight: bold;
}
.form-check-label .pass,
.form-check-label .fail {
transition: all 0.3s ease;
}
.form-check-label .pass {
color: #198754;
}
.form-check-label .fail {
color: #dc3545;
display: none;
}
.pass-row {
background-color: rgba(25, 135, 84, 0.1) !important;
}
.fail-row {
background-color: rgba(220, 53, 69, 0.1) !important;
}
/* 按钮样式 */
#resultTable .btn-primary {
transition: all 0.3s ease;
}
#resultTable .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,123,255,0.3);
}
.btn-success {
background-color: #28a745;
border-color: #28a745;
transition: all 0.3s ease;
}
.btn-success:hover {
background-color: #218838;
border-color: #1e7e34;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3);
}
.serial-number:disabled {
background-color: #e9ecef;
opacity: 1;
cursor: not-allowed;
}
.add-row-btn {
transition: all 0.3s ease;
}
.add-row-btn:hover {
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* 将 #lightCheckList 替换为 #statusCheckList */
#statusCheckList {
padding: 0;
margin-bottom: 0;
}
#statusCheckList .list-group-item {
padding: 0.75rem 1rem;
border: none;
background-color: transparent;
font-size: 0.9rem;
}
#statusCheckList .badge {
transition: all 0.3s ease;
}
#statusCheckList .badge.bg-success {
background-color: #28a745 !important;
}
.card-title {
color: #495057;
font-weight: bold;
}
.col-md-8 {
width: 80%;
}
.col-md-4 {
width: 20%;
}
/* 响应式调整 */
@media (max-width: 768px) {
.col-md-8, .col-md-4 {
margin-bottom: 1rem;
}
}
/* 添加或修改以下样式 */
.form-check-input:disabled {
opacity: 1;
}
.form-check-label .pending {
color: #6c757d;
}
.form-check-label .pass {
color: #28a745;
}
.form-check-label .fail {
color: #dc3545;
}
.pending-row {
background-color: rgba(108, 117, 125, 0.1) !important;
}
.pass-row {
background-color: rgba(40, 167, 69, 0.1) !important;
}
.fail-row {
background-color: rgba(220, 53, 69, 0.1) !important;
}

244
templates/index.html Normal file
View File

@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备测试监控</title>
<!-- 引入 Bootstrap CSS -->
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
<!-- 引入 Flatpickr CSS -->
<link href="../static/css/flatpickr.min.css" rel="stylesheet">
<!-- 引入自定义 CSS -->
<link href="../static/styles.css" rel="stylesheet">
<!-- 在 <head> 部分添加 Font Awesome 图标库 -->
<link href="../static/css/all.min.css" rel="stylesheet">
<script src="../static/js/xlsx.full.min.js"></script>
</head>
<body>
<div class="container mt-5">
<h1 class="text-center mb-4">设备测试监控</h1>
<!-- 信息区域 -->
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">测试信息</h5>
<div class="row">
<!-- 现有的输入字段 -->
<div class="col-md-3">
<label for="deviceType" class="form-label">设备型号</label>
<select class="form-select info-input" id="deviceType">
<option selected>请选择...</option>
<option value="4">四孔</option>
<option value="5">五孔</option>
</select>
</div>
<div class="col-md-3">
<label for="tester" class="form-label">检测人员</label>
<select class="form-select info-input" id="tester">
<option selected>请选择...</option>
<option value="邱东财">邱东财</option>
<option value="陈翠芹">陈翠芹</option>
</select>
</div>
<div class="col-md-3">
<label for="orderNumber" class="form-label">订单编号</label>
<input type="text" class="form-control info-input" id="orderNumber">
</div>
<div class="col-md-3">
<label for="testDate" class="form-label">测试日期</label>
<input type="text" class="form-control flatpickr info-input" id="testDate">
</div>
<!-- 添加开始测试按钮 -->
<div class="col-md-3 mb-3 mt-3">
<button id="startTestBtn" class="btn btn-primary ">开始测试</button>
</div>
<div class="col-md-3 mb-3 mt-3">
<button id="selectPortBtn" class="btn btn-secondary">选择串口</button>
</div>
</div>
</div>
</div>
<!-- 添加模态框 -->
<div class="modal fade" id="portModal" tabindex="-1" aria-labelledby="portModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="portModalLabel">选择串口</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<select class="form-select" id="portSelect">
<!-- 串口选项将通过 JavaScript 动态添加 -->
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmPortBtn">确认</button>
</div>
</div>
</div>
</div>
<div class="row mb-4" style="height:300px;">
<!-- 灯光状态监控 -->
<div class="col-md-8">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center mb-4">灯光状态监控</h5>
<!-- 五孔灯光状态 -->
<div id="fiveHoleLights" class="light-container">
<div class="light-item">
<div class="light common off" id="common-5"></div>
<p>公共灯</p>
</div>
<div class="light-item">
<div class="light alarm off" id="alarm-5"></div>
<p>警铃灯</p>
</div>
<div class="light-item">
<div class="light up off" id="up-5"></div>
<p>上行灯</p>
</div>
<div class="light-item">
<div class="light down off" id="down-5"></div>
<p>下行灯</p>
</div>
<div class="light-item">
<div class="light emergency_stop off" id="emergency_stop-5"></div>
<p>急停灯</p>
</div>
</div>
<!-- 四孔灯光状态 -->
<div id="fourHoleLights" class="light-container" style="display: none;">
<div class="light-item">
<div class="light common off" id="common-4"></div>
<p>公共灯</p>
</div>
<div class="light-item">
<div class="light emergency_stop off" id="emergency_stop-4"></div>
<p>急停灯</p>
</div>
<div class="light-item">
<div class="light up off" id="up-4"></div>
<p>上行灯</p>
</div>
<div class="light-item">
<div class="light down off" id="down-4"></div>
<p>下行灯</p>
</div>
</div>
</div>
</div>
</div>
<!-- 检测状态记录 -->
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center mb-4">检测状态记录</h5>
<ul class="list-group" id="statusCheckList5" style="display: none;">
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="common">
公共
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="alarm">
警铃
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="up">
上行
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="down">
下行
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="emergency_stop">
急停
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
</ul>
<ul class="list-group" id="statusCheckList4" style="display: none;">
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="common">
公共
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="up">
上行
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="down">
下行
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" data-light="emergency_stop">
急停
<span class="badge bg-success rounded-pill" style="display: none;"></span>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 测试结果记录模块 -->
<div class="card">
<div class="card-body">
<h5 class="card-title text-center mb-4">测试结果记录</h5>
<table class="table" id="resultTable">
<thead>
<tr>
<th>设备序列号</th>
<th>测试结果</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" class="form-control serial-number" id="firstSerialNumber" placeholder="输入设备序列号"></td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="result-0" disabled>
<label class="form-check-label" for="result-0">
<span class="pending">待测试</span>
<span class="pass" style="display: none;">合格</span>
<span class="fail" style="display: none;">不合格</span>
</label>
</div>
</td>
<td><button class="btn btn-primary add-row-btn" onclick="addNewRow(this)">增加下一条</button></td>
</tr>
</tbody>
</table>
<div class="text-end mt-3">
<button class="btn btn-success" onclick="exportToExcel()">
<i class="fas fa-file-excel me-2"></i>导出结果到Excel
</button>
</div>
</div>
</div>
</div>
<!-- 引入 Bootstrap JS 和 Popper.js -->
<script src="../static/js/popper.min.js"></script>
<script src="../static/js/bootstrap.min.js"></script>
<!-- 引入 Flatpickr JS -->
<script src="../static/js/flatpickr.js"></script>
<!-- 引入 Flatpickr 中文语言包 -->
<script src="../static/js/zh.js"></script>
<script src="../static/main.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
flatpickr("#testDate", {
dateFormat: "Y-m-d",
locale: "zh",
disableMobile: "true"
});
});
</script>
</body>
</html>