Files
bat_manage/templates/index.html
T

792 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<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;
padding: 20px;
margin: 0;
}
/* 主容器样式调整:适配拖拽分隔线 */
.main-container {
display: grid;
/* 初始布局:左侧280px + 分隔线4px + 右侧自适应 */
grid-template-columns: 540px 4px 1fr;
gap: 0; /* 取消间隙,避免分隔线与面板之间有空隙 */
height: calc(100vh - 40px);
}
/* 左侧脚本列表面板 */
#script-list-panel {
background: white;
border-radius: 8px 0 0 8px; /* 左侧圆角,右侧与分隔线贴合 */
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow-y: auto;
width: 100%; /* 宽度由grid控制,内部自适应 */
}
/* 右侧面板样式调整 */
#right-panel {
display: flex;
flex-direction: column;
padding-left: 15px; /* 右侧与分隔线保持原有的15px间隙 */
}
/* 拖拽分隔线样式 */
.resize-handle {
background-color: #e9ecef;
cursor: col-resize; /* 鼠标悬停时显示水平调整光标 */
transition: background-color 0.2s ease;
}
.resize-handle:hover {
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 */
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-rows: auto 1fr;
gap: 15px;
height: auto;
}
#resize-handle {
display: none; /* 小屏幕隐藏分隔线 */
}
#right-panel {
padding-left: 0; /* 取消右侧间隙 */
}
#script-list-panel {
border-radius: 8px; /* 恢复全圆角 */
}
}
.output-line .timestamp {
color: #888; /* 时间戳灰色 */
}
.output-line.success {
color: #4CAF50; /* 成功绿色 */
}
.output-line.error {
color: #f44336; /* 错误红色 */
}
.output-line.warning {
color: #ff9800; /* 警告橙色 */
}
.output-line.info {
color: #2196F3; /* 信息蓝色 */
}
/* 配置面板 */
#config-panel {
background: white;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 脚本列表项 */
.script-item {
padding: 12px;
margin-bottom: 8px;
border-left: 3px solid transparent;
border-radius: 4px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.2s ease;
}
.script-item:hover {
background: #e9ecef;
transform: translateX(2px);
}
/* 脚本项激活状态 */
.script-item.active {
border-left-color: #0d6efd; /* 蓝色边框 */
background: #e9ecef;
}
/* 状态指示器(圆形) */
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
/* 状态颜色:运行中(绿)、调度中(蓝)、停止(红) */
.status-running {
background: #198754;
}
.status-scheduled {
background: #0d6efd;
}
.status-stopped {
background: #dc3545;
}
/* 状态文本样式 */
.status-text {
font-size: 12px;
margin-left: 8px;
padding: 2px 6px;
border-radius: 12px;
}
.status-text.running {
background: #d1e7dd;
color: #198754;
}
.status-text.scheduled {
background: #cfe2ff;
color: #0d6efd;
}
.status-text.stopped {
background: #f8d7da;
color: #dc3545;
}
/* 按钮样式优化 */
.btn-sm {
padding: 2px 8px;
font-size: 12px;
}
/* 清空输出按钮 */
#clear-output {
position: absolute;
top: 15px;
right: 15px;
z-index: 10;
}
/* 响应式调整(小屏幕单列布局) */
@media (max-width: 768px) {
.main-container {
grid-template-columns: 1fr;
height: auto;
}
#output-area {
height: 300px;
}
}
</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>
</span>
<input type="text" id="script-search" class="form-control" placeholder="搜索脚本(名称模糊匹配)"
oninput="searchScripts()">
<button class="btn btn-outline-secondary" type="button" onclick="clearSearch()">
<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">
<select id="script-sort" class="form-select form-select-sm" onchange="sortScripts()">
<option value="modify_time_desc">最近修改时间 ↓</option>
<option value="name_asc">名称 ↓(A-Z</option>
<option value="name_desc">名称 ↑(Z-A</option>
</select>
<button class="btn btn-sm btn-outline-secondary" onclick="loadScripts()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</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> 清空输出
</button>
<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>
<!-- 运行模式 -->
<div class="mb-3">
<label class="form-label">运行模式 <span class="text-danger">*</span></label>
<select class="form-select" id="config-mode" required>
<option value="single-run">单次运行(执行后自动停止)</option>
<option value="long-running">长期运行(退出自动重启)</option>
<option value="interval">定时执行</option>
</select>
</div>
<!-- 定时执行配置(默认隐藏) -->
<div id="interval-config" style="display: none;">
<label class="form-label">执行周期 <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" class="form-control" id="config-interval" value="1" min="1" required>
<select class="form-select" id="config-unit" required>
<option value="minutes">分钟</option>
<option value="hours">小时</option>
<option value="days"></option>
</select>
</div>
<div class="form-text">定时任务将按设定周期自动执行</div>
</div>
<!-- 保存配置按钮 -->
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-save"></i> 保存配置
</button>
</form>
</div>
</div>
</div>
<!-- 引入依赖JS -->
<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; // 当前选中的脚本名称
let currentSearchKeyword = "";
// ====================== 初始化 ======================
$(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) {
// 校验后端返回数据格式
if (!Array.isArray(scripts)) {
alert('加载脚本失败:后端返回非数组格式');
allScripts = [];
filteredScripts = [];
renderScriptList();
updateConfigScriptSelect();
return;
}
// 过滤并验证脚本对象
allScripts = scripts.filter(item => {
return typeof item === 'object' && item !== null &&
'name' in item && typeof item.name === 'string' && item.name.trim() !== '';
});
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));
allScripts = [];
filteredScripts = [];
renderScriptList();
updateConfigScriptSelect();
});
}
/**
* 搜索脚本(实时模糊匹配)
*/
function searchScripts() {
const keyword = $('#script-search').val().trim().toLowerCase();
currentSearchKeyword = keyword;
if (keyword === "") {
filteredScripts = [...allScripts];
} else {
filteredScripts = allScripts.filter(scriptObj =>
scriptObj.name.toLowerCase().includes(keyword)
);
}
sortScripts(false);
}
/**
* 清空搜索
*/
function clearSearch() {
$('#script-search').val("");
currentSearchKeyword = "";
filteredScripts = [...allScripts];
sortScripts(false);
}
/**
* 排序脚本(支持按修改时间/名称排序)
* @param {boolean} reload - 是否重新加载全部脚本(默认true)
*/
function sortScripts(reload = true) {
if (reload) {
filteredScripts = [...allScripts];
}
const sortType = $('#script-sort').val();
switch (sortType) {
case "modify_time_desc":
filteredScripts.sort((a, b) => b.modify_time - a.modify_time);
break;
case "name_asc":
filteredScripts.sort((a, b) => a.name.localeCompare(b.name));
break;
case "name_desc":
filteredScripts.sort((a, b) => b.name.localeCompare(a.name));
break;
}
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)`;
container.append(`
<div class="text-center text-muted py-3">
${tip}
</div>
`);
return;
}
// 遍历过滤后的脚本对象数组
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}`;
// 按钮处理
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)}')`;
}
// 构建脚本项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>
</div>
</div>
<button class="btn btn-sm ${btnClass}"
onclick="event.stopPropagation(); ${btnClick}">
<i class="bi ${btnIcon}"></i> ${btnText}
</button>
</div>
</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>
`);
});
});
});
}
/**
* 更新配置面板的脚本下拉框
*/
function updateConfigScriptSelect() {
const select = $('#config-script').empty();
select.append('<option value="">请选择脚本...</option>');
filteredScripts.forEach(scriptObj => {
if (typeof scriptObj === 'object' && scriptObj !== null &&
typeof scriptObj.name === 'string' && scriptObj.name.trim()) {
const scriptName = scriptObj.name.trim();
const escapedName = escapeHtml(scriptName);
select.append(`<option value="${escapedName}">${escapedName}</option>`);
}
});
// 恢复当前选中状态
if (currentScript) {
const escapedScript = escapeHtml(currentScript);
if (select.find(`option[value="${escapedScript}"]`).length) {
select.val(escapedScript);
} else {
currentScript = null;
}
}
}
// ====================== 脚本操作相关 ======================
/**
* 选择脚本
* @param {string} scriptName - 脚本名称
*/
function selectScript(scriptName) {
currentScript = scriptName;
$('#config-script').val(scriptName);
loadConfig(scriptName);
updateOutput();
renderScriptList(); // 刷新列表以更新选中状态
}
/**
* 启动脚本
* @param {string} scriptName - 脚本名称
*/
function startScript(scriptName) {
$.get(`/api/start/${encodeURIComponent(scriptName)}`)
.done(function(data) {
alert(data.msg);
updateOutput();
renderScriptList();
})
.fail(function(xhr) {
alert('启动失败:' + (xhr.responseJSON?.error || xhr.statusText));
});
}
/**
* 停止脚本
* @param {string} scriptName - 脚本名称
*/
function stopScript(scriptName) {
$.get(`/api/stop/${encodeURIComponent(scriptName)}`)
.done(function(data) {
alert(data.msg);
updateOutput();
renderScriptList();
})
.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' };
$('#config-mode').val(config.mode);
toggleIntervalConfig(config.mode);
if (config.mode === 'interval') {
$('#config-interval').val(config.interval || 1);
$('#config-unit').val(config.unit || 'hours');
}
})
.fail(function() {
alert('加载配置失败');
});
}
/**
* 保存脚本配置
*/
function saveConfig() {
const scriptName = $('#config-script').val();
if (!scriptName) {
alert('请选择脚本');
return;
}
const mode = $('#config-mode').val();
const config = { mode };
if (mode === 'interval') {
config.interval = parseInt($('#config-interval').val());
config.unit = $('#config-unit').val();
}
$.ajax({
url: '/api/config',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ [scriptName]: config }),
success: function(data) {
alert(data.msg);
renderScriptList();
},
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();
}
}
// ====================== 输出相关 ======================
/**
* 更新输出区域
*/
function updateOutput() {
if (!currentScript) return;
$.get(`/api/status/${encodeURIComponent(currentScript)}`)
.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';
else if (line.includes('⚠️')) className += ' warning';
else if (line.includes('⏰') || line.includes('️')) className += ' info';
outputArea.append(`<div class="${className}">${escapeHtml(line)}</div>`);
});
// 滚动到底部
outputArea.scrollTop(outputArea[0].scrollHeight);
});
}
/**
* 清空输出区域
*/
function clearOutput() {
$('#output-area').empty();
}
// ====================== 工具函数 ======================
/**
* HTML特殊字符转义
* @param {string} unsafe - 待转义的字符串
* @returns {string} 转义后的字符串
*/
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* 初始化拖拽分隔线功能
*/
function initResizeHandle() {
const handle = $('#resize-handle');
const leftPanel = $('#script-list-panel');
let isResizing = false;
handle.mousedown(function(e) {
isResizing = true;
$(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() {
isResizing = false;
});
e.preventDefault();
});
}
</script>
</body>
</html>