Files
bat_manage/templates/index.html
T

813 lines
28 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;
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();
});
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() !== ''
);
// 批量获取所有脚本的状态和模式
$.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));
});
}).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> 暂无配置的脚本`;
container.append(`
<div class="text-center text-muted py-3">
${tip}
</div>
`);
return;
}
// 使用文档片段减少DOM重绘
const fragment = document.createDocumentFragment();
filteredScripts.forEach(scriptObj => {
const scriptName = scriptObj.name.trim();
if (!scriptName) return;
const modifyTimeStr = scriptObj.modify_time_str || '未知时间';
// 从缓存的状态映射表中获取状态和模式
const statusInfo = scriptStatusMap[scriptName] || {
status: 'stopped',
running: false,
scheduled: false,
mode: 'single-run',
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;
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>
</div>
<button class="btn btn-sm ${btnClass}"
onclick="event.stopPropagation(); ${btnClick}">
<i class="bi ${btnIcon}"></i> ${btnText}
</button>
</div>
</div>
`)[0];
fragment.appendChild(scriptItem);
});
// 一次性将所有元素添加到容器
container.append(fragment);
}
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="${scriptName}">${escapedName}</option>`);
}
});
if (currentScript && 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);
loadScripts(); // 重新加载列表和状态
})
.fail(function (xhr) {
alert('启动失败:' + (xhr.responseJSON?.error || xhr.statusText));
});
}
function stopScript(scriptName) {
$.get(`/api/stop/${encodeURIComponent(scriptName)}`)
.done(function (data) {
alert(data.msg);
loadScripts(); // 重新加载列表和状态
})
.fail(function (xhr) {
alert('停止失败:' + (xhr.responseJSON?.error || xhr.statusText));
});
}
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);
loadScripts(); // 保存后刷新列表
},
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;
$('#config-script').val(scriptName);
loadConfig(scriptName);
selectScript(scriptName);
const modal = bootstrap.Modal.getInstance(document.getElementById('directoryBrowserModal'));
modal.hide();
}
});
</script>
</body>
</html>