添加后台进程修复等功能
This commit is contained in:
+312
-291
@@ -4,11 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>脚本管理平台</title>
|
||||
<!-- 引入Bootstrap和图标库 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
@@ -16,116 +14,113 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 主容器样式调整:适配拖拽分隔线 */
|
||||
.main-container {
|
||||
display: grid;
|
||||
/* 初始布局:左侧280px + 分隔线4px + 右侧自适应 */
|
||||
grid-template-columns: 540px 4px 1fr;
|
||||
gap: 0; /* 取消间隙,避免分隔线与面板之间有空隙 */
|
||||
gap: 0;
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
/* 左侧脚本列表面板 */
|
||||
#script-list-panel {
|
||||
background: white;
|
||||
border-radius: 8px 0 0 8px; /* 左侧圆角,右侧与分隔线贴合 */
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
width: 100%; /* 宽度由grid控制,内部自适应 */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 右侧面板样式调整 */
|
||||
#right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 15px; /* 右侧与分隔线保持原有的15px间隙 */
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
/* 拖拽分隔线样式 */
|
||||
.resize-handle {
|
||||
background-color: #e9ecef;
|
||||
cursor: col-resize; /* 鼠标悬停时显示水平调整光标 */
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background-color: #0d6efd; /* 悬停时变蓝色,提示可拖拽 */
|
||||
}
|
||||
.resize-handle:active {
|
||||
background-color: #0b5ed7; /* 拖拽时加深蓝色 */
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
|
||||
.resize-handle:active {
|
||||
background-color: #0b5ed7;
|
||||
}
|
||||
|
||||
/* 右侧输出区域 */
|
||||
#output-area {
|
||||
background: #1e1e1e;
|
||||
color: #dcdcdc;
|
||||
font-family: 'Consolas', 'Microsoft YaHei Mono', monospace;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto; /* 保持滚动 */
|
||||
height: 500px; /* 固定高度 500px */
|
||||
overflow-y: auto;
|
||||
height: 500px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* 输出行样式(区分类型) */
|
||||
|
||||
.output-line {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* 搜索框样式优化 */
|
||||
#script-search {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* 排序下拉框样式 */
|
||||
|
||||
#script-sort {
|
||||
width: auto;
|
||||
min-width: 160px;
|
||||
}
|
||||
/* 脚本项的修改时间显示优化 */
|
||||
|
||||
.mode-info .text-xs {
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
/* 响应式调整:小屏幕时搜索框与排序控件换行 */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr; /* 单列布局 */
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#resize-handle {
|
||||
display: none; /* 小屏幕隐藏分隔线 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
#right-panel {
|
||||
padding-left: 0; /* 取消右侧间隙 */
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#script-list-panel {
|
||||
border-radius: 8px; /* 恢复全圆角 */
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.output-line .timestamp {
|
||||
color: #888; /* 时间戳灰色 */
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.output-line.success {
|
||||
color: #4CAF50; /* 成功绿色 */
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.output-line.error {
|
||||
color: #f44336; /* 错误红色 */
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.output-line.warning {
|
||||
color: #ff9800; /* 警告橙色 */
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.output-line.info {
|
||||
color: #2196F3; /* 信息蓝色 */
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
/* 配置面板 */
|
||||
#config-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
@@ -134,7 +129,6 @@
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 脚本列表项 */
|
||||
.script-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
@@ -150,13 +144,11 @@
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* 脚本项激活状态 */
|
||||
.script-item.active {
|
||||
border-left-color: #0d6efd; /* 蓝色边框 */
|
||||
border-left-color: #0d6efd;
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
/* 状态指示器(圆形) */
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
@@ -166,7 +158,6 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 状态颜色:运行中(绿)、调度中(蓝)、停止(红) */
|
||||
.status-running {
|
||||
background: #198754;
|
||||
}
|
||||
@@ -179,7 +170,6 @@
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
/* 状态文本样式 */
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
@@ -202,13 +192,11 @@
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* 按钮样式优化 */
|
||||
.btn-sm {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 清空输出按钮 */
|
||||
#clear-output {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
@@ -216,7 +204,6 @@
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 响应式调整(小屏幕单列布局) */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -230,18 +217,14 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 页面标题 -->
|
||||
<h4 class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<span>脚本管理平台</span>
|
||||
<small class="text-muted" id="system-status">系统运行中</small>
|
||||
</h4>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="main-container">
|
||||
<!-- 左侧脚本列表面板 -->
|
||||
<div id="script-list-panel" class="panel-resizable">
|
||||
<div class="d-flex flex-column gap-3 mb-3">
|
||||
<!-- 新增:搜索框 -->
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-search"></i>
|
||||
@@ -252,7 +235,6 @@
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 新增:排序控制 -->
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">脚本列表</h5>
|
||||
<div class="d-flex gap-2">
|
||||
@@ -267,14 +249,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 脚本列表容器 -->
|
||||
<div id="script-list"></div>
|
||||
</div>
|
||||
<!-- 新增:拖拽分隔线 -->
|
||||
<div id="resize-handle" class="resize-handle" title="拖拽调整宽度"></div>
|
||||
<!-- 右侧:输出区域 + 配置面板 -->
|
||||
<div id="right-panel">
|
||||
<!-- 输出控制台(带清空按钮) -->
|
||||
<div class="position-relative">
|
||||
<button id="clear-output" class="btn btn-sm btn-outline-light" onclick="clearOutput()">
|
||||
<i class="bi bi-trash"></i> 清空输出
|
||||
@@ -282,19 +260,28 @@
|
||||
<div id="output-area"></div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本配置面板 -->
|
||||
<div id="config-panel">
|
||||
<h5 class="mb-3">脚本配置</h5>
|
||||
<form id="config-form">
|
||||
<!-- 选择脚本 -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">选择脚本 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="config-script" required>
|
||||
<option value="">请选择脚本...</option>
|
||||
</select>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="config-script" required onchange="handleScriptChange()">
|
||||
<option value="">请选择脚本...</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openDirectoryBrowser()">
|
||||
<i class="bi bi-folder"></i> 浏览
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="python-path-config" style="display: none;">
|
||||
<label class="form-label">Python解释器路径</label>
|
||||
<input type="text" class="form-control" id="python-path"
|
||||
placeholder="例如: C:/Python39/python.exe 或 /usr/bin/python3">
|
||||
<div class="form-text">留空则使用系统默认Python解释器</div>
|
||||
</div>
|
||||
|
||||
<!-- 运行模式 -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">运行模式 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="config-mode" required>
|
||||
@@ -304,7 +291,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 定时执行配置(默认隐藏) -->
|
||||
<div id="interval-config" style="display: none;">
|
||||
<label class="form-label">执行周期 <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
@@ -318,7 +304,6 @@
|
||||
<div class="form-text">定时任务将按设定周期自动执行</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存配置按钮 -->
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-save"></i> 保存配置
|
||||
</button>
|
||||
@@ -327,78 +312,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入依赖JS -->
|
||||
<!-- 目录浏览模态框 -->
|
||||
<div class="modal fade" id="directoryBrowserModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">浏览服务器目录</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3" id="pathNavigator">
|
||||
<span class="badge bg-primary" onclick="navigateToDirectory('')">/</span>
|
||||
</div>
|
||||
|
||||
<div class="list-group" id="directoryContent">
|
||||
<!-- 目录内容将动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" id="selectScriptBtn" disabled>选择此脚本</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 全局变量
|
||||
const OUTPUT_UPDATE_INTERVAL = 2000; // 输出更新间隔(2秒)
|
||||
let allScripts = []; // 存储所有脚本对象:[{name: "xxx.bat", modify_time: ...}, ...]
|
||||
let filteredScripts = []; // 过滤后的脚本列表
|
||||
let currentScript = null; // 当前选中的脚本名称
|
||||
const OUTPUT_UPDATE_INTERVAL = 2000;
|
||||
let allScripts = [];
|
||||
let filteredScripts = [];
|
||||
let currentScript = null;
|
||||
let currentSearchKeyword = "";
|
||||
let currentServerDirectory = "";
|
||||
let selectedScriptPath = null;
|
||||
let scriptStatusMap = {}; // 缓存脚本状态映射
|
||||
|
||||
// ====================== 初始化 ======================
|
||||
$(function () {
|
||||
// 加载脚本列表
|
||||
loadScripts();
|
||||
// 监听运行模式切换(显示/隐藏定时配置)
|
||||
$('#config-mode').change(function () {
|
||||
toggleIntervalConfig($(this).val());
|
||||
});
|
||||
// 监听配置表单提交
|
||||
$('#config-form').submit(function (e) {
|
||||
e.preventDefault();
|
||||
saveConfig();
|
||||
});
|
||||
// 定时更新输出区域
|
||||
setInterval(() => {
|
||||
if (currentScript) updateOutput();
|
||||
}, OUTPUT_UPDATE_INTERVAL);
|
||||
// 初始化拖拽功能
|
||||
initResizeHandle();
|
||||
});
|
||||
|
||||
// ====================== 脚本列表相关 ======================
|
||||
/**
|
||||
* 加载脚本列表(从后端API获取)
|
||||
*/
|
||||
function loadScripts() {
|
||||
$.get('/api/scripts', function(scripts) {
|
||||
// 校验后端返回数据格式
|
||||
$.get('/api/scripts', function (scripts) {
|
||||
if (!Array.isArray(scripts)) {
|
||||
alert('加载脚本失败:后端返回非数组格式');
|
||||
allScripts = [];
|
||||
filteredScripts = [];
|
||||
renderScriptList();
|
||||
updateConfigScriptSelect();
|
||||
return;
|
||||
}
|
||||
// 只保留有配置的脚本(后端已过滤,前端二次确认)
|
||||
allScripts = scripts.filter(item =>
|
||||
typeof item === 'object' && item !== null &&
|
||||
'name' in item && typeof item.name === 'string' && item.name.trim() !== ''
|
||||
);
|
||||
|
||||
// 过滤并验证脚本对象
|
||||
allScripts = scripts.filter(item => {
|
||||
return typeof item === 'object' && item !== null &&
|
||||
'name' in item && typeof item.name === 'string' && item.name.trim() !== '';
|
||||
// 批量获取所有脚本的状态和模式
|
||||
$.get('/api/scripts/status', function(statusMap) {
|
||||
scriptStatusMap = statusMap;
|
||||
currentSearchKeyword = "";
|
||||
$('#script-search').val("");
|
||||
sortScripts();
|
||||
updateConfigScriptSelect();
|
||||
|
||||
if (currentScript) {
|
||||
const scriptExists = allScripts.some(s => s.name === currentScript);
|
||||
if (scriptExists) {
|
||||
$('#config-script').val(currentScript);
|
||||
loadConfig(currentScript);
|
||||
updateOutput();
|
||||
} else {
|
||||
currentScript = null;
|
||||
$('#output-area').empty();
|
||||
}
|
||||
}
|
||||
}).fail(function(xhr) {
|
||||
alert('加载脚本状态失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||
});
|
||||
|
||||
currentSearchKeyword = "";
|
||||
$('#script-search').val("");
|
||||
sortScripts(); // 排序后渲染列表
|
||||
updateConfigScriptSelect();
|
||||
|
||||
// 恢复当前选中脚本状态
|
||||
if (currentScript) {
|
||||
const scriptExists = allScripts.some(s => s.name === currentScript);
|
||||
if (scriptExists) {
|
||||
$('#config-script').val(currentScript);
|
||||
loadConfig(currentScript);
|
||||
updateOutput();
|
||||
} else {
|
||||
currentScript = null;
|
||||
$('#output-area').empty();
|
||||
}
|
||||
}
|
||||
}).fail(function(xhr) {
|
||||
}).fail(function (xhr) {
|
||||
alert('加载脚本列表失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||
allScripts = [];
|
||||
filteredScripts = [];
|
||||
@@ -407,26 +408,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索脚本(实时模糊匹配)
|
||||
*/
|
||||
function searchScripts() {
|
||||
const keyword = $('#script-search').val().trim().toLowerCase();
|
||||
currentSearchKeyword = keyword;
|
||||
|
||||
if (keyword === "") {
|
||||
filteredScripts = [...allScripts];
|
||||
} else {
|
||||
filteredScripts = allScripts.filter(scriptObj =>
|
||||
filteredScripts = keyword === ""
|
||||
? [...allScripts]
|
||||
: allScripts.filter(scriptObj =>
|
||||
scriptObj.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
sortScripts(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空搜索
|
||||
*/
|
||||
function clearSearch() {
|
||||
$('#script-search').val("");
|
||||
currentSearchKeyword = "";
|
||||
@@ -434,10 +427,6 @@
|
||||
sortScripts(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序脚本(支持按修改时间/名称排序)
|
||||
* @param {boolean} reload - 是否重新加载全部脚本(默认true)
|
||||
*/
|
||||
function sortScripts(reload = true) {
|
||||
if (reload) {
|
||||
filteredScripts = [...allScripts];
|
||||
@@ -458,15 +447,12 @@
|
||||
renderScriptList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染脚本列表
|
||||
*/
|
||||
function renderScriptList() {
|
||||
const container = $('#script-list').empty();
|
||||
if (filteredScripts.length === 0) {
|
||||
const tip = currentSearchKeyword ?
|
||||
`<i class="bi bi-search"></i> 未找到包含"${currentSearchKeyword}"的脚本` :
|
||||
`<i class="bi bi-folder-open"></i> tasks目录下无脚本文件(支持.bat/.py)`;
|
||||
`<i class="bi bi-folder-open"></i> 暂无配置的脚本`;
|
||||
container.append(`
|
||||
<div class="text-center text-muted py-3">
|
||||
${tip}
|
||||
@@ -475,77 +461,81 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历过滤后的脚本对象数组
|
||||
// 使用文档片段减少DOM重绘
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
filteredScripts.forEach(scriptObj => {
|
||||
const scriptName = scriptObj.name.trim();
|
||||
if (!scriptName) return;
|
||||
|
||||
const modifyTimeStr = scriptObj.modify_time_str || '未知时间';
|
||||
getScriptStatus(scriptName, function(status) {
|
||||
// 状态样式处理
|
||||
const statusClass = status.status === 'running' ? 'status-running' :
|
||||
status.status === 'scheduled' ? 'status-scheduled' : 'status-stopped';
|
||||
const statusText = status.status === 'running' ? '运行中' :
|
||||
status.status === 'scheduled' ? '已调度' : '停止';
|
||||
const statusTextClass = `status-text ${status.status}`;
|
||||
// 从缓存的状态映射表中获取状态和模式
|
||||
const statusInfo = scriptStatusMap[scriptName] || {
|
||||
status: 'stopped',
|
||||
running: false,
|
||||
scheduled: false,
|
||||
mode: 'single-run',
|
||||
schedule_info: ''
|
||||
};
|
||||
|
||||
// 按钮处理
|
||||
let btnClass, btnIcon, btnText, btnClick;
|
||||
if (status.running || status.scheduled) {
|
||||
btnClass = 'btn-danger';
|
||||
btnIcon = 'bi-stop-fill';
|
||||
btnText = '停止';
|
||||
btnClick = `stopScript('${escapeHtml(scriptName)}')`;
|
||||
} else {
|
||||
btnClass = 'btn-success';
|
||||
btnIcon = 'bi-play-fill';
|
||||
btnText = '启动';
|
||||
btnClick = `startScript('${escapeHtml(scriptName)}')`;
|
||||
}
|
||||
// 状态样式处理
|
||||
const statusClass = statusInfo.status === 'running' ? 'status-running' :
|
||||
statusInfo.status === 'scheduled' ? 'status-scheduled' : 'status-stopped';
|
||||
const statusText = statusInfo.status === 'running' ? '运行中' :
|
||||
statusInfo.status === 'scheduled' ? '已调度' : '停止';
|
||||
const statusTextClass = `status-text ${statusInfo.status}`;
|
||||
|
||||
// 构建脚本项HTML
|
||||
const scriptItemHtml = `
|
||||
<div class="script-item ${currentScript === scriptName ? 'active' : ''}"
|
||||
onclick="selectScript('${escapeHtml(scriptName)}')">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="status-indicator ${statusClass}"></span>
|
||||
<span class="script-name">${escapeHtml(scriptName)}</span>
|
||||
<span class="${statusTextClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="mode-info text-xs mt-1">
|
||||
<span class="text-muted">加载模式信息中...</span>
|
||||
// 按钮样式处理
|
||||
let btnClass, btnIcon, btnText, btnClick;
|
||||
if (statusInfo.running || statusInfo.scheduled) {
|
||||
btnClass = 'btn-danger';
|
||||
btnIcon = 'bi-stop-fill';
|
||||
btnText = '停止';
|
||||
btnClick = `stopScript('${escapeHtml(scriptName)}')`;
|
||||
} else {
|
||||
btnClass = 'btn-success';
|
||||
btnIcon = 'bi-play-fill';
|
||||
btnText = '启动';
|
||||
btnClick = `startScript('${escapeHtml(scriptName)}')`;
|
||||
}
|
||||
|
||||
// 模式文本处理
|
||||
const modeText = statusInfo.mode === 'long-running' ? '长期运行' :
|
||||
statusInfo.mode === 'interval' ? '定时执行' : '单次运行';
|
||||
const scheduleInfo = statusInfo.schedule_info ? ` | ${statusInfo.schedule_info}` : '';
|
||||
|
||||
// 创建DOM元素并添加到文档片段
|
||||
const scriptItem = $(`
|
||||
<div class="script-item ${currentScript === scriptName ? 'active' : ''}"
|
||||
onclick="selectScript("${escapeHtml(scriptName)}")">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="status-indicator ${statusClass}"></span>
|
||||
<span class="script-name">${escapeHtml(scriptName)}</span>
|
||||
<span class="${statusTextClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="mode-info text-xs mt-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="text-muted">模式:${modeText}${scheduleInfo}</span>
|
||||
<span class="text-muted">最近修改:${modifyTimeStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm ${btnClass}"
|
||||
onclick="event.stopPropagation(); ${btnClick}">
|
||||
<i class="bi ${btnIcon}"></i> ${btnText}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-sm ${btnClass}"
|
||||
onclick="event.stopPropagation(); ${btnClick}">
|
||||
<i class="bi ${btnIcon}"></i> ${btnText}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
container.append(scriptItemHtml);
|
||||
|
||||
// 获取并更新模式信息
|
||||
getScriptMode(scriptName, function(mode) {
|
||||
const modeText = mode === 'long-running' ? '长期运行' :
|
||||
mode === 'interval' ? '定时执行' : '单次运行';
|
||||
const scheduleInfo = status.schedule_info ? ` | ${status.schedule_info}` : '';
|
||||
$(`.script-item:has(.script-name:contains('${escapeHtml(scriptName)}')) .mode-info`).html(`
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="text-muted">模式:${modeText}${scheduleInfo}</span>
|
||||
<span class="text-muted">最近修改:${modifyTimeStr}</span>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
</div>
|
||||
`)[0];
|
||||
fragment.appendChild(scriptItem);
|
||||
});
|
||||
|
||||
// 一次性将所有元素添加到容器
|
||||
container.append(fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置面板的脚本下拉框
|
||||
*/
|
||||
function updateConfigScriptSelect() {
|
||||
const select = $('#config-script').empty();
|
||||
select.append('<option value="">请选择脚本...</option>');
|
||||
@@ -556,122 +546,64 @@
|
||||
|
||||
const scriptName = scriptObj.name.trim();
|
||||
const escapedName = escapeHtml(scriptName);
|
||||
select.append(`<option value="${escapedName}">${escapedName}</option>`);
|
||||
select.append(`<option value="${scriptName}">${escapedName}</option>`);
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复当前选中状态
|
||||
if (currentScript) {
|
||||
const escapedScript = escapeHtml(currentScript);
|
||||
if (select.find(`option[value="${escapedScript}"]`).length) {
|
||||
select.val(escapedScript);
|
||||
} else {
|
||||
currentScript = null;
|
||||
}
|
||||
if (currentScript && select.find(`option[value="${currentScript}"]`).length) {
|
||||
select.val(currentScript);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 脚本操作相关 ======================
|
||||
/**
|
||||
* 选择脚本
|
||||
* @param {string} scriptName - 脚本名称
|
||||
*/
|
||||
function selectScript(scriptName) {
|
||||
currentScript = scriptName;
|
||||
$('#config-script').val(scriptName);
|
||||
loadConfig(scriptName);
|
||||
updateOutput();
|
||||
renderScriptList(); // 刷新列表以更新选中状态
|
||||
renderScriptList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动脚本
|
||||
* @param {string} scriptName - 脚本名称
|
||||
*/
|
||||
function startScript(scriptName) {
|
||||
$.get(`/api/start/${encodeURIComponent(scriptName)}`)
|
||||
.done(function(data) {
|
||||
.done(function (data) {
|
||||
alert(data.msg);
|
||||
updateOutput();
|
||||
renderScriptList();
|
||||
loadScripts(); // 重新加载列表和状态
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
.fail(function (xhr) {
|
||||
alert('启动失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止脚本
|
||||
* @param {string} scriptName - 脚本名称
|
||||
*/
|
||||
function stopScript(scriptName) {
|
||||
$.get(`/api/stop/${encodeURIComponent(scriptName)}`)
|
||||
.done(function(data) {
|
||||
.done(function (data) {
|
||||
alert(data.msg);
|
||||
updateOutput();
|
||||
renderScriptList();
|
||||
loadScripts(); // 重新加载列表和状态
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
.fail(function (xhr) {
|
||||
alert('停止失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取脚本状态
|
||||
* @param {string} scriptName - 脚本名称
|
||||
* @param {function} callback - 回调函数
|
||||
*/
|
||||
function getScriptStatus(scriptName, callback) {
|
||||
$.get(`/api/status/${encodeURIComponent(scriptName)}`)
|
||||
.done(function(data) {
|
||||
callback(data);
|
||||
})
|
||||
.fail(function() {
|
||||
callback({ status: 'stopped', running: false, scheduled: false });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取脚本运行模式
|
||||
* @param {string} scriptName - 脚本名称
|
||||
* @param {function} callback - 回调函数
|
||||
*/
|
||||
function getScriptMode(scriptName, callback) {
|
||||
$.get('/api/config')
|
||||
.done(function(configs) {
|
||||
const mode = configs[scriptName]?.mode || 'single-run';
|
||||
callback(mode);
|
||||
})
|
||||
.fail(function() {
|
||||
callback('single-run');
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 配置相关 ======================
|
||||
/**
|
||||
* 加载脚本配置
|
||||
* @param {string} scriptName - 脚本名称
|
||||
*/
|
||||
function loadConfig(scriptName) {
|
||||
$.get('/api/config')
|
||||
.done(function(configs) {
|
||||
const config = configs[scriptName] || { mode: 'single-run' };
|
||||
.done(function (configs) {
|
||||
const config = configs[scriptName] || {mode: 'single-run'};
|
||||
$('#config-mode').val(config.mode);
|
||||
$('#python-path').val(config.python_path || '');
|
||||
toggleIntervalConfig(config.mode);
|
||||
handleScriptChange();
|
||||
|
||||
if (config.mode === 'interval') {
|
||||
$('#config-interval').val(config.interval || 1);
|
||||
$('#config-unit').val(config.unit || 'hours');
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
.fail(function () {
|
||||
alert('加载配置失败');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存脚本配置
|
||||
*/
|
||||
function saveConfig() {
|
||||
const scriptName = $('#config-script').val();
|
||||
if (!scriptName) {
|
||||
@@ -679,8 +611,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptExists = allScripts.some(script => script.name.trim() === scriptName.trim());
|
||||
if (!scriptExists) {
|
||||
alert('脚本不存在或已被移除,请重新选择');
|
||||
return;
|
||||
}
|
||||
|
||||
if (scriptName.endsWith('.py')) {
|
||||
const pythonPath = $('#python-path').val().trim();
|
||||
if (pythonPath && !pythonPath.toLowerCase().includes('python')) {
|
||||
if (!confirm('Python路径似乎不正确,是否继续保存?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mode = $('#config-mode').val();
|
||||
const config = { mode };
|
||||
const config = {
|
||||
mode,
|
||||
python_path: $('#python-path').val().trim()
|
||||
};
|
||||
|
||||
if (mode === 'interval') {
|
||||
config.interval = parseInt($('#config-interval').val());
|
||||
@@ -691,43 +641,30 @@
|
||||
url: '/api/config',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ [scriptName]: config }),
|
||||
success: function(data) {
|
||||
data: JSON.stringify({[scriptName]: config}),
|
||||
success: function (data) {
|
||||
alert(data.msg);
|
||||
renderScriptList();
|
||||
loadScripts(); // 保存后刷新列表
|
||||
},
|
||||
error: function(xhr) {
|
||||
error: function (xhr) {
|
||||
alert('保存失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示/隐藏定时配置区域
|
||||
* @param {string} mode - 运行模式
|
||||
*/
|
||||
function toggleIntervalConfig(mode) {
|
||||
if (mode === 'interval') {
|
||||
$('#interval-config').show();
|
||||
} else {
|
||||
$('#interval-config').hide();
|
||||
}
|
||||
$('#interval-config').toggle(mode === 'interval');
|
||||
}
|
||||
|
||||
// ====================== 输出相关 ======================
|
||||
/**
|
||||
* 更新输出区域
|
||||
*/
|
||||
function updateOutput() {
|
||||
if (!currentScript) return;
|
||||
|
||||
$.get(`/api/status/${encodeURIComponent(currentScript)}`)
|
||||
.done(function(data) {
|
||||
.done(function (data) {
|
||||
const outputArea = $('#output-area');
|
||||
outputArea.empty();
|
||||
|
||||
(data.output || []).forEach(line => {
|
||||
// 简单的日志类型识别和样式处理
|
||||
let className = 'output-line';
|
||||
if (line.includes('❌')) className += ' error';
|
||||
else if (line.includes('✅')) className += ' success';
|
||||
@@ -737,24 +674,20 @@
|
||||
outputArea.append(`<div class="${className}">${escapeHtml(line)}</div>`);
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
outputArea.scrollTop(outputArea[0].scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空输出区域
|
||||
*/
|
||||
function handleScriptChange() {
|
||||
const scriptName = $('#config-script').val();
|
||||
const pythonConfig = $('#python-path-config');
|
||||
pythonConfig.toggle(scriptName && scriptName.endsWith('.py'));
|
||||
}
|
||||
|
||||
function clearOutput() {
|
||||
$('#output-area').empty();
|
||||
}
|
||||
|
||||
// ====================== 工具函数 ======================
|
||||
/**
|
||||
* HTML特殊字符转义
|
||||
* @param {string} unsafe - 待转义的字符串
|
||||
* @returns {string} 转义后的字符串
|
||||
*/
|
||||
function escapeHtml(unsafe) {
|
||||
if (!unsafe) return '';
|
||||
return unsafe
|
||||
@@ -765,28 +698,116 @@
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化拖拽分隔线功能
|
||||
*/
|
||||
function initResizeHandle() {
|
||||
const handle = $('#resize-handle');
|
||||
const leftPanel = $('#script-list-panel');
|
||||
let isResizing = false;
|
||||
|
||||
handle.mousedown(function(e) {
|
||||
handle.mousedown(function (e) {
|
||||
isResizing = true;
|
||||
$(document).mousemove(function(e) {
|
||||
$(document).mousemove(function (e) {
|
||||
if (!isResizing) return;
|
||||
// 限制最小宽度为200px
|
||||
const newWidth = Math.max(200, e.pageX - leftPanel.offset().left);
|
||||
$('.main-container').css('grid-template-columns', `${newWidth}px 4px 1fr`);
|
||||
});
|
||||
$(document).mouseup(function() {
|
||||
$(document).mouseup(function () {
|
||||
isResizing = false;
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
// 目录浏览功能
|
||||
function openDirectoryBrowser() {
|
||||
currentServerDirectory = "";
|
||||
selectedScriptPath = null;
|
||||
loadServerDirectoryContent();
|
||||
const modal = new bootstrap.Modal(document.getElementById('directoryBrowserModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function loadServerDirectoryContent() {
|
||||
$.get(`/api/directory?path=${encodeURIComponent(currentServerDirectory)}`)
|
||||
.done(function (data) {
|
||||
renderDirectoryContent(data);
|
||||
updatePathNavigator();
|
||||
document.getElementById('selectScriptBtn').disabled = !selectedScriptPath;
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
alert('加载目录失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||
});
|
||||
}
|
||||
|
||||
function renderDirectoryContent(data) {
|
||||
const container = $('#directoryContent').empty();
|
||||
|
||||
data.directories.forEach(dir => {
|
||||
container.append(`
|
||||
<div class="list-group-item list-group-item-action d-flex align-items-center"
|
||||
onclick="navigateToDirectory('${escapeHtml(dir.path)}')">
|
||||
<i class="bi bi-folder-fill me-3 text-primary"></i>
|
||||
<span>${escapeHtml(dir.name)}</span>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
data.files.forEach(file => {
|
||||
if (file.name.endsWith('.bat') || file.name.endsWith('.py')) {
|
||||
const isSelected = selectedScriptPath === file.path;
|
||||
container.append(`
|
||||
<div class="list-group-item list-group-item-action d-flex align-items-center ${isSelected ? 'active' : ''}"
|
||||
onclick="selectServerScript('${escapeHtml(file.path)}', '${escapeHtml(file.name)}')">
|
||||
<i class="bi bi-file-code-fill me-3 text-secondary"></i>
|
||||
<span>${escapeHtml(file.name)}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function navigateToDirectory(path) {
|
||||
currentServerDirectory = path;
|
||||
selectedScriptPath = null;
|
||||
loadServerDirectoryContent();
|
||||
}
|
||||
|
||||
function selectServerScript(path, name) {
|
||||
selectedScriptPath = path;
|
||||
$('#directoryContent .list-group-item').removeClass('active');
|
||||
$(`#directoryContent .list-group-item:has(span:contains('${escapeHtml(name)}'))`).addClass('active');
|
||||
document.getElementById('selectScriptBtn').disabled = false;
|
||||
}
|
||||
|
||||
function updatePathNavigator() {
|
||||
const navigator = $('#pathNavigator').empty();
|
||||
navigator.append(`<span class="badge bg-primary" onclick="navigateToDirectory('')">/</span>`);
|
||||
|
||||
if (!currentServerDirectory) return;
|
||||
|
||||
const pathParts = currentServerDirectory.split('/').filter(p => p);
|
||||
let currentPath = '';
|
||||
|
||||
pathParts.forEach((part, index) => {
|
||||
currentPath += part + '/';
|
||||
const fullPath = currentPath.replace(/\/$/, '');
|
||||
navigator.append(`
|
||||
<span class="badge bg-secondary" onclick="navigateToDirectory('${escapeHtml(fullPath)}')">
|
||||
/ ${escapeHtml(part)}
|
||||
</span>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('selectScriptBtn').addEventListener('click', function () {
|
||||
if (selectedScriptPath) {
|
||||
const scriptName = selectedScriptPath;
|
||||
$('#config-script').val(scriptName);
|
||||
loadConfig(scriptName);
|
||||
selectScript(scriptName);
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('directoryBrowserModal'));
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user