Compare commits

..

8 Commits

Author SHA1 Message Date
wangfq
8a6b5c6d07 测试信息页面优化: 限制6000条、字段调整、车检器序列号搜索
- 查询/导出/图表统一 LIMIT 6000 条
- 列顺序: 时间→第一列, 测试模式→最后一列, 隐藏ID
- 设备编码只显示后6位, 默认每页100条
- 新增车检器序列号搜索 (detector_serial LIKE)
- 四个文件同步修改: models.py, test_data.py, test_data.js, test_data.html
2026-06-18 09:44:18 +08:00
wangfq
f0ec79ca2f chore: 更新 edc_server 子模块 (fix detector_serial B4 缺失) 2026-06-15 16:45:53 +08:00
wangfq
4b082e35df refactor: 调整车检器序列号相关命名 2026-06-15 13:41:22 +08:00
wangfq
521cbe4107 feat: edc-web 支持车检器序列号输入与显示
- 自动化测试区域新增「车检器序列号」输入框
- 回车键自动触发「开始」按钮
- /api/automation/start 接收 detector_serial,写入 tb_pending_detector
- 测试操作页 + 测试信息页(全部/B2/B4)显示序列号,空时显示 '-'
- 页面加载和测试结束后焦点自动回到序列号输入框(全选),方便连续测试
2026-06-15 10:02:51 +08:00
wangfq
4ac6cbb2fe docs: 2026.06.09~06.12 周报 2026-06-12 18:22:30 +08:00
wangfq
0dfb928375 feat: 被动轮询间隔改为 3 秒 + 新增 B2 记录条数显示
- 数据轮询独立于设备状态刷新:refreshLatestData 每 3 秒,refreshAll 每 5 秒
- 「当前测试数据」标题旁显示新记录条数 (x 条新记录)
- 仅 B2(灵敏度测试) 新记录计数,页面加载/自动化开始时复位
- pollProgress 同步更新计数器,自动化与被动模式无冲突
2026-06-12 11:10:09 +08:00
wangfq
366c7f909a feat: 测试操作页添加被动轮询,实时显示工装本地按键触发的上报数据
- 新增 refreshLatestData() 每 5 秒轮询最新测试数据(不传 since 参数)
- lastLatestId/lastWaveId 跟踪最新记录,避免重复渲染
- 自动化运行中跳过被动轮询,避免与 pollProgress 冲突
- pollProgress 同步更新跟踪 ID,自动化结束后无缝衔接
2026-06-12 10:45:58 +08:00
wangfq
b4b7387b39 fix: 前端型号显示改为从 API 动态获取,修复新增加型号显示 Unknown(3)
- test_op.js: renderConfigOverview 硬编码 devTypeMap → devTypeNameCache[DevType]
- test_op.js: renderLatest 增加 sub_type 回退查找,兼容旧数据
- test_op.js: 每 5 秒刷新型号名称缓存,工装页新增型号后自动同步
- test_data.js: 型号列三元硬编码 → getDevTypeName(sub_type)
- 子模块 edc_server: 同步设备型号名称数据库查询
2026-06-12 10:00:33 +08:00
9 changed files with 331 additions and 81 deletions

View File

