{% extends "base.html" %}
{% block title %}{{ step.title }} - 执行{% endblock %}
{% block content %}
<div class="page-header">
<h1>{{ profile }} - {{ step.title }}</h1>
<div class="header-actions">
<a href="{{ url_for('profile_page', name=profile) }}" class="btn-back">返回流程</a>
<a href="{{ url_for('index') }}" class="btn-back">返回首页</a>
</div>
</div>
<div class="step-layout">
<div class="step-sidebar">
<h3>部署步骤</h3>
<div class="step-nav">
{% for s in workflow %}
<div class="step-nav-item {% if loop.index == current_idx + 1 %}active{% endif %}"
data-step="{{ s.name }}">
<div class="step-nav-num">{{ loop.index }}</div>
<div class="step-nav-info">
<span class="step-nav-title">{{ s.title }}</span>
{% if s.standalone %}
<small class="step-nav-tag">可单独执行</small>
{% endif %}
</div>
{% if loop.index == current_idx + 1 and s.standalone %}
<div class="step-nav-skip">
{% if workflow|length > loop.index %}
<button class="btn-skip" data-next-step="{{ workflow[loop.index].name }}">
跳过
</button>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<div class="step-content">
<div class="step-form-section" id="form-section">
<h2>{{ step.title }}</h2>
<p class="step-description">{{ step.description }}</p>
{% if step.needs_reboot %}
<div class="warning-box">
<strong>注意:</strong> 此步骤执行后需要重启服务器
</div>
{% endif %}
<form id="step-form" class="param-form">
<input type="hidden" name="profile" value="{{ profile }}">
<input type="hidden" name="step" value="{{ step.name }}">
{% if step.params %}
<div class="form-params">
{% for param in step.params %}
{% if param.type == 'help_panel' %}
<div class="form-group">
<div class="help-panel" id="{{ param.name }}-panel">
<div class="help-panel-header" onclick="toggleHelpPanel('{{ param.name }}')">
<span class="help-panel-title">{{ param.title }}</span>
<span class="help-panel-toggle" id="{{ param.name }}-toggle">展开</span>
</div>
<div class="help-panel-content" id="{{ param.name }}-content" style="display: none;">
{% if param.content %}
{% for key, info in param.content.items() %}
<div class="help-section">
<div class="help-section-title">{{ info.title }}</div>
<div class="help-section-models">
{% if info.models %}
{% for model in info.models %}
<span class="help-model-tag">{{ model }}</span>
{% endfor %}
{% else %}
<span class="help-model-empty">(暂无支持模型)</span>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="form-group">
<label for="{{ param.name }}">
{{ param.label }}
{% if param.required %}
<span class="required">*</span>
{% endif %}
</label>
{% if param.description %}
<small class="param-hint">{{ param.description }}</small>
{% endif %}
{% if param.type == 'select' %}
<select name="{{ param.name }}" id="{{ param.name }}"
{% if param.required %}required{% endif %}
{% if param.dynamic_filter %}data-dynamic-filter="true"{% endif %}
{% if param.filter_depends %}data-filter-depends='{{ param.filter_depends | tojson }}'{% endif %}
{% if param.filter_rules %}data-filter-rules='{{ param.filter_rules | tojson }}'{% endif %}
{% if param.default %}data-default="{{ param.default }}"{% else %}data-default=""{% endif %}>
<option value="">-- 请选择 --</option>
{% for option in param.options %}
<option value="{{ option }}"
{% if option == param.default %}selected{% endif %}
data-option="{{ option }}">
{{ option }}
</option>
{% endfor %}
</select>
{% elif param.type == 'servers' %}
<div class="servers-config">
<div class="servers-toolbar">
<button type="button" class="btn-add-server" onclick="addServerRow('{{ param.name }}')">
+ 添加服务器
</button>
<span class="servers-hint">
添加远程部署服务器,不添加则默认本地部署
</span>
</div>
<div id="{{ param.name }}-container" class="servers-container">
</div>
</div>
{% elif param.type == 'software_list' %}
<div class="software-list-config">
<div class="software-selected">
<label>已选择软件:</label>
<div class="software-tags" id="{{ param.name }}-tags">
{% if param.default %}
{% set default_items = param.default.split(',') %}
{% for item in default_items %}
{% if item %}
<span class="software-tag" data-value="{{ item }}">
{{ item }}
<button type="button" class="tag-remove" onclick="removeSoftwareItem('{{ param.name }}', '{{ item }}')">×</button>
</span>
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
<div class="software-add">
<label>添加软件:</label>
<div class="software-add-row">
<input type="text"
id="{{ param.name }}-input-new"
class="software-input"
placeholder="输入软件名称">
<button type="button" class="btn-add-software" onclick="addSoftwareItem('{{ param.name }}')">添加</button>
</div>
</div>
<input type="hidden" name="{{ param.name }}" id="{{ param.name }}-input" value="{{ param.default }}">
</div>
{% elif param.type == 'number' %}
<input type="number" name="{{ param.name }}" id="{{ param.name }}"
value="{{ param.default }}"
{% if param.min %}min="{{ param.min }}"{% endif %}
{% if param.max %}max="{{ param.max }}"{% endif %}
{% if param.required %}required{% endif %}
{% if param.placeholder %}placeholder="{{ param.placeholder }}"{% endif %}>
{% elif param.type == 'password' %}
<input type="password" name="{{ param.name }}" id="{{ param.name }}"
{% if param.required %}required{% endif %}
{% if param.placeholder %}placeholder="{{ param.placeholder }}"{% endif %}>
{% else %}
<input type="text" name="{{ param.name }}" id="{{ param.name }}"
value="{{ param.default }}"
{% if param.required %}required{% endif %}
{% if param.placeholder %}placeholder="{{ param.placeholder }}"{% endif %}>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
<div class="form-actions">
<button type="submit" class="btn-primary btn-large" id="execute-btn">
执行此步骤
</button>
{% if workflow|length > current_idx + 1 %}
<button type="button" class="btn-secondary" id="skip-next-btn">
跳过并进入下一步
</button>
{% endif %}
</div>
{% else %}
<div class="no-params-message">
<p>此步骤无需参数配置</p>
<div class="form-actions">
<button type="button" class="btn-primary btn-large" id="execute-no-params-btn">
直接执行
</button>
{% if workflow|length > current_idx + 1 %}
<button type="button" class="btn-secondary" id="skip-next-btn-2">
跳过并进入下一步
</button>
{% endif %}
</div>
</div>
{% endif %}
</form>
</div>
<div class="step-log-section" id="log-section" style="display: none;">
<h3>执行日志</h3>
<div class="log-container">
<pre id="log-output"></pre>
</div>
<div class="log-status">
<span id="log-status-text">执行中...</span>
<span id="log-time"></span>
<button id="kill-btn" class="btn-danger" onclick="killTask()" style="display: none;">
中止执行
</button>
</div>
<div class="log-actions" style="display: none;">
{% if workflow|length > current_idx + 1 %}
<button class="btn-primary" onclick="goToNextStep()">
继续执行下一步
</button>
{% endif %}
<button class="btn-secondary" onclick="showForm()">
修改参数重新执行
</button>
<a href="{{ url_for('profile_page', name=profile) }}" class="btn-secondary">
查看完整流程
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const profile = "{{ profile }}";
const stepName = "{{ step.name }}";
const currentIdx = {{ current_idx }};
const workflow = {{ workflow | tojson }};
let currentTaskId = null;
let logPos = 0;
let logTimer = null;
function goToNextStep() {
if (workflow.length > currentIdx + 1) {
const nextStep = workflow[currentIdx + 1];
location.href = `/step/${profile}/${nextStep.name}`;
}
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.step-nav-item').forEach(item => {
item.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-skip')) return;
const stepName = this.dataset.step;
location.href = `/step/${profile}/${stepName}`;
});
});
document.querySelectorAll('.btn-skip').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const nextStep = this.dataset.nextStep;
if (nextStep) {
location.href = `/step/${profile}/${nextStep}`;
}
});
});
const skipNextBtn = document.getElementById('skip-next-btn');
if (skipNextBtn) {
skipNextBtn.addEventListener('click', goToNextStep);
}
const skipNextBtn2 = document.getElementById('skip-next-btn-2');
if (skipNextBtn2) {
skipNextBtn2.addEventListener('click', goToNextStep);
}
const executeNoParamsBtn = document.getElementById('execute-no-params-btn');
if (executeNoParamsBtn) {
executeNoParamsBtn.addEventListener('click', executeStep);
}
const stepForm = document.getElementById('step-form');
if (stepForm) {
stepForm.addEventListener('submit', function(e) {
e.preventDefault();
executeStep();
});
}
});
function executeStep() {
const form = document.getElementById('step-form');
const formData = new FormData(form);
const params = {};
formData.forEach((value, key) => {
if (key !== 'profile' && key !== 'step') {
params[key] = value;
}
});
const serversContainers = document.querySelectorAll('.servers-container');
serversContainers.forEach(container => {
const paramName = container.id.replace('-container', '');
const rows = container.querySelectorAll('.server-row');
const servers = [];
rows.forEach(row => {
const ipInput = row.querySelector('.server-ip-input');
const passInput = row.querySelector('.server-pass-input');
if (ipInput && ipInput.value.trim()) {
servers.push({
ip: ipInput.value.trim(),
pass: passInput ? passInput.value.trim() : ''
});
}
});
if (servers.length > 0) {
params[paramName] = servers;
}
});
const executeBtn = document.getElementById('execute-btn');
executeBtn.disabled = true;
executeBtn.textContent = '执行中...';
fetch('/api/execute', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
profile: profile,
step: stepName,
params: params
})
})
.then(r => r.json())
.then(data => {
if (data.error) {
alert('执行失败: ' + data.error);
executeBtn.disabled = false;
executeBtn.textContent = '执行此步骤';
} else {
currentTaskId = data.task_id;
showLogSection();
startLogPolling();
}
})
.catch(e => {
alert('请求失败: ' + e);
executeBtn.disabled = false;
executeBtn.textContent = '执行此步骤';
});
}
function showLogSection() {
document.getElementById('log-section').style.display = 'block';
document.getElementById('form-section').style.display = 'none';
document.getElementById('log-output').textContent = '';
document.getElementById('log-status-text').textContent = '执行中...';
document.getElementById('kill-btn').style.display = 'inline-block';
}
function showForm() {
document.getElementById('log-section').style.display = 'none';
document.getElementById('form-section').style.display = 'block';
document.getElementById('kill-btn').style.display = 'none';
}
function startLogPolling() {
logPos = 0;
pollLogs();
}
function killTask() {
if (!currentTaskId) return;
if (!confirm('确定要中止当前任务吗?')) return;
const killBtn = document.getElementById('kill-btn');
killBtn.disabled = true;
killBtn.textContent = '中止中...';
fetch(`/api/kill/${currentTaskId}`, {method: 'POST'})
.then(r => r.json())
.then(data => {
if (data.success) {
document.getElementById('log-status-text').textContent = '已中止 ✗';
document.getElementById('log-status-text').className = 'status-killed';
killBtn.style.display = 'none';
clearTimeout(logTimer);
const executeBtn = document.getElementById('execute-btn');
executeBtn.disabled = false;
executeBtn.textContent = '重新执行';
} else {
alert('中止失败: ' + data.error);
killBtn.disabled = false;
killBtn.textContent = '中止执行';
}
})
.catch(e => {
alert('请求失败: ' + e);
killBtn.disabled = false;
killBtn.textContent = '中止执行';
});
}
function pollLogs() {
if (!currentTaskId) return;
fetch(`/api/logs/${currentTaskId}?pos=${logPos}`)
.then(r => r.json())
.then(data => {
if (data.content) {
const logOutput = document.getElementById('log-output');
const coloredContent = parseLogColors(data.content);
logOutput.insertAdjacentHTML('beforeend', coloredContent);
logPos = data.pos;
logOutput.scrollTop = logOutput.scrollHeight;
}
if (data.status === 'running') {
document.getElementById('kill-btn').style.display = 'inline-block';
logTimer = setTimeout(pollLogs, 500);
} else {
document.getElementById('kill-btn').style.display = 'none';
finishExecution(data.status);
}
})
.catch(e => {
console.error('日志获取失败:', e);
logTimer = setTimeout(pollLogs, 1000);
});
}
function finishExecution(status) {
const statusText = document.getElementById('log-status-text');
const executeBtn = document.getElementById('execute-btn');
const logActions = document.querySelector('.log-actions');
const killBtn = document.getElementById('kill-btn');
killBtn.style.display = 'none';
if (status === 'success') {
statusText.textContent = '执行成功 ✓';
statusText.className = 'status-success';
executeBtn.textContent = '重新执行';
logActions.style.display = 'block';
} else if (status === 'failed') {
statusText.textContent = '执行失败 ✗';
statusText.className = 'status-failed';
executeBtn.textContent = '重新执行';
logActions.style.display = 'block';
} else {
statusText.textContent = '执行出错';
statusText.className = 'status-error';
executeBtn.textContent = '重新执行';
logActions.style.display = 'block';
}
executeBtn.disabled = false;
}
</script>
{% endblock %}