添加后台进程修复等功能

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
+415 -404
View File
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
{
"3.bat": {
"mode": "interval",
"interval": 1,
"unit": "minutes"
},
"2.bat": {
"mode": "long-running"
},
"1.bat": {
"mode": "single-run"
},
"[object Object]": {
"mode": "interval",
"interval": 1,
"unit": "minutes"
}
}
-1
View File
@@ -22,4 +22,3 @@ echo.
echo ============================= echo =============================
echo 报告生成于 %date% %time% echo 报告生成于 %date% %time%
echo ============================= echo =============================
pause
-1
View File
@@ -29,4 +29,3 @@ echo =============================
echo 测试完成。 echo 测试完成。
echo 诊断时间: %time% echo 诊断时间: %time%
echo ============================= echo =============================
pause
+12
View File
@@ -0,0 +1,12 @@
import chardet
def detect_file_encoding(file_path):
with open(file_path, 'rb') as file:
raw_data = file.read()
result = chardet.detect(raw_data)
encoding = result['encoding']
confidence = result['confidence']
print(f"Detected encoding: {encoding}, Confidence: {confidence}")
file_path = r"C:\Users\zy187\Desktop\销售明细.csv"
detect_file_encoding(file_path)
+256 -235
View File
@@ -4,11 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>脚本管理平台</title> <title>脚本管理平台</title>
<!-- 引入Bootstrap和图标库 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <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"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style> <style>
/* 全局样式 */
body { body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa; background-color: #f8f9fa;
@@ -16,116 +14,113 @@
margin: 0; margin: 0;
} }
/* 主容器样式调整:适配拖拽分隔线 */
.main-container { .main-container {
display: grid; display: grid;
/* 初始布局:左侧280px + 分隔线4px + 右侧自适应 */
grid-template-columns: 540px 4px 1fr; grid-template-columns: 540px 4px 1fr;
gap: 0; /* 取消间隙,避免分隔线与面板之间有空隙 */ gap: 0;
height: calc(100vh - 40px); height: calc(100vh - 40px);
} }
/* 左侧脚本列表面板 */
#script-list-panel { #script-list-panel {
background: white; background: white;
border-radius: 8px 0 0 8px; /* 左侧圆角,右侧与分隔线贴合 */ border-radius: 8px 0 0 8px;
padding: 15px; 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; overflow-y: auto;
width: 100%; /* 宽度由grid控制,内部自适应 */ width: 100%;
} }
/* 右侧面板样式调整 */
#right-panel { #right-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 15px; /* 右侧与分隔线保持原有的15px间隙 */ padding-left: 15px;
} }
/* 拖拽分隔线样式 */
.resize-handle { .resize-handle {
background-color: #e9ecef; background-color: #e9ecef;
cursor: col-resize; /* 鼠标悬停时显示水平调整光标 */ cursor: col-resize;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
.resize-handle:hover { .resize-handle:hover {
background-color: #0d6efd; /* 悬停时变蓝色,提示可拖拽 */ background-color: #0d6efd;
} }
.resize-handle:active {
background-color: #0b5ed7; /* 拖拽时加深蓝色 */ .resize-handle:active {
background-color: #0b5ed7;
} }
/* 右侧输出区域 */
#output-area { #output-area {
background: #1e1e1e; background: #1e1e1e;
color: #dcdcdc; color: #dcdcdc;
font-family: 'Consolas', 'Microsoft YaHei Mono', monospace; font-family: 'Consolas', 'Microsoft YaHei Mono', monospace;
padding: 15px; padding: 15px;
border-radius: 8px; border-radius: 8px;
overflow-y: auto; /* 保持滚动 */ overflow-y: auto;
height: 500px; /* 固定高度 500px */ height: 500px;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
} }
/* 输出行样式(区分类型) */
.output-line { .output-line {
margin: 2px 0; margin: 2px 0;
} }
/* 搜索框样式优化 */
#script-search { #script-search {
font-size: 13px; font-size: 13px;
} }
/* 排序下拉框样式 */
#script-sort { #script-sort {
width: auto; width: auto;
min-width: 160px; min-width: 160px;
} }
/* 脚本项的修改时间显示优化 */
.mode-info .text-xs { .mode-info .text-xs {
font-size: 11px; font-size: 11px;
line-height: 1.2; line-height: 1.2;
} }
/* 响应式调整:小屏幕时搜索框与排序控件换行 */
@media (max-width: 768px) { @media (max-width: 768px) {
.main-container { .main-container {
grid-template-columns: 1fr; /* 单列布局 */ grid-template-columns: 1fr;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
gap: 15px; gap: 15px;
height: auto; height: auto;
} }
#resize-handle { #resize-handle {
display: none; /* 小屏幕隐藏分隔线 */ display: none;
} }
#right-panel { #right-panel {
padding-left: 0; /* 取消右侧间隙 */ padding-left: 0;
} }
#script-list-panel { #script-list-panel {
border-radius: 8px; /* 恢复全圆角 */ border-radius: 8px;
} }
} }
.output-line .timestamp { .output-line .timestamp {
color: #888; /* 时间戳灰色 */ color: #888;
} }
.output-line.success { .output-line.success {
color: #4CAF50; /* 成功绿色 */ color: #4CAF50;
} }
.output-line.error { .output-line.error {
color: #f44336; /* 错误红色 */ color: #f44336;
} }
.output-line.warning { .output-line.warning {
color: #ff9800; /* 警告橙色 */ color: #ff9800;
} }
.output-line.info { .output-line.info {
color: #2196F3; /* 信息蓝色 */ color: #2196F3;
} }
/* 配置面板 */
#config-panel { #config-panel {
background: white; background: white;
border-radius: 8px; border-radius: 8px;
@@ -134,7 +129,6 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
/* 脚本列表项 */
.script-item { .script-item {
padding: 12px; padding: 12px;
margin-bottom: 8px; margin-bottom: 8px;
@@ -150,13 +144,11 @@
transform: translateX(2px); transform: translateX(2px);
} }
/* 脚本项激活状态 */
.script-item.active { .script-item.active {
border-left-color: #0d6efd; /* 蓝色边框 */ border-left-color: #0d6efd;
background: #e9ecef; background: #e9ecef;
} }
/* 状态指示器(圆形) */
.status-indicator { .status-indicator {
display: inline-block; display: inline-block;
width: 12px; width: 12px;
@@ -166,7 +158,6 @@
vertical-align: middle; vertical-align: middle;
} }
/* 状态颜色:运行中(绿)、调度中(蓝)、停止(红) */
.status-running { .status-running {
background: #198754; background: #198754;
} }
@@ -179,7 +170,6 @@
background: #dc3545; background: #dc3545;
} }
/* 状态文本样式 */
.status-text { .status-text {
font-size: 12px; font-size: 12px;
margin-left: 8px; margin-left: 8px;
@@ -202,13 +192,11 @@
color: #dc3545; color: #dc3545;
} }
/* 按钮样式优化 */
.btn-sm { .btn-sm {
padding: 2px 8px; padding: 2px 8px;
font-size: 12px; font-size: 12px;
} }
/* 清空输出按钮 */
#clear-output { #clear-output {
position: absolute; position: absolute;
top: 15px; top: 15px;
@@ -216,7 +204,6 @@
z-index: 10; z-index: 10;
} }
/* 响应式调整(小屏幕单列布局) */
@media (max-width: 768px) { @media (max-width: 768px) {
.main-container { .main-container {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -230,18 +217,14 @@
</style> </style>
</head> </head>
<body> <body>
<!-- 页面标题 -->
<h4 class="mb-3 d-flex justify-content-between align-items-center"> <h4 class="mb-3 d-flex justify-content-between align-items-center">
<span>脚本管理平台</span> <span>脚本管理平台</span>
<small class="text-muted" id="system-status">系统运行中</small> <small class="text-muted" id="system-status">系统运行中</small>
</h4> </h4>
<!-- 主容器 -->
<div class="main-container"> <div class="main-container">
<!-- 左侧脚本列表面板 -->
<div id="script-list-panel" class="panel-resizable"> <div id="script-list-panel" class="panel-resizable">
<div class="d-flex flex-column gap-3 mb-3"> <div class="d-flex flex-column gap-3 mb-3">
<!-- 新增:搜索框 -->
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-text"> <span class="input-group-text">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
@@ -252,7 +235,6 @@
<i class="bi bi-x"></i> <i class="bi bi-x"></i>
</button> </button>
</div> </div>
<!-- 新增:排序控制 -->
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">脚本列表</h5> <h5 class="mb-0">脚本列表</h5>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
@@ -267,14 +249,10 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 脚本列表容器 -->
<div id="script-list"></div> <div id="script-list"></div>
</div> </div>
<!-- 新增:拖拽分隔线 -->
<div id="resize-handle" class="resize-handle" title="拖拽调整宽度"></div> <div id="resize-handle" class="resize-handle" title="拖拽调整宽度"></div>
<!-- 右侧:输出区域 + 配置面板 -->
<div id="right-panel"> <div id="right-panel">
<!-- 输出控制台(带清空按钮) -->
<div class="position-relative"> <div class="position-relative">
<button id="clear-output" class="btn btn-sm btn-outline-light" onclick="clearOutput()"> <button id="clear-output" class="btn btn-sm btn-outline-light" onclick="clearOutput()">
<i class="bi bi-trash"></i> 清空输出 <i class="bi bi-trash"></i> 清空输出
@@ -282,19 +260,28 @@
<div id="output-area"></div> <div id="output-area"></div>
</div> </div>
<!-- 脚本配置面板 -->
<div id="config-panel"> <div id="config-panel">
<h5 class="mb-3">脚本配置</h5> <h5 class="mb-3">脚本配置</h5>
<form id="config-form"> <form id="config-form">
<!-- 选择脚本 -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">选择脚本 <span class="text-danger">*</span></label> <label class="form-label">选择脚本 <span class="text-danger">*</span></label>
<select class="form-select" id="config-script" required> <div class="d-flex gap-2">
<select class="form-select" id="config-script" required onchange="handleScriptChange()">
<option value="">请选择脚本...</option> <option value="">请选择脚本...</option>
</select> </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>
<!-- 运行模式 -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">运行模式 <span class="text-danger">*</span></label> <label class="form-label">运行模式 <span class="text-danger">*</span></label>
<select class="form-select" id="config-mode" required> <select class="form-select" id="config-mode" required>
@@ -304,7 +291,6 @@
</select> </select>
</div> </div>
<!-- 定时执行配置(默认隐藏) -->
<div id="interval-config" style="display: none;"> <div id="interval-config" style="display: none;">
<label class="form-label">执行周期 <span class="text-danger">*</span></label> <label class="form-label">执行周期 <span class="text-danger">*</span></label>
<div class="input-group"> <div class="input-group">
@@ -318,7 +304,6 @@
<div class="form-text">定时任务将按设定周期自动执行</div> <div class="form-text">定时任务将按设定周期自动执行</div>
</div> </div>
<!-- 保存配置按钮 -->
<button type="submit" class="btn btn-primary w-100"> <button type="submit" class="btn btn-primary w-100">
<i class="bi bi-save"></i> 保存配置 <i class="bi bi-save"></i> 保存配置
</button> </button>
@@ -327,66 +312,78 @@
</div> </div>
</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/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 src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// 全局变量 const OUTPUT_UPDATE_INTERVAL = 2000;
const OUTPUT_UPDATE_INTERVAL = 2000; // 输出更新间隔(2秒) let allScripts = [];
let allScripts = []; // 存储所有脚本对象:[{name: "xxx.bat", modify_time: ...}, ...] let filteredScripts = [];
let filteredScripts = []; // 过滤后的脚本列表 let currentScript = null;
let currentScript = null; // 当前选中的脚本名称
let currentSearchKeyword = ""; let currentSearchKeyword = "";
let currentServerDirectory = "";
let selectedScriptPath = null;
let scriptStatusMap = {}; // 缓存脚本状态映射
// ====================== 初始化 ======================
$(function () { $(function () {
// 加载脚本列表
loadScripts(); loadScripts();
// 监听运行模式切换(显示/隐藏定时配置)
$('#config-mode').change(function () { $('#config-mode').change(function () {
toggleIntervalConfig($(this).val()); toggleIntervalConfig($(this).val());
}); });
// 监听配置表单提交
$('#config-form').submit(function (e) { $('#config-form').submit(function (e) {
e.preventDefault(); e.preventDefault();
saveConfig(); saveConfig();
}); });
// 定时更新输出区域
setInterval(() => { setInterval(() => {
if (currentScript) updateOutput(); if (currentScript) updateOutput();
}, OUTPUT_UPDATE_INTERVAL); }, OUTPUT_UPDATE_INTERVAL);
// 初始化拖拽功能
initResizeHandle(); initResizeHandle();
}); });
// ====================== 脚本列表相关 ======================
/**
* 加载脚本列表(从后端API获取)
*/
function loadScripts() { function loadScripts() {
$.get('/api/scripts', function (scripts) { $.get('/api/scripts', function (scripts) {
// 校验后端返回数据格式
if (!Array.isArray(scripts)) { if (!Array.isArray(scripts)) {
alert('加载脚本失败:后端返回非数组格式'); alert('加载脚本失败:后端返回非数组格式');
allScripts = [];
filteredScripts = [];
renderScriptList();
updateConfigScriptSelect();
return; return;
} }
// 只保留有配置的脚本(后端已过滤,前端二次确认)
allScripts = scripts.filter(item =>
typeof item === 'object' && item !== null &&
'name' in item && typeof item.name === 'string' && item.name.trim() !== ''
);
// 过滤并验证脚本对象 // 批量获取所有脚本的状态和模式
allScripts = scripts.filter(item => { $.get('/api/scripts/status', function(statusMap) {
return typeof item === 'object' && item !== null && scriptStatusMap = statusMap;
'name' in item && typeof item.name === 'string' && item.name.trim() !== '';
});
currentSearchKeyword = ""; currentSearchKeyword = "";
$('#script-search').val(""); $('#script-search').val("");
sortScripts(); // 排序后渲染列表 sortScripts();
updateConfigScriptSelect(); updateConfigScriptSelect();
// 恢复当前选中脚本状态
if (currentScript) { if (currentScript) {
const scriptExists = allScripts.some(s => s.name === currentScript); const scriptExists = allScripts.some(s => s.name === currentScript);
if (scriptExists) { if (scriptExists) {
@@ -398,6 +395,10 @@
$('#output-area').empty(); $('#output-area').empty();
} }
} }
}).fail(function(xhr) {
alert('加载脚本状态失败:' + (xhr.responseJSON?.error || xhr.statusText));
});
}).fail(function (xhr) { }).fail(function (xhr) {
alert('加载脚本列表失败:' + (xhr.responseJSON?.error || xhr.statusText)); alert('加载脚本列表失败:' + (xhr.responseJSON?.error || xhr.statusText));
allScripts = []; allScripts = [];
@@ -407,26 +408,18 @@
}); });
} }
/**
* 搜索脚本(实时模糊匹配)
*/
function searchScripts() { function searchScripts() {
const keyword = $('#script-search').val().trim().toLowerCase(); const keyword = $('#script-search').val().trim().toLowerCase();
currentSearchKeyword = keyword; currentSearchKeyword = keyword;
if (keyword === "") { filteredScripts = keyword === ""
filteredScripts = [...allScripts]; ? [...allScripts]
} else { : allScripts.filter(scriptObj =>
filteredScripts = allScripts.filter(scriptObj =>
scriptObj.name.toLowerCase().includes(keyword) scriptObj.name.toLowerCase().includes(keyword)
); );
}
sortScripts(false); sortScripts(false);
} }
/**
* 清空搜索
*/
function clearSearch() { function clearSearch() {
$('#script-search').val(""); $('#script-search').val("");
currentSearchKeyword = ""; currentSearchKeyword = "";
@@ -434,10 +427,6 @@
sortScripts(false); sortScripts(false);
} }
/**
* 排序脚本(支持按修改时间/名称排序)
* @param {boolean} reload - 是否重新加载全部脚本(默认true)
*/
function sortScripts(reload = true) { function sortScripts(reload = true) {
if (reload) { if (reload) {
filteredScripts = [...allScripts]; filteredScripts = [...allScripts];
@@ -458,15 +447,12 @@
renderScriptList(); renderScriptList();
} }
/**
* 渲染脚本列表
*/
function renderScriptList() { function renderScriptList() {
const container = $('#script-list').empty(); const container = $('#script-list').empty();
if (filteredScripts.length === 0) { if (filteredScripts.length === 0) {
const tip = currentSearchKeyword ? const tip = currentSearchKeyword ?
`<i class="bi bi-search"></i> 未找到包含"${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(` container.append(`
<div class="text-center text-muted py-3"> <div class="text-center text-muted py-3">
${tip} ${tip}
@@ -475,23 +461,33 @@
return; return;
} }
// 遍历过滤后的脚本对象数组 // 使用文档片段减少DOM重绘
const fragment = document.createDocumentFragment();
filteredScripts.forEach(scriptObj => { filteredScripts.forEach(scriptObj => {
const scriptName = scriptObj.name.trim(); const scriptName = scriptObj.name.trim();
if (!scriptName) return; if (!scriptName) return;
const modifyTimeStr = scriptObj.modify_time_str || '未知时间'; const modifyTimeStr = scriptObj.modify_time_str || '未知时间';
getScriptStatus(scriptName, function(status) { // 从缓存的状态映射表中获取状态和模式
// 状态样式处理 const statusInfo = scriptStatusMap[scriptName] || {
const statusClass = status.status === 'running' ? 'status-running' : status: 'stopped',
status.status === 'scheduled' ? 'status-scheduled' : 'status-stopped'; running: false,
const statusText = status.status === 'running' ? '运行中' : scheduled: false,
status.status === 'scheduled' ? '已调度' : '停止'; mode: 'single-run',
const statusTextClass = `status-text ${status.status}`; schedule_info: ''
};
// 按钮处理 // 状态样式处理
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}`;
// 按钮样式处理
let btnClass, btnIcon, btnText, btnClick; let btnClass, btnIcon, btnText, btnClick;
if (status.running || status.scheduled) { if (statusInfo.running || statusInfo.scheduled) {
btnClass = 'btn-danger'; btnClass = 'btn-danger';
btnIcon = 'bi-stop-fill'; btnIcon = 'bi-stop-fill';
btnText = '停止'; btnText = '停止';
@@ -503,10 +499,15 @@
btnClick = `startScript('${escapeHtml(scriptName)}')`; btnClick = `startScript('${escapeHtml(scriptName)}')`;
} }
// 构建脚本项HTML // 模式文本处理
const scriptItemHtml = ` 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' : ''}" <div class="script-item ${currentScript === scriptName ? 'active' : ''}"
onclick="selectScript('${escapeHtml(scriptName)}')"> onclick="selectScript(&quot;${escapeHtml(scriptName)}&quot;)">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -515,7 +516,10 @@
<span class="${statusTextClass}">${statusText}</span> <span class="${statusTextClass}">${statusText}</span>
</div> </div>
<div class="mode-info text-xs mt-1"> <div class="mode-info text-xs mt-1">
<span class="text-muted">加载模式信息中...</span> <div class="d-flex justify-content-between">
<span class="text-muted">模式:${modeText}${scheduleInfo}</span>
<span class="text-muted">最近修改:${modifyTimeStr}</span>
</div>
</div> </div>
</div> </div>
<button class="btn btn-sm ${btnClass}" <button class="btn btn-sm ${btnClass}"
@@ -524,28 +528,14 @@
</button> </button>
</div> </div>
</div> </div>
`; `)[0];
container.append(scriptItemHtml); fragment.appendChild(scriptItem);
});
// 获取并更新模式信息 // 一次性将所有元素添加到容器
getScriptMode(scriptName, function(mode) { container.append(fragment);
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>
`);
});
});
});
} }
/**
* 更新配置面板的脚本下拉框
*/
function updateConfigScriptSelect() { function updateConfigScriptSelect() {
const select = $('#config-script').empty(); const select = $('#config-script').empty();
select.append('<option value="">请选择脚本...</option>'); select.append('<option value="">请选择脚本...</option>');
@@ -556,108 +546,53 @@
const scriptName = scriptObj.name.trim(); const scriptName = scriptObj.name.trim();
const escapedName = escapeHtml(scriptName); const escapedName = escapeHtml(scriptName);
select.append(`<option value="${escapedName}">${escapedName}</option>`); select.append(`<option value="${scriptName}">${escapedName}</option>`);
} }
}); });
// 恢复当前选中状态 if (currentScript && select.find(`option[value="${currentScript}"]`).length) {
if (currentScript) { select.val(currentScript);
const escapedScript = escapeHtml(currentScript);
if (select.find(`option[value="${escapedScript}"]`).length) {
select.val(escapedScript);
} else {
currentScript = null;
}
} }
} }
// ====================== 脚本操作相关 ======================
/**
* 选择脚本
* @param {string} scriptName - 脚本名称
*/
function selectScript(scriptName) { function selectScript(scriptName) {
currentScript = scriptName; currentScript = scriptName;
$('#config-script').val(scriptName); $('#config-script').val(scriptName);
loadConfig(scriptName); loadConfig(scriptName);
updateOutput(); updateOutput();
renderScriptList(); // 刷新列表以更新选中状态 renderScriptList();
} }
/**
* 启动脚本
* @param {string} scriptName - 脚本名称
*/
function startScript(scriptName) { function startScript(scriptName) {
$.get(`/api/start/${encodeURIComponent(scriptName)}`) $.get(`/api/start/${encodeURIComponent(scriptName)}`)
.done(function (data) { .done(function (data) {
alert(data.msg); alert(data.msg);
updateOutput(); loadScripts(); // 重新加载列表和状态
renderScriptList();
}) })
.fail(function (xhr) { .fail(function (xhr) {
alert('启动失败:' + (xhr.responseJSON?.error || xhr.statusText)); alert('启动失败:' + (xhr.responseJSON?.error || xhr.statusText));
}); });
} }
/**
* 停止脚本
* @param {string} scriptName - 脚本名称
*/
function stopScript(scriptName) { function stopScript(scriptName) {
$.get(`/api/stop/${encodeURIComponent(scriptName)}`) $.get(`/api/stop/${encodeURIComponent(scriptName)}`)
.done(function (data) { .done(function (data) {
alert(data.msg); alert(data.msg);
updateOutput(); loadScripts(); // 重新加载列表和状态
renderScriptList();
}) })
.fail(function (xhr) { .fail(function (xhr) {
alert('停止失败:' + (xhr.responseJSON?.error || xhr.statusText)); 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) { function loadConfig(scriptName) {
$.get('/api/config') $.get('/api/config')
.done(function (configs) { .done(function (configs) {
const config = configs[scriptName] || {mode: 'single-run'}; const config = configs[scriptName] || {mode: 'single-run'};
$('#config-mode').val(config.mode); $('#config-mode').val(config.mode);
$('#python-path').val(config.python_path || '');
toggleIntervalConfig(config.mode); toggleIntervalConfig(config.mode);
handleScriptChange();
if (config.mode === 'interval') { if (config.mode === 'interval') {
$('#config-interval').val(config.interval || 1); $('#config-interval').val(config.interval || 1);
@@ -669,9 +604,6 @@
}); });
} }
/**
* 保存脚本配置
*/
function saveConfig() { function saveConfig() {
const scriptName = $('#config-script').val(); const scriptName = $('#config-script').val();
if (!scriptName) { if (!scriptName) {
@@ -679,8 +611,26 @@
return; 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 mode = $('#config-mode').val();
const config = { mode }; const config = {
mode,
python_path: $('#python-path').val().trim()
};
if (mode === 'interval') { if (mode === 'interval') {
config.interval = parseInt($('#config-interval').val()); config.interval = parseInt($('#config-interval').val());
@@ -694,7 +644,7 @@
data: JSON.stringify({[scriptName]: config}), data: JSON.stringify({[scriptName]: config}),
success: function (data) { success: function (data) {
alert(data.msg); alert(data.msg);
renderScriptList(); loadScripts(); // 保存后刷新列表
}, },
error: function (xhr) { error: function (xhr) {
alert('保存失败:' + (xhr.responseJSON?.error || xhr.statusText)); alert('保存失败:' + (xhr.responseJSON?.error || xhr.statusText));
@@ -702,22 +652,10 @@
}); });
} }
/**
* 显示/隐藏定时配置区域
* @param {string} mode - 运行模式
*/
function toggleIntervalConfig(mode) { function toggleIntervalConfig(mode) {
if (mode === 'interval') { $('#interval-config').toggle(mode === 'interval');
$('#interval-config').show();
} else {
$('#interval-config').hide();
}
} }
// ====================== 输出相关 ======================
/**
* 更新输出区域
*/
function updateOutput() { function updateOutput() {
if (!currentScript) return; if (!currentScript) return;
@@ -727,7 +665,6 @@
outputArea.empty(); outputArea.empty();
(data.output || []).forEach(line => { (data.output || []).forEach(line => {
// 简单的日志类型识别和样式处理
let className = 'output-line'; let className = 'output-line';
if (line.includes('❌')) className += ' error'; if (line.includes('❌')) className += ' error';
else if (line.includes('✅')) className += ' success'; else if (line.includes('✅')) className += ' success';
@@ -737,24 +674,20 @@
outputArea.append(`<div class="${className}">${escapeHtml(line)}</div>`); outputArea.append(`<div class="${className}">${escapeHtml(line)}</div>`);
}); });
// 滚动到底部
outputArea.scrollTop(outputArea[0].scrollHeight); 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() { function clearOutput() {
$('#output-area').empty(); $('#output-area').empty();
} }
// ====================== 工具函数 ======================
/**
* HTML特殊字符转义
* @param {string} unsafe - 待转义的字符串
* @returns {string} 转义后的字符串
*/
function escapeHtml(unsafe) { function escapeHtml(unsafe) {
if (!unsafe) return ''; if (!unsafe) return '';
return unsafe return unsafe
@@ -765,9 +698,6 @@
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
/**
* 初始化拖拽分隔线功能
*/
function initResizeHandle() { function initResizeHandle() {
const handle = $('#resize-handle'); const handle = $('#resize-handle');
const leftPanel = $('#script-list-panel'); const leftPanel = $('#script-list-panel');
@@ -777,7 +707,6 @@
isResizing = true; isResizing = true;
$(document).mousemove(function (e) { $(document).mousemove(function (e) {
if (!isResizing) return; if (!isResizing) return;
// 限制最小宽度为200px
const newWidth = Math.max(200, e.pageX - leftPanel.offset().left); const newWidth = Math.max(200, e.pageX - leftPanel.offset().left);
$('.main-container').css('grid-template-columns', `${newWidth}px 4px 1fr`); $('.main-container').css('grid-template-columns', `${newWidth}px 4px 1fr`);
}); });
@@ -787,6 +716,98 @@
e.preventDefault(); 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> </script>
</body> </body>
</html> </html>
+78
View File
@@ -0,0 +1,78 @@
# batManage项目说明文档
## 项目概述
batManage是一个脚本管理与执行工具,主要用于对批处理脚本(.bat)和Python脚本(.py)进行统一管理、运行监控和日志记录,支持单次脚本运行模式,具备进程状态检测、重复启动防护等功能。
## 核心功能
1. **脚本运行管理**
- 支持.bat和.py两种类型脚本的启动执行
- 防止脚本重复运行(通过内存记录和系统进程检测双重校验)
- 实时捕获脚本输出并记录日志
2. **进程监控**
- 维护运行中脚本的进程列表
- 实时检测进程状态,处理进程结束后的资源清理
- 异常处理与日志记录
3. **日志管理**
- 记录脚本启动、运行状态、结束信息
- 捕获脚本输出内容并写入日志
- 记录错误信息与异常情况
## 关键模块说明
### 脚本启动模块(start_single_run_script函数)
该函数是项目核心功能实现,主要流程包括:
1. **运行状态检查**
- 检查内存中是否已有记录的运行进程
- 检测系统中是否存在对应脚本的运行进程
2. **脚本合法性验证**
- 检查脚本文件是否存在
- 验证脚本类型是否支持(仅支持.bat和.py)
3. **命令构建**
- 对.bat脚本:使用`cmd /c`命令执行
- 对.py脚本:使用配置的Python路径执行(默认使用"python"
4. **异步执行**
- 通过线程异步执行脚本,避免阻塞主程序
- 实时读取脚本输出并记录
- 处理脚本执行完成后的状态记录与资源清理
## 技术实现细节
- 使用`subprocess.Popen`创建子进程执行脚本
- 通过`threading.Thread`实现异步执行
- 使用进程返回码判断脚本执行结果(0为成功,非0为失败)
- 采用字典`running_processes`维护内存中的进程记录
- 提供`save_processes`方法持久化进程信息(具体实现未展示)
## 项目结构
```
batManage/
├── app.py # 核心功能实现
├── tasks/ # 脚本存放目录
│ ├── 1.bat
│ ├── 3.bat
│ └── 文件编码检测.py
├── templates/ # 前端模板目录
│ └── index.html
├── .idea/ # 项目配置目录
└── config.json # 配置文件
```
## 使用说明
1. 将需要管理的脚本放入tasks目录
2. 通过调用`start_single_run_script(script_name)`启动脚本
3. 系统会自动处理脚本运行过程中的监控与日志记录
## 注意事项
- 确保脚本路径正确,且具有可执行权限
- 对于Python脚本,可在配置中指定特定的Python解释器路径
- 系统会自动处理进程异常退出的情况,并清理相关记录
## 扩展建议
1. 增加定时任务功能,支持脚本周期性执行
2. 完善前端界面,提供可视化的脚本管理与监控
3. 增加脚本执行权限管理,限制特定脚本的运行权限
4. 实现脚本执行历史记录查询与统计分析功能