function formatDateTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp * 1000);
return date.toLocaleString('zh-CN');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function parseANSI(text) {
const ansiColors = {
'30': '#4a4a4a', '31': '#ff4757', '32': '#00ff88', '33': '#ffaa00',
'34': '#00d4ff', '35': '#ff6b9d', '36': '#00e5cc', '37': '#e0e6ed',
'90': '#6b7280', '91': '#ff6b6b', '92': '#51cf66', '93': '#fcc419',
'94': '#339af0', '95': '#cc5de8', '96': '#22d3ee', '97': '#f8f9fa',
'1': 'bold'
};
let result = text;
const ansiRegex = /\x1b\u005b(\d+(?:;\d+)*)m/g;
result = result.replace(ansiRegex, (match, codes) => {
const codeList = codes.split(';');
let style = '';
for (const code of codeList) {
if (ansiColors[code]) {
if (code === '1') {
style += 'font-weight:bold;';
} else {
style += `color:${ansiColors[code]};`;
}
}
}
return style ? `<span style="${style}">` : '</span>';
});
return result;
}
function parseLogKeywords(text) {
const colorRules = [
{ class: 'log-success', keywords: ['SUCCESS', '成功', '完成', '完成安装', 'installed', 'done', '✔', '✓', 'OK', 'ok', 'API', 'Model Name'] },
{ class: 'log-error', keywords: ['ERROR', 'FAILED', '失败', '错误', 'fatal', 'Fatal', '✗', '✖', 'ERR'] },
{ class: 'log-warning', keywords: ['WARNING', 'WARN', '警告', '注意', 'WARNI'] },
{ class: 'log-info', keywords: ['INFO', '===', '---', '开始', '结束', 'Starting', 'Finished', 'TASK'] },
{ class: 'log-command', keywords: ['Command:', '$ ', 'bash', 'ansible', 'EXECUTING'] },
];
const lines = text.split('\n');
return lines.map(line => {
const escapedLine = escapeHtml(line);
for (const rule of colorRules) {
for (const keyword of rule.keywords) {
if (line.toUpperCase().includes(keyword.toUpperCase())) {
return `<span class="${rule.class}">${escapedLine}</span>`;
}
}
}
return escapedLine;
}).join('\n');
}
function parseLogColors(text) {
let result = text;
result = parseANSI(result);
const lines = result.split('\n');
const colorRules = [
{ class: 'log-success', keywords: ['SUCCESS', '成功', '完成', '完成安装', 'installed', 'done', '✔', '✓', 'OK', 'ok', 'API', 'Model Name'] },
{ class: 'log-error', keywords: ['ERROR', 'FAILED', '失败', '错误', 'fatal', 'Fatal', '✗', '✖', 'ERR'] },
{ class: 'log-warning', keywords: ['WARNING', 'WARN', '警告', '注意'] },
{ class: 'log-info', keywords: ['===', '---', '开始', '结束', 'Starting', 'Finished', 'TASK', 'PLAY'] },
{ class: 'log-command', keywords: ['Command:', '$ '] },
];
return lines.map(line => {
if (line.includes('<span')) {
return line;
}
for (const rule of colorRules) {
for (const keyword of rule.keywords) {
if (line.toUpperCase().includes(keyword.toUpperCase())) {
return `<span class="${rule.class}">${line}</span>`;
}
}
}
return line;
}).join('\n');
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
padding: 1rem 1.5rem;
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
color: white;
border-radius: 0.375rem;
z-index: 1000;
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
async function apiCall(url, options = {}) {
try {
const response = await fetch(url, options);
const data = await response.json();
return data;
} catch (error) {
console.error('API调用失败:', error);
throw error;
}
}
class LogPoller {
constructor(taskId, logElement, statusElement) {
this.taskId = taskId;
this.logElement = logElement;
this.statusElement = statusElement;
this.pos = 0;
this.timer = null;
this.stopped = false;
}
start() {
this.poll();
}
stop() {
this.stopped = true;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
async poll() {
if (this.stopped) return;
try {
const data = await apiCall(`/api/logs/${this.taskId}?pos=${this.pos}`);
if (data.content) {
const coloredContent = parseLogColors(data.content);
this.logElement.innerHTML += coloredContent;
this.pos = data.pos;
this.logElement.scrollTop = this.logElement.scrollHeight;
}
if (data.status === 'running') {
this.timer = setTimeout(() => this.poll(), 500);
} else {
this.onComplete(data.status);
}
} catch (error) {
console.error('日志获取失败:', error);
this.timer = setTimeout(() => this.poll(), 1000);
}
}
onComplete(status) {
if (this.statusElement) {
this.statusElement.textContent = status === 'success' ? '执行成功 ✓' : '执行失败 ✗';
this.statusElement.className = `status-${status}`;
}
showToast(`任务执行${status === 'success' ? '成功' : '失败'}`, status);
}
}
function addServerRow(paramName) {
const container = document.getElementById(`${paramName}-container`);
const row = document.createElement('div');
row.className = 'server-row';
row.innerHTML = `
<div class="server-fields">
<div class="server-field">
<label>IP</label>
<input type="text" name="${paramName}_ip"
class="server-ip-input"
placeholder="10.10.10.1 或 10.10.10.1-10.10.10.9">
</div>
<div class="server-field server-user-field">
<label>User</label>
<span class="server-user-fixed">root</span>
</div>
<div class="server-field">
<label>Password</label>
<input type="password" name="${paramName}_pass"
class="server-pass-input"
placeholder="SSH密码">
</div>
</div>
<button type="button" class="btn-remove-server" onclick="removeServerRow(this)">删除</button>
`;
container.appendChild(row);
}
function removeServerRow(button) {
button.closest('.server-row').remove();
}
function addSoftwareItem(paramName) {
const inputNew = document.getElementById(`${paramName}-input-new`);
const tagsContainer = document.getElementById(`${paramName}-tags`);
const inputField = document.getElementById(`${paramName}-input`);
const item = inputNew.value.trim();
if (!item) return;
const currentValues = inputField.value.split(',').filter(v => v);
if (currentValues.includes(item)) {
inputNew.value = '';
return;
}
const tag = document.createElement('span');
tag.className = 'software-tag';
tag.dataset.value = item;
tag.innerHTML = `
${item}
<button type="button" class="tag-remove" onclick="removeSoftwareItem('${paramName}', '${item}')">×</button>
`;
tagsContainer.appendChild(tag);
currentValues.push(item);
inputField.value = currentValues.join(',');
inputNew.value = '';
}
function removeSoftwareItem(paramName, item) {
const tagsContainer = document.getElementById(`${paramName}-tags`);
const inputField = document.getElementById(`${paramName}-input`);
const tag = tagsContainer.querySelector(`span[data-value="${item}"]`);
if (tag) tag.remove();
const currentValues = inputField.value.split(',').filter(v => v && v !== item);
inputField.value = currentValues.join(',');
}
function toggleHelpPanel(paramName) {
const content = document.getElementById(`${paramName}-content`);
const toggle = document.getElementById(`${paramName}-toggle`);
if (content.style.display === 'none') {
content.style.display = 'block';
toggle.textContent = '收起';
} else {
content.style.display = 'none';
toggle.textContent = '展开';
}
}
function filterModelOptions() {
console.log('[filterModelOptions] 开始执行');
const dynamicSelects = document.querySelectorAll('select[data-dynamic-filter="true"]');
console.log('[filterModelOptions] 找到动态过滤select:', dynamicSelects.length);
if (dynamicSelects.length === 0) return;
dynamicSelects.forEach(select => {
console.log('[filterModelOptions] 处理select:', select.id);
const filterDepends = JSON.parse(select.dataset.filterDepends || '[]');
const filterRules = JSON.parse(select.dataset.filterRules || '{}');
console.log('[filterModelOptions] filterDepends:', filterDepends);
console.log('[filterModelOptions] filterRules keys:', Object.keys(filterRules));
let serverType = 'a2';
let deployMode = 'single';
filterDepends.forEach(dep => {
const depElement = document.getElementById(dep);
console.log('[filterModelOptions] 检查依赖:', dep, '元素:', depElement ? '找到' : '未找到');
if (depElement) {
const value = depElement.value || depElement.dataset.default || '';
console.log('[filterModelOptions] 依赖值:', dep, '=', value, '(default:', depElement.dataset.default + ')');
if (dep === 'server-type') {
serverType = value || 'a2';
} else if (dep === 'dual-node') {
deployMode = (value === 'true') ? 'multi' : 'single';
}
}
});
const filterKey = `${serverType}_${deployMode}`;
console.log('[filterModelOptions] 最终过滤key:', filterKey);
const allowedModels = filterRules[filterKey] || [];
console.log('[filterModelOptions] 允许的模型:', allowedModels);
const options = select.querySelectorAll('option[data-option]');
console.log('[filterModelOptions] 选项数量:', options.length);
options.forEach(option => {
const modelValue = option.dataset.option;
const isAllowed = allowedModels.includes(modelValue);
if (isAllowed) {
option.disabled = false;
option.style.display = '';
console.log('[filterModelOptions] ✓ 启用:', modelValue);
} else {
option.disabled = true;
option.style.display = 'none';
console.log('[filterModelOptions] ✗ 禁用:', modelValue);
}
});
const currentValue = select.value;
if (currentValue && !allowedModels.includes(currentValue)) {
console.log('[filterModelOptions] 当前值不允许,清空选择:', currentValue);
select.value = '';
}
console.log('[filterModelOptions] 完成');
});
}
document.addEventListener('DOMContentLoaded', function() {
const runningTasks = document.querySelectorAll('.status-running');
if (runningTasks.length > 0) {
setInterval(() => {
location.reload();
}, 30000);
}
const serverTypeSelect = document.getElementById('server-type');
const dualNodeSelect = document.getElementById('dual-node');
if (serverTypeSelect) {
serverTypeSelect.addEventListener('change', filterModelOptions);
}
if (dualNodeSelect) {
dualNodeSelect.addEventListener('change', filterModelOptions);
}
filterModelOptions();
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
const requiredInputs = form.querySelectorAll('[required]');
let valid = true;
requiredInputs.forEach(input => {
if (!input.value.trim()) {
valid = false;
input.style.borderColor = '#ef4444';
showToast(`${input.previousElementSibling?.textContent || input.name} 是必填项`, 'error');
} else {
input.style.borderColor = '';
}
});
if (!valid) {
e.preventDefault();
}
});
});
});
if (typeof Vue !== 'undefined') {
Vue.filter('datetime', formatDateTime);
}