From 939867d100f2b76cadfbb136ba56a9811a02787c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 2 Dec 2022 03:56:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=B9=E6=80=A7=EF=BC=88=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=EF=BC=89=EF=BC=9A=E6=96=B0=E5=A2=9E=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Kernel.php | 17 + app/Http/Controllers/Api/CronsController.php | 205 ++++ app/Http/Controllers/Api/InfosController.php | 24 +- app/Models/Cron.php | 22 + .../2022_12_01_212925_create_crons_table.php | 36 + public/panel/modules/cron.css | 194 ++++ public/panel/modules/cron.js | 961 ++++++++++++++++++ resources/views/cron.blade.php | 281 +++++ resources/views/file.blade.php | 4 +- resources/views/login.blade.php | 4 +- resources/views/monitor.blade.php | 2 +- resources/views/plugin.blade.php | 2 +- resources/views/safe.blade.php | 2 +- resources/views/setting.blade.php | 4 +- routes/api.php | 17 +- routes/web.php | 13 +- 16 files changed, 1753 insertions(+), 35 deletions(-) create mode 100644 app/Http/Controllers/Api/CronsController.php create mode 100644 app/Models/Cron.php create mode 100644 database/migrations/2022_12_01_212925_create_crons_table.php create mode 100644 public/panel/modules/cron.css create mode 100644 public/panel/modules/cron.js create mode 100644 resources/views/cron.blade.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index adb9810a..a7ff71f0 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -4,6 +4,8 @@ namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use App\Models\Cron; +use Illuminate\Support\Carbon; class Kernel extends ConsoleKernel { @@ -16,6 +18,21 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule) { $schedule->command('monitor')->everyMinute(); + // 查询所有计划任务 + $crons = Cron::all(); + foreach ($crons as $cron) { + $schedule->exec('bash /www/server/cron/'.$cron->shell)->withoutOverlapping()->cron($cron->time)->appendOutputTo('/www/server/cron/logs/'.$cron->id.'.log')->when(function ( + ) use ($cron) { + return (boolean) $cron->status; + })->after(function () use ($cron) { + $cron->updated_at = now(); + $cron->save(); + })->onSuccess(function () use ($cron) { + shell_exec('echo "'.Carbon::now()->toDateTimeString().' 任务执行成功" >> /www/server/cron/logs/'.$cron->id.'.log'); + })->onFailure(function () use ($cron) { + shell_exec('echo "'.Carbon::now()->toDateTimeString().' 任务执行失败" >> /www/server/cron/logs/'.$cron->id.'.log'); + }); + } } /** diff --git a/app/Http/Controllers/Api/CronsController.php b/app/Http/Controllers/Api/CronsController.php new file mode 100644 index 00000000..7488c867 --- /dev/null +++ b/app/Http/Controllers/Api/CronsController.php @@ -0,0 +1,205 @@ +input('limit', 10); + + $crons = Cron::query()->orderBy('id', 'desc')->paginate($limit); + $cronData = []; + + foreach ($crons as $k => $v) { + // 格式化时间 + $cronData[$k]['id'] = $v['id']; + $cronData[$k]['name'] = $v['name']; + $cronData[$k]['status'] = $v['status']; + $cronData[$k]['type'] = $v['type']; + $cronData[$k]['time'] = $v['time']; + $cronData[$k]['shell'] = $v['shell']; + $cronData[$k]['script'] = @file_get_contents('/www/server/cron/'.$v['shell']); + $cronData[$k]['created_at'] = Carbon::create($v['created_at'])->toDateTimeString(); + $cronData[$k]['updated_at'] = Carbon::create($v['updated_at'])->toDateTimeString(); + } + + $data['code'] = 0; + $data['msg'] = 'success'; + $data['count'] = $crons->total(); + $data['data'] = $cronData; + return response()->json($data); + } + + /** + * 添加计划任务 + */ + public function add(Request $request): JsonResponse + { + // 消毒 + try { + $credentials = $this->validate($request, [ + 'name' => 'required|max:255', + 'time' => ['required', 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+))*\s?){5}$/'], + 'script' => 'required', + ]); + } catch (ValidationException $e) { + return response()->json(['code' => 1, 'msg' => $e->getMessage()]); + } + + // 将script写入shell文件 + $shellDir = '/www/server/cron/'; + $shellLogDir = '/www/server/cron/logs/'; + if (!is_dir($shellDir)) { + mkdir($shellDir, 0755, true); + } + if (!is_dir($shellLogDir)) { + mkdir($shellLogDir, 0755, true); + } + $shellFile = uniqid().'.sh'; + file_put_contents($shellDir.$shellFile, $credentials['script']); + + $cron = new Cron(); + $cron->name = $credentials['name']; + $cron->status = 1; + $cron->type = '脚本'; + $cron->time = $credentials['time']; + $cron->shell = $shellFile; + $cron->save(); + + $data['code'] = 0; + $data['msg'] = 'success'; + return response()->json($data); + } + + /** + * 修改计划任务 + */ + public function edit(Request $request): JsonResponse + { + // 消毒 + try { + $credentials = $this->validate($request, [ + 'id' => 'required|integer', + 'name' => 'required|max:255', + 'time' => ['required', 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+))*\s?){5}$/'], + 'script' => 'required', + ]); + } catch (ValidationException $e) { + return response()->json(['code' => 1, 'msg' => $e->getMessage()]); + } + + $cron = Cron::query()->find($credentials['id']); + $cron->name = $credentials['name']; + $cron->time = $credentials['time']; + // 将script写入shell文件 + $shellDir = '/www/server/cron/'; + $shellLogDir = '/www/server/cron/logs/'; + if (!is_dir($shellDir)) { + mkdir($shellDir, 0755, true); + } + if (!is_dir($shellLogDir)) { + mkdir($shellLogDir, 0755, true); + } + $shellFile = $cron->shell; + file_put_contents($shellDir.$shellFile, $credentials['script']); + $cron->save(); + + $data['code'] = 0; + $data['msg'] = 'success'; + return response()->json($data); + } + + /** + * 删除计划任务 + */ + public function delete(Request $request): JsonResponse + { + // 消毒 + try { + $credentials = $this->validate($request, [ + 'id' => 'required|integer', + ]); + } catch (ValidationException $e) { + return response()->json(['code' => 1, 'msg' => $e->getMessage()]); + } + + $cron = Cron::query()->find($credentials['id']); + // 删除shell文件 + $shellDir = '/www/server/cron/'; + $shellFile = $cron->shell; + @unlink($shellDir.$shellFile); + // 删除日志文件 + $shellLogDir = '/www/server/cron/logs/'; + $shellLogFile = $shellFile.'.log'; + @unlink($shellLogDir.$shellLogFile); + $cron->delete(); + + $data['code'] = 0; + $data['msg'] = 'success'; + return response()->json($data); + } + + /** + * 修改计划任务状态 + */ + public function setStatus(Request $request): JsonResponse + { + // 消毒 + try { + $credentials = $this->validate($request, [ + 'id' => 'required|integer', + 'status' => 'required|integer', + ]); + } catch (ValidationException $e) { + return response()->json(['code' => 1, 'msg' => $e->getMessage()]); + } + + $cron = Cron::query()->find($credentials['id']); + $cron->status = $credentials['status']; + $cron->save(); + + $data['code'] = 0; + $data['msg'] = 'success'; + return response()->json($data); + } + + /** + * 获取计划任务日志 + */ + public function getLog(Request $request): JsonResponse + { + // 消毒 + try { + $credentials = $this->validate($request, [ + 'id' => 'required|integer', + ]); + } catch (ValidationException $e) { + return response()->json(['code' => 1, 'msg' => $e->getMessage()]); + } + + $log = @file_get_contents('/www/server/cron/logs/'.$credentials['id'].'.log'); + if ($log === false) { + $log = '暂无日志'; + } + + $data['code'] = 0; + $data['msg'] = 'success'; + $data['data'] = $log; + return response()->json($data); + } +} diff --git a/app/Http/Controllers/Api/InfosController.php b/app/Http/Controllers/Api/InfosController.php index 0612a1c5..5f4a0ddb 100644 --- a/app/Http/Controllers/Api/InfosController.php +++ b/app/Http/Controllers/Api/InfosController.php @@ -27,45 +27,45 @@ class InfosController extends Controller ), array( "name" => "website", - "title" => "网站", + "title" => "网站管理", "icon" => "layui-icon-website", "jump" => "website/list" ), array( "name" => "monitor", - "title" => "监控", + "title" => "资源监控", "icon" => "layui-icon-chart-screen", "jump" => "monitor" ), array( "name" => "safe", - "title" => "安全", + "title" => "系统安全", "icon" => "layui-icon-auz", "jump" => "safe" ), array( "name" => "file", - "title" => "文件", + "title" => "文件管理", "icon" => "layui-icon-file", "jump" => "file" ), + array( + "name" => "cron", + "title" => "计划任务", + "icon" => "layui-icon-date", + "jump" => "cron" + ), array( "name" => "plugin", - "title" => "插件", + "title" => "插件中心", "icon" => "layui-icon-app", "jump" => "plugin" ), array( "name" => "setting", - "title" => "设置", + "title" => "面板设置", "icon" => "layui-icon-set", "jump" => "setting" - ), - array( - "name" => "logout", - "title" => "退出", - "icon" => "layui-icon-logout", - "jump" => "logout" ) ) ); diff --git a/app/Models/Cron.php b/app/Models/Cron.php new file mode 100644 index 00000000..74cde163 --- /dev/null +++ b/app/Models/Cron.php @@ -0,0 +1,22 @@ +id(); + $table->string('name')->nullable()->comment('任务名称'); + $table->boolean('status')->comment('任务状态'); + $table->string('type')->comment('任务类型'); + $table->string('time')->comment('任务周期'); + $table->text('shell')->comment('任务脚本文件'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('crons'); + } +}; diff --git a/public/panel/modules/cron.css b/public/panel/modules/cron.css new file mode 100644 index 00000000..241bad3b --- /dev/null +++ b/public/panel/modules/cron.css @@ -0,0 +1,194 @@ + +/* 样式加载完毕的标识 */ +html #layuicss-cron { + display: none; + position: absolute; + width: 1989px; +} + + +/* 主体结构 */ +.layui-cron { + width: 700px; + position: absolute; + z-index: 99999999; + margin: 5px 0; + border-radius: 2px; + font-size: 14px; + -webkit-animation-duration: 0.3s; + animation-duration: 0.3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background-color: white; + display: flex; + flex-direction: column; + box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 5px 0px; + -webkit-animation-name: cron-upbit; + animation-name: cron-upbit; + border: 1px solid #e6e6e6; +} + +.layui-cron-main ul { + padding-left: 10px; +} + +@-webkit-keyframes cron-upbit { + + /* 微微往上滑入 */ + from { + -webkit-transform: translate3d(0, 20px, 0); + opacity: 0.3; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes cron-upbit { + from { + transform: translate3d(0, 20px, 0); + opacity: 0.3; + } + + to { + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +/* tabs */ +.layui-cron>.layui-tab { + margin: 0; + box-shadow: none; + border: none; +} + +/* 行 */ +.cron-row { + padding-left: 13px; +} +/* 格 */ +.cron-grid { + padding-left: 15px; +} + +/* 表达式 */ +.cron-title { + font-weight: 700; + font-size: 14px; + margin: 10px; + margin-bottom: 0; +} + +.cron-box { + margin: 10px; +} + +.cron-box+.cron-box { + margin-top: 0; +} + +/* 按钮 */ +.cron-footer-btns { + text-align: right; + margin-right: 10px; + margin-bottom: 10px; +} + +.cron-footer-btns span { + height: 26px; + line-height: 26px; + margin: 0 0 0 -1px; + padding: 0 10px; + border: 1px solid #C9C9C9; + background-color: #fff; + white-space: nowrap; + vertical-align: top; + border-radius: 2px; + display: inline-block; + cursor: pointer; + font-size: 12px; + box-sizing: border-box; + color: #666; +} + +.cron-footer-btns span:hover { + color: #5FB878; +} + + +/* 表单 */ +.layui-cron .layui-form-radio { + margin-right: 0; +} + +.cron-form { + line-height: 28px; + font-size: 14px; +} + +.cron-input-mid { + display: inline-block; + vertical-align: middle; + margin-top: 6px; + background-color: #e5e5e5; + padding: 0 12px; + height: 28px; + line-height: 28px; + border: 1px solid #ccc; + box-sizing: border-box; +} + +.cron-input { + display: inline-block; + vertical-align: middle; + margin-top: 6px; + padding: 0 8px; + background-color: #fff; + border: 1px solid #ccc; + height: 28px; + line-height: 28px; + box-sizing: border-box; + width: 80px; + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; +} + +.cron-input:focus { + outline: 0; + border: 1px solid #01AAED; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 4px 0px #01AAED; + translate: 1s; +} + +.layui-cron .layui-form-checkbox[lay-skin="primary"] span { + padding-right: 10px; + min-width: 16px; +} + +.layui-cron .layui-form-checkbox[lay-skin="primary"] { + padding-left: 22px; + margin-top: 5px; +} +.layui-cron input[type=number] { + -moz-appearance:textfield; +} +.layui-cron input[type=number]::-webkit-inner-spin-button, +.layui-cron input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +.cron-tips{ + color: grey; + line-height: 28px; + height: 28px; + display: inline-block; + vertical-align: middle; + margin-top: 8px; + margin-left: 5px; +} diff --git a/public/panel/modules/cron.js b/public/panel/modules/cron.js new file mode 100644 index 00000000..4bc9931a --- /dev/null +++ b/public/panel/modules/cron.js @@ -0,0 +1,961 @@ +/** + @ Name:layui.cron Cron表达式解析器 + @ Author:贝哥哥 + @ License:MIT + */ + +layui.define(['lay', 'element', 'form'], function (exports) { //假如该组件依赖 layui.form + var $ = layui.$, layer = layui.layer, lay = layui.lay, element = layui.element, form = layui.form + + + //字符常量 + , MOD_NAME = 'cron', ELEM = '.layui-cron', THIS = 'layui-this', SHOW = 'layui-show', HIDE = 'layui-hide' + + , ELEM_STATIC = 'layui-cron-static', ELEM_FOOTER = 'layui-cron-footer', ELEM_CONFIRM = '.cron-btns-confirm', + ELEM_HINT = 'layui-cron-hint' + + , ELEM_RUN_HINT = 'layui-cron-run-hint' + + //外部接口 + , cron = { + v: '2.0.0' // cron 组件当前版本 + , index: layui.cron ? (layui.cron.index + 10000) : 0 // corn 实例标识 + + //设置全局项 + , set: function (options) { + var that = this; + that.config = $.extend({}, that.config, options); + return that; + } + + //事件监听 + , on: function (events, callback) { + return layui.onevent.call(this, MOD_NAME, events, callback); + } + + //主体CSS等待事件 + , ready: function (fn) { + var cssPath = layui.cache.base + "cron.css?v=" + cron.v; + layui.link(cssPath, fn, "cron"); //此处的“cron”要对应 cron.css 中的样式: html #layuicss-cron{} + return this; + } + } + + //操作当前实例 + , thisIns = function () { + var that = this, options = that.config, id = options.id || options.index; + + return { + //提示框 + hint: function (content) { + that.hint.call(that, content); + }, config: options + } + } + + //构造器,创建实例 + , Class = function (options) { + var that = this; + that.index = ++cron.index; + that.config = $.extend({}, that.config, cron.config, options); + cron.ready(function () { + that.init(); + }); + }; + + //默认配置 + Class.prototype.config = { + value: null // 当前表达式值,每秒执行一次 + , + isInitValue: true //用于控制是否自动向元素填充初始值(需配合 value 参数使用) + , + lang: "cn" //语言,只支持cn/en,即中文和英文 + , + tabs: [{key: 'minutes', range: '0-59'}, { + key: 'hours', range: '0-23' + }, {key: 'days', range: '1-31'}, {key: 'months', range: '1-12'}, {key: 'weeks', range: '1-7'}], + defaultCron: {minutes: "*", hours: "*", days: "*", months: "*", weeks: "*"}, + trigger: "click" //呼出控件的事件 + , + btns: ['run', 'confirm'] //右下角显示的按钮,会按照数组顺序排列 + , + position: null //控件定位方式定位, 默认absolute,支持:fixed/absolute/static + , + zIndex: null //控件层叠顺序 + , + show: false //是否直接显示,如果设置 true,则默认直接显示控件 + , + showBottom: true //是否显示底部栏 + , + done: null //控件选择完毕后的回调,点击运行/确定也均会触发 + , + run: null // 最近运行时间接口 + }; + + //多语言 + Class.prototype.lang = function () { + var that = this, options = that.config, text = { + cn: { + tabs: [{title: "分"}, {title: "时"}, {title: "日"}, {title: "月"}, { + title: "周", + rateBegin: "第", + rateMid: "周的星期", + rateEnd: "" + }], + every: "每", + unspecified: "不指定", + period: "周期", + periodFrom: "从", + rate: "按照", + rateBegin: "从", + rateMid: "开始,每", + rateEnd: "执行一次", + weekday: "工作日", + weekdayPrefix: "每月", + weekdaySuffix: "号最近的那个工作日", + lastday: "本月最后一日", + lastweek: "本月最后一个星期", + custom: "指定", + tools: { + confirm: '确定', run: '运行' + }, + formatError: ['Cron格式不合法', '
已为你重置'] + }, en: { + tabs: [{title: "Minutes"}, {title: "Hours"}, {title: "Days"}, {title: "Months"}, {title: "Weeks"}], + every: "Every ", + unspecified: "Unspecified", + period: "Period", + periodFrom: "From", + rate: "According to", + rateBegin: "begin at", + rateMid: ", every", + rateEnd: " execute once", + weekday: "Weekday", + weekdayPrefix: "Every month at ", + weekdaySuffix: "号最近的那个工作日", + lastday: "Last day of the month", + lastweek: "本月最后一个星期", + custom: "Custom", + tools: { + confirm: 'Confirm', run: 'Run' + }, + formatError: ['The cron format error', '
It has been reset'] + } + }; + return text[options.lang] || text['cn']; + }; + + //初始准备 + Class.prototype.init = function () { + var that = this, options = that.config, isStatic = options.position === 'static'; + + options.elem = lay(options.elem); + + options.eventElem = lay(options.eventElem); + + if (!options.elem[0]) return; + + //如果不是input|textarea元素,则默认采用click事件 + if (!that.isInput(options.elem[0])) { + if (options.trigger === 'focus') { + options.trigger = 'click'; + } + } + + // 设置渲染所绑定元素的唯一KEY + if (!options.elem.attr('lay-key')) { + options.elem.attr('lay-key', that.index); + options.eventElem.attr('lay-key', that.index); + } + + // 当前实例主面板ID + that.elemID = 'layui-icon' + options.elem.attr('lay-key'); + + //默认赋值 + if (options.value && options.isInitValue) { + that.setValue(options.value); + } + if (!options.value) { + options.value = options.elem[0].value || ''; + } + var cronArr = options.value.split(' '); + if (cronArr.length >= 6) { + options.cron = { + minutes: cronArr[0], + hours: cronArr[1], + days: cronArr[2], + months: cronArr[3], + weeks: cronArr[4], + }; + } else { + options.cron = lay.extend({}, options.defaultCron); + } + + + if (options.show || isStatic) that.render(); + isStatic || that.events(); + + + }; + + + // 控件主体渲染 + Class.prototype.render = function () { + var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static', + tabFilter = 'cron-tab' + options.elem.attr('lay-key') + //主面板 + , elem = that.elem = lay.elem('div', { + id: that.elemID, 'class': ['layui-cron', isStatic ? (' ' + ELEM_STATIC) : ''].join('') + }) + + // tab 内容区域 + , elemTab = that.elemTab = lay.elem('div', { + 'class': 'layui-tab layui-tab-card', 'lay-filter': tabFilter + }), tabHead = lay.elem('ul', { + 'class': 'layui-tab-title' + }), tabContent = lay.elem('div', { + 'class': 'layui-tab-content' + }) + + //底部区域 + , divFooter = that.footer = lay.elem('div', { + 'class': ELEM_FOOTER + }); + + if (options.zIndex) elem.style.zIndex = options.zIndex; + + // 生成tab 内容区域 + elemTab.appendChild(tabHead); + elemTab.appendChild(tabContent); + lay.each(lang.tabs, function (i, item) { + // 表头 + var li = lay.elem('li', { + 'class': i === 0 ? THIS : "", 'lay-id': i + }); + li.innerHTML = item.title; + tabHead.appendChild(li); + + // 表体 + tabContent.appendChild(that.getTabContentChildElem(i)); + }); + + // 主区域 + elemMain = that.elemMain = lay.elem('div', { + 'class': 'layui-cron-main' + }); + elemMain.appendChild(elemTab); + + //生成底部栏 + lay(divFooter).html(function () { + var html = [], btns = []; + lay.each(options.btns, function (i, item) { + var title = lang.tools[item] || 'btn'; + btns.push('' + title + ''); + }); + html.push(''); + return html.join(''); + }()); + + //插入到主区域 + elem.appendChild(elemMain); + + options.showBottom && elem.appendChild(divFooter); + + + //移除上一个控件 + that.remove(Class.thisElemCron); + + //如果是静态定位,则插入到指定的容器中,否则,插入到body + isStatic ? options.elem.append(elem) : (document.body.appendChild(elem) + , that.position()); + + + that.checkCron(); + + that.elemEvent(); // 主面板事件 + + Class.thisElemCron = that.elemID; + + form.render(); + + } + + // 渲染 tab 子控件 + Class.prototype.getTabContentChildElem = function (index) { + var that = this, options = that.config, tabItem = options.tabs[index], tabItemKey = tabItem.key, + lang = that.lang(), tabItemLang = lang.tabs[index], cron = options.cron, + formFilter = 'cronForm' + tabItemKey + options.elem.attr('lay-key'), data = function () { + if (cron[tabItemKey].indexOf('-') != -1) { + // 周期数据 + var arr = cron[tabItemKey].split('-'); + return { + type: 'range', start: arr[0], end: arr[1] + }; + } + if (cron[tabItemKey].indexOf('/') != -1) { + // 频率数据 + var arr = cron[tabItemKey].split('/'); + return { + type: 'rate', begin: arr[0], rate: arr[1] + }; + } + if (cron[tabItemKey].indexOf(',') != -1 || /^\+?[0-9][0-9]*$/.test(cron[tabItemKey])) { + // 按照指定执行 + var arr = cron[tabItemKey].split(',').map(Number); + return { + type: 'custom', values: arr + }; + } + if (cron[tabItemKey].indexOf('W') != -1) { + // 最近的工作日 + var value = cron[tabItemKey].replace('W', ''); + return { + type: 'weekday', value: value + }; + } + if (index === 2 && cron[tabItemKey] === 'L') { + // 本月最后一日 + return { + type: 'lastday', value: 'L' + }; + } + if (index === 4 && cron[tabItemKey].indexOf('L') != -1) { + // 本月最后一个周 value + var value = cron[tabItemKey].replace('L', ''); + return { + type: 'lastweek', value: value + }; + } + if (cron[tabItemKey] === '*') { + // 每次 + return { + type: 'every', value: '*' + }; + } + if (cron[tabItemKey] === '?' || cron[tabItemKey] === undefined || cron[tabItemKey] === '') { + // 不指定 + return { + //type: 'unspecified', value: cron[tabItemKey] + type: 'every', value: '*' + }; + } + }(), rangeData = function () { + if (tabItem.range) { + var arr = tabItem.range.split('-'); + return { + min: parseInt(arr[0]), max: parseInt(arr[1]) + }; + } + }(); + var elem = lay.elem('div', { + 'class': 'layui-tab-item layui-form ' + (index === 0 ? SHOW : ""), 'lay-filter': formFilter + }); + + // 每次 + elem.appendChild(function () { + var everyRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'every', + 'title': lang.every + tabItemLang.title + }); + if (data.type === 'every') { + lay(everyRadio).attr('checked', true); + } + var everyDiv = lay.elem('div', { + 'class': 'cron-row' + }); + everyDiv.appendChild(everyRadio); + return everyDiv; + }()); + + // 不指定,从日开始 + /*if (index >= 2) { + elem.appendChild(function () { + var unspecifiedRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'unspecified', 'title': lang.unspecified + }); + if (data.type === 'unspecified') { + lay(unspecifiedRadio).attr('checked', true); + } + var unspecifiedDiv = lay.elem('div', { + 'class': 'cron-row' + }); + unspecifiedDiv.appendChild(unspecifiedRadio); + return unspecifiedDiv; + }()); + }*/ + + // 周期 + var rangeChild = [function () { + var rangeRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'range', 'title': lang.period + }); + if (data.type === 'range') { + lay(rangeRadio).attr('checked', true); + } + return rangeRadio; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = lang.periodFrom; + return elem; + }(), function () { + var elem = lay.elem('input', { + 'class': 'cron-input', 'type': 'number', 'name': 'rangeStart', 'value': data.start || '' + }); + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = '-'; + return elem; + }(), function () { + var elem = lay.elem('input', { + 'class': 'cron-input', 'type': 'number', 'name': 'rangeEnd', 'value': data.end || '' + }); + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = tabItemLang.title; + return elem; + }()] + + , rangeDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(rangeChild, function (i, item) { + rangeDiv.appendChild(item); + }); + if (tabItem.range) { + var rangeTip = lay.elem('div', { + 'class': 'cron-tips' + }); + rangeTip.innerHTML = ['(', tabItem.range, ')'].join(''); + rangeDiv.appendChild(rangeTip); + } + elem.appendChild(rangeDiv); + + // 频率,年没有 + if (index < 6) { + var rateChild = [function () { + var rateRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'rate', 'title': lang.rate + }); + if (data.type === 'rate') { + lay(rateRadio).attr('checked', true); + } + return rateRadio; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = tabItemLang.rateBegin || lang.rateBegin; + return elem; + }(), function () { + var elem = lay.elem('input', { + 'class': 'cron-input', 'type': 'number', 'name': 'begin', 'value': data.begin || '' + }); + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = tabItemLang.rateMid || (tabItemLang.title + lang.rateMid); + return elem; + }(), function () { + var elem = lay.elem('input', { + 'class': 'cron-input', 'type': 'number', 'name': 'rate', 'value': data.rate || '' + }); + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = undefined != tabItemLang.rateEnd ? tabItemLang.rateEnd : (tabItemLang.title + lang.rateEnd); + if (undefined != tabItemLang.rateEnd && tabItemLang.rateEnd === '') { + lay(elem).addClass(HIDE); + } + return elem; + }()] + + , rateDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(rateChild, function (i, item) { + rateDiv.appendChild(item); + }); + if (tabItem.range) { + var rateTip = lay.elem('div', { + 'class': 'cron-tips' + }); + if (index === 4) { + // 周 + rateTip.innerHTML = '(1-4/1-7)'; + } else { + rateTip.innerHTML = ['(', rangeData.min, '/', (rangeData.max + (index <= 1 ? 1 : 0)), ')'].join(''); + } + rateDiv.appendChild(rateTip); + } + elem.appendChild(rateDiv); + } + + // 特殊:日(最近的工作日、最后一日),周(最后一周) + /*if (index === 2) { + // 日 + // 最近的工作日 + var weekChild = [function () { + var weekRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'weekday', 'title': lang.weekday + }); + if (data.type === 'weekday') { + lay(weekRadio).attr('checked', true); + } + return weekRadio; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = lang.weekdayPrefix; + return elem; + }(), function () { + var elem = lay.elem('input', { + 'class': 'cron-input', 'type': 'number', 'name': 'weekday', 'value': data.value || '' + }); + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = lang.weekdaySuffix; + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-tips' + }); + elem.innerHTML = ['(', tabItem.range, ')'].join(''); + return elem; + }()] + + , weekDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(weekChild, function (i, item) { + weekDiv.appendChild(item); + }); + elem.appendChild(weekDiv); + + // 本月最后一日 + elem.appendChild(function () { + var lastRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'lastday', 'title': lang.lastday + }); + if (data.type === 'lastday') { + lay(lastRadio).attr('checked', true); + } + var lastDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lastDiv.appendChild(lastRadio); + return lastDiv; + }()); + + } + + if (index === 4) { + // 本月最后一个周几 + var lastWeekChild = [function () { + var lastWeekRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'lastweek', 'title': lang.lastweek + }); + if (data.type === 'lastweek') { + lay(lastWeekRadio).attr('checked', true); + } + return lastWeekRadio; + }(), function () { + var elem = lay.elem('input', { + 'class': 'cron-input', 'type': 'number', 'name': 'lastweek', 'value': data.value || '' + }); + return elem; + }(), function () { + var elem = lay.elem('div', { + 'class': 'cron-tips' + }); + elem.innerHTML = ['(', tabItem.range, ')'].join(''); + return elem; + }()] + + , lastWeekDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(lastWeekChild, function (i, item) { + lastWeekDiv.appendChild(item); + }); + elem.appendChild(lastWeekDiv); + + }*/ + + // 指定 + if (index <= 4) { + elem.appendChild(function () { + var customRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'custom', 'title': lang.custom + }); + if (data.type === 'custom') { + lay(customRadio).attr('checked', true); + } + var customDiv = lay.elem('div', { + 'class': 'cron-row' + }); + customDiv.appendChild(customRadio); + return customDiv; + }()); + + // 指定数值,时分秒显示两位数,自动补零 + elem.appendChild(function () { + var customGrid = lay.elem('div', { + 'class': 'cron-grid' + }); + var i = rangeData.min; + while (i <= rangeData.max) { + // 时分秒显示两位数,自动补零 + var gridItemValue = index <= 1 ? lay.digit(i, 2) : i; + var gridItem = lay.elem('input', { + 'type': 'checkbox', + 'title': gridItemValue, + 'lay-skin': 'primary', + 'name': tabItemKey + '[custom]', + 'value': i + }); + if (data.values && data.values.includes(i)) { + lay(gridItem).attr('checked', true); + } + customGrid.appendChild(gridItem); + i++; + } + return customGrid; + }()); + } + + + return elem; + } + + //是否输入框 + Class.prototype.isInput = function (elem) { + return /input|textarea/.test(elem.tagName.toLocaleLowerCase()); + }; + + // 绑定的元素事件处理 + Class.prototype.events = function () { + var that = this, options = that.config + + //绑定呼出控件事件 + , showEvent = function (elem, bind) { + elem.on(options.trigger, function () { + bind && (that.bindElem = this); + that.render(); + }); + }; + + if (!options.elem[0] || options.elem[0].eventHandler) return; + + showEvent(options.elem, 'bind'); + showEvent(options.eventElem); + + //绑定关闭控件事件 + lay(document).on('click', function (e) { + if (e.target === options.elem[0] || e.target === options.eventElem[0] || e.target === lay(options.closeStop)[0]) { + return; + } + that.remove(); + }).on('keydown', function (e) { + if (e.keyCode === 13) { + if (lay('#' + that.elemID)[0] && that.elemID === Class.thisElemDate) { + e.preventDefault(); + lay(that.footer).find(ELEM_CONFIRM)[0].click(); + } + } + }); + + //自适应定位 + lay(window).on('resize', function () { + if (!that.elem || !lay(ELEM)[0]) { + return false; + } + that.position(); + }); + + options.elem[0].eventHandler = true; + }; + + // 主面板事件 + Class.prototype.elemEvent = function () { + var that = this, options = that.config, tabFilter = 'cron-tab' + options.elem.attr('lay-key'); + + // 阻止主面板点击冒泡,避免因触发文档事件而关闭主面 + lay(that.elem).on('click', function (e) { + lay.stope(e); + }); + + // tab选项卡切换 + var lis = lay(that.elemTab).find('li'); + lis.on('click', function () { + var layid = lay(this).attr('lay-id'); + if (undefined === layid) { + return; + } + element.tabChange(tabFilter, layid); + }); + + // cron选项点击 + form.on('radio', function (data) { + var $parent = data.othis.parent(); + var formFilter = $parent.parent().attr('lay-filter'); + var formData = form.val(formFilter); + var radioType = data.value; + if ('range' === radioType) { + // 范围 + form.val(formFilter, { + rangeStart: formData.rangeStart || 0, rangeEnd: formData.rangeEnd || 2 + }); + } + if ('rate' === radioType) { + // 频率 + form.val(formFilter, { + begin: formData.begin || 0, rate: formData.rate || 2 + }); + } + if ('custom' === radioType) { + // custom + var $grid = $parent.next(); + if ($grid.find(':checkbox:checked').length <= 0) { + $grid.children(':checkbox:first').next().click() + } + } + if ('weekday' === radioType) { + // weekday + form.val(formFilter, { + weekday: formData.weekday || 1 + }); + } + if ('lastweek' === radioType) { + // lastweek + form.val(formFilter, { + lastweek: formData.lastweek || 1 + }); + } + + }); + + //点击底部按钮 + lay(that.footer).find('span').on('click', function () { + var type = lay(this).attr('lay-type'); + that.tool(this, type); + }); + }; + + //底部按钮点击事件 + Class.prototype.tool = function (btn, type) { + var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static', active = { + //运行 + run: function () { + var value = that.parse(); + var loading = layer.load(); + $.get(options.run, {cron: value}, function (res) { + layer.close(loading); + if (res.code !== 0) { + return that.hint(res.msg); + } + that.runHint(res.data); + }, 'json').fail(function () { + layer.close(loading); + that.hint('服务器异常!'); + }); + } + + //确定 + , confirm: function () { + var value = that.parse(); + that.done([value]); + that.setValue(value).remove() + } + }; + active[type] && active[type](); + }; + + //执行 done/change 回调 + Class.prototype.done = function (param, type) { + var that = this, options = that.config; + + param = param || [that.parse()]; + typeof options[type || 'done'] === 'function' && options[type || 'done'].apply(options, param); + + return that; + }; + + // 解析cron表达式 + Class.prototype.parse = function () { + var that = this, options = that.config, valueArr = []; + + lay.each(options.tabs, function (index, item) { + var key = item.key; + var formFilter = 'cronForm' + key + options.elem.attr('lay-key'); + var formData = form.val(formFilter); + var radioType = (key + '[type]'); + var current = ""; + if (formData[radioType] === 'every') { + // 每次 + current = "*"; + } + if (formData[radioType] === 'range') { + // 范围 + current = formData.rangeStart + "-" + formData.rangeEnd; + } + if (formData[radioType] === 'rate') { + // 频率 + current = formData.begin + "/" + formData.rate; + } + if (formData[radioType] === 'custom') { + // 指定 + var checkboxName = (item.key + '[custom]'); + var customArr = []; + $('input[name="' + checkboxName + '"]:checked').each(function () { + customArr.push($(this).val()); + }); + current = customArr.join(','); + } + if (formData[radioType] === 'weekday') { + // 每月 formData.weekday 号最近的那个工作日 + current = formData.weekday + "W"; + } + if (formData[radioType] === 'lastday') { + // 本月最后一日 + current = "L"; + } + if (formData[radioType] === 'lastweek') { + // 本月最后星期 + current = formData.lastweek + "L"; + } + + if (formData[radioType] === 'unspecified' && index != 6) { + // 不指定 + current = "?"; + } + if (current !== "") { + valueArr.push(current); + options.cron[key] = current; + } + }); + return valueArr.join(' '); + }; + + //控件移除 + Class.prototype.remove = function (prev) { + var that = this, options = that.config, elem = lay('#' + (prev || that.elemID)); + if (!elem[0]) return that; + + if (!elem.hasClass(ELEM_STATIC)) { + that.checkCron(function () { + elem.remove(); + }); + } + return that; + }; + + //定位算法 + Class.prototype.position = function () { + var that = this, options = that.config; + lay.position(that.bindElem || options.elem[0], that.elem, { + position: options.position + }); + return that; + }; + + //提示 + Class.prototype.hint = function (content) { + var that = this, options = that.config, div = lay.elem('div', { + 'class': ELEM_HINT + }); + + if (!that.elem) return; + + div.innerHTML = content || ''; + lay(that.elem).find('.' + ELEM_HINT).remove(); + that.elem.appendChild(div); + + clearTimeout(that.hinTimer); + that.hinTimer = setTimeout(function () { + lay(that.elem).find('.' + ELEM_HINT).remove(); + }, 3000); + }; + + //运行提示 + Class.prototype.runHint = function (runList) { + var that = this, options = that.config, div = lay.elem('div', { + 'class': ELEM_RUN_HINT + }); + // debugger; + if (!that.elem || !runList || !runList.length) return; + + + lay(div).html(function () { + var html = []; + lay.each(runList, function (i, item) { + html.push('
' + item + '
'); + }); + return html.join(''); + }()); + + lay(that.elem).find('.' + ELEM_RUN_HINT).remove(); + that.elem.appendChild(div); + }; + + //赋值 + Class.prototype.setValue = function (value = '') { + var that = this, options = that.config, elem = that.bindElem || options.elem[0], + valType = that.isInput(elem) ? 'val' : 'html' + + options.position === 'static' || lay(elem)[valType](value || ''); + elem.textContent = '生成'; + return this; + }; + + //cron校验 + Class.prototype.checkCron = function (fn) { + var that = this, options = that.config, lang = that.lang(), elem = that.bindElem || options.elem[0], + value = that.isInput(elem) ? elem.value : (options.position === 'static' ? '' : elem.innerHTML) + + , checkValid = function (value = "") { + + }; + + // cron 值,多个空格替换为一个空格,去掉首尾空格 + value = value || options.value; + if (typeof value === 'string') { + value = value.replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); + } + + if (fn === 'init') return checkValid(value), that; + + value = that.parse(); + if (value) { + that.setValue(value); + } + fn && fn(); + return that; + }; + + //核心入口 + cron.render = function (options) { + var ins = new Class(options); + return thisIns.call(ins); + }; + + exports('cron', cron); +}); + \ No newline at end of file diff --git a/resources/views/cron.blade.php b/resources/views/cron.blade.php new file mode 100644 index 00000000..dd47067f --- /dev/null +++ b/resources/views/cron.blade.php @@ -0,0 +1,281 @@ +计划任务 + +
+
+
添加计划任务
+
+
+
+ +
+ +
+
请填写任务名称
+
+
+ +
+ +
+
请务必正确填写执行周期
+
+
+ +
+
# 在此输入你要执行的脚本内容
+
+
+
+
+ + +
+
+
+
+
+
+
计划任务列表
+
+
+ + + + +
+
+
+ + diff --git a/resources/views/file.blade.php b/resources/views/file.blade.php index e2d37821..5804966d 100644 --- a/resources/views/file.blade.php +++ b/resources/views/file.blade.php @@ -1,2 +1,2 @@ -文件 -

管理正在开发中!

\ No newline at end of file +文件管理 +

文件管理正在开发中!

\ No newline at end of file diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php index 11d14b92..f4d2dd1a 100644 --- a/resources/views/login.blade.php +++ b/resources/views/login.blade.php @@ -24,8 +24,8 @@ placeholder="密码" class="layui-input">
- - --}} + 忘记密码?
diff --git a/resources/views/monitor.blade.php b/resources/views/monitor.blade.php index b0859a49..52d693fb 100644 --- a/resources/views/monitor.blade.php +++ b/resources/views/monitor.blade.php @@ -1,4 +1,4 @@ -监控 +资源监控
diff --git a/resources/views/plugin.blade.php b/resources/views/plugin.blade.php index 59750c3b..d9841d51 100644 --- a/resources/views/plugin.blade.php +++ b/resources/views/plugin.blade.php @@ -1,4 +1,4 @@ -插件 +插件中心
diff --git a/resources/views/safe.blade.php b/resources/views/safe.blade.php index 9eef3b36..9e712e2b 100644 --- a/resources/views/safe.blade.php +++ b/resources/views/safe.blade.php @@ -1,4 +1,4 @@ -安全 +系统安全
diff --git a/resources/views/setting.blade.php b/resources/views/setting.blade.php index 4887a56f..e7927d73 100755 --- a/resources/views/setting.blade.php +++ b/resources/views/setting.blade.php @@ -1,9 +1,9 @@ -设置 +面板设置
diff --git a/routes/api.php b/routes/api.php index 7763cb3d..53f40d88 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ group(function () { Route::get('getInstalledDbAndPhp', [InfosController::class, 'getInstalledDbAndPhp']); }); - Route::middleware('auth:sanctum')->prefix('settings')->group(function () { - // 获取面板设置 - Route::get('get', [TasksController::class, 'get']); - // 保存面板设置 - Route::post('save', [TasksController::class, 'save']); - }); // 网站 Route::middleware('auth:sanctum')->prefix('website')->group(function () { // 获取网站列表 @@ -128,6 +123,16 @@ Route::prefix('panel')->group(function () { Route::post('update', [PluginsController::class, 'update']); Route::post('setShowHome', [PluginsController::class, 'setShowHome']); }); + // 计划任务 + Route::middleware('auth:sanctum')->prefix('cron')->group(function () { + // 获取计划任务列表 + Route::get('getList', [CronsController::class, 'getList']); + Route::post('add', [CronsController::class, 'add']); + Route::post('edit', [CronsController::class, 'edit']); + Route::post('delete', [CronsController::class, 'delete']); + Route::post('setStatus', [CronsController::class, 'setStatus']); + Route::get('getLog', [CronsController::class, 'getLog']); + }); // 设置 Route::middleware('auth:sanctum')->prefix('setting')->group(function () { // 获取设置 diff --git a/routes/web.php b/routes/web.php index 2a2ecdf7..7fc8b227 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,7 +26,6 @@ Route::prefix('panel/views')->group(function () { // 主页 Route::view('index', 'home'); - Route::view('setting', 'setting'); // 网站 Route::prefix('website')->group(function () { //全局设置 @@ -38,12 +37,6 @@ Route::prefix('panel/views')->group(function () { // 编辑 Route::view('edit', 'website.edit'); }); - - // 数据库-MySQL - Route::prefix('database')->group(function () { - Route::view('mysql', 'database.mysql'); - Route::view('postgresql', 'database.postgresql'); - }); // 监控 Route::view('monitor', 'monitor'); // 安全 @@ -52,6 +45,10 @@ Route::prefix('panel/views')->group(function () { Route::view('file', 'file'); // 插件 Route::view('plugin', 'plugin'); + // 插件 + Route::view('cron', 'cron'); + // 设置 + Route::view('setting', 'setting'); // 其他独立页面 // 登录 @@ -60,6 +57,6 @@ Route::prefix('panel/views')->group(function () { Route::view('logout', 'logout'); // 主题设置 Route::view('theme', 'theme'); - // 任务 + // 任务中心 Route::view('task', 'task'); });