添加后台进程修复等功能

This commit is contained in:
z66
2025-08-26 17:55:30 +08:00
parent c0f7bef56a
commit 37c42e17da
7 changed files with 817 additions and 715 deletions
+312 -291
View File
@@ -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(&quot;${escapeHtml(scriptName)}&quot;)">
<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, "&#039;");
}
/**
* 初始化拖拽分隔线功能
*/
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>