feat(shop): добавление/редактирование товара в модальном окне
Инлайн-панель формы внизу страницы заменена на модалку через LS.modal: - shopAdminCreateItem/EditItem открывают окно openItemModal (create/edit) - валидация: обязательное название + проверка JSON в поле «Данные» - блокировка кнопки на время сохранения, ошибки через m.setError - удалены инлайн-форма из admin.html и неактуальные shopAdminSaveItem/shopAdminCancelForm/showShopForm + стейт Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1337,56 +1337,6 @@
|
||||
<tbody id="shop-items-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="adm-panel" id="shop-item-form" style="display:none">
|
||||
<div class="adm-panel-title" id="shop-form-title">Добавить товар</div>
|
||||
<div class="adm-form-row">
|
||||
<div class="adm-form-group" style="flex:1">
|
||||
<label>Название</label>
|
||||
<input type="text" id="shop-f-name" placeholder="Название товара" />
|
||||
</div>
|
||||
<div class="adm-form-group" style="flex:1">
|
||||
<label>Тип</label>
|
||||
<select id="shop-f-type">
|
||||
<option value="frame">Рамка</option>
|
||||
<option value="title">Титул</option>
|
||||
<option value="background">Фон</option>
|
||||
<option value="effect">Эффект</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="adm-form-group" style="width:100px">
|
||||
<label>Цена</label>
|
||||
<input type="number" id="shop-f-price" min="0" value="100" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="adm-form-row">
|
||||
<div class="adm-form-group" style="flex:1">
|
||||
<label>Описание</label>
|
||||
<textarea id="shop-f-desc" rows="2" placeholder="Описание товара"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adm-form-row">
|
||||
<div class="adm-form-group" style="flex:1">
|
||||
<label>Иконка (emoji/код)</label>
|
||||
<input type="text" id="shop-f-icon" placeholder="SVG-код или эмодзи" />
|
||||
</div>
|
||||
<div class="adm-form-group" style="flex:1">
|
||||
<label>Данные (JSON)</label>
|
||||
<input type="text" id="shop-f-data" placeholder='{"key":"value"}' />
|
||||
</div>
|
||||
<div class="adm-form-group">
|
||||
<label>Активен</label>
|
||||
<label class="adm-toggle">
|
||||
<input type="checkbox" id="shop-f-active" checked />
|
||||
<span class="track"></span><span class="thumb"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<button class="adm-btn adm-btn-primary" onclick="shopAdminSaveItem()">Сохранить</button>
|
||||
<button class="adm-btn" style="background:var(--border-h);color:var(--text-3)" onclick="shopAdminCancelForm()">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Геймификация ── -->
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
'use strict';
|
||||
let inited = false;
|
||||
let _shopItems = [];
|
||||
let _shopEditId = null;
|
||||
let _shopSaving = false;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -67,73 +65,120 @@
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function showShopForm() {
|
||||
const form = document.getElementById('shop-item-form');
|
||||
form.style.display = '';
|
||||
form.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
document.getElementById('shop-f-name').focus();
|
||||
const TYPE_OPTIONS = [
|
||||
{ v: 'frame', l: 'Рамка' },
|
||||
{ v: 'title', l: 'Титул' },
|
||||
{ v: 'background', l: 'Фон' },
|
||||
{ v: 'effect', l: 'Эффект' },
|
||||
];
|
||||
|
||||
/* Open the add/edit item modal. item = null → create, object → edit. */
|
||||
function openItemModal(item) {
|
||||
const isEdit = !!item;
|
||||
const dataStr = item && item.data
|
||||
? (typeof item.data === 'string' ? item.data : JSON.stringify(item.data))
|
||||
: '';
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.innerHTML = `
|
||||
<div class="adm-form-row">
|
||||
<div class="adm-form-group" style="flex:1;min-width:160px">
|
||||
<label>Название</label>
|
||||
<input type="text" id="shop-f-name" placeholder="Название товара" />
|
||||
</div>
|
||||
<div class="adm-form-group" style="flex:1;min-width:120px">
|
||||
<label>Тип</label>
|
||||
<select id="shop-f-type">
|
||||
${TYPE_OPTIONS.map(o => `<option value="${o.v}">${o.l}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="adm-form-group" style="width:96px">
|
||||
<label>Цена</label>
|
||||
<input type="number" id="shop-f-price" min="0" value="100" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="adm-form-row">
|
||||
<div class="adm-form-group" style="flex:1">
|
||||
<label>Описание</label>
|
||||
<textarea id="shop-f-desc" rows="2" placeholder="Описание товара"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adm-form-row" style="margin-bottom:0">
|
||||
<div class="adm-form-group" style="flex:1;min-width:140px">
|
||||
<label>Иконка (emoji/код)</label>
|
||||
<input type="text" id="shop-f-icon" placeholder="SVG-код или эмодзи" />
|
||||
</div>
|
||||
<div class="adm-form-group" style="flex:1;min-width:140px">
|
||||
<label>Данные (JSON)</label>
|
||||
<input type="text" id="shop-f-data" placeholder='{"key":"value"}' />
|
||||
</div>
|
||||
<div class="adm-form-group">
|
||||
<label>Активен</label>
|
||||
<label class="adm-toggle">
|
||||
<input type="checkbox" id="shop-f-active" checked />
|
||||
<span class="track"></span><span class="thumb"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const $ = sel => body.querySelector(sel);
|
||||
$('#shop-f-name').value = item?.name || '';
|
||||
$('#shop-f-type').value = item?.type || 'frame';
|
||||
$('#shop-f-price').value = item?.price ?? 100;
|
||||
$('#shop-f-desc').value = item?.description || '';
|
||||
$('#shop-f-icon').value = item?.icon || '';
|
||||
$('#shop-f-data').value = dataStr;
|
||||
$('#shop-f-active').checked = item ? !!item.is_active : true;
|
||||
|
||||
let saving = false;
|
||||
const m = LS.modal({
|
||||
title: isEdit ? ('Редактировать товар #' + item.id) : 'Добавить товар',
|
||||
content: body,
|
||||
size: 'md',
|
||||
actions: [
|
||||
{ label: 'Отмена', onClick: () => m.close() },
|
||||
{ label: 'Сохранить', primary: true, id: 'shop-save-btn', onClick: async () => {
|
||||
if (saving) return;
|
||||
const payload = {
|
||||
name: $('#shop-f-name').value.trim(),
|
||||
type: $('#shop-f-type').value,
|
||||
price: parseInt($('#shop-f-price').value, 10) || 0,
|
||||
description: $('#shop-f-desc').value.trim(),
|
||||
icon: $('#shop-f-icon').value.trim(),
|
||||
data: $('#shop-f-data').value.trim() || null,
|
||||
is_active: $('#shop-f-active').checked ? 1 : 0,
|
||||
};
|
||||
if (!payload.name) { m.setError('Введите название'); return; }
|
||||
if (payload.data) {
|
||||
try { JSON.parse(payload.data); }
|
||||
catch { m.setError('Поле «Данные» — некорректный JSON'); return; }
|
||||
}
|
||||
saving = true;
|
||||
const btn = document.getElementById('shop-save-btn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Сохранение…'; }
|
||||
try {
|
||||
if (isEdit) { await LS.adminShopUpdateItem(item.id, payload); LS.toast('Товар обновлён', 'success'); }
|
||||
else { await LS.adminShopCreateItem(payload); LS.toast('Товар создан', 'success'); }
|
||||
m.close();
|
||||
inited = false;
|
||||
await load();
|
||||
inited = true;
|
||||
} catch(e) {
|
||||
m.setError('Ошибка: ' + e.message);
|
||||
saving = false;
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Сохранить'; }
|
||||
}
|
||||
} },
|
||||
],
|
||||
});
|
||||
setTimeout(() => $('#shop-f-name')?.focus(), 80);
|
||||
}
|
||||
|
||||
function shopAdminCreateItem() {
|
||||
_shopEditId = null;
|
||||
document.getElementById('shop-form-title').textContent = 'Добавить товар';
|
||||
document.getElementById('shop-f-name').value = '';
|
||||
document.getElementById('shop-f-type').value = 'frame';
|
||||
document.getElementById('shop-f-price').value = '100';
|
||||
document.getElementById('shop-f-desc').value = '';
|
||||
document.getElementById('shop-f-icon').value = '';
|
||||
document.getElementById('shop-f-data').value = '';
|
||||
document.getElementById('shop-f-active').checked = true;
|
||||
showShopForm();
|
||||
}
|
||||
function shopAdminCreateItem() { openItemModal(null); }
|
||||
|
||||
function shopAdminEditItem(id) {
|
||||
const it = _shopItems.find(i => i.id === id);
|
||||
if (!it) return;
|
||||
_shopEditId = id;
|
||||
document.getElementById('shop-form-title').textContent = 'Редактировать товар #' + id;
|
||||
document.getElementById('shop-f-name').value = it.name || '';
|
||||
document.getElementById('shop-f-type').value = it.type || 'frame';
|
||||
document.getElementById('shop-f-price').value = it.price ?? 100;
|
||||
document.getElementById('shop-f-desc').value = it.description || '';
|
||||
document.getElementById('shop-f-icon').value = it.icon || '';
|
||||
document.getElementById('shop-f-data').value = it.data ? (typeof it.data === 'string' ? it.data : JSON.stringify(it.data)) : '';
|
||||
document.getElementById('shop-f-active').checked = !!it.is_active;
|
||||
showShopForm();
|
||||
}
|
||||
|
||||
function shopAdminCancelForm() {
|
||||
document.getElementById('shop-item-form').style.display = 'none';
|
||||
_shopEditId = null;
|
||||
}
|
||||
|
||||
async function shopAdminSaveItem() {
|
||||
if (_shopSaving) return;
|
||||
_shopSaving = true;
|
||||
const data = {
|
||||
name: document.getElementById('shop-f-name').value.trim(),
|
||||
type: document.getElementById('shop-f-type').value,
|
||||
price: parseInt(document.getElementById('shop-f-price').value) || 0,
|
||||
description: document.getElementById('shop-f-desc').value.trim(),
|
||||
icon: document.getElementById('shop-f-icon').value.trim(),
|
||||
data: document.getElementById('shop-f-data').value.trim() || null,
|
||||
is_active: document.getElementById('shop-f-active').checked ? 1 : 0
|
||||
};
|
||||
if (!data.name) { LS.toast('Введите название', 'error'); _shopSaving = false; return; }
|
||||
try {
|
||||
if (_shopEditId) {
|
||||
await LS.adminShopUpdateItem(_shopEditId, data);
|
||||
LS.toast('Товар обновлён', 'success');
|
||||
} else {
|
||||
await LS.adminShopCreateItem(data);
|
||||
LS.toast('Товар создан', 'success');
|
||||
}
|
||||
shopAdminCancelForm();
|
||||
inited = false;
|
||||
await load();
|
||||
inited = true;
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
finally { _shopSaving = false; }
|
||||
if (it) openItemModal(it);
|
||||
}
|
||||
|
||||
async function shopAdminDeleteItem(id) {
|
||||
@@ -157,8 +202,6 @@
|
||||
// Expose onclick handlers
|
||||
window.shopAdminCreateItem = shopAdminCreateItem;
|
||||
window.shopAdminEditItem = shopAdminEditItem;
|
||||
window.shopAdminCancelForm = shopAdminCancelForm;
|
||||
window.shopAdminSaveItem = shopAdminSaveItem;
|
||||
window.shopAdminDeleteItem = shopAdminDeleteItem;
|
||||
window.shopAdminToggleActive = shopAdminToggleActive;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user