feat: 条件判断步骤(if_el 双分支:找到/未找到各执行独立步骤,可嵌套循环/条件;OCR识别选择器匹配图片文字,RapidOCR 跨平台实现含命中点击)+ 动作编辑器布局优化(操作库分组/加宽、任务弹窗加宽)
This commit is contained in:
+124
-53
@@ -769,6 +769,8 @@ function openTaskModal(jobId){
|
||||
const job=jobId?(_jobsData||[]).find(j=>j.id===jobId):null;
|
||||
const isEdit=!!job;
|
||||
document.getElementById('modal-title').textContent=isEdit?'编辑任务':'新建任务';
|
||||
// 任务编辑器含步骤编辑,加宽弹窗;关闭时在 closeModal 移除
|
||||
document.getElementById('modal-box').classList.add('wide-modal');
|
||||
|
||||
const j=job||{name:'',task_type:'douyin_nurture',enabled:true,target:{mode:'all'},params:{},schedule:{mode:'once'},retry:{max_attempts:1,delay:60}};
|
||||
const tt=_taskTypes.map(t=>'<option value="'+t.task_type+'"'+(t.task_type===j.task_type?' selected':'')+'>'+esc(t.name)+'</option>').join('');
|
||||
@@ -943,41 +945,60 @@ function onTaskTypeChange(){
|
||||
|
||||
// ================== 步骤编辑器 ==================
|
||||
var STEP_LIB=[
|
||||
{type:'open_app',label:'打开App',icon:'▶',params:{package:'',wait_home:false,home_feature:''}},
|
||||
{type:'stop_app',label:'结束App',icon:'⏹',params:{package:''}},
|
||||
{type:'screen_on',label:'亮屏',icon:'💡',params:{}},
|
||||
{type:'screen_off',label:'息屏',icon:'🌙',params:{}},
|
||||
{type:'keep_screen',label:'保持亮屏',icon:'🔆',params:{mode:'on'}},
|
||||
{type:'key_event',label:'按键',icon:'⌨️',params:{key:'back'}},
|
||||
{type:'swipe',label:'滑动',icon:'↕',params:{direction:'up',duration_min:0.25,duration_max:0.50}},
|
||||
{type:'swipe_until',label:'滑动直到元素',icon:'🔍',params:{selector_type:'xpath',selector_value:'',direction:'up',max_swipes:8,click_when_found:true}},
|
||||
{type:'click_xy',label:'点击坐标',icon:'🎯',params:{x:50,y:50}},
|
||||
{type:'long_click',label:'长按元素',icon:'👆',params:{selector_type:'xpath',selector_value:'',duration:1.0,wait_timeout:2}},
|
||||
{type:'wait_el',label:'等待元素',icon:'⏳',params:{selector_type:'xpath',selector_value:'',timeout:10}},
|
||||
{type:'click',label:'点击元素',icon:'✦',params:{selector_type:'xpath',selector_value:'',wait_timeout:2}},
|
||||
{type:'input_text',label:'输入文字',icon:'⌨',params:{mode:'random',texts:'你好\n有趣\n支持',fixed_text:''}},
|
||||
{type:'wait',label:'等待',icon:'⏱',params:{min:1.0,max:3.0}},
|
||||
{type:'loop',label:'循环块',icon:'↻',params:{loop_mode:'rounds',max_iterations:10,loop_duration:600,children:[]}},
|
||||
{type:'group',label:'动作组',icon:'📦',params:{children:[]}}
|
||||
// cat:interact=交互操作 / screen=屏幕与App / flow=流程控制(操作库按此分组展示)
|
||||
{type:'click',label:'点击元素',icon:'✦',cat:'interact',params:{selector_type:'xpath',selector_value:'',wait_timeout:2}},
|
||||
{type:'click_xy',label:'点击坐标',icon:'🎯',cat:'interact',params:{x:50,y:50}},
|
||||
{type:'long_click',label:'长按元素',icon:'👆',cat:'interact',params:{selector_type:'xpath',selector_value:'',duration:1.0,wait_timeout:2}},
|
||||
{type:'swipe',label:'滑动',icon:'↕',cat:'interact',params:{direction:'up',duration_min:0.25,duration_max:0.50}},
|
||||
{type:'swipe_until',label:'滑动直到元素',icon:'🔍',cat:'interact',params:{selector_type:'xpath',selector_value:'',direction:'up',max_swipes:8,click_when_found:true}},
|
||||
{type:'input_text',label:'输入文字',icon:'⌨',cat:'interact',params:{mode:'random',texts:'你好\n有趣\n支持',fixed_text:'',clear_first:true}},
|
||||
{type:'key_event',label:'按键',icon:'⌨️',cat:'interact',params:{key:'back'}},
|
||||
{type:'open_app',label:'打开App',icon:'▶',cat:'screen',params:{package:'',wait_home:false,home_feature:''}},
|
||||
{type:'stop_app',label:'结束App',icon:'⏹',cat:'screen',params:{package:''}},
|
||||
{type:'screen_on',label:'亮屏',icon:'💡',cat:'screen',params:{}},
|
||||
{type:'screen_off',label:'息屏',icon:'🌙',cat:'screen',params:{}},
|
||||
{type:'keep_screen',label:'保持亮屏',icon:'🔆',cat:'screen',params:{mode:'on'}},
|
||||
{type:'wait',label:'等待',icon:'⏱',cat:'flow',params:{min:1.0,max:3.0}},
|
||||
{type:'wait_el',label:'等待元素',icon:'⏳',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:10}},
|
||||
{type:'loop',label:'循环块',icon:'↻',cat:'flow',params:{loop_mode:'rounds',max_iterations:10,loop_duration:600,children:[]}},
|
||||
{type:'group',label:'动作组',icon:'📦',cat:'flow',params:{children:[]}},
|
||||
{type:'if_el',label:'条件判断',icon:'❓',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:3,ocr_click:false,then:[],else:[]}}
|
||||
];
|
||||
var STEP_LIB_CATS=[['interact','交互操作'],['screen','屏幕与App'],['flow','流程控制']];
|
||||
var _customActions=[]; // 后端加载的自定义动作
|
||||
var _stepIdCounter=0;
|
||||
function _newStepId(){_stepIdCounter++;return 'step_'+Date.now().toString(36)+'_'+_stepIdCounter;}
|
||||
// 步骤的所有子步骤数组(loop/group 的 children、if_el 的 then/else 分支)
|
||||
function _childArrs(s){
|
||||
var arrs=[];
|
||||
if(s&&s.params){
|
||||
if(Array.isArray(s.params.children))arrs.push(s.params.children);
|
||||
if(Array.isArray(s.params.then))arrs.push(s.params.then);
|
||||
if(Array.isArray(s.params.else))arrs.push(s.params.else);
|
||||
}
|
||||
return arrs;
|
||||
}
|
||||
// 递归去掉步骤 id(保存到自定义动作库/拖入画布时重新生成,避免重复拖入后 id 冲突导致多选错乱)
|
||||
function _stripIds(steps){
|
||||
steps.forEach(function(s){
|
||||
delete s.id;
|
||||
if(s.params&&s.params.children&&Array.isArray(s.params.children))_stripIds(s.params.children);
|
||||
_childArrs(s).forEach(_stripIds);
|
||||
});
|
||||
}
|
||||
function _findLib(type){return STEP_LIB.find(function(s){return s.type===type;});}
|
||||
// 选择器组件(类型下拉 + 值输入 + 抓取元素 + 测试按钮),click/long_click/swipe_until/wait_el 复用
|
||||
function _selRowHtml(p, extra){
|
||||
var opts=['xpath','description','text','resourceId','descriptionContains','className'].map(function(d){
|
||||
return '<option value="'+d+'"'+(p.selector_type===d?' selected':'')+'>'+d+'</option>';}).join('');
|
||||
// 选择器组件(类型下拉 + 值输入 + 抓取元素 + 测试按钮),click/long_click/swipe_until/wait_el/if_el 复用
|
||||
// withOcr=true 时额外提供 OCR识别 选项(图片/画布里的文字,仅条件判断支持)
|
||||
function _selRowHtml(p, extra, withOcr){
|
||||
var types=withOcr?['xpath','description','text','resourceId','descriptionContains','className','ocr']:
|
||||
['xpath','description','text','resourceId','descriptionContains','className'];
|
||||
var labels={xpath:'xpath',description:'description',text:'text',resourceId:'resourceId',
|
||||
descriptionContains:'descriptionContains',className:'className',ocr:'OCR识别'};
|
||||
var opts=types.map(function(d){
|
||||
return '<option value="'+d+'"'+(p.selector_type===d?' selected':'')+'>'+labels[d]+'</option>';}).join('');
|
||||
var toggle=withOcr?' onchange="_stepEditor._toggleOcrMode(this)"':'';
|
||||
var pick=_can('devices')?'<button class="btn btn-xs btn-pick" onclick="_stepEditor.openPicker(this)">抓取元素</button>':'';
|
||||
var test=_can('devices')?'<button class="btn btn-xs" onclick="_stepEditor._testStep(this)" title="在设备上试执行本步骤,验证选择器是否命中">测试</button>':'';
|
||||
return '<div class="form-row"><div class="form-group"><label>选择器类型</label><select class="form-control" data-key="selector_type">'+opts+'</select></div>'+
|
||||
return '<div class="form-row"><div class="form-group"><label>选择器类型</label><select class="form-control" data-key="selector_type"'+toggle+'>'+opts+'</select></div>'+
|
||||
'<div class="form-group" style="grid-column:span 2"><label>选择器值</label><div class="sel-picker">'+
|
||||
'<input type="text" class="form-control" data-key="selector_value" value="'+esc(p.selector_value||'')+'" placeholder="如 点赞">'+pick+test+
|
||||
'</div></div></div>'+(extra||'');
|
||||
@@ -1041,17 +1062,19 @@ var _stepEditor={
|
||||
var self=this;
|
||||
steps.forEach(function(s){
|
||||
if(!s.id)s.id=_newStepId();
|
||||
if(s.params&&s.params.children&&Array.isArray(s.params.children)){
|
||||
self._ensureIds(s.params.children);
|
||||
}
|
||||
_childArrs(s).forEach(function(a){self._ensureIds(a);});
|
||||
});
|
||||
},
|
||||
_renderLib:function(){
|
||||
var lib=this._lib;
|
||||
if(!lib)return; // 未初始化编辑器(如仅加载动作列表时)跳过
|
||||
var items=STEP_LIB.map(function(s){
|
||||
return '<div class="lib-item" draggable="true" data-type="'+s.type+'">'+
|
||||
'<span class="ico">'+s.icon+'</span><span>'+s.label+'</span></div>';
|
||||
// 按分类分组展示(STEP_LIB_CATS 定义分类顺序)
|
||||
var items=STEP_LIB_CATS.map(function(c){
|
||||
var list=STEP_LIB.filter(function(s){return s.cat===c[0];}).map(function(s){
|
||||
return '<div class="lib-item" draggable="true" data-type="'+s.type+'" title="'+esc(s.label)+'">'+
|
||||
'<span class="ico">'+s.icon+'</span><span>'+s.label+'</span></div>';
|
||||
}).join('');
|
||||
return '<div class="lib-section"><div class="lib-title">'+c[1]+'</div>'+list+'</div>';
|
||||
}).join('');
|
||||
// 自定义动作区
|
||||
var customHtml='';
|
||||
@@ -1064,7 +1087,7 @@ var _stepEditor={
|
||||
}).join('');
|
||||
customHtml+='</div>';
|
||||
}
|
||||
lib.innerHTML='<div class="lib-title">操作库</div>'+items+customHtml;
|
||||
lib.innerHTML=items+customHtml;
|
||||
var self=this;
|
||||
// 绑定拖拽 + 点击(系统步骤)—— 用 _dragSuppressClick 防止拖拽后重复触发 click
|
||||
lib.querySelectorAll('.lib-item[data-type]').forEach(function(item){
|
||||
@@ -1114,9 +1137,8 @@ var _stepEditor={
|
||||
_renderCard:function(step,idx,parentArr){
|
||||
var self=this;
|
||||
var lib=_findLib(step.type)||{icon:'?',label:step.type};
|
||||
var isContainer=(step.type==='loop'||step.type==='group');
|
||||
var card=document.createElement('div');
|
||||
card.className='step-card'+(step.type==='loop'?' loop-block':'')+(step.type==='group'?' group-block':'');
|
||||
card.className='step-card'+(step.type==='loop'?' loop-block':'')+(step.type==='group'?' group-block':'')+(step.type==='if_el'?' if-block':'');
|
||||
card.draggable=true;
|
||||
card.dataset.idx=idx;
|
||||
card.dataset.stepId=step.id||'';
|
||||
@@ -1165,11 +1187,29 @@ var _stepEditor={
|
||||
self._handleDrop(e,parentArr,idx);
|
||||
});
|
||||
// loop/group 子步骤
|
||||
if(isContainer&&step.params&&step.params.children){
|
||||
var childWrap=document.createElement('div');
|
||||
childWrap.className='step-children';
|
||||
this._renderStepList(childWrap,step.params.children);
|
||||
card.appendChild(childWrap);
|
||||
if(step.type==='loop'||step.type==='group'){
|
||||
if(step.params&&step.params.children){
|
||||
var childWrap=document.createElement('div');
|
||||
childWrap.className='step-children';
|
||||
this._renderStepList(childWrap,step.params.children);
|
||||
card.appendChild(childWrap);
|
||||
}
|
||||
}
|
||||
// if_el 双分支:找到时(then) / 未找到时(else),各自独立容器接收拖入
|
||||
if(step.type==='if_el'&&step.params){
|
||||
[['then','✅ 找到时执行','if-then'],['else','❌ 未找到时执行','if-else']].forEach(function(b){
|
||||
var branch=document.createElement('div');
|
||||
branch.className='if-branch';
|
||||
var title=document.createElement('div');
|
||||
title.className='branch-title '+b[0];
|
||||
title.textContent=b[1];
|
||||
var wrap=document.createElement('div');
|
||||
wrap.className='step-children '+b[2];
|
||||
self._renderStepList(wrap,step.params[b[0]]||[]);
|
||||
branch.appendChild(title);
|
||||
branch.appendChild(wrap);
|
||||
card.appendChild(branch);
|
||||
});
|
||||
}
|
||||
return card;
|
||||
},
|
||||
@@ -1250,6 +1290,13 @@ var _stepEditor={
|
||||
}else if(step.type==='group'){
|
||||
h+='<div class="help">动作组:按顺序执行下方子步骤一次。把多个步骤打包成一个可复用的整体。</div>';
|
||||
h+='<div class="form-group"><button class="btn btn-xs btn-primary" onclick="_stepEditor.saveAsCustomAction(\''+esc(step.id||'')+'\')">存为自定义动作</button> <span class="help" style="display:inline">把本组步骤保存到左侧自定义动作库,方便复用</span></div>';
|
||||
}else if(step.type==='if_el'){
|
||||
h+=_selRowHtml(p,
|
||||
'<div class="form-group"><label>查找超时(秒)</label><input type="number" class="form-control" data-key="timeout" value="'+(p.timeout||3)+'" min="0.5" max="30" step="0.5"></div>'+
|
||||
'<div class="form-group form-check" data-ocr-field style="display:'+(p.selector_type==='ocr'?'block':'none')+'">'+
|
||||
'<input type="checkbox" data-key="ocr_click" '+(p.ocr_click?'checked':'')+'><label>OCR 命中后自动点击该文字</label></div>'+
|
||||
'<div class="help">超时内找到元素 → 执行下方"✅ 找到时"分支;未找到 → 执行"❌ 未找到时"分支。两分支可放任意步骤,也可再嵌套条件判断/循环;'+
|
||||
'<b>OCR识别</b>可匹配图片/画布里的文字(截屏识别,跨平台)</div>', true);
|
||||
}
|
||||
// 共享:触发概率(所有步骤通用,0~100)
|
||||
h+='<div class="form-row" style="margin-top:8px"><div class="form-group"><label>触发概率(%)</label><input type="number" class="form-control" data-key="probability" value="'+(p.probability!=null?p.probability:100)+'" min="0" max="100"><div class="help">0~100,<100 表示按概率执行(如 30=隔几次才触发一次,适合"偶尔点赞")</div></div></div>';
|
||||
@@ -1468,21 +1515,21 @@ var _stepEditor={
|
||||
}else showToast(res.error||'保存失败','error');
|
||||
});
|
||||
},
|
||||
// 在嵌套结构中按 id 查找步骤(group/loop 的子步骤也找得到)
|
||||
// 在嵌套结构中按 id 查找步骤(group/loop 的子步骤、if_el 的分支步骤也找得到)
|
||||
_findStepById:function(steps,id){
|
||||
var self=this;
|
||||
for(var i=0;i<steps.length;i++){
|
||||
var s=steps[i];
|
||||
if(s.id===id)return s;
|
||||
if(s.params&&s.params.children&&Array.isArray(s.params.children)){
|
||||
var f=this._findStepById(s.params.children,id);
|
||||
if(f)return f;
|
||||
}
|
||||
var f=null;
|
||||
_childArrs(s).forEach(function(a){ if(!f)f=self._findStepById(a,id); });
|
||||
if(f)return f;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
_collectByIds:function(steps,ids){
|
||||
// 收集选中的步骤(深拷贝,去掉 id)。
|
||||
// 递归搜索嵌套子步骤:☑ 也能选中 group/loop 内部的步骤,只搜顶层会漏
|
||||
// 递归搜索嵌套子步骤:☑ 也能选中 group/loop/if_el 内部的步骤,只搜顶层会漏
|
||||
var result=[];
|
||||
var self=this;
|
||||
steps.forEach(function(s){
|
||||
@@ -1490,8 +1537,10 @@ var _stepEditor={
|
||||
var copy=JSON.parse(JSON.stringify(s));
|
||||
_stripIds([copy]);
|
||||
result.push(copy); // 容器选中时子步骤已包含在拷贝里,不再单独收集
|
||||
}else if(s.params&&s.params.children&&Array.isArray(s.params.children)){
|
||||
result=result.concat(self._collectByIds(s.params.children,ids));
|
||||
}else{
|
||||
_childArrs(s).forEach(function(a){
|
||||
result=result.concat(self._collectByIds(a,ids));
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
@@ -1514,6 +1563,12 @@ var _stepEditor={
|
||||
g.style.display=g.dataset.lmode===mode?'block':'none';
|
||||
});
|
||||
},
|
||||
// 条件判断:选择器类型切到 OCR 时显示"命中后点击"开关
|
||||
_toggleOcrMode:function(sel){
|
||||
var card=sel.closest('.step-card');
|
||||
var f=card&&card.querySelector('[data-ocr-field]');
|
||||
if(f)f.style.display=sel.value==='ocr'?'block':'none';
|
||||
},
|
||||
// 给数组分配唯一 id,用于 move/duplicate/remove/拖拽定位到正确的父数组
|
||||
_arrId:function(arr){
|
||||
if(arr===this._steps)return 'root';
|
||||
@@ -1532,10 +1587,13 @@ var _stepEditor={
|
||||
},
|
||||
// 判断 step 的子孙数组中是否包含 arr(防循环块套进自己导致渲染无限递归)
|
||||
_containsArr:function(step,arr){
|
||||
if(!step||!step.params||!Array.isArray(step.params.children))return false;
|
||||
if(step.params.children===arr)return true;
|
||||
for(var i=0;i<step.params.children.length;i++){
|
||||
if(this._containsArr(step.params.children[i],arr))return true;
|
||||
var arrs=_childArrs(step);
|
||||
if(!arrs.length)return false;
|
||||
if(arrs.indexOf(arr)>=0)return true;
|
||||
for(var i=0;i<arrs.length;i++){
|
||||
for(var j=0;j<arrs[i].length;j++){
|
||||
if(this._containsArr(arrs[i][j],arr))return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -1601,10 +1659,14 @@ var _stepEditor={
|
||||
}else if(s.params.mode==='random'&&!s.params.texts){
|
||||
warnings.push(name+' "'+label+'": 候选文字为空');
|
||||
}
|
||||
}else if(s.type==='if_el'){
|
||||
if(!s.params.selector_value){
|
||||
warnings.push(name+' "'+label+'": 条件判断的选择器为空,会直接走"未找到"分支。');
|
||||
}
|
||||
}
|
||||
if(s.params&&s.params.children&&s.params.children.length){
|
||||
check(s.params.children,name+' 子步骤');
|
||||
}
|
||||
_childArrs(s).forEach(function(a){
|
||||
if(a.length)check(a,name+' 子步骤');
|
||||
});
|
||||
});
|
||||
}
|
||||
check(steps,'');
|
||||
@@ -1631,11 +1693,20 @@ var _stepEditor={
|
||||
}
|
||||
}
|
||||
});
|
||||
// 递归子步骤
|
||||
// 递归子步骤:loop/group 的 children
|
||||
var childWrap=card.querySelector(':scope > .step-children');
|
||||
if(childWrap&&steps[i].params&&steps[i].params.children){
|
||||
self._syncParams(childWrap,steps[i].params.children);
|
||||
}
|
||||
// if_el 双分支(then/else 在 .if-branch 内,不会与上面的直接子级选择器冲突)
|
||||
var thenWrap=card.querySelector(':scope > .if-branch .step-children.if-then');
|
||||
if(thenWrap&&steps[i].params&&Array.isArray(steps[i].params.then)){
|
||||
self._syncParams(thenWrap,steps[i].params.then);
|
||||
}
|
||||
var elseWrap=card.querySelector(':scope > .if-branch .step-children.if-else');
|
||||
if(elseWrap&&steps[i].params&&Array.isArray(steps[i].params.else)){
|
||||
self._syncParams(elseWrap,steps[i].params.else);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 把画布 DOM 上的参数同步回 _steps,并自动保存草稿(操作前调用防丢失)
|
||||
@@ -2742,7 +2813,7 @@ function closeModal(){
|
||||
stopShotTimer();
|
||||
_shotSerial='';
|
||||
const box=document.getElementById('modal-box');
|
||||
if(box)box.classList.remove('shot-modal');
|
||||
if(box){box.classList.remove('shot-modal');box.classList.remove('wide-modal');}
|
||||
}catch(e){console.warn('closeModal cleanup error:',e);}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user