@@ -0,0 +1,68 @@
# 周报 — 2026.06.09 ~ 2026.06.12
## 一、设备型号动态管理
**问题**:车检器型号名称(如 DLD110SV在代码中硬编码新增型号后测试操作页显示 `Unknown(3)``0x03`,测试信息页型号列显示 `-`
**修复**
- **后端** `edc_server`B2/B4 数据写入时,废弃硬编码 `{1:"PD132",2:"DLD110"}` 映射,改为查询 `tb_vechicle_base_test` 表获取 `type_num → dev_name`,带内存缓存避免高频 DB 查询。
- **前端** `edc-web`
- `test_op.js` 工装配置概览面板、最新测试结果区域,从 `/api/vehicle-base-test` 动态获取型号映射。
- `test_data.js` 测试信息页型号列,同样改为动态查询。
- 每 5 秒自动刷新型号缓存,工装配置页新增型号后无需手动刷新。
## 二、测试操作页实时数据改进
**问题**
1. 工装本地按键触发的测试数据上报后,网页端无法实时显示,必须依赖网页端"开始"按钮。
2. 数据轮询间隔偏长5 秒),新记录无计数提示。
**实现**
- 新增被动轮询机制,**每 3 秒**自动拉取最新测试数据,覆盖工装本地按键和网页手动指令两种触发方式。
- 自动化测试运行期间,被动轮询自动让位给 `pollProgress`500ms 高频轮询),结束后无缝接回。
- 「当前测试数据」标题旁新增 **B2 新记录条数**显示 `(N 条新记录)`,页面加载/自动化开始时自动复位。
## 三、角色权限体系
| 角色 | 权限 |
|---|---|
| `admin` | 全部功能(含用户管理、删除数据) |
| `manager` | 管理功能(用户管理除外)— 工装配置、数据删除等 |
| `analyst` | 仅测试数据查询/下载 + 修改密码 |
| `operator` | 测试操作 + 测试数据查看(不含工装配置) |
- `analyst` 角色访问受限页面时自动跳转到测试数据页并提示。
- 所有用户可自行修改密码。
## 四、设备日志管理
- 新增设备日志管理页面,记录设备 TCP 连接/断开、异常事件。
- 支持按设备编码、事件类型、时间范围筛选。
- 支持 **CSV 导出**,修复时区偏移 8 小时问题。
- 设备列表页在线/离线状态每 5 秒实时刷新。
- 后端 `device_status_monitor` 增加全表扫描,修正状态不一致问题。
## 五、UI/UX 优化
- **继电器输出格式化**:明确区分"✅有输出"/"❌无输出",前端直接显示 DB 字段。
- **工装配置概览面板**:测试操作页顶部展示当前配置参数(型号、频率范围、线圈、车辆等),可折叠。
- **测试信息页重构**
- 三视图切换(全部数据 / B2 灵敏度 / B4 波动),差异字段自动隐藏。
- 表格支持横向滚动,列宽自适应不换行。
- 故障信息列限制 12em 宽度,超长截断省略 + hover 显示全文。
- 配置页频率/峰峰值前端显示与 DB 原始值双向转换修复。
## 六、Bug 修复
| 问题 | 修复 |
|---|---|
| 浏览器缓存导致工装参数 GET 返回旧数据 | 响应头 `Cache-Control: no-store` |
| `renderLatest` 覆盖测试模式,灵敏度/波动显示回退 | 分离测试模式更新与数据渲染逻辑 |
| 测试操作页工装配置修改后不同步 | 每 5 秒刷新 + 禁用缓存 |
| 设备离线时仍可发送指令 | 在线状态检查,离线/通信不良时弹窗提示并阻止 |
| HeartBeat 大小写不匹配,交互未记录 | 统一大小写匹配 |
| 后端 `device_status_monitor` 状态不一致 | 增加 `dnt_info` 全表扫描修正 |
---
**总计提交**vd_test_fixture 24 次edc_server 8 次。

View File

