833 lines
29 KiB
HTML
833 lines
29 KiB
HTML
<!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("${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); // 转义仅用于显示
|
||
// 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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
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> |