'+
'
'+
@@ -753,7 +753,25 @@ var STEP_LIB=[
var _customActions=[]; // 后端加载的自定义动作
var _stepIdCounter=0;
function _newStepId(){_stepIdCounter++;return 'step_'+Date.now().toString(36)+'_'+_stepIdCounter;}
+// 递归去掉步骤 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);
+ });
+}
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 '
';}).join('');
+ var pick=_can('devices')?'
':'';
+ var test=_can('devices')?'
':'';
+ return '
'+(extra||'');
+}
// 加载自定义动作库
function _loadCustomActions(){
@@ -770,6 +788,7 @@ var _stepEditor={
init:function(steps,maxDuration,draftKey,canvasId,libId){
this._draftKey=draftKey||'new';
this._selected=[];
+ this._updatePackBtns();
this._dragSuppressClick=false;
// 可指定编辑容器(任务编辑器 vs 动作编辑器),默认任务编辑器
this._canvas=document.getElementById(canvasId||'se-canvas');
@@ -986,7 +1005,7 @@ var _stepEditor={
h+='
将下方子步骤(拖拽到本卡片内)循环执行指定轮次或时长;按时间时配合"运行时长上限"看预计结束时间
';
}else if(step.type==='group'){
h+='
动作组:按顺序执行下方子步骤一次。把多个步骤打包成一个可复用的整体。
';
- h+='
把本组步骤保存到左侧自定义动作库,方便复用
';
+ h+='
把本组步骤保存到左侧自定义动作库,方便复用
';
}
// 共享:触发概率(所有步骤通用,0~100)
h+='
';
@@ -1014,9 +1033,9 @@ var _stepEditor={
e.preventDefault();
e.stopPropagation(); // 阻止冒泡到外层卡片/容器,避免重复添加
container.classList.remove('drag-over');
- if(e.target===container||e.target.classList.contains('se-empty')){
- self._handleDrop(e,self._dropArrs.get(container),-1);
- }
+ // 子卡片都会 stopPropagation,能冒泡到这里的 drop 一定落在空白区
+ // (含 .se-empty 的提示文字),直接追加到数组末尾
+ self._handleDrop(e,self._dropArrs.get(container),-1);
});
},
_handleDrop:function(e,arr,idx){
@@ -1027,17 +1046,32 @@ var _stepEditor={
// 延迟到下一帧渲染,保证一次拖入只加一次。
function rerender(){ setTimeout(function(){self._renderCanvas();},0); }
if(data.startsWith('move:')){
- // 内部排序
+ // 拖拽排序:同层级排序 / 跨层级移入(把步骤拖进循环块、循环套循环)
var parts=data.split(':');
var fromIdx=parseInt(parts[1]);
var fromPath=parts[2];
- if(fromPath===this._arrId(arr)&&fromIdx!==idx){
- this._syncFromDom();
- var moved=arr.splice(fromIdx,1)[0];
- if(idx<0)arr.push(moved);
- else if(fromIdx
=arr.length)arr.push(moved2);
+ else arr.splice(idx,0,moved2);
+ rerender();
+ }
+ }
}
}else if(data.startsWith('action:')){
// 从自定义动作库拖入 → 展开为 group 步骤
@@ -1047,6 +1081,7 @@ var _stepEditor={
var ns=this._makeStep({type:'group',label:action.name,icon:action.icon||'📦',params:{children:[]}});
ns.label=action.name;
ns.params.children=JSON.parse(JSON.stringify(action.steps));
+ _stripIds(ns.params.children); // 重新生成 id,避免同一动作拖入多次后 id 冲突
this._ensureIds(ns.params.children);
if(idx<0||idx>=arr.length)arr.push(ns);
else arr.splice(idx,0,ns);
@@ -1081,15 +1116,17 @@ var _stepEditor={
var ns=this._makeStep({type:'group',label:action.name,icon:action.icon||'📦',params:{children:[]}});
ns.label=action.name;
ns.params.children=JSON.parse(JSON.stringify(action.steps));
+ _stripIds(ns.params.children); // 重新生成 id,避免同一动作拖入多次后 id 冲突
this._ensureIds(ns.params.children);
this._steps.push(ns);
this._renderCanvas();
},
delCustomAction:function(actionId){
if(!confirm('确定删除这个自定义动作?'))return;
- fetch('/api/custom_actions/'+actionId,{method:'DELETE'}).then(function(r){return r.json();}).then(function(res){
- if(res.ok){_loadCustomActions();toast('已删除','success');}
- else toast(res.error||'删除失败','error');
+ apiDelete('/api/custom_actions/'+actionId).then(function(res){
+ if(!res)return;
+ if(res.ok){_loadCustomActions();showToast('已删除','success');}
+ else showToast(res.error||'删除失败','error');
});
},
toggleSelect:function(stepId){
@@ -1099,44 +1136,74 @@ var _stepEditor={
if(i>=0)this._selected.splice(i,1);
else this._selected.push(stepId);
this._renderCanvas();
- // 更新工具栏按钮状态
- var bar=document.getElementById('se-pack-btn');
- if(bar)bar.disabled=this._selected.length<1;
+ this._updatePackBtns();
},
- saveAsCustomAction:function(){
- // 从 group 步骤的"存为自定义动作"按钮调用:收集当前展开的 group 步骤的 children
- // 或从选中步骤收集
+ // 同步所有"打包选中步骤"按钮的可用状态(任务编辑器 + 动作编辑器)
+ _updatePackBtns:function(){
+ var n=this._selected&&this._selected.length;
+ ['se-pack-btn','se-pack-btn2'].forEach(function(id){
+ var bar=document.getElementById(id);
+ if(bar)bar.disabled=n<1;
+ });
+ },
+ saveAsCustomAction:function(stepId){
+ // 从 group 步骤的"存为自定义动作"按钮调用时传入该 group 的 id:
+ // 优先收集该 group 的子步骤;否则收集 ☑ 选中的步骤
var stepsToSave=[];
- if(this._selected&&this._selected.length){
+ if(stepId){
+ var group=this._findStepById(this._steps,stepId);
+ if(group&&group.params&&group.params.children&&group.params.children.length){
+ stepsToSave=JSON.parse(JSON.stringify(group.params.children));
+ _stripIds(stepsToSave);
+ }
+ }
+ if(!stepsToSave.length&&this._selected&&this._selected.length){
// 从选中步骤收集
stepsToSave=this._collectByIds(this._steps,this._selected);
}
if(!stepsToSave.length){
- toast('请先用 ☑ 选中要打包的步骤','error');
+ showToast('请先用 ☑ 选中要打包的步骤','error');
return;
}
var name=prompt('输入动作名称:','我的动作');
if(!name)return;
var self=this;
- fetch('/api/custom_actions',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({name:name,steps:stepsToSave})})
- .then(function(r){return r.json();})
+ apiPost('/api/custom_actions',{name:name,steps:stepsToSave})
.then(function(res){
+ if(!res)return;
if(res.ok){
- toast('已保存到自定义动作库','success');
+ showToast('已保存到自定义动作库','success');
self._selected=[];
_loadCustomActions();
self._renderCanvas();
- }else toast(res.error||'保存失败','error');
+ self._updatePackBtns();
+ }else showToast(res.error||'保存失败','error');
});
},
+ // 在嵌套结构中按 id 查找步骤(group/loop 的子步骤也找得到)
+ _findStepById:function(steps,id){
+ for(var i=0;i=0){
var copy=JSON.parse(JSON.stringify(s));
- result.push(copy);
+ _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));
}
});
return result;
@@ -1175,6 +1242,15 @@ var _stepEditor={
if(path==='root')return this._steps;
return this._arrById&&this._arrById[path];
},
+ // 判断 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'+
'用下方步骤编辑器编排动作内容,保存后可拖入任务步骤编辑器复用
'+
''+
- '
动作步骤支持循环/分组/概率触发/拖拽排序
'+
+ '
动作步骤支持循环/分组/概率触发/拖拽排序'+
+ (_can('tasks')?'':'')+'
'+
'
'+
'
'+
'
'+
@@ -1619,6 +1696,7 @@ async function saveAction(actionId){
if(!name){showToast('动作名不能为空','error');return;}
const icon=document.getElementById('a-icon').value.trim()||'📦';
const steps=_stepEditor.collect();
+ _stripIds(steps); // 入库前去掉 id,避免重复使用后 id 冲突
if(!steps.length){showToast('至少需要1个步骤','error');return;}
const data={name:name,icon:icon,steps:steps};
if(actionId){