RPU v2, promo/bank система, админка, оптимизации
— RPU v2: бюджет удачи, потолок (ceiling), комбек после проигрышей, hot/cold анализ, интеграция с promo-фазами — Bank API: mock-банк для карты (депозит/вывод с балансом 1000₽) — Promo-сервис: автогенерация промокодов каждый час, типы (card_to_site, luck_boost, ceiling_boost, free_case, reset_streak) — Promo-фазы: LUCKY/NEUTRAL/UNLUCKY, переключение каждые 1-6ч — Admin: история транзакций (user_history), управление промокодами, управление promo-фазами, API фильтров/коллекций, RPU info — Database: WAL mode, новые поля RPU v2, TransactionLog, карточный баланс, статистики (streaks, ceiling, budget) — Frontend: /deposit, /promo/activate, /withdraw, /card-balance, /online-count, /rpu-info, lifespan вместо startup event — UI: user_history.html, улучшения профиля, кейсов, навигации — Cases: обновление cases.json (1892 строки новых данных) — Achievements: кэш коллекций, оптимизация is_custom проверок — Sounds: дополнительные звуковые эффекты — Upgrade: интеграция с RPU (множители, streaks) — .gitignore: исключены .db, .bak, __pycache__
This commit is contained in:
+162
-10
@@ -140,6 +140,32 @@
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.withdraw-selected-btn {
|
||||
padding: 0.45rem 1.25rem;
|
||||
background: var(--success-color);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.78rem;
|
||||
transition: all 0.2s;
|
||||
font-family: 'Russo One', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.withdraw-selected-btn:hover {
|
||||
filter: brightness(1.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.withdraw-selected-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sell-all-btn {
|
||||
padding: 0.45rem 1.25rem;
|
||||
background: transparent;
|
||||
@@ -234,6 +260,25 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-withdraw-single {
|
||||
flex: 1;
|
||||
padding: 0.3rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--success-color);
|
||||
color: var(--success-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
transition: all 0.2s;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-withdraw-single:hover {
|
||||
background: var(--success-color);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -628,6 +673,7 @@
|
||||
</div>
|
||||
<div style="display:flex;gap:0.75rem;">
|
||||
<button class="sell-selected-btn" id="sellSelectedBtn" onclick="sellSelected()" disabled>💰 Продать</button>
|
||||
<button class="withdraw-selected-btn" id="withdrawSelectedBtn" onclick="withdrawSelected()" disabled>💳 Вывести</button>
|
||||
<button class="sell-all-btn" onclick="openBulkSellModal()">📦 Массовая</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -658,12 +704,12 @@
|
||||
data-rarity="{{ item.rarity }}"
|
||||
data-type="{{ item.type }}"
|
||||
data-name="{{ item.name|lower }}"
|
||||
data-price="{{ item.price_rub or 100 }}">
|
||||
data-price="{{ item.price_rub }}">
|
||||
{% if not is_public %}
|
||||
<input type="checkbox" class="item-select-checkbox"
|
||||
onchange="onItemSelect(this)"
|
||||
data-id="{{ item.id }}"
|
||||
data-price="{{ item.price_rub or 100 }}">
|
||||
data-price="{{ item.price_rub }}">
|
||||
{% endif %}
|
||||
<div class="skin-image-container">
|
||||
<img src="{{ item.image_url or '/static/placeholder.png' }}"
|
||||
@@ -681,11 +727,12 @@
|
||||
<span>{{ item.wear }}</span>
|
||||
</div>
|
||||
<div class="skin-details" style="margin-top:3px;color:var(--success-color);">
|
||||
<span>💰 {{ (item.price_rub or 100)|money }} ₽</span>
|
||||
<span>💰 {{ (item.price_rub)|money }} ₽</span>
|
||||
</div>
|
||||
{% if not is_public %}
|
||||
<div class="item-actions">
|
||||
<button onclick="sellSingleItem({{ item.id }})" class="btn-sell-single">Продать</button>
|
||||
<button onclick="withdrawSingleItem({{ item.id }}, {{ (item.price_rub)|int }})" class="btn-withdraw-single">Вывести</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -719,7 +766,8 @@
|
||||
</div>
|
||||
<div class="bulk-sell-actions">
|
||||
<button class="btn btn-outline" onclick="closeBulkSellModal()">Отмена</button>
|
||||
<button class="btn btn-primary" onclick="executeBulkSell()" style="background:var(--danger-color);">Продать</button>
|
||||
<button class="btn btn-primary" onclick="executeBulkSell()" style="background:var(--danger-color);">💰 Продать</button>
|
||||
<button class="btn btn-primary" onclick="executeBulkWithdraw()" style="background:var(--success-color);color:#000;">💳 Вывести</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -826,6 +874,7 @@
|
||||
const countBadge = document.getElementById('selectedCountBadge');
|
||||
const priceBadge = document.getElementById('totalPriceBadge');
|
||||
const sellBtn = document.getElementById('sellSelectedBtn');
|
||||
const withdrawBtn = document.getElementById('withdrawSelectedBtn');
|
||||
if (count > 0) {
|
||||
countBadge.style.display = 'inline-block';
|
||||
countBadge.textContent = count + ' выбрано';
|
||||
@@ -834,10 +883,12 @@
|
||||
priceBadge.style.display = 'inline-block';
|
||||
priceBadge.textContent = total.toFixed(0) + ' ₽';
|
||||
sellBtn.disabled = false;
|
||||
withdrawBtn.disabled = false;
|
||||
} else {
|
||||
countBadge.style.display = 'none';
|
||||
priceBadge.style.display = 'none';
|
||||
sellBtn.disabled = true;
|
||||
withdrawBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,9 +897,10 @@
|
||||
const r = await fetch('/web/api/user/balance');
|
||||
const d = await r.json();
|
||||
if (d.success) {
|
||||
document.querySelectorAll('.user-balance').forEach(el => {
|
||||
el.textContent = Math.floor(d.balance) + ' ₽';
|
||||
});
|
||||
const el = document.getElementById('siteBalanceDisplay');
|
||||
if (el) {
|
||||
el.innerHTML = Math.floor(d.balance).toLocaleString('ru') + ' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
@@ -906,6 +958,64 @@
|
||||
});
|
||||
}
|
||||
|
||||
function withdrawSingleItem(inventoryId, price) {
|
||||
Notify.confirm('Вывести ' + (price||'').toLocaleString() + ' ₽ на карту? (Комиссия 10%)', 'Вывод', function(confirmed) {
|
||||
if (!confirmed) return;
|
||||
SoundManager.click();
|
||||
const formData = new FormData();
|
||||
formData.append('inventory_item_id', inventoryId);
|
||||
fetch('/web/api/withdraw', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
SoundManager.balanceUpdate();
|
||||
Notify.success('Выведено ' + data.net.toFixed(0) + ' ₽ на карту');
|
||||
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + inventoryId + '"]');
|
||||
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
|
||||
refreshBalance();
|
||||
} else {
|
||||
SoundManager.error();
|
||||
Notify.error(data.error || 'Ошибка при выводе');
|
||||
}
|
||||
})
|
||||
.catch(() => { SoundManager.error(); Notify.error('Ошибка соединения'); });
|
||||
});
|
||||
}
|
||||
|
||||
function withdrawSelected() {
|
||||
if (selectedItems.size === 0) return;
|
||||
let total = 0;
|
||||
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
|
||||
const fee = total * 0.1;
|
||||
Notify.confirm('Вывести ' + selectedItems.size + ' предметов на карту?\nСумма: ' + total.toFixed(0) + ' ₽\nКомиссия: ' + fee.toFixed(0) + ' ₽\nК зачислению: ' + (total - fee).toFixed(0) + ' ₽', 'Массовый вывод', function(confirmed) {
|
||||
if (!confirmed) return;
|
||||
SoundManager.click();
|
||||
const ids = Array.from(selectedItems);
|
||||
let completed = 0, errors = 0;
|
||||
ids.forEach(function(invId) {
|
||||
const formData = new FormData();
|
||||
formData.append('inventory_item_id', invId);
|
||||
fetch('/web/api/withdraw', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
completed++;
|
||||
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + invId + '"]');
|
||||
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
|
||||
} else {
|
||||
errors++;
|
||||
}
|
||||
if (completed + errors === ids.length) {
|
||||
Notify.success('Выведено ' + completed + ' предметов' + (errors ? ', ошибок: ' + errors : ''));
|
||||
selectedItems.clear();
|
||||
refreshBalance();
|
||||
}
|
||||
})
|
||||
.catch(function() { errors++; });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openBulkSellModal() {
|
||||
const modal = document.getElementById('bulkSellModal');
|
||||
const optionsDiv = document.getElementById('bulkSellOptions');
|
||||
@@ -997,6 +1107,45 @@
|
||||
});
|
||||
}
|
||||
|
||||
function executeBulkWithdraw() {
|
||||
const selected = [];
|
||||
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => { if (cb.value && cb.value !== 'all') selected.push(cb.value); });
|
||||
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
|
||||
const ids = [];
|
||||
document.querySelectorAll('.inventory-skin-card').forEach(card => {
|
||||
if (card.style.display !== 'none') {
|
||||
if (allChecked || selected.includes(card.dataset.rarity)) {
|
||||
ids.push(parseInt(card.dataset.inventoryId));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (ids.length === 0) { Notify.error('Выберите хотя бы одну редкость'); return; }
|
||||
let totalPrice = 0;
|
||||
ids.forEach(id => { totalPrice += itemPrices.get(id) || 100; });
|
||||
const fee = totalPrice * 0.1;
|
||||
Notify.confirm('Вывести ' + ids.length + ' предметов на карту?\nСумма: ' + totalPrice.toFixed(0) + ' ₽\nКомиссия: ' + fee.toFixed(0) + ' ₽\nК зачислению: ' + (totalPrice - fee).toFixed(0) + ' ₽', 'Массовый вывод', function(confirmed) {
|
||||
if (!confirmed) return;
|
||||
let completed = 0, errors = 0;
|
||||
ids.forEach(function(invId) {
|
||||
const formData = new FormData();
|
||||
formData.append('inventory_item_id', invId);
|
||||
fetch('/web/api/withdraw', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) { completed++; }
|
||||
else { errors++; }
|
||||
if (completed + errors === ids.length) {
|
||||
Notify.success('Выведено ' + completed + ' предметов' + (errors ? ', ошибок: ' + errors : ''));
|
||||
refreshBalance();
|
||||
closeBulkSellModal();
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function() { errors++; });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterItems() {
|
||||
const rf = document.getElementById('rarityFilter').value.toLowerCase();
|
||||
const tf = document.getElementById('typeFilter').value.toLowerCase();
|
||||
@@ -1071,7 +1220,7 @@
|
||||
+ '<div class="skin-details"><span class="rarity-' + rc + '">' + (item.rarity||'') + '</span><span>' + fs + '</span></div>'
|
||||
+ '<div class="skin-details" style="margin-top:3px;"><span>' + (item.type||'') + '</span><span>' + (item.wear||'') + '</span></div>'
|
||||
+ '<div class="skin-details" style="margin-top:3px;color:var(--success-color);"><span>💰 ' + ((item.price_rub||100)).toLocaleString() + ' ₽</span></div>'
|
||||
+ '<div class="item-actions"><button onclick="sellSingleItem(' + item.id + ')" class="btn-sell-single">Продать</button></div></div></div>';
|
||||
+ '<div class="item-actions"><button onclick="sellSingleItem(' + item.id + ')" class="btn-sell-single">Продать</button><button onclick="withdrawSingleItem(' + item.id + ', ' + (item.price_rub||100) + ')" class="btn-withdraw-single">Вывести</button></div></div></div>';
|
||||
});
|
||||
grid.innerHTML = html;
|
||||
selectedItems.clear();
|
||||
@@ -1096,7 +1245,7 @@
|
||||
grid.addEventListener('click', function(e) {
|
||||
const card = e.target.closest('.inventory-skin-card');
|
||||
if (!card) return;
|
||||
if (e.target.closest('.item-select-checkbox, .btn-sell-single')) return;
|
||||
if (e.target.closest('.item-select-checkbox, .btn-sell-single, .btn-withdraw-single')) return;
|
||||
const invId = card.dataset.inventoryId;
|
||||
if (!invId) return;
|
||||
openItemCard(invId);
|
||||
@@ -1155,7 +1304,10 @@
|
||||
+ '<div class="ic-detail"><span class="ic-label">Тип</span><span class="ic-value">' + (item.type || 'Normal') + '</span></div>'
|
||||
+ '<div class="ic-detail"><span class="ic-label">Цена</span><span class="ic-value" style="color:var(--success-color)">💰 ' + (item.price_rub || 0).toLocaleString() + ' ₽</span></div>'
|
||||
+ (collHtml ? '<div class="ic-collection-title">🎨 Скины коллекции</div><div class="ic-collection-list">' + collHtml + '</div>' : '')
|
||||
+ '<button class="btn-sell-single ic-sell-btn" onclick="sellSingleItem(' + item.id + '); closeItemCard();">Продать за ' + (item.price_rub || 0).toLocaleString() + ' ₽</button>'
|
||||
+ '<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">'
|
||||
+ '<button class="ic-sell-btn" style="flex:1;" onclick="sellSingleItem(' + item.id + '); closeItemCard();">Продать за ' + (item.price_rub || 0).toLocaleString() + ' ₽</button>'
|
||||
+ '<button class="ic-sell-btn" style="flex:1;background:var(--success-color);color:#000;" onclick="withdrawSingleItem(' + item.id + ', ' + (item.price_rub || 100) + '); closeItemCard();">Вывести</button>'
|
||||
+ '</div>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user