pMotor 网页修复快照刷新导致输入光标跳动:页面改为加载时一次性构建静态骨架,WebSocket 快照到达后仅就地更新各数值叶节点(setH),不再整块 innerHTML 重建;输入框/按钮常驻 DOM,聚焦与光标不再被每秒推送打断,移除原 __lastSpeed 保值 hack
build-test-deploy / ci (push) Failing after 1h31m36s
build-test-deploy / ci (push) Failing after 1h31m36s
This commit is contained in:
+148
-110
@@ -12,6 +12,9 @@
|
|||||||
// (fetch /api/motor_cmd -> pMotor 入队 -> MOOS CAN_TX -> pCanBridge -> CAN1)
|
// (fetch /api/motor_cmd -> pMotor 入队 -> MOOS CAN_TX -> pCanBridge -> CAN1)
|
||||||
// - 展示:反馈转速/母线电压、温度、支路一/支路二故障与报警(按位解析中文)
|
// - 展示:反馈转速/母线电压、温度、支路一/支路二故障与报警(按位解析中文)
|
||||||
// - 协议定义:docs/推进电机通信协议20260321.docx
|
// - 协议定义:docs/推进电机通信协议20260321.docx
|
||||||
|
//
|
||||||
|
// 重要:页面 DOM 只在加载时构建一次,收到快照后仅就地更新各数值叶节点,
|
||||||
|
// 绝不再整块重建 pages(重建会销毁正被聚焦/输入的控件导致光标跳动)。
|
||||||
//============================================================================
|
//============================================================================
|
||||||
|
|
||||||
namespace motor {
|
namespace motor {
|
||||||
@@ -56,7 +59,7 @@ input[type=number]:focus{outline:none;border-color:#1f6feb;}
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>pMotor 推进电机控制 <span id="status" class="badge">连接中...</span></h1>
|
<h1>pMotor 推进电机控制 <span id="status" class="badge">连接中...</span></h1>
|
||||||
<div class="grid" id="pages">加载中...</div>
|
<div class="grid" id="pages"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
@@ -65,7 +68,6 @@ const statusEl = $('status');
|
|||||||
|
|
||||||
function row(k, v){ return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'; }
|
function row(k, v){ return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'; }
|
||||||
function sub(t){ return '<div class="sub">'+t+'</div>'; }
|
function sub(t){ return '<div class="sub">'+t+'</div>'; }
|
||||||
function hex(v){ return '0x' + (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
|
|
||||||
function hex16(v){ return '0x' + (v & 0xFFFF).toString(16).toUpperCase().padStart(4,'0'); }
|
function hex16(v){ return '0x' + (v & 0xFFFF).toString(16).toUpperCase().padStart(4,'0'); }
|
||||||
function card(title, html){ return '<div class="card"><h2>'+title+'</h2>'+html+'</div>'; }
|
function card(title, html){ return '<div class="card"><h2>'+title+'</h2>'+html+'</div>'; }
|
||||||
function ageText(a){
|
function ageText(a){
|
||||||
@@ -80,6 +82,8 @@ function ageCell(d){
|
|||||||
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
|
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
|
||||||
+ ageText(d.age) + (online ? ' (在线)' : ' (超时)') + '</span>';
|
+ ageText(d.age) + (online ? ' (在线)' : ' (超时)') + '</span>';
|
||||||
}
|
}
|
||||||
|
// 就地更新:只替换叶节点内容,绝不重建含输入控件的父级 DOM
|
||||||
|
function setH(id, html){ const el = $(id); if(el) el.innerHTML = html; }
|
||||||
|
|
||||||
//------------------ 控制指令(/api/motor_cmd) ------------------
|
//------------------ 控制指令(/api/motor_cmd) ------------------
|
||||||
// 链路: 网页 fetch -> pMotor 入队 -> Iterate 周期编码 0x18EF2010
|
// 链路: 网页 fetch -> pMotor 入队 -> Iterate 周期编码 0x18EF2010
|
||||||
@@ -117,135 +121,172 @@ function motorSetSpeed(){
|
|||||||
sendCmd('speed', v, '确认设定期望转速 '+v+' rpm?\n(对应指令帧 Byte2/3,使能状态保持不变)');
|
sendCmd('speed', v, '确认设定期望转速 '+v+' rpm?\n(对应指令帧 Byte2/3,使能状态保持不变)');
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------ 控制卡片 ------------------
|
//------------------ 页面骨架(只在加载时构建一次) ------------------
|
||||||
function cmdCard(s){
|
// 所有可被聚焦的控件(输入框/按钮)都在这份骨架里,之后绝不重建。
|
||||||
const c = s || {};
|
function buildUI(){
|
||||||
const cmd = c.cmd || {};
|
let html = '';
|
||||||
const en = cmd.enable;
|
|
||||||
let h = '';
|
// 控制卡片(含输入框,永不重建)
|
||||||
h += row('使能状态', en
|
let c = '';
|
||||||
? '<span class="fc-ok">已使能(运行)</span>'
|
c += row('使能状态', '<span id="cEnable">…</span>');
|
||||||
: '<span style="color:var(--dim)">未使能(停机)</span>');
|
c += row('期望转速', '<span id="cSpeed">…</span> rpm');
|
||||||
h += row('期望转速', (cmd.speed||0) + ' rpm');
|
c += row('复位命令', '<span id="cReset">…</span>');
|
||||||
h += row('复位命令', cmd.resetActive ? '<span class="fc-warn">生效中(保持数秒)</span>' : '无效');
|
c += row('最近指令', '<span id="cLast">…</span>');
|
||||||
if(cmd.lastCmdValid){
|
c += sub('使能命令 (Byte0 bit0)') +
|
||||||
const names = {enable:'使能命令=1', disable:'使能命令=0(停机)', reset:'复位命令=1', speed:'期望转速'};
|
|
||||||
h += row('最近指令', (names[cmd.lastCmdAction]||cmd.lastCmdAction)
|
|
||||||
+ (cmd.lastCmdAction==='speed' ? ' ('+(cmd.lastCmdSpeed||0)+' rpm)' : '')
|
|
||||||
+ ' · ' + ageText(cmd.lastCmdAge));
|
|
||||||
} else {
|
|
||||||
h += row('最近指令', '<span style="color:var(--dim)">无</span>');
|
|
||||||
}
|
|
||||||
h += '<div class="sub">使能命令 (Byte0 bit0)</div>' +
|
|
||||||
'<div>' +
|
'<div>' +
|
||||||
'<button class="btn on" onclick="motorEnable()">使能 (1)</button>' +
|
'<button class="btn on" onclick="motorEnable()">使能 (1)</button>' +
|
||||||
'<button class="btn off" onclick="motorStop()">去使能 (0)</button></div>' +
|
'<button class="btn off" onclick="motorStop()">去使能 (0)</button></div>';
|
||||||
'<div class="sub">复位命令 (Byte0 bit1,仅故障状态下有效)</div>' +
|
c += sub('复位命令 (Byte0 bit1,仅故障状态下有效)') +
|
||||||
'<div>' +
|
'<div><button class="btn warn" onclick="motorReset()">复位</button></div>';
|
||||||
'<button class="btn warn" onclick="motorReset()">复位</button></div>' +
|
c += sub('期望转速 (Byte2/3,s16,单位 rpm)') +
|
||||||
'<div class="sub">期望转速 (Byte2/3,s16,单位 rpm)</div>' +
|
|
||||||
'<div>' +
|
'<div>' +
|
||||||
'<input type="number" id="speedInput" value="1000" min="-32767" max="32767" step="100" title="期望转速 (rpm)">' +
|
'<input type="number" id="speedInput" value="1000" min="-32767" max="32767" step="100" title="期望转速 (rpm)">' +
|
||||||
'<span style="color:var(--dim);margin-left:6px;">rpm</span> ' +
|
'<span style="color:var(--dim);margin-left:6px;">rpm</span> ' +
|
||||||
'<button class="btn" onclick="motorSetSpeed()">设定</button></div>' +
|
'<button class="btn" onclick="motorSetSpeed()">设定</button></div>' +
|
||||||
'<div class="sub" style="margin-top:8px;">指令帧 0x18EF2010 周期 ' + ((c.link&&c.link.periodMs)||500) +
|
'<div class="sub" style="margin-top:8px;">指令帧 0x18EF2010 周期 <span id="cPeriod">…</span>' +
|
||||||
'ms 持续发送(协议要求);停机=使能0且转速清0(协议示例 00 00 00 00 00 00 00 00)</div>';
|
' ms 持续发送(协议要求);停机=使能0且转速清0(协议示例 00 00 00 00 00 00 00 00)</div>';
|
||||||
return card('电机指令 (下行 0x18EF2010)', h);
|
html += card('电机指令 (下行 0x18EF2010)', c);
|
||||||
|
|
||||||
|
// 运行状态(状态帧3:转速 / 母线电压)
|
||||||
|
let s3 = '';
|
||||||
|
s3 += row('反馈转速', '<span id="s3speed" class="state-big">…</span> rpm');
|
||||||
|
s3 += row('母线电压', '<span id="s3volt">…</span>');
|
||||||
|
s3 += row('最后更新', '<span id="s3age">…</span>');
|
||||||
|
s3 += sub('来源 0x18EF1220(100ms,冗余 0x18EF1221)');
|
||||||
|
html += card('运行状态', s3);
|
||||||
|
|
||||||
|
// 温度(状态帧4)
|
||||||
|
let s4 = '';
|
||||||
|
s4 += row('A相绕组温度', '<span id="tA">…</span>');
|
||||||
|
s4 += row('B相绕组温度', '<span id="tB">…</span>');
|
||||||
|
s4 += row('C相绕组温度', '<span id="tC">…</span>');
|
||||||
|
s4 += row('逆变组件底板温度', '<span id="tInv">…</span>');
|
||||||
|
s4 += row('输出滤波器温度', '<span id="tFilter">…</span>');
|
||||||
|
s4 += row('筒内空气温度', '<span id="tAir">…</span>');
|
||||||
|
s4 += row('最后更新', '<span id="tAge">…</span>');
|
||||||
|
s4 += sub('来源 0x18EF1320(100ms,冗余 0x18EF1321,Byte0~5 各 1 字节 s8)');
|
||||||
|
html += card('温度', s4);
|
||||||
|
|
||||||
|
// 支路一 / 支路二 故障报警卡片
|
||||||
|
function branchCard(title, pfx){
|
||||||
|
let b = '';
|
||||||
|
b += '<div class="row"><span class="k" id="'+pfx+'fK">故障</span>' +
|
||||||
|
'<span class="v"><span id="'+pfx+'faults">…</span></span></div>';
|
||||||
|
b += '<div class="row"><span class="k" id="'+pfx+'aK">报警</span>' +
|
||||||
|
'<span class="v"><span id="'+pfx+'alarms">…</span></span></div>';
|
||||||
|
b += row('故障字原始值', '<span id="'+pfx+'words">…</span>');
|
||||||
|
b += row('最后更新', '<span id="'+pfx+'age">…</span>');
|
||||||
|
return card(title, b);
|
||||||
|
}
|
||||||
|
html += branchCard('支路一故障/报警 (0x18EF1020)', 'f1');
|
||||||
|
html += branchCard('支路二故障/报警 (0x18EF1120)', 'f2');
|
||||||
|
|
||||||
|
// 通信链路卡片
|
||||||
|
let l = '';
|
||||||
|
l += row('下行通道 (CAN1)', '<span id="lch">…</span>');
|
||||||
|
l += row('冗余通道', '<span id="lred">…</span>');
|
||||||
|
l += row('指令帧周期', '<span id="lperiod">…</span> ms');
|
||||||
|
l += row('指令帧累计', '<span id="ltx">…</span>');
|
||||||
|
l += row('最近下发', '<span id="llast">…</span>');
|
||||||
|
l += row('电机报文接收', '<span id="lrx">…</span> 帧');
|
||||||
|
l += sub('链路: 电机 <-> CAN1 <-> CANET <-> pCanBridge <-> MOOS <-> pMotor');
|
||||||
|
html += card('通信链路 (pCanBridge)', l);
|
||||||
|
|
||||||
|
// 协议参考卡片(纯静态)
|
||||||
|
let p = '';
|
||||||
|
p += row('0x18EF2010', '主控指令帧(下行 500ms,冗余 0x18EF2011)');
|
||||||
|
p += row('0x18EF2110', '主控限制帧(下行,全预留,暂不使用)');
|
||||||
|
p += row('0x18EF1020', '支路一故障报警状态帧1(上行 200ms,冗余 0x18EF1021)');
|
||||||
|
p += row('0x18EF1120', '支路二故障报警状态帧2(上行 200ms,冗余 0x18EF1121)');
|
||||||
|
p += row('0x18EF1220', '状态帧3:反馈转速/母线电压(上行 100ms,冗余 0x18EF1221)');
|
||||||
|
p += row('0x18EF1320', '状态帧4:温度(上行 100ms,冗余 0x18EF1321)');
|
||||||
|
p += row('帧格式', 'CAN2.0B 扩展帧 29 位 ID,数据域 8 字节,Intel 字节序');
|
||||||
|
p += row('波特率', '250 kbps(默认,按客户可定制 125~1000)');
|
||||||
|
html += card('报文定义 (推进电机通信协议 20260321)', p);
|
||||||
|
|
||||||
|
pagesEl.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------ 状态帧3:转速 / 母线电压 ------------------
|
//------------------ 快照就地更新(不触碰输入控件) ------------------
|
||||||
function state3Card(d){
|
function updateCmd(c){
|
||||||
if(!d) return '';
|
c = c || {};
|
||||||
let h = '';
|
setH('cEnable', c.enable
|
||||||
h += row('反馈转速', '<span class="state-big">'+(d.valid?(d.speed||0):'—')+'</span> rpm');
|
? '<span class="fc-ok">已使能(运行)</span>'
|
||||||
h += row('母线电压', (d.valid?(d.voltage||0):'—') + (d.valid?' V':''));
|
: '<span style="color:var(--dim)">未使能(停机)</span>');
|
||||||
h += row('最后更新', ageCell(d));
|
setH('cSpeed', String(c.speed || 0));
|
||||||
h += sub('来源 0x18EF1220(100ms,冗余 0x18EF1221)');
|
setH('cReset', c.resetActive ? '<span class="fc-warn">生效中(保持数秒)</span>' : '无效');
|
||||||
return card('运行状态', h);
|
if(c.lastCmdValid){
|
||||||
|
const names = {enable:'使能命令=1', disable:'使能命令=0(停机)', reset:'复位命令=1', speed:'期望转速'};
|
||||||
|
setH('cLast', (names[c.lastCmdAction] || c.lastCmdAction)
|
||||||
|
+ (c.lastCmdAction==='speed' ? ' ('+(c.lastCmdSpeed||0)+' rpm)' : '')
|
||||||
|
+ ' · ' + ageText(c.lastCmdAge));
|
||||||
|
} else {
|
||||||
|
setH('cLast', '<span style="color:var(--dim)">无</span>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------ 状态帧4:温度 ------------------
|
function updateState3(d){
|
||||||
function state4Card(d){
|
if(!d) return;
|
||||||
if(!d) return '';
|
setH('s3speed', d.valid ? String(d.speed || 0) : '—');
|
||||||
const v = d.valid;
|
setH('s3volt', d.valid ? ((d.voltage||0) + ' V') : '—');
|
||||||
let h = '';
|
setH('s3age', ageCell(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateState4(d){
|
||||||
|
if(!d) return;
|
||||||
const temps = [
|
const temps = [
|
||||||
['A相绕组温度', d.tA], ['B相绕组温度', d.tB], ['C相绕组温度', d.tC],
|
['tA', d.tA], ['tB', d.tB], ['tC', d.tC],
|
||||||
['逆变组件底板温度', d.tInv], ['输出滤波器温度', d.tFilter], ['筒内空气温度', d.tAir],
|
['tInv', d.tInv], ['tFilter', d.tFilter], ['tAir', d.tAir],
|
||||||
];
|
];
|
||||||
for(const t of temps){
|
for(const t of temps){
|
||||||
h += row(t[0], v ? (t[1]|0) + ' ℃' : '—');
|
setH(t[0], d.valid ? ((t[1]|0) + ' ℃') : '—');
|
||||||
}
|
}
|
||||||
h += row('最后更新', ageCell(d));
|
setH('tAge', ageCell(d));
|
||||||
h += sub('来源 0x18EF1320(100ms,冗余 0x18EF1321,Byte0~5 各 1 字节 s8)');
|
|
||||||
return card('温度', h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------ 支路故障/报警卡片 ------------------
|
function updateBranch(pfx, d){
|
||||||
function branchCard(title, d){
|
if(!d) return;
|
||||||
if(!d) return '';
|
|
||||||
const words = d.words || [];
|
|
||||||
let h = '';
|
|
||||||
const faults = d.faults || [];
|
const faults = d.faults || [];
|
||||||
const alarms = d.alarms || [];
|
const alarms = d.alarms || [];
|
||||||
if(faults.length){
|
setH(pfx+'fK', '故障' + (faults.length ? '(' + faults.length + '条)' : ''));
|
||||||
h += row('故障(' + faults.length + '条)', faults.map(function(f){ return '<span class="fc-bad">'+f+'</span>'; }).join(''));
|
setH(pfx+'aK', '报警' + (alarms.length ? '(' + alarms.length + '条)' : ''));
|
||||||
} else {
|
setH(pfx+'faults', faults.length
|
||||||
h += row('故障', d.valid ? '<span class="fc-ok">无</span>' : '—');
|
? faults.map(function(f){ return '<span class="fc-bad">'+f+'</span>'; }).join('')
|
||||||
}
|
: (d.valid ? '<span class="fc-ok">无</span>' : '—'));
|
||||||
if(alarms.length){
|
setH(pfx+'alarms', alarms.length
|
||||||
h += row('报警(' + alarms.length + '条)', alarms.map(function(a){ return '<span class="fc-warn">'+a+'</span>'; }).join(''));
|
? alarms.map(function(a){ return '<span class="fc-warn">'+a+'</span>'; }).join('')
|
||||||
} else {
|
: (d.valid ? '<span class="fc-ok">无</span>' : '—'));
|
||||||
h += row('报警', d.valid ? '<span class="fc-ok">无</span>' : '—');
|
setH(pfx+'words', (d.words || []).map(hex16).join(' '));
|
||||||
}
|
setH(pfx+'age', ageCell(d));
|
||||||
h += row('故障字原始值', words.map(hex16).join(' '));
|
|
||||||
h += row('最后更新', ageCell(d));
|
|
||||||
return card(title, h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------ 通信链路卡片 ------------------
|
function updateLink(l){
|
||||||
function linkCard(d){
|
l = l || {};
|
||||||
const l = (d && d.link) || {};
|
setH('lch', l.channel || 'CAN1');
|
||||||
let h = '';
|
setH('lred', l.redChannel ? l.redChannel : '<span style="color:var(--dim)">未配置</span>');
|
||||||
h += row('下行通道 (CAN1)', l.channel || 'CAN1');
|
setH('lperiod', String(l.periodMs || 0));
|
||||||
h += row('冗余通道', l.redChannel ? l.redChannel : '<span style="color:var(--dim)">未配置</span>');
|
setH('ltx', (l.txCount||0) + ' 成功 / ' + (l.txErr||0) + ' 失败');
|
||||||
h += row('指令帧周期', (l.periodMs||0) + ' ms');
|
setH('llast', ageCell({valid:!!l.lastTxValid, age:l.lastTxAge}));
|
||||||
h += row('指令帧累计', (l.txCount||0) + ' 成功 / ' + (l.txErr||0) + ' 失败');
|
setH('lrx', String(l.rxCount || 0));
|
||||||
h += row('最近下发', ageCell({valid:!!l.lastTxValid, age:l.lastTxAge}));
|
|
||||||
h += row('电机报文接收', (l.rxCount||0) + ' 帧');
|
|
||||||
h += sub('链路: 电机 <-> CAN1 <-> CANET <-> pCanBridge <-> MOOS <-> pMotor');
|
|
||||||
return card('通信链路 (pCanBridge)', h);
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------ 协议参考卡片 ------------------
|
|
||||||
function protoCard(){
|
|
||||||
let h = '';
|
|
||||||
h += row('0x18EF2010', '主控指令帧(下行 500ms,冗余 0x18EF2011)');
|
|
||||||
h += row('0x18EF2110', '主控限制帧(下行,全预留,暂不使用)');
|
|
||||||
h += row('0x18EF1020', '支路一故障报警状态帧1(上行 200ms,冗余 0x18EF1021)');
|
|
||||||
h += row('0x18EF1120', '支路二故障报警状态帧2(上行 200ms,冗余 0x18EF1121)');
|
|
||||||
h += row('0x18EF1220', '状态帧3:反馈转速/母线电压(上行 100ms,冗余 0x18EF1221)');
|
|
||||||
h += row('0x18EF1320', '状态帧4:温度(上行 100ms,冗余 0x18EF1321)');
|
|
||||||
h += row('帧格式', 'CAN2.0B 扩展帧 29 位 ID,数据域 8 字节,Intel 字节序');
|
|
||||||
h += row('波特率', '250 kbps(默认,按客户可定制 125~1000)');
|
|
||||||
return card('报文定义 (推进电机通信协议 20260321)', h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(snap){
|
function render(snap){
|
||||||
let html = '';
|
snap = snap || {};
|
||||||
html += cmdCard(snap);
|
const link = snap.link || {};
|
||||||
html += state3Card(snap.state3);
|
updateCmd(snap.cmd);
|
||||||
html += state4Card(snap.state4);
|
updateState3(snap.state3);
|
||||||
html += branchCard('支路一故障/报警 (0x18EF1020)', snap.fault1);
|
updateState4(snap.state4);
|
||||||
html += branchCard('支路二故障/报警 (0x18EF1120)', snap.fault2);
|
updateBranch('f1', snap.fault1);
|
||||||
html += linkCard(snap);
|
updateBranch('f2', snap.fault2);
|
||||||
html += protoCard();
|
updateLink(link);
|
||||||
pagesEl.innerHTML = html;
|
setH('cPeriod', String(link.periodMs || 500));
|
||||||
// 保留转速输入值(render 会重建 DOM)
|
|
||||||
const prev = window.__lastSpeed;
|
|
||||||
if(prev !== undefined){ const el = $('speedInput'); if(el) el.value = prev; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 骨架构建一次后即保持稳定;render 只做叶节点就地更新,
|
||||||
|
// 输入框不再被销毁重建,光标与焦点因此不受快照刷新影响。
|
||||||
|
buildUI();
|
||||||
|
render({});
|
||||||
|
|
||||||
function connect(){
|
function connect(){
|
||||||
const proto = location.protocol==='https:'?'wss':'ws';
|
const proto = location.protocol==='https:'?'wss':'ws';
|
||||||
const ws = new WebSocket(proto+'://'+location.host+'/ws');
|
const ws = new WebSocket(proto+'://'+location.host+'/ws');
|
||||||
@@ -254,10 +295,7 @@ function connect(){
|
|||||||
ws.onerror = ()=>{ ws.close(); };
|
ws.onerror = ()=>{ ws.close(); };
|
||||||
ws.onmessage = (ev)=>{
|
ws.onmessage = (ev)=>{
|
||||||
try{
|
try{
|
||||||
const snap = JSON.parse(ev.data);
|
render(JSON.parse(ev.data));
|
||||||
const el = $('speedInput');
|
|
||||||
if(el && el === document.activeElement) window.__lastSpeed = el.value;
|
|
||||||
render(snap);
|
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user