fix(抓取): 元素框跟着图片走(ResizeObserver)+ 界面放大 + 右侧层级/属性/颜色三页签
用户报的准确现象:**点某些元素会让画面变大,但框没跟着变大 → 错位**。
根因:预览图是 `max-width:100%` —— 窗口够宽时恒为原始尺寸(我这儿复现不出),
但窗口窄时图片被压小;此时任何**布局微动**都会改变预览列宽度 → 图片重新缩放,
而框是上一次算好的**像素坐标**,不会跟着变。触发布局微动的正是"点元素":
`.el-picker-item.active` 用 `border-left:3px`(占宽度)→ 列表变宽 → 预览变窄 →
图片变小 ✗。实测(视口压到 900):图片从 720 → 363,框仍停在 720 的位置。
修法:
1. **ResizeObserver 盯着图片**:显示尺寸一变就重算所有框 —— 窗口缩放、面板宽度
变化、滚动条出现…都会自动跟上,这是"完全不错位"的兜底保证。
2. 高亮一律改用 **outline / box-shadow**(不占布局)→ 选中元素不再引发任何重排。
3. 弹窗放大到 **98vw × 88vh**(用户要"整个界面大一点"),图片在宽窗口下 1:1。
4. 右侧改成和 uiauto.dev 一样的**三页签**:
- **层级**:元素树(原列表,缩进 + 搜索)
- **属性**:选中元素的全部属性(建议选择器/class/resource-id/text/desc/package/
clickable/bounds/深度)
- **颜色**:鼠标在图上移动实时取**像素色**;选中元素显示**元素中心色**
5. 交互对齐云检查器:**点层级条目 = 选中**(看属性/颜色/高亮,不关窗),
回填改由每行的「✓ 填入」按钮触发。
实测(视口 900 窄窗口):图片 720→363、框同步变 363 ✓;点元素不再错位 ✓;
三页签可用、属性/取色正确(选中元素中心 #f7f7f7)✓;无 JS 报错 ✓。
This commit is contained in:
+100
-3
@@ -839,8 +839,20 @@ var _stepEditor={
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<div class="el-list-wrap">'+
|
||||
'<input type="text" class="el-picker-search" id="el-search" placeholder="搜索 resource-id/text/description..." oninput="_stepEditor._filterEl(this.value)">'+
|
||||
'<div class="el-picker-list" id="el-list"><div class="se-empty">加载元素树...</div></div>'+
|
||||
'<div class="el-tabs">'+
|
||||
'<button class="el-tab active" data-pane="tree" onclick="_stepEditor._tab(this)">层级</button>'+
|
||||
'<button class="el-tab" data-pane="attr" onclick="_stepEditor._tab(this)">属性</button>'+
|
||||
'<button class="el-tab" data-pane="color" onclick="_stepEditor._tab(this)">颜色</button>'+
|
||||
'</div>'+
|
||||
'<div class="el-pane active" id="el-pane-tree">'+
|
||||
'<input type="text" class="el-picker-search" id="el-search" placeholder="搜索 resource-id/text/description..." oninput="_stepEditor._filterEl(this.value)">'+
|
||||
'<div class="el-picker-list" id="el-list" style="max-height:calc(100% - 42px)"><div class="se-empty">加载元素树...</div></div>'+
|
||||
'</div>'+
|
||||
'<div class="el-pane" id="el-pane-attr"><div class="se-empty">点左侧图上的元素,或点「层级」里的条目看属性</div></div>'+
|
||||
'<div class="el-pane" id="el-pane-color">'+
|
||||
'<div style="padding:8px 12px;font-size:11.5px;color:var(--text-light)">把鼠标移到左侧图上取色;点元素可看该元素的主色</div>'+
|
||||
'<div id="el-color-list"></div>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>';
|
||||
document.getElementById('el-picker-footer').innerHTML=
|
||||
@@ -852,6 +864,71 @@ var _stepEditor={
|
||||
// 一次取齐截图 + 元素树(见 _loadSnapshot 的说明)
|
||||
self._loadSnapshot();
|
||||
},
|
||||
// 右侧页签切换
|
||||
_tab:function(btn){
|
||||
var pane=btn.dataset.pane;
|
||||
document.querySelectorAll('#el-picker-body .el-tab').forEach(function(b){
|
||||
b.classList.toggle('active', b===btn);
|
||||
});
|
||||
['tree','attr','color'].forEach(function(p){
|
||||
var el=document.getElementById('el-pane-'+p);
|
||||
if(el)el.classList.toggle('active', p===pane);
|
||||
});
|
||||
},
|
||||
// 属性页:选中元素的全部属性
|
||||
_renderAttrs:function(i){
|
||||
var el=(this._elData||[])[i];
|
||||
var box=document.getElementById('el-pane-attr');
|
||||
if(!box)return;
|
||||
if(!el){box.innerHTML='<div class="se-empty">点左侧图上的元素,或点「层级」里的条目看属性</div>';return;}
|
||||
var sg=el.suggested||{};
|
||||
var rows=[['建议选择器', sg.value||'(无可靠选择器)'],
|
||||
['class', el.class||''], ['resource-id', el.resource_id||''],
|
||||
['text', el.text||''], ['content-desc', el.description||''],
|
||||
['package', el.package||''], ['clickable', el.clickable||''],
|
||||
['bounds', el.bounds||''], ['层级深度', String(el.depth||0)]];
|
||||
box.innerHTML=rows.map(function(r){
|
||||
return '<div class="el-attr"><b>'+esc(r[0])+'</b><span>'+esc(r[1]||'-')+'</span></div>';
|
||||
}).join('');
|
||||
},
|
||||
// 颜色页:鼠标位置的像素色 + 选中元素的主色
|
||||
_pixelAt:function(x,y){
|
||||
try{
|
||||
var c=this._colorCanvas, img=document.getElementById('el-shot-container').querySelector('img');
|
||||
if(!c||!img)return null;
|
||||
if(c.width!==img.naturalWidth){c.width=img.naturalWidth;c.height=img.naturalHeight;}
|
||||
var ctx=c.getContext('2d');
|
||||
ctx.drawImage(img,0,0);
|
||||
var d=ctx.getImageData(Math.max(0,Math.min(c.width-1,x)),Math.max(0,Math.min(c.height-1,y)),1,1).data;
|
||||
return '#'+[d[0],d[1],d[2]].map(function(v){return ('0'+v.toString(16)).slice(-2);}).join('');
|
||||
}catch(e){return null;}
|
||||
},
|
||||
_updatePixel:function(x,y){
|
||||
var box=document.getElementById('el-color-list');
|
||||
if(!box)return;
|
||||
var col=this._pixelAt(Math.round(x),Math.round(y));
|
||||
if(!col)return;
|
||||
box.innerHTML='<div class="el-color-row"><span class="el-swatch" style="background:'+col+'"></span>'+
|
||||
'<span>光标处 '+esc(col)+'</span></div>'+
|
||||
(box.dataset.dom||'');
|
||||
},
|
||||
_renderColors:function(i){
|
||||
var box=document.getElementById('el-color-list');
|
||||
if(!box)return;
|
||||
var el=(this._elData||[])[i];
|
||||
var dom='';
|
||||
if(el&&el.bounds){
|
||||
var m=el.bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
||||
if(m){
|
||||
var x=Math.round((+m[1]+ +m[3])/2), y=Math.round((+m[2]+ +m[4])/2);
|
||||
var c=this._pixelAt(x,y);
|
||||
if(c) dom='<div class="el-color-row"><span class="el-swatch" style="background:'+c+'"></span>'+
|
||||
'<span>选中元素中心 '+esc(c)+'</span></div>';
|
||||
}
|
||||
}
|
||||
box.dataset.dom=dom;
|
||||
box.innerHTML=dom||'<div class="se-empty" style="padding:12px">把鼠标移到左侧图上取色</div>';
|
||||
},
|
||||
// 一次取齐:截图 + 元素树(后端用原生 u2 背靠背取)
|
||||
// 为什么不再分两个接口:截图与元素树分两次拿,中间隔着 dump 本身的 1.3~1.8 秒,
|
||||
// 界面只要在动(信息流/视频/加载),框就会落在旧位置上("抓取错位")。
|
||||
@@ -888,7 +965,17 @@ var _stepEditor={
|
||||
info.textContent=d.width+'x'+d.height+' · '+self._elData.length+' 个元素 · '
|
||||
+(d.cost_ms||0)+'ms'+(d.unstable?' · ⚠ 界面在变化':'');
|
||||
}
|
||||
// 图片显示尺寸一变(窗口缩放、面板宽度变化、滚动条出现…)就重算所有框 ——
|
||||
// 这是"完全不错位"的保证:框永远跟着图片走,不留旧坐标
|
||||
if(window.ResizeObserver){
|
||||
if(self._ro)self._ro.disconnect();
|
||||
self._ro=new ResizeObserver(function(){ self._renderOverlays(); });
|
||||
self._ro.observe(img);
|
||||
}
|
||||
self._colorCanvas=document.createElement('canvas');
|
||||
self._renderOverlays();
|
||||
self._renderAttrs(null);
|
||||
self._renderColors(null);
|
||||
};
|
||||
img.src=d.image;
|
||||
self._renderEl(self._elData);
|
||||
@@ -952,6 +1039,7 @@ var _stepEditor={
|
||||
var a=+mb[1],b=+mb[2],c=+mb[3],dd=+mb[4];
|
||||
if(px>=a&&px<=c&&py>=b&&py<=dd){var ar=(c-a)*(dd-b); if(ar<bestArea){bestArea=ar;best=i;}}
|
||||
});
|
||||
self._updatePixel(px, py);
|
||||
var hov=container.querySelector('.el-shot-hover');
|
||||
if(best<0){ if(hov)hov.remove(); return; }
|
||||
var b2=self._elData[best].bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
||||
@@ -989,6 +1077,8 @@ var _stepEditor={
|
||||
},
|
||||
// 高亮元素(截图框 + 列表项联动)
|
||||
_highlightEl:function(i){
|
||||
this._renderAttrs(i);
|
||||
this._renderColors(i);
|
||||
// 清除旧高亮(只在 el-picker-overlay 范围内查找)
|
||||
var picker=document.getElementById('el-picker-overlay');
|
||||
picker.querySelectorAll('.el-shot-overlay.active').forEach(function(o){o.classList.remove('active');});
|
||||
@@ -1027,8 +1117,10 @@ var _stepEditor={
|
||||
? '<button class="btn btn-xs" title="用将填入的选择器在设备上真跑一次 click,验证是否命中" '
|
||||
+'onclick="event.stopPropagation();_stepEditor._testSelector('+idx+')">✓ 测选择器</button>'
|
||||
: '')
|
||||
+'<button class="btn btn-xs" title="把这条的选择器填入步骤" '
|
||||
+'onclick="event.stopPropagation();_stepEditor._fillEl('+idx+')">✓ 填入</button>'
|
||||
+'</div>';
|
||||
return '<div class="el-picker-item" data-idx="'+idx+'" title="点击填入: '+esc(sg.value||'')+'" onclick="_stepEditor._pickEl('+idx+')">'+
|
||||
return '<div class="el-picker-item" data-idx="'+idx+'" title="'+esc(sg.value||'')+'" onclick="_stepEditor._pickEl('+idx+')">'+
|
||||
'<span class="ep-depth">'+(el.depth||0)+'</span>'+
|
||||
'<div class="ep-info"><div class="ep-name">'+indent+esc(el.name||el.class||'')+'</div>'+
|
||||
'<div class="ep-attrs">'+attrs.join('')+'</div></div>'+
|
||||
@@ -1080,7 +1172,12 @@ var _stepEditor={
|
||||
});
|
||||
this._renderEl(f);
|
||||
},
|
||||
// 点层级条目 = **选中**(看属性/颜色/高亮),不关窗不回填
|
||||
_pickEl:function(i){
|
||||
this._highlightEl(i);
|
||||
},
|
||||
// 「✓ 填入」才回填选择器并关窗
|
||||
_fillEl:function(i){
|
||||
var el=this._elData[i];
|
||||
if(!el||!this._elTargetInput)return;
|
||||
if(el.suggested&&el.suggested.invalid){
|
||||
|
||||
Reference in New Issue
Block a user