feat: 工装配置页面 + 车检器基准参数管理
- 设备页增加「配置」按钮(devices.js) - 新增工装配置页面(fixture.html+js): 参数表单、5个操作按钮、通信日志区、基准参数表 - 新增车检器基准参数管理页面(vehicle_base_test.html+js): CRUD + 搜索 - 新增 fixture 蓝图(routes/fixture.py): 0x4A~0x4E 指令发送、参数CRUD、serialnet状态查询 - models.py: 新增 get_serialnet_by_id, tb_fixture_param/tb_vechicle_base_test CRUD - edc_server 子模块更新
This commit is contained in:
@@ -17,12 +17,14 @@ def create_app() -> Flask:
|
|||||||
from app.routes.devices import bp as devices_bp
|
from app.routes.devices import bp as devices_bp
|
||||||
from app.routes.test_op import bp as test_op_bp
|
from app.routes.test_op import bp as test_op_bp
|
||||||
from app.routes.test_data import bp as test_data_bp
|
from app.routes.test_data import bp as test_data_bp
|
||||||
|
from app.routes.fixture import bp as fixture_bp
|
||||||
from app.routes.users import bp as users_bp
|
from app.routes.users import bp as users_bp
|
||||||
from app.routes.logs import bp as logs_bp
|
from app.routes.logs import bp as logs_bp
|
||||||
|
|
||||||
app.register_blueprint(devices_bp)
|
app.register_blueprint(devices_bp)
|
||||||
app.register_blueprint(test_op_bp)
|
app.register_blueprint(test_op_bp)
|
||||||
app.register_blueprint(test_data_bp)
|
app.register_blueprint(test_data_bp)
|
||||||
|
app.register_blueprint(fixture_bp)
|
||||||
app.register_blueprint(users_bp)
|
app.register_blueprint(users_bp)
|
||||||
app.register_blueprint(logs_bp)
|
app.register_blueprint(logs_bp)
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,19 @@ def get_serialnet_records(dnt_id: int, limit: int = 50) -> list[dict]:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_serialnet_by_id(record_id: int) -> dict | None:
|
||||||
|
"""根据 ID 获取 tb_serialnet 记录"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_serialnet WHERE id=%s", (record_id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def clear_serialnet_records(dnt_id: int):
|
def clear_serialnet_records(dnt_id: int):
|
||||||
"""清除指定设备的所有透传记录"""
|
"""清除指定设备的所有透传记录"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -375,3 +388,143 @@ def get_logs(page: int = 1, per_page: int = 30,
|
|||||||
return cur.fetchall(), total
|
return cur.fetchall(), total
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── tb_fixture_param ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_fixture_param(dnt_id: int) -> dict | None:
|
||||||
|
"""获取设备的工装测试参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_fixture_param(dnt_id: int, **kwargs):
|
||||||
|
"""插入或更新工装测试参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,),
|
||||||
|
)
|
||||||
|
existing = cur.fetchone()
|
||||||
|
fields = [
|
||||||
|
"Addr", "DevType", "TestMode", "RestDis", "MinusDis",
|
||||||
|
"SensMin", "SensMax", "FreMin", "FreMax", "PeakMin", "PeakMax",
|
||||||
|
]
|
||||||
|
if existing:
|
||||||
|
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
||||||
|
values = [kwargs.get(f, 0) for f in fields] + [dnt_id]
|
||||||
|
cur.execute(
|
||||||
|
f"UPDATE tb_fixture_param SET {sets} WHERE dnt_id=%s",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
placeholders = ", ".join(["%s"] * len(fields))
|
||||||
|
col_names = ", ".join(f"`{f}`" for f in fields)
|
||||||
|
values = [kwargs.get(f, 0) for f in fields]
|
||||||
|
cur.execute(
|
||||||
|
f"INSERT INTO tb_fixture_param (dnt_id, {col_names}) "
|
||||||
|
f"VALUES (%s, {placeholders})",
|
||||||
|
[dnt_id] + values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── tb_vechicle_base_test ─────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_vehicle_base_tests(search: str = "") -> list[dict]:
|
||||||
|
"""获取车检器测试基准参数列表"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if search:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_vechicle_base_test "
|
||||||
|
"WHERE dev_name LIKE %s OR type_num LIKE %s "
|
||||||
|
"ORDER BY type_num ASC",
|
||||||
|
(f"%{search}%", f"%{search}%"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_vechicle_base_test ORDER BY type_num ASC",
|
||||||
|
)
|
||||||
|
return cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vehicle_base_test_by_id(test_id: int) -> dict | None:
|
||||||
|
"""根据 ID 获取车检器测试基准"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_vechicle_base_test WHERE id=%s", (test_id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_vehicle_base_test(**kwargs) -> int:
|
||||||
|
"""创建车检器测试基准,返回新记录 ID"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
fields = [
|
||||||
|
"dev_name", "type_num", "SensMin", "SensMax",
|
||||||
|
"FreMin", "FreMax", "PeakMin", "PeakMax", "remark",
|
||||||
|
]
|
||||||
|
col_names = ", ".join(f"`{f}`" for f in fields)
|
||||||
|
placeholders = ", ".join(["%s"] * len(fields))
|
||||||
|
values = [kwargs.get(f, "" if f in ("dev_name", "remark") else 0) for f in fields]
|
||||||
|
cur.execute(
|
||||||
|
f"INSERT INTO tb_vechicle_base_test ({col_names}) VALUES ({placeholders})",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_vehicle_base_test(test_id: int, **kwargs):
|
||||||
|
"""更新车检器测试基准"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
fields = [
|
||||||
|
"dev_name", "type_num", "SensMin", "SensMax",
|
||||||
|
"FreMin", "FreMax", "PeakMin", "PeakMax", "remark",
|
||||||
|
]
|
||||||
|
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
||||||
|
values = [kwargs.get(f, "" if f in ("dev_name", "remark") else 0) for f in fields] + [test_id]
|
||||||
|
cur.execute(
|
||||||
|
f"UPDATE tb_vechicle_base_test SET {sets} WHERE id=%s",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_vehicle_base_test(test_id: int):
|
||||||
|
"""删除车检器测试基准"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM tb_vechicle_base_test WHERE id=%s", (test_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
258
edc-web/app/routes/fixture.py
Normal file
258
edc-web/app/routes/fixture.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
"""工装配置 API"""
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app.models import (
|
||||||
|
get_device_by_id,
|
||||||
|
insert_serialnet,
|
||||||
|
get_fixture_param,
|
||||||
|
upsert_fixture_param,
|
||||||
|
get_vehicle_base_tests,
|
||||||
|
get_vehicle_base_test_by_id,
|
||||||
|
create_vehicle_base_test,
|
||||||
|
update_vehicle_base_test,
|
||||||
|
delete_vehicle_base_test,
|
||||||
|
get_serialnet_by_id,
|
||||||
|
insert_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
bp = Blueprint("fixture", __name__)
|
||||||
|
|
||||||
|
# DG430 工装配置指令 (addr=0x01)
|
||||||
|
FIXTURE_COMMANDS = {
|
||||||
|
"4A": "7F81014ACACC", # 获取设备版本号
|
||||||
|
"4C": "7F81014CCCCE", # 查询设备测试参数
|
||||||
|
"4D": "7F81014DCDCF", # 出厂初始化
|
||||||
|
"4E": "7F81014ECED0", # 设备复位
|
||||||
|
}
|
||||||
|
|
||||||
|
CMD_NAMES = {
|
||||||
|
"4A": "获取设备版本号",
|
||||||
|
"4B": "配置设备测试参数",
|
||||||
|
"4C": "查询设备测试参数",
|
||||||
|
"4D": "出厂初始化",
|
||||||
|
"4E": "设备复位",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _xor_sum(data: bytes) -> tuple[int, int]:
|
||||||
|
"""计算异或和校验 (从 ADDR 到 DATA 末)"""
|
||||||
|
xor = 0
|
||||||
|
s = 0
|
||||||
|
for b in data:
|
||||||
|
xor ^= b
|
||||||
|
s += b
|
||||||
|
return xor & 0xFF, s & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def _le16(val: int) -> bytes:
|
||||||
|
"""int → 小端 2 字节"""
|
||||||
|
return bytes([val & 0xFF, (val >> 8) & 0xFF])
|
||||||
|
|
||||||
|
|
||||||
|
def _be16(val: int) -> bytes:
|
||||||
|
"""int → 大端 2 字节"""
|
||||||
|
return bytes([(val >> 8) & 0xFF, val & 0xFF])
|
||||||
|
|
||||||
|
|
||||||
|
def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
||||||
|
reset_dis: int, minus_dis: int,
|
||||||
|
sens_min: int, sens_max: int,
|
||||||
|
fre_min: int, fre_max: int,
|
||||||
|
peak_min: int, peak_max: int) -> str:
|
||||||
|
"""构造 0x4B 配置指令 hex 字符串
|
||||||
|
|
||||||
|
格式: 7F | 81 | 12 | 4B | Addr(1) | DevType(1) | TestMode(1) |
|
||||||
|
ResetDis(1) | MinusDis(1) |
|
||||||
|
SensMin(2 LE) | SensMax(2 LE) | FreMin(2 LE) | FreMax(2 LE) |
|
||||||
|
PeakMin(2 LE) | PeakMax(2 LE) | XOR | SUM
|
||||||
|
"""
|
||||||
|
payload = bytes([
|
||||||
|
0x4B, # CMD
|
||||||
|
addr & 0xFF,
|
||||||
|
dev_type & 0xFF,
|
||||||
|
test_mode & 0xFF,
|
||||||
|
reset_dis & 0xFF,
|
||||||
|
minus_dis & 0xFF,
|
||||||
|
])
|
||||||
|
payload += (_le16(sens_min) + _le16(sens_max) +
|
||||||
|
_le16(fre_min) + _le16(fre_max) +
|
||||||
|
_le16(peak_min) + _le16(peak_max))
|
||||||
|
|
||||||
|
pkt = bytes([0x7F, 0x81, len(payload)]) + payload
|
||||||
|
xor, total = _xor_sum(pkt[1:])
|
||||||
|
pkt += bytes([xor, total])
|
||||||
|
return pkt.hex().upper()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 页面 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route("/fixture/<int:dnt_id>")
|
||||||
|
@login_required
|
||||||
|
def fixture_page(dnt_id):
|
||||||
|
"""工装配置页面"""
|
||||||
|
device = get_device_by_id(dnt_id)
|
||||||
|
if not device:
|
||||||
|
return "设备不存在", 404
|
||||||
|
return render_template("fixture.html", device=device)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/vehicle-base-test")
|
||||||
|
@login_required
|
||||||
|
def vehicle_base_test_page():
|
||||||
|
"""车检器测试基准参数管理页面"""
|
||||||
|
return render_template("vehicle_base_test.html")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 工装配置指令 API ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route("/api/fixture/command", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_fixture_command():
|
||||||
|
"""发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)"""
|
||||||
|
data = request.get_json()
|
||||||
|
dnt_id = data.get("dnt_id")
|
||||||
|
cmd = data.get("cmd", "").upper()
|
||||||
|
|
||||||
|
device = get_device_by_id(dnt_id)
|
||||||
|
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
||||||
|
|
||||||
|
if cmd == "4B":
|
||||||
|
# 动态构造 0x4B 指令
|
||||||
|
params = data.get("params", {})
|
||||||
|
send_pkg = build_4b_packet(
|
||||||
|
addr=params.get("addr", 1),
|
||||||
|
dev_type=params.get("dev_type", 0),
|
||||||
|
test_mode=params.get("test_mode", 0),
|
||||||
|
reset_dis=params.get("reset_dis", 0),
|
||||||
|
minus_dis=params.get("minus_dis", 0),
|
||||||
|
sens_min=params.get("sens_min", 0),
|
||||||
|
sens_max=params.get("sens_max", 0),
|
||||||
|
fre_min=params.get("fre_min", 0),
|
||||||
|
fre_max=params.get("fre_max", 0),
|
||||||
|
peak_min=params.get("peak_min", 0),
|
||||||
|
peak_max=params.get("peak_max", 0),
|
||||||
|
)
|
||||||
|
elif cmd in FIXTURE_COMMANDS:
|
||||||
|
send_pkg = FIXTURE_COMMANDS[cmd]
|
||||||
|
else:
|
||||||
|
return jsonify({"ok": False, "error": f"未知指令: {cmd}"}), 400
|
||||||
|
|
||||||
|
cmd_name = CMD_NAMES.get(cmd, cmd)
|
||||||
|
try:
|
||||||
|
record_id = insert_serialnet(dnt_id, send_pkg)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "command",
|
||||||
|
target=target,
|
||||||
|
detail=f"工装配置: {cmd_name}(0x{cmd}) → {send_pkg}",
|
||||||
|
result="ok",
|
||||||
|
ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True, "record_id": record_id, "send_pkg": send_pkg})
|
||||||
|
except Exception as e:
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "command",
|
||||||
|
target=target,
|
||||||
|
detail=f"工装配置 {cmd_name}(0x{cmd}) 失败: {e}",
|
||||||
|
result="error",
|
||||||
|
ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/fixture/serialnet/<int:record_id>")
|
||||||
|
@login_required
|
||||||
|
def api_get_serialnet(record_id):
|
||||||
|
"""查询 tb_serialnet 记录状态和返回数据"""
|
||||||
|
rec = get_serialnet_by_id(record_id)
|
||||||
|
if not rec:
|
||||||
|
return jsonify({"error": "记录不存在"}), 404
|
||||||
|
return jsonify({
|
||||||
|
"id": rec["id"],
|
||||||
|
"state": rec["state"],
|
||||||
|
"send_pkg": rec.get("send_pkg", ""),
|
||||||
|
"rcv_pkg": rec.get("rcv_pkg", ""),
|
||||||
|
"create_time": str(rec.get("create_time", "")),
|
||||||
|
"update_time": str(rec.get("update_time", "")),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 工装参数 CRUD API ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route("/api/fixture/param/<int:dnt_id>")
|
||||||
|
@login_required
|
||||||
|
def api_get_fixture_param(dnt_id):
|
||||||
|
"""获取工装测试参数"""
|
||||||
|
param = get_fixture_param(dnt_id)
|
||||||
|
return jsonify(param or {})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/fixture/param/<int:dnt_id>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_save_fixture_param(dnt_id):
|
||||||
|
"""保存工装测试参数(仅数据库,不下发设备)"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
upsert_fixture_param(dnt_id, **data)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 车检器测试基准参数 CRUD API ──────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route("/api/vehicle-base-test")
|
||||||
|
@login_required
|
||||||
|
def api_list_vehicle_base_tests():
|
||||||
|
"""列出车检器测试基准参数"""
|
||||||
|
search = request.args.get("search", "")
|
||||||
|
tests = get_vehicle_base_tests(search)
|
||||||
|
return jsonify(tests)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/vehicle-base-test/<int:test_id>")
|
||||||
|
@login_required
|
||||||
|
def api_get_vehicle_base_test(test_id):
|
||||||
|
"""获取单个车检器测试基准"""
|
||||||
|
test = get_vehicle_base_test_by_id(test_id)
|
||||||
|
if not test:
|
||||||
|
return jsonify({"error": "不存在"}), 404
|
||||||
|
return jsonify(test)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/vehicle-base-test", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_create_vehicle_base_test():
|
||||||
|
"""创建车检器测试基准"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
try:
|
||||||
|
test_id = create_vehicle_base_test(**data)
|
||||||
|
return jsonify({"ok": True, "id": test_id})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/vehicle-base-test/<int:test_id>", methods=["PUT"])
|
||||||
|
@login_required
|
||||||
|
def api_update_vehicle_base_test(test_id):
|
||||||
|
"""更新车检器测试基准"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
try:
|
||||||
|
update_vehicle_base_test(test_id, **data)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/vehicle-base-test/<int:test_id>", methods=["DELETE"])
|
||||||
|
@login_required
|
||||||
|
def api_delete_vehicle_base_test(test_id):
|
||||||
|
"""删除车检器测试基准"""
|
||||||
|
try:
|
||||||
|
delete_vehicle_base_test(test_id)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
@@ -32,6 +32,8 @@ tr:hover { background: #f8f9fa; }
|
|||||||
/* === Buttons === */
|
/* === Buttons === */
|
||||||
.btn-test { padding: 4px 14px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
.btn-test { padding: 4px 14px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||||
.btn-test:hover { background: #2980b9; }
|
.btn-test:hover { background: #2980b9; }
|
||||||
|
.btn-config { padding: 4px 14px; background: #9b59b6; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 4px; }
|
||||||
|
.btn-config:hover { background: #8e44ad; }
|
||||||
|
|
||||||
/* === Test Page Layout === */
|
/* === Test Page Layout === */
|
||||||
.test-header { margin-bottom: 20px; }
|
.test-header { margin-bottom: 20px; }
|
||||||
@@ -103,3 +105,67 @@ tr:hover { background: #f8f9fa; }
|
|||||||
.search-bar { justify-content: center; }
|
.search-bar { justify-content: center; }
|
||||||
.btn-export { margin-left: 0; }
|
.btn-export { margin-left: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Fixture Page === */
|
||||||
|
.fixture-layout { display: flex; gap: 24px; }
|
||||||
|
.fixture-left { flex: 1; min-width: 420px; }
|
||||||
|
.fixture-right { flex: 1; min-width: 380px; }
|
||||||
|
|
||||||
|
.fixture-card { background: #fff; padding: 16px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); margin-bottom: 16px; }
|
||||||
|
.fixture-card h3 { margin: 0 0 12px; font-size: 15px; color: #555; }
|
||||||
|
|
||||||
|
.fixture-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
|
||||||
|
.fixture-form .form-group { display: flex; flex-direction: column; }
|
||||||
|
.fixture-form .form-group label { font-size: 12px; color: #777; margin-bottom: 3px; }
|
||||||
|
.fixture-form .form-group input,
|
||||||
|
.fixture-form .form-group select { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
|
||||||
|
|
||||||
|
.fixture-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||||
|
.btn-fixture { padding: 8px 14px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; font-size: 12px; transition: .15s; background: #ecf0f1; }
|
||||||
|
.btn-fixture:hover { background: #3498db; color: #fff; border-color: #3498db; }
|
||||||
|
.btn-fixture.danger { color: #e74c3c; border-color: #e74c3c; }
|
||||||
|
.btn-fixture.danger:hover { background: #e74c3c; color: #fff; }
|
||||||
|
.btn-fixture.primary { background: #3498db; color: #fff; border-color: #3498db; }
|
||||||
|
.btn-fixture.primary:hover { background: #2980b9; }
|
||||||
|
|
||||||
|
.version-info { margin-top: 8px; padding: 8px 12px; background: #f0f8ff; border-radius: 4px; font-size: 13px; color: #2980b9; }
|
||||||
|
|
||||||
|
.msg-toast { position: fixed; top: 20px; right: 20px; background: #27ae60; color: #fff; padding: 10px 20px; border-radius: 6px; font-size: 14px; z-index: 1000; opacity: 0; transition: opacity .3s; }
|
||||||
|
.msg-toast.show { opacity: 1; }
|
||||||
|
.msg-toast.error { background: #e74c3c; }
|
||||||
|
|
||||||
|
/* === Vehicle base test table in fixture page === */
|
||||||
|
.ref-table-wrapper { max-height: 300px; overflow-y: auto; margin-top: 8px; }
|
||||||
|
.ref-table-wrapper table { font-size: 12px; }
|
||||||
|
.ref-table-wrapper th, .ref-table-wrapper td { padding: 6px 8px; }
|
||||||
|
.ref-table-wrapper tr { cursor: pointer; }
|
||||||
|
.ref-table-wrapper tr:hover { background: #ebf5fb; }
|
||||||
|
.ref-table-wrapper tr.selected { background: #d4e6f1; }
|
||||||
|
|
||||||
|
/* === Vehicle Base Test management === */
|
||||||
|
.vbt-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||||
|
.btn-add { padding: 8px 18px; background: #27ae60; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
||||||
|
.btn-add:hover { background: #219a52; }
|
||||||
|
.btn-edit { padding: 3px 10px; background: #f39c12; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; }
|
||||||
|
.btn-edit:hover { background: #e67e22; }
|
||||||
|
.btn-del { padding: 3px 10px; background: #e74c3c; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; margin-left: 4px; }
|
||||||
|
.btn-del:hover { background: #c0392b; }
|
||||||
|
|
||||||
|
/* === Modal === */
|
||||||
|
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center; z-index: 999; }
|
||||||
|
.modal-box { background: #fff; border-radius: 10px; padding: 24px; width: 480px; max-width: 95vw; box-shadow: 0 8px 32px rgba(0,0,0,.15); }
|
||||||
|
.modal-box h3 { margin: 0 0 16px; font-size: 16px; }
|
||||||
|
.modal-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
|
||||||
|
.modal-form .form-group { display: flex; flex-direction: column; }
|
||||||
|
.modal-form .form-group.full { grid-column: 1 / -1; }
|
||||||
|
.modal-form label { font-size: 12px; color: #777; margin-bottom: 3px; }
|
||||||
|
.modal-form input, .modal-form select, .modal-form textarea { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
|
||||||
|
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||||
|
.btn-save { padding: 8px 20px; background: #3498db; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
.btn-save:hover { background: #2980b9; }
|
||||||
|
.btn-cancel { padding: 8px 20px; background: #ecf0f1; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; }
|
||||||
|
|
||||||
|
/* === Communication Log === */
|
||||||
|
#comm-log { word-break: break-all; }
|
||||||
|
#comm-log::-webkit-scrollbar { width: 6px; }
|
||||||
|
#comm-log::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function renderTable(devices) {
|
|||||||
<td>${d.last_login || '-'}</td>
|
<td>${d.last_login || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
||||||
|
<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join("");
|
`).join("");
|
||||||
|
|||||||
338
edc-web/app/static/js/fixture.js
Normal file
338
edc-web/app/static/js/fixture.js
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
// 工装配置页
|
||||||
|
|
||||||
|
let baseTests = []; // 所有车检器基准参数
|
||||||
|
let selectedBaseTest = null;
|
||||||
|
let pollTimer4C = null; // 0x4C 参数查询轮询
|
||||||
|
let pollTimers = {}; // record_id → timer (指令响应轮询)
|
||||||
|
|
||||||
|
// ─── 初始化 ─────────────────────────────────
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await loadBaseTests();
|
||||||
|
await loadFixtureParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Toast 提示 ─────────────────────────────
|
||||||
|
|
||||||
|
function toast(msg, isError = false) {
|
||||||
|
const el = document.getElementById("toast");
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = "msg-toast " + (isError ? "error" : "") + " show";
|
||||||
|
clearTimeout(el._timeout);
|
||||||
|
el._timeout = setTimeout(() => { el.className = "msg-toast"; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 通信日志 ───────────────────────────────
|
||||||
|
|
||||||
|
function commLog(type, hex, detail) {
|
||||||
|
const el = document.getElementById("comm-log");
|
||||||
|
if (el.querySelector('div') && el.querySelector('div').textContent === '等待操作…') {
|
||||||
|
el.innerHTML = '';
|
||||||
|
}
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const colors = { send: '#4ec9b0', recv: '#ce9178', ok: '#6a9955', fail: '#f44747', info: '#888' };
|
||||||
|
const labels = { send: '→ 发送', recv: '← 收到', ok: '✓ 成功', fail: '✗ 失败', info: '· 信息' };
|
||||||
|
const color = colors[type] || '#888';
|
||||||
|
const label = labels[type] || '';
|
||||||
|
const html = `<div style="margin-bottom:3px;">
|
||||||
|
<span style="color:#569cd6;">${time}</span>
|
||||||
|
<span style="color:${color}; font-weight:600;"> ${label}</span>
|
||||||
|
<span style="color:#aaa;"> ${detail || ''}</span>
|
||||||
|
${hex ? `<br><span style="color:#808080; padding-left:10px;">${hex}</span>` : ''}
|
||||||
|
</div>`;
|
||||||
|
el.insertAdjacentHTML('afterbegin', html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 解析 Flag 响应 ──────────────────────────
|
||||||
|
|
||||||
|
const FLAG_CMDS = { '4B': '配置参数', '4D': '出厂初始化', '4E': '设备复位' };
|
||||||
|
|
||||||
|
function parseFlagResponse(rcvPkg, cmd) {
|
||||||
|
// 格式: 7F8102{CMD}{FLAG}{XOR}{SUM} → FLAG 在位置 8-9
|
||||||
|
if (!rcvPkg || rcvPkg.length < 10) return null;
|
||||||
|
const flag = parseInt(rcvPkg.substring(8, 10), 16);
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 加载车检器测试基准 ──────────────────────
|
||||||
|
|
||||||
|
async function loadBaseTests() {
|
||||||
|
const search = document.getElementById("ref-search").value;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/vehicle-base-test?search=${encodeURIComponent(search)}`);
|
||||||
|
baseTests = await resp.json();
|
||||||
|
renderBaseTestTable();
|
||||||
|
populateDevTypeSelect();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载基准参数失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBaseTestTable() {
|
||||||
|
const tbody = document.getElementById("ref-table-body");
|
||||||
|
if (!baseTests.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" style="color:#999;text-align:center;">暂无数据</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = baseTests.map(t => `
|
||||||
|
<tr onclick="selectBaseTest(${t.id})" data-id="${t.id}"
|
||||||
|
class="${selectedBaseTest && selectedBaseTest.id === t.id ? 'selected' : ''}">
|
||||||
|
<td>${t.type_num}</td>
|
||||||
|
<td>${esc(t.dev_name)}</td>
|
||||||
|
<td>${t.SensMin}~${t.SensMax}</td>
|
||||||
|
<td>${t.FreMin}~${t.FreMax}</td>
|
||||||
|
<td>${t.PeakMin}~${t.PeakMax}</td>
|
||||||
|
</tr>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateDevTypeSelect() {
|
||||||
|
const sel = document.getElementById("param-dev-type");
|
||||||
|
sel.innerHTML = '<option value="0">-- 请选择车检器基准 --</option>' +
|
||||||
|
baseTests.map(t => `<option value="${t.type_num}">${t.type_num} - ${esc(t.dev_name)}</option>`).join("");
|
||||||
|
if (selectedBaseTest) {
|
||||||
|
sel.value = selectedBaseTest.type_num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBaseTest(id) {
|
||||||
|
selectedBaseTest = baseTests.find(t => t.id === id);
|
||||||
|
if (!selectedBaseTest) return;
|
||||||
|
fillFromBaseTest(selectedBaseTest);
|
||||||
|
renderBaseTestTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillFromBaseTest(t) {
|
||||||
|
document.getElementById("param-dev-type").value = t.type_num;
|
||||||
|
document.getElementById("param-sens-min").value = t.SensMin;
|
||||||
|
document.getElementById("param-sens-max").value = t.SensMax;
|
||||||
|
document.getElementById("param-fre-min").value = t.FreMin;
|
||||||
|
document.getElementById("param-fre-max").value = t.FreMax;
|
||||||
|
document.getElementById("param-peak-min").value = t.PeakMin;
|
||||||
|
document.getElementById("param-peak-max").value = t.PeakMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDevTypeChange() {
|
||||||
|
const typeNum = parseInt(document.getElementById("param-dev-type").value);
|
||||||
|
const matched = baseTests.find(t => t.type_num === typeNum);
|
||||||
|
if (matched) selectBaseTest(matched.id);
|
||||||
|
else { selectedBaseTest = null; renderBaseTestTable(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 从 DB 加载/刷新/保存 ────────────────────
|
||||||
|
|
||||||
|
async function loadFixtureParam() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/fixture/param/${DNT_ID}`);
|
||||||
|
const param = await resp.json();
|
||||||
|
if (param && param.dnt_id) {
|
||||||
|
fillFormFromParam(param);
|
||||||
|
commLog('info', null, '已从数据库加载工装参数');
|
||||||
|
}
|
||||||
|
} catch (e) { console.error("加载工装参数失败:", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillFormFromParam(param) {
|
||||||
|
document.getElementById("param-addr").value = param.Addr || 1;
|
||||||
|
document.getElementById("param-test-mode").value = param.TestMode || 0;
|
||||||
|
document.getElementById("param-reset-dis").value = param.RestDis || 0;
|
||||||
|
document.getElementById("param-minus-dis").value = param.MinusDis || 0;
|
||||||
|
document.getElementById("param-dev-type").value = param.DevType || 0;
|
||||||
|
document.getElementById("param-sens-min").value = param.SensMin || 0;
|
||||||
|
document.getElementById("param-sens-max").value = param.SensMax || 0;
|
||||||
|
document.getElementById("param-fre-min").value = param.FreMin || 0;
|
||||||
|
document.getElementById("param-fre-max").value = param.FreMax || 0;
|
||||||
|
document.getElementById("param-peak-min").value = param.PeakMin || 0;
|
||||||
|
document.getElementById("param-peak-max").value = param.PeakMax || 0;
|
||||||
|
const matched = baseTests.find(t => t.type_num === param.DevType);
|
||||||
|
if (matched) { selectedBaseTest = matched; renderBaseTestTable(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshParams() {
|
||||||
|
const param = await (await fetch(`/api/fixture/param/${DNT_ID}`)).json();
|
||||||
|
if (param && param.dnt_id) {
|
||||||
|
fillFormFromParam(param);
|
||||||
|
commLog('info', null, '已刷新:从数据库加载工装参数');
|
||||||
|
toast("已刷新");
|
||||||
|
} else {
|
||||||
|
toast("数据库中没有该工装的参数,请先查询(0x4C)或保存", true);
|
||||||
|
commLog('info', null, '刷新失败:数据库中没有参数');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveToDb() {
|
||||||
|
const data = getFormParams();
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/fixture/param/${DNT_ID}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
Addr: data.addr, DevType: data.dev_type, TestMode: data.test_mode,
|
||||||
|
RestDis: data.reset_dis, MinusDis: data.minus_dis,
|
||||||
|
SensMin: data.sens_min, SensMax: data.sens_max,
|
||||||
|
FreMin: data.fre_min, FreMax: data.fre_max,
|
||||||
|
PeakMin: data.peak_min, PeakMax: data.peak_max,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok) {
|
||||||
|
commLog('info', null, '参数已保存到数据库');
|
||||||
|
toast("已保存到数据库");
|
||||||
|
} else {
|
||||||
|
toast("保存失败: " + (result.error || ""), true);
|
||||||
|
}
|
||||||
|
} catch (e) { toast("保存失败: " + e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormParams() {
|
||||||
|
return {
|
||||||
|
addr: parseInt(document.getElementById("param-addr").value) || 1,
|
||||||
|
dev_type: parseInt(document.getElementById("param-dev-type").value) || 0,
|
||||||
|
test_mode: parseInt(document.getElementById("param-test-mode").value) || 0,
|
||||||
|
reset_dis: parseInt(document.getElementById("param-reset-dis").value) || 0,
|
||||||
|
minus_dis: parseInt(document.getElementById("param-minus-dis").value) || 0,
|
||||||
|
sens_min: parseInt(document.getElementById("param-sens-min").value) || 0,
|
||||||
|
sens_max: parseInt(document.getElementById("param-sens-max").value) || 0,
|
||||||
|
fre_min: parseInt(document.getElementById("param-fre-min").value) || 0,
|
||||||
|
fre_max: parseInt(document.getElementById("param-fre-max").value) || 0,
|
||||||
|
peak_min: parseInt(document.getElementById("param-peak-min").value) || 0,
|
||||||
|
peak_max: parseInt(document.getElementById("param-peak-max").value) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 轮询 serialnet 响应(通用)─────────────
|
||||||
|
|
||||||
|
function startRespPolling(recordId, cmd) {
|
||||||
|
stopRespPolling(recordId);
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
pollTimers[recordId] = setInterval(async () => {
|
||||||
|
attempts++;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/fixture/serialnet/${recordId}`);
|
||||||
|
const rec = await resp.json();
|
||||||
|
|
||||||
|
if (rec.state === 2) {
|
||||||
|
// 已完成
|
||||||
|
stopRespPolling(recordId);
|
||||||
|
const rcvPkg = rec.rcv_pkg || '';
|
||||||
|
if (rcvPkg) {
|
||||||
|
commLog('recv', rcvPkg, '');
|
||||||
|
// 解析 Flag(0x4B/0x4D/0x4E)
|
||||||
|
if (cmd in FLAG_CMDS) {
|
||||||
|
const flag = parseFlagResponse(rcvPkg, cmd);
|
||||||
|
if (flag === 0) {
|
||||||
|
commLog('ok', null, `${FLAG_CMDS[cmd]} 成功`);
|
||||||
|
toast(`${FLAG_CMDS[cmd]} 成功`);
|
||||||
|
} else {
|
||||||
|
commLog('fail', null, `${FLAG_CMDS[cmd]} 失败 (Flag=${flag})`);
|
||||||
|
toast(`${FLAG_CMDS[cmd]} 失败`, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commLog('ok', null, `指令 0x${cmd} 已完成`);
|
||||||
|
toast(`指令 0x${cmd} 已完成`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x4C 返回后刷新参数
|
||||||
|
if (cmd === '4C') {
|
||||||
|
// 参数已在后端 upsert,直接从 DB 加载
|
||||||
|
setTimeout(async () => {
|
||||||
|
const p = await (await fetch(`/api/fixture/param/${DNT_ID}`)).json();
|
||||||
|
if (p && p.dnt_id) fillFormFromParam(p);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commLog('info', null, `指令 0x${cmd} 已完成(无返回数据)`);
|
||||||
|
}
|
||||||
|
} else if (rec.state === 3) {
|
||||||
|
// 超时
|
||||||
|
stopRespPolling(recordId);
|
||||||
|
commLog('fail', null, `指令 0x${cmd} 超时(设备无响应)`);
|
||||||
|
toast(`指令 0x${cmd} 超时`, true);
|
||||||
|
}
|
||||||
|
// state=0/1 → 继续等待
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略网络错误
|
||||||
|
}
|
||||||
|
if (attempts >= 4) {
|
||||||
|
stopRespPolling(recordId);
|
||||||
|
commLog('fail', null, `指令 0x${cmd} 轮询超时(4秒无响应)`);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRespPolling(recordId) {
|
||||||
|
if (pollTimers[recordId]) {
|
||||||
|
clearInterval(pollTimers[recordId]);
|
||||||
|
delete pollTimers[recordId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 发送工装指令(通用)────────────────────
|
||||||
|
|
||||||
|
async function sendFixtureCmd(cmd) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/fixture/command", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ dnt_id: DNT_ID, cmd }),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
commLog('send', data.send_pkg, `${cmdName(cmd)} (0x${cmd})`);
|
||||||
|
toast(`指令 0x${cmd} 已下发`);
|
||||||
|
startRespPolling(data.record_id, cmd);
|
||||||
|
} else {
|
||||||
|
commLog('fail', null, `指令 0x${cmd} 发送失败: ${data.error}`);
|
||||||
|
toast(`失败: ${data.error}`, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
commLog('fail', null, `发送 0x${cmd} 异常: ${e.message}`);
|
||||||
|
toast("发送失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 发送配置指令 0x4B ───────────────────────
|
||||||
|
|
||||||
|
async function sendConfig() {
|
||||||
|
const params = getFormParams();
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/fixture/command", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ dnt_id: DNT_ID, cmd: "4B", params }),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
commLog('send', data.send_pkg, `配置参数 (0x4B)`);
|
||||||
|
toast("配置指令 0x4B 已下发");
|
||||||
|
startRespPolling(data.record_id, "4B");
|
||||||
|
|
||||||
|
// 同时保存到数据库
|
||||||
|
await fetch(`/api/fixture/param/${DNT_ID}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
Addr: params.addr, DevType: params.dev_type, TestMode: params.test_mode,
|
||||||
|
RestDis: params.reset_dis, MinusDis: params.minus_dis,
|
||||||
|
SensMin: params.sens_min, SensMax: params.sens_max,
|
||||||
|
FreMin: params.fre_min, FreMax: params.fre_max,
|
||||||
|
PeakMin: params.peak_min, PeakMax: params.peak_max,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast(`失败: ${data.error}`, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("配置失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdName(cmd) {
|
||||||
|
const names = { '4A': '获取版本号', '4B': '配置参数', '4C': '查询参数', '4D': '出厂初始化', '4E': '设备复位' };
|
||||||
|
return names[cmd] || cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); }
|
||||||
|
|
||||||
|
init();
|
||||||
160
edc-web/app/static/js/vehicle_base_test.js
Normal file
160
edc-web/app/static/js/vehicle_base_test.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// 车检器测试基准参数管理
|
||||||
|
|
||||||
|
let editId = null; // null=新增, number=编辑
|
||||||
|
|
||||||
|
// ─── Toast ───────────────────────────────────
|
||||||
|
|
||||||
|
function toast(msg, isError = false) {
|
||||||
|
const el = document.getElementById("toast");
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = "msg-toast " + (isError ? "error" : "") + " show";
|
||||||
|
clearTimeout(el._timeout);
|
||||||
|
el._timeout = setTimeout(() => { el.className = "msg-toast"; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 列表加载 ────────────────────────────────
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
const search = document.getElementById("search-input").value;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/vehicle-base-test?search=${encodeURIComponent(search)}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
renderTable(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
const tbody = document.querySelector("#vbt-table tbody");
|
||||||
|
if (!data.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="7" style="color:#999;text-align:center;">暂无数据,点右上角「新增」添加</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = data.map(t => `
|
||||||
|
<tr>
|
||||||
|
<td>${t.type_num}</td>
|
||||||
|
<td>${esc(t.dev_name)}</td>
|
||||||
|
<td>${t.SensMin} ~ ${t.SensMax}</td>
|
||||||
|
<td>${t.FreMin} ~ ${t.FreMax}</td>
|
||||||
|
<td>${t.PeakMin} ~ ${t.PeakMax}</td>
|
||||||
|
<td>${esc(t.remark || '-')}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-edit" onclick="openModal(${t.id})">编辑</button>
|
||||||
|
<button class="btn-del" onclick="deleteRecord(${t.id}, '${esc(t.dev_name)}')">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 弹窗 ────────────────────────────────────
|
||||||
|
|
||||||
|
function openModal(id = null) {
|
||||||
|
editId = id;
|
||||||
|
document.getElementById("modal-title").textContent = id ? "编辑车检器测试基准" : "新增车检器测试基准";
|
||||||
|
document.getElementById("edit-modal").style.display = "flex";
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// 编辑:加载数据
|
||||||
|
fetch(`/api/vehicle-base-test/${id}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById("edit-type-num").value = data.type_num;
|
||||||
|
document.getElementById("edit-dev-name").value = data.dev_name;
|
||||||
|
document.getElementById("edit-sens-min").value = data.SensMin;
|
||||||
|
document.getElementById("edit-sens-max").value = data.SensMax;
|
||||||
|
document.getElementById("edit-fre-min").value = data.FreMin;
|
||||||
|
document.getElementById("edit-fre-max").value = data.FreMax;
|
||||||
|
document.getElementById("edit-peak-min").value = data.PeakMin;
|
||||||
|
document.getElementById("edit-peak-max").value = data.PeakMax;
|
||||||
|
document.getElementById("edit-remark").value = data.remark || "";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 新增:清空
|
||||||
|
document.getElementById("edit-type-num").value = "";
|
||||||
|
document.getElementById("edit-dev-name").value = "";
|
||||||
|
document.getElementById("edit-sens-min").value = "0";
|
||||||
|
document.getElementById("edit-sens-max").value = "0";
|
||||||
|
document.getElementById("edit-fre-min").value = "0";
|
||||||
|
document.getElementById("edit-fre-max").value = "0";
|
||||||
|
document.getElementById("edit-peak-min").value = "0";
|
||||||
|
document.getElementById("edit-peak-max").value = "0";
|
||||||
|
document.getElementById("edit-remark").value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById("edit-modal").style.display = "none";
|
||||||
|
editId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 保存 ────────────────────────────────────
|
||||||
|
|
||||||
|
async function saveRecord() {
|
||||||
|
const data = {
|
||||||
|
type_num: parseInt(document.getElementById("edit-type-num").value) || 0,
|
||||||
|
dev_name: document.getElementById("edit-dev-name").value.trim(),
|
||||||
|
SensMin: parseInt(document.getElementById("edit-sens-min").value) || 0,
|
||||||
|
SensMax: parseInt(document.getElementById("edit-sens-max").value) || 0,
|
||||||
|
FreMin: parseInt(document.getElementById("edit-fre-min").value) || 0,
|
||||||
|
FreMax: parseInt(document.getElementById("edit-fre-max").value) || 0,
|
||||||
|
PeakMin: parseInt(document.getElementById("edit-peak-min").value) || 0,
|
||||||
|
PeakMax: parseInt(document.getElementById("edit-peak-max").value) || 0,
|
||||||
|
remark: document.getElementById("edit-remark").value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.dev_name) {
|
||||||
|
toast("请输入型号/名称", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let resp;
|
||||||
|
if (editId) {
|
||||||
|
resp = await fetch(`/api/vehicle-base-test/${editId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resp = await fetch("/api/vehicle-base-test", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok || resp.ok) {
|
||||||
|
toast(editId ? "更新成功" : "新增成功");
|
||||||
|
closeModal();
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
toast("保存失败: " + (result.error || "未知错误"), true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("保存失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 删除 ────────────────────────────────────
|
||||||
|
|
||||||
|
async function deleteRecord(id, name) {
|
||||||
|
if (!confirm(`确定要删除「${name}」吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/vehicle-base-test/${id}`, { method: "DELETE" });
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok) {
|
||||||
|
toast("删除成功");
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
toast("删除失败: " + (result.error || "未知错误"), true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("删除失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
||||||
|
|
||||||
|
loadList();
|
||||||
128
edc-web/app/templates/fixture.html
Normal file
128
edc-web/app/templates/fixture.html
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}工装配置 - {{ device.serial }} - EDC 工装管理系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="test-header">
|
||||||
|
<a href="/">← 返回设备列表</a>
|
||||||
|
<h2>工装配置 —— {{ device.serial }} ({{ device.name or '未命名' }})</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixture-layout">
|
||||||
|
<!-- 左侧:工装测试参数配置区 -->
|
||||||
|
<div class="fixture-left">
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
||||||
|
<h3 style="margin:0;">工装测试参数</h3>
|
||||||
|
<div>
|
||||||
|
<button class="btn-fixture" onclick="refreshParams()" title="从数据库重新加载">🔄 刷新</button>
|
||||||
|
<button class="btn-fixture primary" onclick="saveToDb()" title="仅保存到数据库,不下发设备">💾 保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fixture-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>工装设备地址</label>
|
||||||
|
<input type="number" id="param-addr" value="1" min="0" max="255">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>测试模式</label>
|
||||||
|
<select id="param-test-mode">
|
||||||
|
<option value="0">0 - 灵敏度测试模式</option>
|
||||||
|
<option value="1">1 - 模拟过车模式</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>复位距离 (cm)</label>
|
||||||
|
<input type="number" id="param-reset-dis" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>皮距/开始距离 (cm)</label>
|
||||||
|
<input type="number" id="param-minus-dis" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>被检设备型号</label>
|
||||||
|
<select id="param-dev-type" onchange="onDevTypeChange()">
|
||||||
|
<option value="0">-- 请选择车检器基准 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>灵敏度最小值</label>
|
||||||
|
<input type="number" id="param-sens-min" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>灵敏度最大值</label>
|
||||||
|
<input type="number" id="param-sens-max" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>频率最小值 (Hz)</label>
|
||||||
|
<input type="number" id="param-fre-min" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>频率最大值 (Hz)</label>
|
||||||
|
<input type="number" id="param-fre-max" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>峰峰值最小值</label>
|
||||||
|
<input type="number" id="param-peak-min" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>峰峰值最大值</label>
|
||||||
|
<input type="number" id="param-peak-max" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixture-actions">
|
||||||
|
<button class="btn-fixture" onclick="sendFixtureCmd('4A')">📋 获取版本号 (0x4A)</button>
|
||||||
|
<button class="btn-fixture" onclick="sendFixtureCmd('4C')">🔍 查询参数 (0x4C)</button>
|
||||||
|
<button class="btn-fixture primary" onclick="sendConfig()">📤 配置参数 (0x4B)</button>
|
||||||
|
<button class="btn-fixture danger" onclick="sendFixtureCmd('4D')">🏭 出厂初始化 (0x4D)</button>
|
||||||
|
<button class="btn-fixture danger" onclick="sendFixtureCmd('4E')">🔄 设备复位 (0x4E)</button>
|
||||||
|
</div>
|
||||||
|
<div id="version-info" class="version-info" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 通信日志区 -->
|
||||||
|
<div class="fixture-card">
|
||||||
|
<h3>通信日志</h3>
|
||||||
|
<div id="comm-log" style="background:#1e1e1e; color:#d4d4d4; font-family:monospace; font-size:12px;
|
||||||
|
padding:10px; border-radius:6px; max-height:250px; overflow-y:auto; line-height:1.5;">
|
||||||
|
<div style="color:#888;">等待操作…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:车检器测试基准参数表 -->
|
||||||
|
<div class="fixture-right">
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||||
|
<h3 style="margin:0;">车检器测试基准参数</h3>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="ref-search" placeholder="搜索型号/编码..."
|
||||||
|
style="padding:4px 8px; border:1px solid #ddd; border-radius:4px; font-size:12px; width:140px;"
|
||||||
|
oninput="loadBaseTests()">
|
||||||
|
<button class="btn-config" style="padding:4px 12px; font-size:12px;"
|
||||||
|
onclick="location.href='/vehicle-base-test'">管理</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ref-table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>编码</th><th>名称</th><th>灵敏度</th><th>频率(Hz)</th><th>峰峰值</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="ref-table-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="msg-toast"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
const DNT_ID = {{ device.id }};
|
||||||
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/fixture.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
90
edc-web/app/templates/vehicle_base_test.html
Normal file
90
edc-web/app/templates/vehicle_base_test.html
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}车检器测试基准参数管理 - EDC 工装管理系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="test-header">
|
||||||
|
<a href="/">← 返回设备列表</a>
|
||||||
|
<h2>车检器测试基准参数管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div class="vbt-header">
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<input type="text" id="search-input" placeholder="搜索型号/编码..."
|
||||||
|
style="padding:6px 10px; border:1px solid #ddd; border-radius:4px; font-size:13px; width:200px;"
|
||||||
|
oninput="loadList()">
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" onclick="openModal()">+ 新增</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="vbt-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>类型编码</th>
|
||||||
|
<th>型号/名称</th>
|
||||||
|
<th>灵敏度范围</th>
|
||||||
|
<th>频率范围 (Hz)</th>
|
||||||
|
<th>峰峰值范围</th>
|
||||||
|
<th>备注</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗 -->
|
||||||
|
<div id="edit-modal" class="modal-overlay" style="display:none;" onclick="if(event.target===this)closeModal()">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 id="modal-title">新增车检器测试基准</h3>
|
||||||
|
<div class="modal-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>类型编码 *</label>
|
||||||
|
<input type="number" id="edit-type-num" min="0" max="255">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>型号/名称 *</label>
|
||||||
|
<input type="text" id="edit-dev-name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>灵敏度最小值</label>
|
||||||
|
<input type="number" id="edit-sens-min" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>灵敏度最大值</label>
|
||||||
|
<input type="number" id="edit-sens-max" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>频率最小值 (Hz)</label>
|
||||||
|
<input type="number" id="edit-fre-min" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>频率最大值 (Hz)</label>
|
||||||
|
<input type="number" id="edit-fre-max" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>峰峰值最小值</label>
|
||||||
|
<input type="number" id="edit-peak-min" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>峰峰值最大值</label>
|
||||||
|
<input type="number" id="edit-peak-max" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group full">
|
||||||
|
<label>备注</label>
|
||||||
|
<textarea id="edit-remark" rows="2" style="resize:vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">取消</button>
|
||||||
|
<button class="btn-save" onclick="saveRecord()">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="msg-toast"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/vehicle_base_test.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
Submodule edc_server updated: 43fd3e7be9...e7c20c69d2
Reference in New Issue
Block a user