From e3acb5739c414c2a14660b068d395cde679ec12c Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Tue, 11 Aug 2026 08:27:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8A=A8=E4=BD=9C=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E4=BF=AE=E5=A4=8D=EF=BC=88toast=20=E6=9C=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89/CSRF=20=E7=BC=BA=E5=A4=B1/=E5=B5=8C=E5=A5=97=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E6=89=93=E5=8C=85/=E8=B7=A8=E5=B1=82=E7=BA=A7?= =?UTF-8?q?=E6=8B=96=E6=8B=BD/=E6=AD=A5=E9=AA=A4=20id=20=E5=86=B2=E7=AA=81?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/admin/monitor.js | 142 +++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 32 deletions(-) diff --git a/static/admin/monitor.js b/static/admin/monitor.js index 54dc8a5..1fec537 100644 --- a/static/admin/monitor.js +++ b/static/admin/monitor.js @@ -700,7 +700,7 @@ function openTaskModal(jobId){ '
可留空使用默认参数。抖音任务可设 max_duration(秒,如 1800=30分钟)限制单设备运行时长
'+ '
'+ '
步骤编辑器用步骤卡片上的 ☑ 选中多个步骤,可打包成自定义动作'+ - ''+ + (_can('tasks')?'':'')+ '
'+ '
'+ '
操作库
'+ @@ -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 '
'+ + '
'+ + ''+pick+test+ + '
'+(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+='
0~100,<100 表示按概率执行(如 30=隔几次才触发一次,适合"偶尔点赞")
'; @@ -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){