Files
bat_manage/templates/index.html
T

833 lines
29 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>
<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;
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%;
}
#right-panel {
display: flex;
flex-direction: column;
padding-left: 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;
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>
<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>
<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>
<!-- 目录浏览模态框 -->
<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;
let allScripts = [];
let filteredScripts = [];
let currentScript = null;
let currentSearchKeyword = "";
let currentServerDirectory = "";
let selectedScriptPath = null;
$(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();
});
function loadScripts() {
$.get('/api/scripts', function (scripts) {
if (!Array.isArray(scripts)) {
alert('加载脚本失败:后端返回非数组格式');
return;
}
// 直接保存原始名称,不做额外处理
allScripts = scripts.filter(item =>
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;
filteredScripts = keyword === ""
? [...allScripts]
: allScripts.filter(scriptObj =>
scriptObj.name.toLowerCase().includes(keyword)
);
sortScripts(false);
}
function clearSearch() {
$('#script-search').val("");
currentSearchKeyword = "";
filteredScripts = [...allScripts];
sortScripts(false);
}
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)}')`;
}
const scriptItemHtml = `
<div class="script-item ${currentScript === scriptName ? 'active' : ''}"
onclick="selectScript(&quot;${escapeHtml(scriptName)}&quot;)"> <!-- 使用&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">
<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); // 转义仅用于显示
// value使用原始名称,显示文本用转义后的值
select.append(`<option value="${scriptName}">${escapedName}</option>`);
}
});
// 恢复当前选中状态(使用原始名称匹配)
if (currentScript) {
if (select.find(`option[value="${currentScript}"]`).length) {
select.val(currentScript);
}
}
}
function selectScript(scriptName) {
currentScript = scriptName;
$('#config-script').val(scriptName);
loadConfig(scriptName);
updateOutput();
renderScriptList();
}
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));
});
}
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));
});
}
function getScriptStatus(scriptName, callback) {
$.get(`/api/status/${encodeURIComponent(scriptName)}`)
.done(function (data) {
callback(data);
})
.fail(function () {
callback({status: 'stopped', running: false, scheduled: false});
});
}
function getScriptMode(scriptName, callback) {
$.get('/api/config')
.done(function (configs) {
const mode = configs[scriptName]?.mode || 'single-run';
callback(mode);
})
.fail(function () {
callback('single-run');
});
}
function loadConfig(scriptName) {
$.get('/api/config')
.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 () {
alert('加载配置失败');
});
}
function saveConfig() {
const scriptName = $('#config-script').val();
if (!scriptName) {
alert('请选择脚本');
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,
python_path: $('#python-path').val().trim()
};
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));
}
});
}
function toggleIntervalConfig(mode) {
$('#interval-config').toggle(mode === 'interval');
}
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 handleScriptChange() {
const scriptName = $('#config-script').val();
const pythonConfig = $('#python-path-config');
pythonConfig.toggle(scriptName && scriptName.endsWith('.py'));
}
function clearOutput() {
$('#output-area').empty();
}
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;
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();
});
}
// 目录浏览功能
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;
const $select = $('#config-script');
// 检查下拉框中是否已有该脚本选项,若没有则手动添加
if ($select.find(`option[value="${escapeHtml(scriptName)}"]`).length === 0) {
$select.append(`<option value="${escapeHtml(scriptName)}">${escapeHtml(scriptName)}</option>`);
}
// 设置选中值并同步相关状态
$select.val(scriptName);
loadConfig(scriptName);
selectScript(scriptName);
// 刷新脚本列表,确保所有组件同步
loadScripts();
const modal = bootstrap.Modal.getInstance(document.getElementById('directoryBrowserModal'));
modal.hide();
}
});
</script>
</body>
</html>