2025-02-09 19:56:41 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
|
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
|
|
|
<meta http-equiv="Expires" content="0">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
|
|
|
<title>UI</title>
|
|
|
|
|
<link rel="stylesheet" href="assets/css/sweetalert2.min.css">
|
|
|
|
|
<link rel="stylesheet" href="assets/css/iconfont.css">
|
|
|
|
|
<script type="text/javascript" src="assets/js/jquery.min.js"></script>
|
|
|
|
|
<script type="text/javascript" src="assets/js/vue.min.js"></script>
|
|
|
|
|
<script type="text/javascript" src="assets/js/axios.min.js"></script>
|
|
|
|
|
<script type="text/javascript" src="assets/js/lodash.min.js"></script>
|
|
|
|
|
<script type="text/javascript" src="assets/js/sweetalert2.min.js"></script>
|
2025-02-11 01:01:14 +08:00
|
|
|
|
<script type="text/javascript" src="assets/js/reconnecting-websocket.min.js"></script>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<style>body{user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;background-color:#fff;font-family:Arial,sans-serif;background-color:#f4f4f4;margin:0;padding:0;display:flex;justify-content:center;align-items:center;height:100vh}[v-cloak]{display:none}h1{font-size:22px;color:#555;padding:0 2px}img{-webkit-user-drag:none;user-drag:none;-webkit-user-select:none;user-select:none}button{outline:0}.main{background-color:#fff;padding:0;margin:0;border-radius:0;box-shadow:0 0 10px rgba(0,0,0,.1);width:100%;max-width:480px;max-height:960px;text-align:center;align-items:center;overflow:auto;height:100vh}.head{position:sticky;top:0;margin:0;height:auto;z-index:500}.list{height:auto}.main::-webkit-scrollbar{width:4px}.main::-webkit-scrollbar-thumb{background-color:#ff6c6c;border-radius:0}.main::-webkit-scrollbar-track{background:#f3f3f3}.bar,.list{align-items:center}.bar{display:flex;justify-content:center}.logo{width:38px;height:38px;padding:2px}.user{color:#fff;padding:10px;margin:7px 5px;text-align:center;cursor:pointer;align-items:center;border-radius:5px;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all .15s}.plugin-all{color:#fff;padding:10px;margin:0 5px 0 5px;text-align:center;cursor:pointer;align-items:center;border-radius:0;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all .15s;border:1px solid #ccc;box-shadow:0 2px 5px rgba(80,80,80,.5);background:linear-gradient(rgba(0,0,0,.42),rgba(9,9,9,.38));backdrop-filter:blur(15px)}.user:hover{box-shadow:0 1px 8px rgba(96,96,96,.55)}.running{background-image:linear-gradient(45deg,#88e1c5,#9face6)}.running,.stopped{transition:all .15s}.stopped{background-image:linear-gradient(45deg,#b3b3b3,#e9ebec)}.plugins{background-image:linear-gradient(90deg,#fd9a9e,#fadacf);background-size:200% 200%;animation:gradientAnimation 3.5s ease infinite;transition:all .15s}@keyframes gradientAnimation{0%{background-position:0 50%}50%{background-position:100% 50%}to{background-position:0 50%}}.extend{display:block;position:relative;background-color:transparent}.extend-show-enter-active,.extend-show-leave-active{transition:opacity .12s}.extend-show-enter,.extend-show-leave-to{opacity:.05}.h_div{display:flex;justify-content:space-between;align-items:center;transition:background-color .3s}.v_div{display:flex;flex-direction:column;transition:background-color .3s}.label{display:flex;align-items:left;width:75%}.id,.name{display:inline-block;vertical-align:middle;font-family:Arial,sans-serif;margin:0;padding:0;height:auto;text-align:left;font-size:15px;font-weight:700;text-shadow:0 0 1px #ccc;overflow:hidden;white-space:nowrap}.id{width:auto;margin-right:auto}.name{margin:0 0 0 10px;cursor:pointer;max-width:240px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.logging,.result,.summary,.windows{display:inline-block;vertical-align:middle;font-family:Arial,sans-serif;height:auto;font-size:14px;font-weight:400;text-shadow:0 0 1px #ccc;white-space:nowrap;overflow:hidden;-webkit-marquee-style:slide;-webkit-marquee-speed:normal}.logging{font-weight:500;text-align:left;text-shadow:0 0 2px #ccc;width:222px;margin:0;padding:0;text-overflow:ellipsis}.result{margin:0 0 0 10px;font-weight:700;text-align:left;width:auto;max-width:250px;padding:0}.summary{text-align:left;width:180px;margin:0;padding:0;text-overflow:ellipsis}.windows{margin:0;text-align:right;width:65px}.h_divider{margin:4px 0;width:100%;height:1px}.h_divider,.v_divider{background-color:hsla(0,0%,100%,.35)}.v_divider{margin:0 4px;width:1px;height:24px}.close{font-family:inherit;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 0 0 #fff;position:relative;right:0;background-color:transparent;border:0;cursor:pointer;opacity:.38}.button{color:#fff;margin:2px 0;padding:10px 24px;font-size:16px;border:none;border-radius:999px;cursor:pointer}.button:disabled{pointer-events:none;filter:saturate(20%);opacity:.25}.die-plugin-shutdown-all-users{margin:0 1px;font-size:14px;background-color:rgba(255,0,0,.85);border:1px solid #aaa;border-radius:5px}.run{background-color:#4caf50}.die,.run{padding:6p
|
2025-02-09 19:56:41 +08:00
|
|
|
|
<body>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<div class="main" id="app" v-cloak>
|
|
|
|
|
<div class="bar" v-show="userOnlineCount == 0">
|
2025-02-09 19:56:41 +08:00
|
|
|
|
<img class="logo" src="assets/images/logo-chrome.png">
|
|
|
|
|
<h1 class="unselectable">用户管理</h1>
|
|
|
|
|
</div>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<div class="head" v-show="userOnlineCount">
|
|
|
|
|
<div class="plugin-all">
|
|
|
|
|
<div class="v_div">
|
|
|
|
|
<div class="h_div">
|
|
|
|
|
<p class="unselectable id">{{ pluginBatchExecutionStatus ? '正在执行:' + pluginBatchExecutionName : '批量执行' }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="h_divider"></div>
|
|
|
|
|
<div v-show="!pluginBatchExecutionStatus" class="btn-group">
|
|
|
|
|
<button v-for="(plugin_id, plugin_index) in Object.keys(pluginList).sort()" :key="plugin_index" class="btn" @click.stop="pluginAllUsersRun(plugin_id, pluginList[plugin_id].requirements, $event)"><i class="iconfont top-flashlight_fill"></i><p class="text-item">{{ pluginList[plugin_id].name }}</p></button>
|
|
|
|
|
</div>
|
|
|
|
|
<button v-show="pluginBatchExecutionStatus" class="button unselectable die-plugin-shutdown-all-users" @click.stop="pluginAllUsersShutdown($event)">结束运行</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
<div class="list" id="userList">
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<div v-for="(item, key) in userDetails" :key="key">
|
|
|
|
|
<div :class="{'user': true, 'running': item.is_running && !item.plugin_running, 'stopped': !item.is_running && !item.plugin_running, 'plugins': item.plugin_running}" @mouseleave="closedTab(item.user_id)">
|
2025-02-09 19:56:41 +08:00
|
|
|
|
<div class="v_div">
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<div class="h_div" @dblclick.stop="doubleTab(item.user_id, $event)">
|
|
|
|
|
<div class="label">
|
|
|
|
|
<p class="unselectable id" :title="item.user_id" @click.stop="userSetName(item.user_id, $event)" v-if="!item.user_name || item.user_name.trim() === ''">未命名</p>
|
|
|
|
|
<p class="unselectable id" :title="item.user_id" @click.stop="userSetName(item.user_id, $event)" v-else>{{ item.user_name }}</p>
|
|
|
|
|
<p class="unselectable result" :title="item.plugin_execution_result" :style="{ color: item.plugin_execution_result_color }">{{ item.plugin_execution_result }}</p>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<a class="close" @click.stop="userDel(item.user_id, $event)"><span class="unselectable" aria-hidden="true">×</span></a>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
<div class="h_divider"></div>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<div class="h_div" @click.stop="toggleTab(item.user_id, $event)">
|
2025-02-11 19:02:10 +08:00
|
|
|
|
<div class="v_div">
|
|
|
|
|
<div class="h_div" v-if="item.is_running && userLoggingList[item.user_id] && (parseInt(Date.now() / 1000) - userLoggingList[item.user_id].time) < 10">
|
|
|
|
|
<p class="unselectable logging" :title="userLoggingList[item.user_id].data">{{ userLoggingList[item.user_id].data }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="h_div" v-else>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<p class="unselectable summary" v-if="item.is_running !== true">用户未运行</p>
|
2025-02-11 19:02:10 +08:00
|
|
|
|
<p class="unselectable summary" v-else>{{ item.alert || item.title }}</p>
|
|
|
|
|
<p class="unselectable windows" v-if="item.is_running !== true"></p>
|
|
|
|
|
<p class="unselectable windows" v-else>{{ !item.window_handles ? '0个页面' : item.window_handles.length + '个页面' }}</p>
|
|
|
|
|
</div>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
<div class="v_divider"></div>
|
|
|
|
|
<div class="unselectable">
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<button class="button unselectable run" :disabled="item.is_running == 1" @click.stop="userRun(item.user_id, $event)">运行</button>
|
|
|
|
|
<button v-if="!item.plugin_running" class="button unselectable die" :disabled="item.is_running == 0" @click.stop="userDie(item.user_id, $event)">停止</button>
|
|
|
|
|
<button v-else class="button unselectable die die-plugin-shutdown" :disabled="item.is_running == 0" @click.stop="pluginShutdown(item.user_id, $event)">停止</button>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<transition name="extend-show">
|
|
|
|
|
<div class="extend" v-show="userPluginDisplayStatus[item.user_id] && item.is_running && !item.plugin_running">
|
|
|
|
|
<div class="btn-group">
|
|
|
|
|
<button v-for="(plugin_id, plugin_index) in Object.keys(pluginList).sort()" :key="plugin_index" class="btn" @click.stop="pluginRun(item.user_id, plugin_id, pluginList[plugin_id].requirements, $event)"><i class="iconfont top-flashlight_fill"></i><p class="text-item">{{ pluginList[plugin_id].name }}</p></button>
|
|
|
|
|
</div>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
</transition>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<p class="desc unselectable" v-if="Object.keys(userDetails).length === 0">暂时还没有用户,请先添加~</p>
|
|
|
|
|
<button class="button unselectable" id="addUser" @click.stop="userAdd($event)">+</button>
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="rollbar"><div class="rollbar-item" id="back-to-top" style="height:0" title="返回顶部"><i class="iconfont-x-big top-top"></i></div></div>
|
|
|
|
|
<script>window.onload=function(){window.addEventListener('mousewheel',function(event){if(event.ctrlKey===true||event.metaKey){event.preventDefault()}},{passive:false});document.addEventListener('keydown',function(event){if((event.ctrlKey===true||event.metaKey===true)&&(event.which===61||event.which===107||event.which===173||event.which===109||event.which===187||event.which===189)){event.preventDefault()}},false)}</script>
|
|
|
|
|
<script>window.addEventListener("load",function(){let backBtn=document.getElementById("back-to-top");let bodyObj=document.getElementById("app");let hiddenControl=function(element,isHidden){if(isHidden){backBtn.style.height="0"}else{backBtn.style.height="40px"}};bodyObj.addEventListener("scroll",function(){if(bodyObj.scrollTop>bodyObj.offsetHeight||bodyObj.scrollTop>300){hiddenControl(backBtn,0)}else{hiddenControl(backBtn,1)}});backBtn.addEventListener("click",function(e){e.preventDefault();bodyObj.scrollTo({top:0,behavior:"smooth"})})});</script>
|
2025-03-01 01:05:40 +08:00
|
|
|
|
<script>const Toast=Swal.mixin({toast:true,position:"center",showConfirmButton:false,timer:2250,timerProgressBar:false,});const Backs=Swal.mixin({html:"",timer:750,timerProgressBar:true,showConfirmButton:false,allowOutsideClick:false,});const Message=Swal.mixin({toast:true,position:"top",width:"480px",background:"#727272",color:"#fdfdfd",showConfirmButton:false,timer:3500,timerProgressBar:false,icon:"info",iconColor:"#d5d5d5",didOpen:(toast)=>{toast.onmouseenter=Swal.stopTimer;toast.onmouseleave=Swal.resumeTimer},});const FilePreferred=Swal.mixin({text:"选择文件来源",input:"select",inputOptions:{"preferred":"首选文件","upload":"上传文件"},showConfirmButton:true,showCancelButton:true,confirmButtonText:"确定",cancelButtonText:"取消",allowOutsideClick:false,});const FileUpload=Swal.mixin({text:"打开文件",input:"file",inputAttributes:{accept:"*"},inputValidator:(value)=>{if(value===null){return"还未选择文件"}},showConfirmButton:true,showCancelButton:true,confirmButtonText:"确定",cancelButtonText:"取消",allowOutsideClick:false,showLoaderOnConfirm:true,preConfirm:(files)=>{if(!(files instanceof FileList)){files=[files]}let formData=new FormData();for(let i=0;i<files.length;i++){formData.append("files",files[i])}return axios.post("./upload_files",formData).then((response)=>{if(response.data.code!=0){throw new Error(response.data.info);}return response.data.data[0]}).catch((error)=>{Swal.showValidationMessage(error);return false})},});const MultFileUpload=Swal.mixin({text:"打开文件",input:"file",inputAttributes:{multiple:"multiple",accept:"*"},inputValidator:(value)=>{if(value===null){return"还未选择文件"}},showConfirmButton:true,showCancelButton:true,confirmButtonText:"确定",cancelButtonText:"取消",allowOutsideClick:false,showLoaderOnConfirm:true,preConfirm:(files)=>{if(!(files instanceof FileList)){files=[files]}let formData=new FormData();for(let i=0;i<files.length;i++){formData.append("files",files[i])}return axios.post("./upload_files",formData).then((response)=>{if(response.data.code!=0){throw new Error(response.data.info);}return response.data.data}).catch((error)=>{Swal.showValidationMessage(error);return false})},});const TextUpload=Swal.mixin({text:"输入内容",input:"text",showConfirmButton:true,showCancelButton:true,confirmButtonText:"确定",cancelButtonText:"取消",allowOutsideClick:false,});const MultTextUpload=Swal.mixin({text:"输入内容",input:"textarea",showConfirmButton:true,showCancelButton:true,confirmButtonText:"确定",cancelButtonText:"取消",allowOutsideClick:false,});const MixinInput=async function(requirements,preferred_file){if(Array.isArray(requirements)){let inputClassList={"file-preferred":FilePreferred,"file":FileUpload,"file-mult":MultFileUpload,"text":TextUpload,"text-mult":MultTextUpload,};let _reqs=requirements;let _lens=requirements.length;let _results=[];for(let i=0;i<_lens;i++){let inputClass=inputClassList[_reqs[i]["type"]];if(!inputClass){Toast.fire({icon:"error",title:"不支持的输入类型"});return null}let result=null;if(inputClass===inputClassList["file"]&&preferred_file){result=await inputClassList["file-preferred"].fire({text:_reqs[i]["html"],inputOptions:{"preferred":"首选文件","upload":"上传文件"}});if(!result.isConfirmed){return null}else{if(result.value=="preferred"){_results.push(preferred_file);continue}}}result=await inputClass.fire({text:_reqs[i]["html"]});if(!result.isConfirmed){return null}else{_results.push(result.value)}}return _results}else{return[]}};new Vue({el:"#app",data:{userOnlineCount:null,userDetails:{},pluginBatchExecutionId:null,pluginBatchExecutionName:null,pluginBatchExecutionTime:null,pluginBatchExecutionStatus:null,pluginBatchExecutionResult:null,pluginList:{},pluginPreferredFile:null,userLoggingList:{},userPluginDisplayStatus:{},messageSocket:null,isConnected:true,},created(){this.fetchPluginPreferredFile();this.fetchPluginData();this.fetchData();this.initMessageSocket();setInterval(this.fetchPluginPreferredFile,2250);setInterval(this.fetchData,1750)},methods:{fetchPluginPreferredFi
|
2025-02-09 19:56:41 +08:00
|
|
|
|
</body>
|
|
|
|
|
</html>
|