feat(shop): компактный UX вкладки Магазин — статы-строка, фильтр, поиск
- 4 крупные карточки статистики → компактная строка stat-пиллов - тулбар: фильтр по типу + поиск по названию + счётчик (N из M) - таблица: иконка-чип по типу + название с описанием в одной ячейке, цветные бейджи типов, колонка ID убрана (id ушёл в подпись) - состояния «Нет товаров» / «Ничего не найдено» Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+43
-5
@@ -1318,23 +1318,61 @@
|
||||
</div>
|
||||
<div class="perm-grid" id="perm-student"></div>
|
||||
</div>
|
||||
|
||||
<div class="perm-role-block">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px">
|
||||
<span style="font-weight:600">История изменений прав ролей</span>
|
||||
<button onclick="loadPermLog()" style="padding:7px 14px;border:1.5px solid var(--border);border-radius:9px;background:transparent;color:var(--text-3);cursor:pointer;font-family:inherit;font-size:0.85rem">Показать</button>
|
||||
</div>
|
||||
<div id="perm-log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Магазин ── -->
|
||||
<div class="tab-pane" id="tab-shop">
|
||||
<style>
|
||||
.shop-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:22px; }
|
||||
.shop-stat { display:flex; align-items:center; gap:11px; background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:12px 16px; flex:1 1 0; min-width:165px; }
|
||||
.shop-stat-ic { width:38px; height:38px; border-radius:11px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.shop-stat-ic svg { width:18px; height:18px; }
|
||||
.shop-stat-txt { display:flex; flex-direction:column; line-height:1.18; min-width:0; }
|
||||
.shop-stat-v { font-family:'Unbounded',sans-serif; font-size:1.2rem; font-weight:800; }
|
||||
.shop-stat-v.sm { font-size:0.92rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.shop-stat-l { font-size:0.73rem; color:var(--text-3); font-weight:600; }
|
||||
.shop-cell { display:flex; align-items:center; gap:11px; }
|
||||
.shop-ic { width:34px; height:34px; border-radius:10px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||
.shop-ic svg { width:17px; height:17px; }
|
||||
.shop-cell-txt { display:flex; flex-direction:column; line-height:1.25; min-width:0; }
|
||||
.shop-cell-sub { font-size:0.76rem; color:var(--text-3); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:300px; }
|
||||
.shop-type-badge { display:inline-block; padding:3px 11px; border-radius:var(--r-pill); font-size:0.76rem; font-weight:700; white-space:nowrap; }
|
||||
.shop-price { white-space:nowrap; font-weight:600; }
|
||||
.shop-ic.t-frame, .shop-type-badge.t-frame { background:rgba(155,93,229,.12); color:var(--violet); }
|
||||
.shop-ic.t-title, .shop-type-badge.t-title { background:rgba(255,179,71,.16); color:#d98a17; }
|
||||
.shop-ic.t-background, .shop-type-badge.t-background { background:rgba(6,214,224,.12); color:#0891a3; }
|
||||
.shop-ic.t-effect, .shop-type-badge.t-effect { background:rgba(6,214,160,.13); color:#089f7a; }
|
||||
.shop-ic.t-other, .shop-type-badge.t-other { background:rgba(120,130,150,.14); color:var(--text-3); }
|
||||
</style>
|
||||
<div class="section-title">Магазин</div>
|
||||
<div class="stats-grid" id="shop-stats-grid"><div class="spinner"></div></div>
|
||||
<div class="shop-stats" id="shop-stats-grid"><div class="spinner"></div></div>
|
||||
|
||||
<div class="section-title" style="margin-top:32px">Товары</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<div class="t-toolbar">
|
||||
<select class="t-select" id="shop-filter-type" onchange="shopApplyFilters()">
|
||||
<option value="">Все типы</option>
|
||||
<option value="frame">Рамки</option>
|
||||
<option value="title">Титулы</option>
|
||||
<option value="background">Фоны</option>
|
||||
<option value="effect">Эффекты</option>
|
||||
</select>
|
||||
<input class="t-input" id="shop-search" type="text" placeholder="Поиск по названию…" oninput="shopApplyFilters()" />
|
||||
<span class="t-count" id="shop-count"></span>
|
||||
<button class="btn-add" onclick="shopAdminCreateItem()">+ Добавить товар</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>ID</th><th>Название</th><th>Тип</th><th>Цена</th><th>Продано</th><th>Активен</th><th>Действия</th>
|
||||
<th>Товар</th><th>Тип</th><th>Цена</th><th>Продано</th><th>Активен</th><th>Действия</th>
|
||||
</tr></thead>
|
||||
<tbody id="shop-items-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
|
||||
<tbody id="shop-items-body"><tr><td colspan="6"><div class="spinner"></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
'use strict';
|
||||
let inited = false;
|
||||
let _shopItems = [];
|
||||
let _filterType = '';
|
||||
let _search = '';
|
||||
|
||||
const TYPE_LABELS = { frame:'Рамка', title:'Титул', theme:'Тема', effect:'Эффект', background:'Фон' };
|
||||
const TYPE_ICONS = { frame:'square', title:'award', background:'image', effect:'sparkles', theme:'palette' };
|
||||
const typeCls = t => ['frame','title','background','effect'].includes(t) ? 't-' + t : 't-other';
|
||||
const typeIcon = t => TYPE_ICONS[t] || 'tag';
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -12,27 +19,19 @@
|
||||
LS.adminShopGetItems()
|
||||
]);
|
||||
const topName = stats.topItems?.[0]?.name || '—';
|
||||
document.getElementById('shop-stats-grid').innerHTML = `
|
||||
<div class="stat-card" style="--stat-top:var(--violet)">
|
||||
<div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="shopping-bag" class="stat-icon"></i></div>
|
||||
<div class="stat-val violet">${stats.activeItems}/${stats.totalItems}</div>
|
||||
<div class="stat-label">Товаров</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--stat-top:var(--cyan)">
|
||||
<div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="receipt" class="stat-icon"></i></div>
|
||||
<div class="stat-val cyan">${stats.totalPurchases}</div>
|
||||
<div class="stat-label">Покупок</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--stat-top:var(--green)">
|
||||
<div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="coins" class="stat-icon"></i></div>
|
||||
<div class="stat-val green">${stats.totalCoinsInCirculation}</div>
|
||||
<div class="stat-label">Монет в обороте</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--stat-top:var(--amber, #FFB347)">
|
||||
<div class="stat-card-icon" style="background:rgba(255,179,71,0.1)"><i data-lucide="star" class="stat-icon"></i></div>
|
||||
<div class="stat-val" style="color:var(--amber, #FFB347);font-size:1.1rem">${esc(topName)}</div>
|
||||
<div class="stat-label">Топ товар</div>
|
||||
const stat = (cls, icon, ic, val, label, sm) => `
|
||||
<div class="shop-stat">
|
||||
<span class="shop-stat-ic" style="background:${ic}"><i data-lucide="${icon}" style="color:${cls}"></i></span>
|
||||
<span class="shop-stat-txt">
|
||||
<span class="shop-stat-v${sm ? ' sm' : ''}" style="color:${cls}">${val}</span>
|
||||
<span class="shop-stat-l">${label}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
document.getElementById('shop-stats-grid').innerHTML =
|
||||
stat('var(--violet)', 'shopping-bag', 'rgba(155,93,229,0.12)', `${stats.activeItems}/${stats.totalItems}`, 'товаров активно') +
|
||||
stat('var(--cyan)', 'receipt', 'rgba(6,214,224,0.12)', stats.totalPurchases, 'покупок') +
|
||||
stat('var(--green)', 'coins', 'rgba(6,214,100,0.12)', stats.totalCoinsInCirculation, 'монет в обороте') +
|
||||
stat('#d98a17', 'star', 'rgba(255,179,71,0.16)', esc(topName), 'топ-товар', true);
|
||||
_shopItems = items;
|
||||
renderShopItems();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
@@ -41,15 +40,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
function shopApplyFilters() {
|
||||
_filterType = document.getElementById('shop-filter-type')?.value || '';
|
||||
_search = (document.getElementById('shop-search')?.value || '').trim().toLowerCase();
|
||||
renderShopItems();
|
||||
}
|
||||
|
||||
function renderShopItems() {
|
||||
const body = document.getElementById('shop-items-body');
|
||||
if (!_shopItems.length) { body.innerHTML = '<tr><td colspan="7" class="empty">Нет товаров</td></tr>'; return; }
|
||||
const typeLabels = { frame:'Рамка', title:'Титул', theme:'Тема', effect:'Эффект', background:'Фон' };
|
||||
body.innerHTML = _shopItems.map(it => `<tr>
|
||||
<td>${it.id}</td>
|
||||
<td><strong>${esc(it.name)}</strong></td>
|
||||
<td><span class="mode-badge mode-practice">${typeLabels[it.type] || esc(it.type)}</span></td>
|
||||
<td>${it.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:var(--amber, #FFB347)"></i></td>
|
||||
const countEl = document.getElementById('shop-count');
|
||||
const filtered = _shopItems.filter(it =>
|
||||
(!_filterType || it.type === _filterType) &&
|
||||
(!_search || (it.name || '').toLowerCase().includes(_search))
|
||||
);
|
||||
if (countEl) countEl.textContent = filtered.length === _shopItems.length
|
||||
? `${_shopItems.length} товаров`
|
||||
: `${filtered.length} из ${_shopItems.length}`;
|
||||
if (!_shopItems.length) { body.innerHTML = '<tr><td colspan="6" class="empty">Нет товаров</td></tr>'; return; }
|
||||
if (!filtered.length) { body.innerHTML = '<tr><td colspan="6" class="empty">Ничего не найдено</td></tr>'; return; }
|
||||
body.innerHTML = filtered.map(it => `<tr>
|
||||
<td>
|
||||
<div class="shop-cell">
|
||||
<span class="shop-ic ${typeCls(it.type)}"><i data-lucide="${typeIcon(it.type)}"></i></span>
|
||||
<span class="shop-cell-txt">
|
||||
<strong>${esc(it.name)}</strong>
|
||||
<span class="shop-cell-sub">${it.description ? esc(it.description) : '#' + it.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="shop-type-badge ${typeCls(it.type)}">${TYPE_LABELS[it.type] || esc(it.type)}</span></td>
|
||||
<td class="shop-price">${it.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:#d98a17"></i></td>
|
||||
<td>${it.sold_count || 0}</td>
|
||||
<td>
|
||||
<label class="adm-toggle">
|
||||
@@ -204,6 +224,7 @@
|
||||
window.shopAdminEditItem = shopAdminEditItem;
|
||||
window.shopAdminDeleteItem = shopAdminDeleteItem;
|
||||
window.shopAdminToggleActive = shopAdminToggleActive;
|
||||
window.shopApplyFilters = shopApplyFilters;
|
||||
|
||||
window.AdminSections = window.AdminSections || {};
|
||||
window.AdminSections.shop = {
|
||||
|
||||
Reference in New Issue
Block a user