залил
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
{{define "content"}}
|
||||
<h2>Команды чата</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>Добавить / редактировать команду</h3>
|
||||
<form id="command-form">
|
||||
<input type="hidden" id="command-id" value="0">
|
||||
<label>Триггер (без !):</label>
|
||||
<input type="text" id="trigger" required placeholder="ping">
|
||||
|
||||
<label>Шаблон ответа:</label>
|
||||
<textarea id="template" rows="5" required placeholder="Pong! <USERNAME/>"></textarea>
|
||||
|
||||
<!-- Панель кнопок для вставки тегов -->
|
||||
<div style="margin: 8px 0; display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<button type="button" class="tag-btn" data-tag="<USERNAME/>">👤 USERNAME</button>
|
||||
<button type="button" class="tag-btn" data-tag="<ARG/>">📝 ARG</button>
|
||||
<button type="button" class="tag-btn" data-tag="<AI/>">🤖 AI</button>
|
||||
<button type="button" class="tag-btn" data-tag="<RANDOMUSER/>">🎲 RANDOMUSER</button>
|
||||
<button type="button" id="random-btn">🔢 Случайное число</button>
|
||||
<button type="button" id="song-btn">🎵 Вставить звук</button>
|
||||
<button type="button" id="group-btn">📦 Обертка group</button>
|
||||
<button type="button" id="timeout-btn">⏱ Отстранение</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" id="test-btn">🧪 Тестировать</button>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" id="enabled" checked> Включена
|
||||
</label>
|
||||
<label>Кулдаун (секунды):</label>
|
||||
<input type="number" id="cooldown" value="0" min="0">
|
||||
<label>Права доступа:</label>
|
||||
<select id="permission">
|
||||
<option value="everyone">Все</option>
|
||||
<option value="moderator">Модераторы</option>
|
||||
<option value="subscriber">Подписчики</option>
|
||||
<option value="vip">VIP</option>
|
||||
<option value="broadcaster">Стример</option>
|
||||
</select>
|
||||
<button type="submit">Сохранить</button>
|
||||
<button type="button" id="cancel-edit" style="background: #6c757d;">Отмена</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Список команд</h3>
|
||||
<table style="width:100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr><th>Триггер</th><th>Шаблон</th><th>Кулдаун</th><th>Права</th><th>Статус</th><th>Действия</th></tr>
|
||||
</thead>
|
||||
<tbody id="commands-table">
|
||||
<tr><td colspan="6">Загрузка...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для тестирования -->
|
||||
<div id="test-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Результат тестирования</h3>
|
||||
<p><strong>Текст для чата:</strong></p>
|
||||
<pre id="test-result" style="background:#f0f0f0; padding:10px; border-radius:4px; white-space:pre-wrap;"></pre>
|
||||
<p><strong>Звуковые файлы:</strong> <span id="test-sounds"></span></p>
|
||||
<button id="close-modal" style="margin-top:10px;">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для случайного числа -->
|
||||
<div id="random-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Вставить случайное число</h3>
|
||||
<label>Минимум:</label>
|
||||
<input type="number" id="random-min" value="1">
|
||||
<label>Максимум:</label>
|
||||
<input type="number" id="random-max" value="100">
|
||||
<div style="margin-top:15px;">
|
||||
<button id="insert-random-btn">Вставить</button>
|
||||
<button id="cancel-random-btn">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для звука -->
|
||||
<div id="song-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Вставить звук</h3>
|
||||
<label>Выберите звуковой файл:</label>
|
||||
<select id="song-select" style="width:100%;">
|
||||
<option value="">-- нет звуков --</option>
|
||||
</select>
|
||||
<div style="margin-top:15px;">
|
||||
<button id="insert-song-btn">Вставить</button>
|
||||
<button id="cancel-song-btn">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для группы -->
|
||||
<div id="group-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Создать группу вариантов</h3>
|
||||
<div id="group-variants">
|
||||
<div class="group-variant" style="margin-bottom:8px;">
|
||||
<input type="text" class="variant-input" placeholder="Вариант 1" style="width:80%;">
|
||||
</div>
|
||||
<div class="group-variant" style="margin-bottom:8px;">
|
||||
<input type="text" class="variant-input" placeholder="Вариант 2" style="width:80%;">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="add-variant-btn">+ Добавить вариант</button>
|
||||
<button type="button" id="remove-variant-btn" style="margin-left:10px;">− Удалить последний</button>
|
||||
<div style="margin-top:15px;">
|
||||
<button id="insert-group-btn">Вставить</button>
|
||||
<button id="cancel-group-btn">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для отстранения (timeout) -->
|
||||
<div id="timeout-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Отстранение (таймаут)</h3>
|
||||
<label>Количество минут:</label>
|
||||
<input type="number" id="timeout-minutes" value="5" min="1" max="1440">
|
||||
<div style="margin-top:15px;">
|
||||
<button id="insert-timeout-btn">Вставить</button>
|
||||
<button id="cancel-timeout-btn">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let commands = [];
|
||||
let soundList = [];
|
||||
|
||||
async function loadSoundList() {
|
||||
try {
|
||||
const res = await fetch('/api/sounds/list');
|
||||
if (res.ok) {
|
||||
soundList = await res.json();
|
||||
const select = document.getElementById('song-select');
|
||||
select.innerHTML = '<option value="">-- выберите звук --</option>';
|
||||
soundList.forEach(sound => {
|
||||
const option = document.createElement('option');
|
||||
option.value = sound;
|
||||
option.textContent = sound;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch(e) { console.error('Failed to load sounds', e); }
|
||||
}
|
||||
|
||||
function insertAtCursor(textareaId, text) {
|
||||
const textarea = document.getElementById(textareaId);
|
||||
if (!textarea) return;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const value = textarea.value;
|
||||
textarea.value = value.slice(0, start) + text + value.slice(end);
|
||||
textarea.selectionStart = textarea.selectionEnd = start + text.length;
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
// Простые теги
|
||||
document.querySelectorAll('.tag-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
insertAtCursor('template', btn.dataset.tag);
|
||||
});
|
||||
});
|
||||
|
||||
// Random
|
||||
document.getElementById('random-btn').addEventListener('click', () => {
|
||||
document.getElementById('random-modal').style.display = 'flex';
|
||||
});
|
||||
document.getElementById('insert-random-btn').addEventListener('click', () => {
|
||||
const min = document.getElementById('random-min').value;
|
||||
const max = document.getElementById('random-max').value;
|
||||
const tag = `<random s=${min} e=${max}/>`;
|
||||
insertAtCursor('template', tag);
|
||||
document.getElementById('random-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('cancel-random-btn').addEventListener('click', () => {
|
||||
document.getElementById('random-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
// Song
|
||||
document.getElementById('song-btn').addEventListener('click', async () => {
|
||||
await loadSoundList();
|
||||
document.getElementById('song-modal').style.display = 'flex';
|
||||
});
|
||||
document.getElementById('insert-song-btn').addEventListener('click', () => {
|
||||
const selected = document.getElementById('song-select').value;
|
||||
if (!selected) {
|
||||
alert('Выберите звуковой файл');
|
||||
return;
|
||||
}
|
||||
const tag = `<song f="data/sounds/${selected}"/>`;
|
||||
insertAtCursor('template', tag);
|
||||
document.getElementById('song-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('cancel-song-btn').addEventListener('click', () => {
|
||||
document.getElementById('song-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
// Group
|
||||
document.getElementById('group-btn').addEventListener('click', () => {
|
||||
const container = document.getElementById('group-variants');
|
||||
container.innerHTML = `
|
||||
<div class="group-variant" style="margin-bottom:8px;">
|
||||
<input type="text" class="variant-input" placeholder="Вариант 1" style="width:80%;">
|
||||
</div>
|
||||
<div class="group-variant" style="margin-bottom:8px;">
|
||||
<input type="text" class="variant-input" placeholder="Вариант 2" style="width:80%;">
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('group-modal').style.display = 'flex';
|
||||
});
|
||||
document.getElementById('add-variant-btn').addEventListener('click', () => {
|
||||
const container = document.getElementById('group-variants');
|
||||
const newDiv = document.createElement('div');
|
||||
newDiv.className = 'group-variant';
|
||||
newDiv.style.marginBottom = '8px';
|
||||
newDiv.innerHTML = `<input type="text" class="variant-input" placeholder="Новый вариант" style="width:80%;">`;
|
||||
container.appendChild(newDiv);
|
||||
});
|
||||
document.getElementById('remove-variant-btn').addEventListener('click', () => {
|
||||
const container = document.getElementById('group-variants');
|
||||
if (container.children.length > 2) {
|
||||
container.removeChild(container.lastChild);
|
||||
} else {
|
||||
alert('Должно быть хотя бы два варианта');
|
||||
}
|
||||
});
|
||||
document.getElementById('insert-group-btn').addEventListener('click', () => {
|
||||
const inputs = document.querySelectorAll('#group-variants .variant-input');
|
||||
let variants = [];
|
||||
inputs.forEach(inp => {
|
||||
let val = inp.value.trim();
|
||||
if (val !== '') variants.push(val);
|
||||
});
|
||||
if (variants.length < 2) {
|
||||
alert('Введите хотя бы два непустых варианта');
|
||||
return;
|
||||
}
|
||||
let groupHtml = '<group>\n';
|
||||
variants.forEach(v => {
|
||||
groupHtml += ` <g>${escapeHtmlForGroup(v)}</g>\n`;
|
||||
});
|
||||
groupHtml += '</group>';
|
||||
insertAtCursor('template', groupHtml);
|
||||
document.getElementById('group-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('cancel-group-btn').addEventListener('click', () => {
|
||||
document.getElementById('group-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
// Timeout
|
||||
document.getElementById('timeout-btn').addEventListener('click', () => {
|
||||
document.getElementById('timeout-modal').style.display = 'flex';
|
||||
});
|
||||
document.getElementById('insert-timeout-btn').addEventListener('click', () => {
|
||||
const minutes = document.getElementById('timeout-minutes').value;
|
||||
const tag = `<timeout minutes="${minutes}"/>`;
|
||||
insertAtCursor('template', tag);
|
||||
document.getElementById('timeout-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('cancel-timeout-btn').addEventListener('click', () => {
|
||||
document.getElementById('timeout-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
function escapeHtmlForGroup(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
// Закрытие модалок по клику вне области
|
||||
window.addEventListener('click', (e) => {
|
||||
const modals = ['random-modal', 'song-modal', 'group-modal', 'test-modal', 'timeout-modal'];
|
||||
modals.forEach(id => {
|
||||
const modal = document.getElementById(id);
|
||||
if (e.target === modal) modal.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// --- Остальной код команд (без изменений) ---
|
||||
async function loadCommands() {
|
||||
const res = await fetch('/api/commands');
|
||||
commands = await res.json();
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('commands-table');
|
||||
if (!commands.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">Нет команд. Добавьте первую!</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = commands.map(cmd => `
|
||||
<tr>
|
||||
<td>!${escapeHtml(cmd.Trigger)}</td>
|
||||
<td style="max-width: 300px; overflow-x: auto;">${escapeHtml(cmd.Template)}</td>
|
||||
<td>${cmd.CooldownSec}</td>
|
||||
<td>${cmd.Permission}</td>
|
||||
<td>${cmd.Enabled ? '✅ Вкл' : '❌ Выкл'}</td>
|
||||
<td>
|
||||
<button onclick="editCommand(${cmd.ID})">✏️</button>
|
||||
<button onclick="deleteCommand(${cmd.ID})">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
function editCommand(id) {
|
||||
const cmd = commands.find(c => c.ID === id);
|
||||
if (!cmd) return;
|
||||
document.getElementById('command-id').value = cmd.ID;
|
||||
document.getElementById('trigger').value = cmd.Trigger;
|
||||
document.getElementById('template').value = cmd.Template;
|
||||
document.getElementById('enabled').checked = cmd.Enabled;
|
||||
document.getElementById('cooldown').value = cmd.CooldownSec;
|
||||
document.getElementById('permission').value = cmd.Permission;
|
||||
document.querySelector('.card').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function deleteCommand(id) {
|
||||
if (!confirm('Удалить команду?')) return;
|
||||
await fetch(`/api/commands?id=${id}`, { method: 'DELETE' });
|
||||
loadCommands();
|
||||
if (document.getElementById('command-id').value == id) resetForm();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById('command-id').value = 0;
|
||||
document.getElementById('trigger').value = '';
|
||||
document.getElementById('template').value = '';
|
||||
document.getElementById('enabled').checked = true;
|
||||
document.getElementById('cooldown').value = 0;
|
||||
document.getElementById('permission').value = 'everyone';
|
||||
}
|
||||
|
||||
document.getElementById('cancel-edit').addEventListener('click', resetForm);
|
||||
|
||||
document.getElementById('command-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = parseInt(document.getElementById('command-id').value);
|
||||
const trigger = document.getElementById('trigger').value.trim();
|
||||
const template = document.getElementById('template').value;
|
||||
const enabled = document.getElementById('enabled').checked;
|
||||
const cooldown = parseInt(document.getElementById('cooldown').value);
|
||||
const permission = document.getElementById('permission').value;
|
||||
|
||||
if (!trigger || !template) {
|
||||
alert('Заполните триггер и шаблон');
|
||||
return;
|
||||
}
|
||||
|
||||
const method = id === 0 ? 'POST' : 'PUT';
|
||||
const url = '/api/commands';
|
||||
const body = JSON.stringify({ ID: id, Trigger: trigger, Template: template, Enabled: enabled, CooldownSec: cooldown, Permission: permission });
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body });
|
||||
if (res.ok) {
|
||||
loadCommands();
|
||||
resetForm();
|
||||
} else {
|
||||
const err = await res.text();
|
||||
alert('Ошибка: ' + err);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Ошибка соединения: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('test-btn').addEventListener('click', async () => {
|
||||
const template = document.getElementById('template').value;
|
||||
if (!template) {
|
||||
alert('Введите шаблон для тестирования');
|
||||
return;
|
||||
}
|
||||
const username = prompt('Введите имя пользователя для подстановки (без @):', 'TestUser');
|
||||
if (!username) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/commands/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template, username })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
alert('Ошибка тестирования: ' + err);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
document.getElementById('test-result').textContent = data.result;
|
||||
const soundsSpan = document.getElementById('test-sounds');
|
||||
if (data.soundFiles && data.soundFiles.length) {
|
||||
soundsSpan.innerHTML = data.soundFiles.join('<br>');
|
||||
} else {
|
||||
soundsSpan.textContent = 'нет';
|
||||
}
|
||||
document.getElementById('test-modal').style.display = 'flex';
|
||||
} catch (err) {
|
||||
alert('Ошибка: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('close-modal').addEventListener('click', () => {
|
||||
document.getElementById('test-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
loadCommands();
|
||||
loadSoundList();
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user