Добавить редактор конфигурационных файлов XLX

Этот коммит содержится в:
2026-05-18 06:50:36 +09:00
родитель 94ea0bbda1
Коммит c2f62feb09
8 изменённых файлов: 383 добавлений и 2 удалений
+108
Просмотреть файл
@@ -5,6 +5,15 @@ const adminResult = document.querySelector('#adminResult');
const loadButton = document.querySelector('#loadSettings');
const saveButton = document.querySelector('#saveSettings');
const applyButton = document.querySelector('#applySettings');
const loadConfigFilesButton = document.querySelector('#loadConfigFiles');
const saveConfigFileButton = document.querySelector('#saveConfigFile');
const reloadConfigFileButton = document.querySelector('#reloadConfigFile');
const fileTabs = document.querySelector('#fileTabs');
const fileMeta = document.querySelector('#fileMeta');
const fileEditor = document.querySelector('#fileEditor');
const fileResult = document.querySelector('#fileResult');
let configFiles = [];
let selectedFileKey = null;
function token() {
return tokenInput.value.trim();
@@ -67,6 +76,105 @@ loadButton?.addEventListener('click', loadSettings);
saveButton?.addEventListener('click', () => saveSettings(false));
applyButton?.addEventListener('click', () => saveSettings(true));
function selectedFile() {
return configFiles.find((file) => file.key === selectedFileKey) || null;
}
function setFileResult(text) {
fileResult.textContent = text;
}
function renderFileTabs() {
fileTabs.innerHTML = '';
for (const file of configFiles) {
const button = document.createElement('button');
button.type = 'button';
button.className = `file-tab${file.key === selectedFileKey ? ' active' : ''}`;
button.textContent = file.name;
button.addEventListener('click', () => selectFile(file.key));
fileTabs.append(button);
}
}
function renderFileMeta(file) {
if (!file) {
fileMeta.innerHTML = '<span>Файл не выбран</span>';
return;
}
const writable = file.writable ? 'доступен для записи' : 'нет прав на запись';
const exists = file.exists ? 'существует' : 'будет создан при сохранении';
fileMeta.innerHTML = `
<span>${file.path}</span>
<span>${exists}</span>
<span>${writable}</span>
<span>${file.updated_at || 'без даты'}</span>
`;
}
function selectFile(key) {
selectedFileKey = key;
const file = selectedFile();
renderFileTabs();
renderFileMeta(file);
fileEditor.value = file?.content || '';
setFileResult(file ? file.description : '');
}
async function loadConfigFiles() {
setFileResult('Загружаем файлы XLX...');
const response = await fetch('/api/admin/xlxd/config-files', {
headers: { 'X-Admin-Token': token() },
});
const payload = await response.json();
if (!payload.ok) {
setFileResult(payload.error || 'Ошибка загрузки файлов');
return;
}
configFiles = payload.data;
selectedFileKey = selectedFileKey || configFiles[0]?.key || null;
renderFileTabs();
selectFile(selectedFileKey);
}
async function saveConfigFile() {
const file = selectedFile();
if (!file) {
setFileResult('Сначала выбери файл.');
return;
}
setFileResult(`Сохраняем ${file.filename}...`);
const response = await fetch('/api/admin/xlxd/config-files', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Admin-Token': token(),
},
body: JSON.stringify({ file: file.key, content: fileEditor.value }),
});
const payload = await response.json();
if (!payload.ok) {
setFileResult(payload.error || 'Ошибка сохранения файла');
return;
}
configFiles = configFiles.map((item) => item.key === payload.data.key ? payload.data : item);
selectFile(payload.data.key);
setFileResult(`${payload.data.filename} сохранен.`);
}
loadConfigFilesButton?.addEventListener('click', loadConfigFiles);
saveConfigFileButton?.addEventListener('click', saveConfigFile);
reloadConfigFileButton?.addEventListener('click', () => {
const file = selectedFile();
if (file) {
fileEditor.value = file.content || '';
setFileResult('Изменения в редакторе отменены.');
}
});
form?.addEventListener('input', () => {
const data = formData();
const lines = {
+84 -2
Просмотреть файл
@@ -207,7 +207,22 @@ input {
font-size: 17px;
}
input:focus {
textarea {
width: 100%;
min-height: 420px;
resize: vertical;
border: 1px solid var(--line);
border-radius: 8px;
padding: 16px;
background: #06070b;
color: var(--text);
font-family: Consolas, "Courier New", monospace;
font-size: 15px;
line-height: 1.55;
}
input:focus,
textarea:focus {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@@ -230,14 +245,81 @@ input:focus {
min-height: 420px;
}
.muted-text {
max-width: 760px;
margin: 12px 0 0;
color: var(--muted);
font-size: 17px;
line-height: 1.45;
}
.config-editor {
display: grid;
gap: 18px;
}
.config-editor-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
}
.file-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.file-tab {
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px 16px;
background: transparent;
color: var(--text);
font: inherit;
font-weight: 800;
cursor: pointer;
}
.file-tab.active {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.file-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
color: var(--muted);
font-size: 14px;
}
.file-meta span {
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 12px;
background: var(--panel-2);
}
.editor-label {
color: var(--text);
font-size: 16px;
}
@media (max-width: 880px) {
.hero,
.grid,
.panel.split,
.settings-form {
.settings-form,
.config-editor-head {
grid-template-columns: 1fr;
}
.config-editor-head {
display: grid;
}
h1 {
font-size: 44px;
}
+13
Просмотреть файл
@@ -7,6 +7,7 @@ use Xlx\Domain\AccessExportService;
use Xlx\Domain\IdAllocator;
use Xlx\Domain\PaymentService;
use Xlx\Domain\RegistrationService;
use Xlx\Domain\XlxdConfigFileService;
use Xlx\Domain\XlxdSettingsService;
use Xlx\Domain\YooKassaClient;
use Xlx\Support\Database;
@@ -117,6 +118,18 @@ try {
Response::json(['ok' => true, 'data' => $service->save($pdo, $input['settings'] ?? [], (bool) ($input['apply'] ?? false))]);
}
if ($method === 'GET' && $path === '/api/admin/xlxd/config-files') {
Security::requireAdminToken($config);
Response::json(['ok' => true, 'data' => (new XlxdConfigFileService($config))->list()]);
}
if ($method === 'POST' && $path === '/api/admin/xlxd/config-files') {
Security::requireAdminToken($config);
$input = Input::json();
$service = new XlxdConfigFileService($config);
Response::json(['ok' => true, 'data' => $service->save((string) ($input['file'] ?? ''), (string) ($input['content'] ?? ''))]);
}
Response::error('Route not found.', 404);
} catch (Throwable $throwable) {
Response::error($throwable->getMessage(), 400);
+27
Просмотреть файл
@@ -81,6 +81,33 @@
<pre class="result" id="adminResult"></pre>
</div>
</section>
<section class="panel config-editor">
<div class="config-editor-head">
<div>
<p class="eyebrow">Файлы XLX</p>
<h2>Редактор blacklist, whitelist, interlink и terminal</h2>
<p class="muted-text">Изменения сохраняются прямо в `/xlxd`. После правки перезапусти `xlxd`, если файл требует перечитывания сервисом.</p>
</div>
<button class="button ghost" id="loadConfigFiles" type="button">Загрузить файлы</button>
</div>
<div class="file-tabs" id="fileTabs"></div>
<div class="file-meta" id="fileMeta">
<span>Файл не выбран</span>
</div>
<label class="editor-label">Содержимое файла
<textarea id="fileEditor" spellcheck="false" placeholder="Загрузите файл для редактирования"></textarea>
</label>
<div class="actions">
<button class="button" id="saveConfigFile" type="button">Сохранить файл</button>
<button class="button ghost" id="reloadConfigFile" type="button">Отменить изменения</button>
</div>
<pre class="result" id="fileResult"></pre>
</section>
</main>
<script src="/assets/admin.js"></script>
</body>