@@ -149,11 +149,12 @@ def get_latest_test_state(dnt_id: int) -> dict | None:
conn.close()
def get_test_data(page: int = 1, per_page: int = 20,
def get_test_data(page: int = 1, per_page: int = 100,
serial: str = "", date_from: str = "",
date_to: str = "", test_mode: str = "",
data_source: str = "") -> tuple[list[dict], int]:
"""分页查询测试数据JOIN dnt_info返回 (records, total)
data_source: str = "",
detector_serial: str = "") -> tuple[list[dict], int]:
"""分页查询测试数据JOIN dnt_info最多返回最近 6000 条,返回 (records, total)
test_mode: ''=全部, '0'=灵敏度, '1'=波动
data_source: ''=全部, 'B2', 'B4'
@@ -166,6 +167,9 @@ def get_test_data(page: int = 1, per_page: int = 20,
if serial:
where.append("d.serial LIKE %s")
params.append(f"%{serial}%")
if detector_serial:
where.append("t.detector_serial LIKE %s")
params.append(f"%{detector_serial}%")
if date_from:
where.append("t.create_time >= %s")
params.append(date_from if len(date_from) > 10 else date_from)
@@ -181,17 +185,21 @@ def get_test_data(page: int = 1, per_page: int = 20,
where_clause = " AND ".join(where) if where else "1=1"
# count
# count — 最多 6000 条
cur.execute(
f"SELECT COUNT(*) as total FROM tb_state_tst t "
f"JOIN dnt_info d ON t.dnt_id = d.id WHERE {where_clause}",
f"SELECT COUNT(*) as total FROM ("
f"SELECT 1 FROM tb_state_tst t "
f"JOIN dnt_info d ON t.dnt_id = d.id "
f"WHERE {where_clause} ORDER BY t.id DESC LIMIT 6000"
f") sub",
params,
)
total = cur.fetchone()["total"]
# data
# data — 子查询限 6000 后再分页
offset = (page - 1) * per_page
cur.execute(
f"SELECT * FROM ("
f"SELECT t.*, d.serial, "
f"c.coil_num, c.name as coil_name, "
f"sc.simulate_num, sc.name as car_name "
@@ -200,7 +208,8 @@ def get_test_data(page: int = 1, per_page: int = 20,
f"LEFT JOIN tb_coil_info c ON t.coil_id = c.id "
f"LEFT JOIN tb_simulate_car sc ON t.simulate_car_id = sc.id "
f"WHERE {where_clause} "
f"ORDER BY t.id DESC LIMIT %s OFFSET %s",
f"ORDER BY t.id DESC LIMIT 6000"
f") sub ORDER BY id DESC LIMIT %s OFFSET %s",
params + [per_page, offset],
)
records = cur.fetchall()
@@ -212,8 +221,9 @@ def get_test_data(page: int = 1, per_page: int = 20,
def get_all_test_data_for_export(serial: str = "", date_from: str = "",
date_to: str = "", test_mode: str = "",
data_source: str = "") -> list[dict]:
"""导出全部数据
data_source: str = "",
detector_serial: str = "") -> list[dict]:
"""导出全部数据(最多最近 6000 条)
test_mode: ''=全部, '0'=灵敏度, '1'=波动
data_source: ''=全部, 'B2', 'B4'
@@ -226,6 +236,9 @@ def get_all_test_data_for_export(serial: str = "", date_from: str = "",
if serial:
where.append("d.serial LIKE %s")
params.append(f"%{serial}%")
if detector_serial:
where.append("t.detector_serial LIKE %s")
params.append(f"%{detector_serial}%")
if date_from:
where.append("t.create_time >= %s")
params.append(date_from if len(date_from) > 10 else date_from)
@@ -248,7 +261,7 @@ def get_all_test_data_for_export(serial: str = "", date_from: str = "",
f"JOIN dnt_info d ON t.dnt_id = d.id "
f"LEFT JOIN tb_coil_info c ON t.coil_id = c.id "
f"LEFT JOIN tb_simulate_car sc ON t.simulate_car_id = sc.id "
f"WHERE {where_clause} ORDER BY t.id DESC",
f"WHERE {where_clause} ORDER BY t.id DESC LIMIT 6000",
params,
)
return cur.fetchall()
@@ -942,3 +955,21 @@ def delete_device_logs(serial: str = "", event_type: str = "",
return cnt
finally:
conn.close()
# ─── tb_pending_detector ───────────────────────────────────────────
def set_pending_detector_serial(dnt_id: int, detector_serial: str):
"""设置待插入的车检器序列号UPSERT"""
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute(
"""INSERT INTO tb_pending_detector (dnt_id, detector_serial)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE detector_serial = VALUES(detector_serial)""",
(dnt_id, detector_serial),
)
conn.commit()
finally:
conn.close()

View File

@@ -21,15 +21,16 @@ def test_data_page():
def api_test_data():
"""分页查询测试数据"""
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 20, type=int)
per_page = request.args.get("per_page", 100, type=int)
serial = request.args.get("serial", "", type=str)
detector_serial = request.args.get("detector_serial", "", type=str)
date_from = request.args.get("date_from", "", type=str)
date_to = request.args.get("date_to", "", type=str)
test_mode = request.args.get("test_mode", "", type=str)
data_source = request.args.get("data_source", "", type=str)
records, total = get_test_data(page, per_page, serial, date_from, date_to,
test_mode, data_source)
test_mode, data_source, detector_serial)
return jsonify({
"records": records,
"total": total,
@@ -42,29 +43,33 @@ def api_test_data():
@bp.route("/api/test-data/chart")
@login_required
def api_chart_data():
"""返回图表所需全部数据(不分页)"""
"""返回图表所需全部数据(不分页,最多 6000 条"""
serial = request.args.get("serial", "", type=str)
detector_serial = request.args.get("detector_serial", "", type=str)
date_from = request.args.get("date_from", "", type=str)
date_to = request.args.get("date_to", "", type=str)
test_mode = request.args.get("test_mode", "", type=str)
data_source = request.args.get("data_source", "", type=str)
records = get_all_test_data_for_export(serial, date_from, date_to,
test_mode, data_source)
test_mode, data_source,
detector_serial)
return jsonify({"records": records, "total": len(records)})
@bp.route("/api/test-data/export")
@login_required
def api_export():
"""导出测试数据为 CSV"""
"""导出测试数据为 CSV(最多 6000 条)"""
serial = request.args.get("serial", "", type=str)
detector_serial = request.args.get("detector_serial", "", type=str)
date_from = request.args.get("date_from", "", type=str)
date_to = request.args.get("date_to", "", type=str)
test_mode = request.args.get("test_mode", "", type=str)
data_source = request.args.get("data_source", "", type=str)
records = get_all_test_data_for_export(serial, date_from, date_to,
test_mode, data_source)
test_mode, data_source,
detector_serial)
output = io.StringIO()
writer = csv.writer(output)

View File

@@ -13,6 +13,7 @@ from app.models import (
get_wave_records,
clear_serialnet_records,
insert_log,
set_pending_detector_serial,
)
bp = Blueprint("test_op", __name__)
@@ -89,10 +90,15 @@ def api_automation_start():
data = request.get_json()
dnt_id = data.get("dnt_id")
count = int(data.get("count", 1))
detector_serial = (data.get("detector_serial") or "").strip()
device = get_device_by_id(dnt_id)
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
# 存储待插入的车检器序列号
if detector_serial:
set_pending_detector_serial(dnt_id, detector_serial)
# 清除旧记录,然后插入第一条 0xB0
clear_serialnet_records(dnt_id)
record_id = insert_serialnet(dnt_id, COMMANDS["B0"])

View File

@@ -1,5 +1,26 @@
// 测试信息页 — 三视图 (全部 / B2 / B4)
// ─── 型号名称缓存 ─────────────────────────────────
let devTypeNameCache = {};
async function initDevTypeNames() {
try {
const resp = await fetch('/api/vehicle-base-test');
const tests = await resp.json();
devTypeNameCache = {};
tests.forEach(t => {
if (t.type_num != null && t.dev_name) {
devTypeNameCache[t.type_num] = t.dev_name;
}
});
} catch (e) { console.error('加载型号名称失败:', e); }
}
function getDevTypeName(subType) {
if (subType == null || subType === 0) return '-';
return devTypeNameCache[subType] || `Unknown(${subType})`;
}
// ─── 视图定义 ───────────────────────────────────
const VIEWS = {
@@ -7,18 +28,18 @@ const VIEWS = {
label: '全部数据',
data_source: '', // '' = 不过滤
cols: [
{ key: 'id', title: 'ID' },
{ key: 'serial', title: '设备编码' },
{ key: 'model', title: '号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
{ key: 'model', title: '型号', render: r => getDevTypeName(r.sub_type) },
{ key: 'data_source', title: '来源' },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
{ key: 'iffinish', title: '完成', render: r => r.data_source === 'B4' ? '-' : (r.iffinish === '1' ? '是' : '否') },
{ key: 'fault_info', title: '故障信息', render: r => r.data_source === 'B4' ? '-' : `<span style="display:inline-block;max-width:12em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.fault_info || '')}">${escHtml(r.fault_info || '-')}</span>` },
{ key: 'relay_out', title: '继电器', render: r => fmtRelay(r.relay_out) },
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.data_source === 'B4' ? '-' : (r.ppvalue != null ? r.ppvalue.toFixed(2) : '-') },
{ key: 'idle_freq', title: '开始频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.idle_freq || '-') },
{ key: 'enter_freq', title: '进入频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.enter_freq || '-') },
{ key: 'exit_freq', title: '离开频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.exit_freq || '-') },
{ key: 'enter_freq', title: '触发频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.enter_freq || '-') },
{ key: 'exit_freq', title: '释放频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.exit_freq || '-') },
{ key: 'enter_dist', title: '触发距离(mm)', render: r => {
const v = r.data_source === 'B4' ? r.b4_enter_dist : r.enter_dist;
return v != null ? v + ' ' : '-';
@@ -27,8 +48,8 @@ const VIEWS = {
const v = r.data_source === 'B4' ? r.b4_leave_dist : r.exit_dist;
return v != null ? v + ' ' : '-';
}},
{ key: 'enter_speed', title: '进入速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.enter_speed) },
{ key: 'exit_speed', title: '离开速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.exit_speed) },
{ key: 'enter_speed', title: '触发速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.enter_speed) },
{ key: 'exit_speed', title: '释放速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.exit_speed) },
{ key: 'remain_count', title: '剩余次数', render: r => r.data_source === 'B2' ? '-' : (r.remain_count ?? '-') },
{ key: 'work_freq', title: '工作频率(Hz)', render: r => r.data_source === 'B2' ? '-' : (r.work_freq ?? '-') },
{ key: 'curr_dist', title: '当前距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.curr_dist != null ? r.curr_dist + ' ' : '-') },
@@ -36,38 +57,39 @@ const VIEWS = {
{ key: 'near_dist', title: '最近距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.near_dist != null ? r.near_dist + ' ' : '-') },
{ key: 'far_dist', title: '最远距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.far_dist != null ? r.far_dist + ' ' : '-') },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
],
},
b2: {
label: '灵敏度测试',
data_source: 'B2',
cols: [
{ key: 'id', title: 'ID' },
{ key: 'serial', title: '设备编码' },
{ key: 'model', title: '号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
{ key: 'model', title: '型号', render: r => getDevTypeName(r.sub_type) },
{ key: 'iffinish', title: '完成', render: r => r.iffinish === '1' ? '是' : '否' },
{ key: 'fault_info', title: '故障信息', render: r => `<span style="display:inline-block;max-width:12em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.fault_info || '')}">${escHtml(r.fault_info || '-')}</span>` },
{ key: 'relay_out', title: '继电器', render: r => fmtRelay(r.relay_out) },
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' },
{ key: 'idle_freq', title: '开始频率(Hz)' },
{ key: 'enter_freq', title: '进入频率(Hz)' },
{ key: 'exit_freq', title: '离开频率(Hz)' },
{ key: 'enter_freq', title: '触发频率(Hz)' },
{ key: 'exit_freq', title: '释放频率(Hz)' },
{ key: 'enter_dist', title: '触发距离(mm)' },
{ key: 'exit_dist', title: '释放距离(mm)' },
{ key: 'enter_speed', title: '进入速度', render: r => toSpeed(r.enter_speed) },
{ key: 'exit_speed', title: '离开速度', render: r => toSpeed(r.exit_speed) },
{ key: 'enter_speed', title: '触发速度', render: r => toSpeed(r.enter_speed) },
{ key: 'exit_speed', title: '释放速度', render: r => toSpeed(r.exit_speed) },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
],
},
b4: {
label: '波动测试',
data_source: 'B4',
cols: [
{ key: 'id', title: 'ID' },
{ key: 'serial', title: '设备编码' },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
{ key: 'remain_count', title: '剩余次数' },
{ key: 'work_freq', title: '工作频率(Hz)' },
{ key: 'curr_dist', title: '当前距离(mm)' },
@@ -78,7 +100,7 @@ const VIEWS = {
{ key: 'b4_leave_dist', title: '释放高度(mm)' },
{ key: 'relay_out', title: '继电器', render: r => fmtRelay(r.relay_out) },
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
],
},
};
@@ -169,14 +191,16 @@ function getDatetime(dateId, timeId) {
async function searchData(page = 1) {
currentPage = page;
const serial = document.getElementById("search-serial").value;
const detectorSerial = document.getElementById("search-detector-serial").value;
const dateFrom = getDatetime("search-date-from", "search-time-from");
const dateTo = getDatetime("search-date-to", "search-time-to");
const v = VIEWS[currentView];
const perPage = parseInt(document.getElementById("per-page").value) || 20;
const perPage = parseInt(document.getElementById("per-page").value) || 100;
const params = new URLSearchParams({ page, per_page: perPage });
if (serial) params.set("serial", serial);
if (detectorSerial) params.set("detector_serial", detectorSerial);
if (dateFrom) params.set("date_from", dateFrom);
if (dateTo) params.set("date_to", dateTo);
// 按 data_source 过滤(全部不过滤)
@@ -247,12 +271,14 @@ function renderPagination() {
function exportCSV() {
const serial = document.getElementById("search-serial").value;
const detectorSerial = document.getElementById("search-detector-serial").value;
const dateFrom = getDatetime("search-date-from", "search-time-from");
const dateTo = getDatetime("search-date-to", "search-time-to");
const v = VIEWS[currentView];
const params = new URLSearchParams();
if (serial) params.set("serial", serial);
if (detectorSerial) params.set("detector_serial", detectorSerial);
if (dateFrom) params.set("date_from", dateFrom);
if (dateTo) params.set("date_to", dateTo);
if (v.data_source) params.set("data_source", v.data_source);
@@ -270,12 +296,12 @@ const CHART_SERIES = {
b2: [
{ key: 'ppvalue', name: '峰峰值', unit: 'V', yAxisIndex: 0 },
{ key: 'idle_freq', name: '开始频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'enter_freq', name: '进入频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'exit_freq', name: '离开频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'enter_dist', name: '进入距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'exit_dist', name: '离开距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'enter_speed', name: '进入速度', unit: 'dm/s',yAxisIndex: 2 },
{ key: 'exit_speed', name: '离开速度', unit: 'dm/s',yAxisIndex: 2 },
{ key: 'enter_freq', name: '触发频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'exit_freq', name: '释放频率', unit: 'Hz', yAxisIndex: 0 },
{ key: 'enter_dist', name: '触发距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'exit_dist', name: '释放距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'enter_speed', name: '触发速度', unit: 'dm/s',yAxisIndex: 2 },
{ key: 'exit_speed', name: '释放速度', unit: 'dm/s',yAxisIndex: 2 },
],
b4: [
{ key: 'work_freq', name: '工作频率', unit: 'Hz', yAxisIndex: 0 },
@@ -283,8 +309,8 @@ const CHART_SERIES = {
{ key: 'speed', name: '速度', unit: 'dm/s',yAxisIndex: 2 },
{ key: 'near_dist', name: '最近距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'far_dist', name: '最远距离', unit: 'mm', yAxisIndex: 1 },
{ key: 'b4_enter_dist', name: '进入高度', unit: 'mm', yAxisIndex: 1 },
{ key: 'b4_leave_dist', name: '离开高度', unit: 'mm', yAxisIndex: 1 },
{ key: 'b4_enter_dist', name: '触发高度', unit: 'mm', yAxisIndex: 1 },
{ key: 'b4_leave_dist', name: '释放高度', unit: 'mm', yAxisIndex: 1 },
],
};
@@ -340,6 +366,7 @@ async function loadChart() {
if (!container || container.style.display === 'none') return;
const serial = document.getElementById('search-serial').value;
const detectorSerial = document.getElementById('search-detector-serial').value;
const dateFrom = getDatetime('search-date-from', 'search-time-from');
const dateTo = getDatetime('search-date-to', 'search-time-to');
const v = VIEWS[currentView];
@@ -349,6 +376,7 @@ async function loadChart() {
const params = new URLSearchParams();
if (serial) params.set('serial', serial);
if (detectorSerial) params.set('detector_serial', detectorSerial);
if (dateFrom) params.set('date_from', dateFrom);
if (dateTo) params.set('date_to', dateTo);
if (ds) params.set('data_source', ds);
@@ -457,7 +485,8 @@ async function loadChart() {
// ─── 初始加载 ────────────────────────────────────
renderHead();
searchData(1);
// 先加载型号名称再查询数据,确保型号列正确渲染
initDevTypeNames().then(() => searchData(1));
// ─── 删除admin─────────────────────────────────

View File

@@ -9,6 +9,7 @@ let autoStartTime = "";
let localSinceStr = "";
let currentTestMode = null; // 0=灵敏度, 1=波动, null=未加载
let currentDeviceState = null; // 当前设备状态 (0=离线 1=在线 2=通信不良 null=未加载)
let devTypeNameCache = {}; // type_num → dev_name 映射(从 tb_vechicle_base_test
let pollInterval = null;
let nextCmdTimer = null; // 间隔等待定时器
@@ -62,9 +63,12 @@ async function toggleAuto() {
async function startAuto() {
if (!checkDeviceOnline()) return;
const count = parseInt(document.getElementById("test-count").value) || 10;
const count = parseInt(document.getElementById("test-count").value) || 1;
if (count < 1) return;
// 读取车检器序列号
const detectorSerial = document.getElementById("detector-serial").value.trim();
// 读取参数
intervalMs = (parseFloat(document.getElementById("interval-sec").value) || 3) * 1000;
timeoutMs = (parseFloat(document.getElementById("timeout-sec").value) || 10) * 1000;
@@ -77,6 +81,8 @@ async function startAuto() {
autoFailed = 0;
autoRemaining = count;
lastDoneCount = 0;
newB2Count = 0;
updateRecordCount();
autoStartTime = new Date().toISOString();
const now = new Date();
@@ -115,7 +121,7 @@ async function startAuto() {
const resp = await fetch("/api/automation/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dnt_id: DNT_ID, count }),
body: JSON.stringify({ dnt_id: DNT_ID, count, detector_serial: detectorSerial }),
});
const data = await resp.json();
if (data.ok) {
@@ -147,6 +153,9 @@ function stopAuto() {
const btn = document.getElementById("btn-auto");
btn.textContent = "开始";
btn.className = "btn-start";
// 自动化结束 → 焦点回到车检器序列号输入框(全选),方便下一个车检器测试
setTimeout(focusDetectorSerial, 100);
}
// ─── 发送下一条 0xB0 ────────────────────────────
@@ -217,9 +226,15 @@ async function pollProgress() {
const stats = data.stats;
// ── 先渲染数据(放在所有 return 之前,避免完成时跳过渲染)──
try { if (data.latest) renderLatest(data.latest); } catch (e) { console.error("renderLatest:", e); }
try { if (data.latest) {
if (data.latest.id !== lastLatestId) {
lastLatestId = data.latest.id;
if (data.latest.data_source === "B2") { newB2Count++; updateRecordCount(); }
}
renderLatest(data.latest);
} } catch (e) { console.error("renderLatest:", e); }
try { if (data.averages) renderAverages(data.averages); } catch (e) { console.error("renderAverages:", e); }
try { if (data.latest_wave) renderLatestWave(data.latest_wave); } catch (e) { console.error("renderLatestWave:", e); }
try { if (data.latest_wave) { renderLatestWave(data.latest_wave); lastWaveId = data.latest_wave.id; } } catch (e) { console.error("renderLatestWave:", e); }
try { if (data.records && data.records.length) renderRecords(data.records); } catch (e) { console.error("renderRecords:", e); }
// 更新计数
@@ -273,6 +288,19 @@ async function pollProgress() {
// ─── 页面加载时获取初始数据 ──────────────────────
async function loadDeviceTypeNames() {
try {
const resp = await fetch(`/api/vehicle-base-test`);
const tests = await resp.json();
devTypeNameCache = {};
tests.forEach(t => {
if (t.type_num != null && t.dev_name) {
devTypeNameCache[t.type_num] = t.dev_name;
}
});
} catch (e) { console.error("加载型号名称失败:", e); }
}
async function loadTestMode() {
try {
const resp = await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`);
@@ -315,8 +343,8 @@ function renderConfigOverview(param) {
}
// 型号
const devTypeMap = {0: '未知', 1: 'PD132', 2: 'DLD110'};
document.getElementById("cfg-dev-type").textContent = devTypeMap[param.DevType] || `0x${(param.DevType||0).toString(16)}`;
document.getElementById("cfg-dev-type").textContent =
devTypeNameCache[param.DevType] || `0x${(param.DevType || 0).toString(16)}`;
// 距离 (DB cm → 显示 mm)
document.getElementById("cfg-reset-dis").textContent = param.RestDis != null ? param.RestDis * 10 : '-';
@@ -369,19 +397,50 @@ function toggleConfig() {
}
async function loadInitialData() {
await loadDeviceTypeNames();
await loadTestMode();
refreshDeviceStatus();
newB2Count = 0;
updateRecordCount();
try {
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
const data = await resp.json();
if (data.latest) renderLatest(data.latest);
if (data.latest_wave) renderLatestWave(data.latest_wave);
if (data.latest) { renderLatest(data.latest); lastLatestId = data.latest.id; }
if (data.latest_wave) { renderLatestWave(data.latest_wave); lastWaveId = data.latest_wave.id; }
} catch (e) {
// 初始加载静默失败
}
// 页面加载后焦点落到车检器序列号输入框(全选)
setTimeout(focusDetectorSerial, 200);
}
loadInitialData();
// ─── 回车键触发"开始"按钮 ─────────────────────
/** 将焦点移到车检器序列号输入框并全选已有文本 */
function focusDetectorSerial() {
const input = document.getElementById("detector-serial");
if (input) {
input.focus();
input.select();
}
}
document.addEventListener("DOMContentLoaded", function() {
const detectorInput = document.getElementById("detector-serial");
if (detectorInput) {
detectorInput.addEventListener("keydown", function(e) {
if (e.key === "Enter") {
e.preventDefault();
const btn = document.getElementById("btn-auto");
if (btn && !autoRunning) {
btn.click();
}
}
});
}
});
// ─── 设备状态异步刷新 ──────────────────────────
async function refreshDeviceStatus() {
@@ -407,8 +466,45 @@ async function refreshDeviceStatus() {
}
}
// 每 5 秒刷新设备状态 + 测试模式(工装页修改后能及时同步)
// ─── 被动轮询:实时显示上报数据(工装本地按键 / 网页手动指令触发)───
let lastLatestId = 0; // 最新测试数据 ID用于判断是否有新数据
let lastWaveId = 0; // 最新波动数据 ID
let newB2Count = 0; // 本轮新收到的 B2(灵敏度测试) 记录条数
function updateRecordCount() {
const el = document.getElementById("new-record-count");
if (el) el.textContent = newB2Count > 0 ? `(${newB2Count} 条新记录)` : "";
}
async function refreshLatestData() {
// 自动化运行中由 pollProgress 负责渲染,避免冲突
if (autoRunning) return;
try {
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
const data = await resp.json();
if (data.latest && data.latest.id !== lastLatestId) {
lastLatestId = data.latest.id;
renderLatest(data.latest);
// 仅 B2(灵敏度测试) 记录计数
if (data.latest.data_source === "B2") {
newB2Count++;
updateRecordCount();
}
}
if (data.latest_wave && data.latest_wave.id !== lastWaveId) {
lastWaveId = data.latest_wave.id;
renderLatestWave(data.latest_wave);
}
} catch (e) { /* 静默失败 */ }
}
// 最新测试数据每 3 秒轮询
setInterval(refreshLatestData, 3000);
// 每 5 秒刷新设备状态 + 测试模式 + 型号名称缓存(工装页修改后能及时同步)
async function refreshAll() {
await loadDeviceTypeNames();
await loadTestMode();
refreshDeviceStatus();
}
@@ -475,17 +571,23 @@ function fmtRelay(s) {
function renderLatest(data) {
const div = document.getElementById("latest-result");
// 优先使用 str_type为空时从缓存查找
let typeName = data.str_type;
if (!typeName && data.sub_type != null) {
typeName = devTypeNameCache[data.sub_type] || `Unknown(${data.sub_type})`;
}
div.innerHTML = `
<p>设备型号:<strong>${data.str_type || '-'}</strong></p>
<p>车检器序列号:<strong>${data.detector_serial || '-'}</strong></p>
<p>设备型号:<strong>${typeName || '-'}</strong></p>
<p>测试模式:<strong>${data.test_mode === 1 ? '波动测试' : '灵敏度测试'}</strong></p>
<p>峰峰值:${data.ppvalue?.toFixed(2) || '-'} V</p>
<p>开始工作频率:${data.idle_freq || '-'} Hz</p>
<p>进入工作频率:${data.enter_freq || '-'} Hz</p>
<p>离开工作频率:${data.exit_freq || '-'} Hz</p>
<p>进入距离:${data.enter_dist || '-'} mm</p>
<p>离开距离:${data.exit_dist || '-'} mm</p>
<p>进入速度:${toSpeed(data.enter_speed)} m/s</p>
<p>离开速度:${toSpeed(data.exit_speed)} m/s</p>
<p>开始频率:${data.idle_freq || '-'} Hz</p>
<p>触发频率:${data.enter_freq || '-'} Hz</p>
<p>释放频率:${data.exit_freq || '-'} Hz</p>
<p>触发距离:${data.enter_dist || '-'} mm</p>
<p>释放距离:${data.exit_dist || '-'} mm</p>
<p>触发速度:${toSpeed(data.enter_speed)} m/s</p>
<p>释放速度:${toSpeed(data.exit_speed)} m/s</p>
<p>是否完成:${data.iffinish === '1' ? '是' : '否'}</p>
<p>故障信息:${data.fault_info || '无'}</p>
<p>继电器:${fmtRelay(data.relay_out) || decodeRelay(data.relay_code)}</p>
@@ -527,8 +629,8 @@ function renderLatestWave(data) {
<p>当前速度:${toSpeed(data.speed)} m/s</p>
<p>最近距离:${data.near_dist || '-'} mm</p>
<p>最远距离:${data.far_dist || '-'} mm</p>
<p>进入高度 (B4)${data.b4_enter_dist || '-'} mm</p>
<p>离开高度 (B4)${data.b4_leave_dist || '-'} mm</p>
<p>触发高度 (B4)${data.b4_enter_dist || '-'} mm</p>
<p>释放高度 (B4)${data.b4_leave_dist || '-'} mm</p>
<p>继电器:${fmtRelay(data.relay_out) || decodeRelay(data.relay_code)}</p>
<p>时间:${fmtTime(data.create_time)}</p>
`;

View File

@@ -21,7 +21,11 @@
<div class="search-bar">
<label>
设备编码:
<input type="text" id="search-serial" placeholder="输入设备编码搜索...">
<input type="text" id="search-serial" placeholder="设备编码...">
</label>
<label>
车检器序列号:
<input type="text" id="search-detector-serial" placeholder="车检器序列号...">
</label>
<label>
时间范围:
@@ -38,7 +42,7 @@
<select id="per-page" onchange="searchData(1)" style="width:70px;">
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="100" selected>100</option>
</select>
</label>
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>

View File

@@ -58,17 +58,22 @@
<h3>自动化测试</h3>
<div class="automation">
<label>
车检器序列号:
<input type="text" id="detector-serial" placeholder="选填" style="width:180px;">
</label>
<br style="margin-bottom:8px;">
<label>
测试次数:
<input type="number" id="test-count" value="10" min="1" max="9999">
<input type="number" id="test-count" value="1" min="1" max="9999">
</label>
<label style="margin-left:16px;">
间隔时间(秒)
<input type="number" id="interval-sec" value="10" min="0" max="300" style="width:60px;">
<input type="number" id="interval-sec" value="5" min="0" max="300" style="width:60px;">
</label>
<label style="margin-left:16px;">
超时时间(秒)
<input type="number" id="timeout-sec" value="5" min="1" max="600" style="width:60px;">
<input type="number" id="timeout-sec" value="4" min="1" max="600" style="width:60px;">
</label>
<button id="btn-auto" class="btn-start" onclick="toggleAuto()">开始</button>
<div class="progress-container">
@@ -89,7 +94,7 @@
<!-- 右侧:测试信息显示区 -->
<div class="test-info">
<h3>当前测试数据</h3>
<h3>当前测试数据 <span id="new-record-count" style="font-size:12px;color:#888;font-weight:normal;margin-left:8px;"></span></h3>
<div id="latest-result">
<p class="placeholder">等待设备上报...</p>
</div>
@@ -104,12 +109,12 @@
<h3>自动化平均值</h3>
<table id="avg-table">
<tr><td>平均峰峰值</td><td id="avg-ppvalue">-</td><td>V</td></tr>
<tr><td>平均开始工作频率</td><td id="avg-idle-freq">-</td><td>Hz</td></tr>
<tr><td>平均进入工作频率</td><td id="avg-enter-freq">-</td><td>Hz</td></tr>
<tr><td>平均进入距离</td><td id="avg-enter-dist">-</td><td>mm</td></tr>
<tr><td>平均离开距离</td><td id="avg-exit-dist">-</td><td>mm</td></tr>
<tr><td>平均进入速度</td><td id="avg-enter-speed">-</td><td>m/s</td></tr>
<tr><td>平均离开速度</td><td id="avg-exit-speed">-</td><td>m/s</td></tr>
<tr><td>平均开始频率</td><td id="avg-idle-freq">-</td><td>Hz</td></tr>
<tr><td>平均触发频率</td><td id="avg-enter-freq">-</td><td>Hz</td></tr>
<tr><td>平均触发距离</td><td id="avg-enter-dist">-</td><td>mm</td></tr>
<tr><td>平均释放距离</td><td id="avg-exit-dist">-</td><td>mm</td></tr>
<tr><td>平均触发速度</td><td id="avg-enter-speed">-</td><td>m/s</td></tr>
<tr><td>平均释放速度</td><td id="avg-exit-speed">-</td><td>m/s</td></tr>
</table>
<h3 style="margin-top:20px;">本轮测试明细</h3>
@@ -117,7 +122,7 @@
<table id="records-table" style="font-size:11px;">
<thead>
<tr>
<th>#</th><th>串口状态</th><th>模式</th><th>峰峰值(V)</th><th>开始频率</th><th>进入距离</th><th>离开距离</th><th>速度(m/s)</th><th>时间</th>
<th>#</th><th>串口状态</th><th>模式</th><th>峰峰值(V)</th><th>开始频率</th><th>触发距离</th><th>释放距离</th><th>速度(m/s)</th><th>时间</th>
</tr>
</thead>
<tbody></tbody>