Итерация банка 1
[html]<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Банк ролевых игр - RUSFF.ME</title>
<style>
:root {
--primary: #4a6fa5;
--secondary: #6b8cbc;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
--light: #f8f9fa;
--dark: #343a40;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
color: var(--dark);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
background: var(--primary);
color: white;
padding: 20px;
border-radius: 10px 10px 0 0;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
h1 {
font-size: 2.2rem;
margin-bottom: 10px;
}
.tagline {
font-size: 1.1rem;
opacity: 0.9;
}
.app-container {
display: grid;
grid-template-columns: 1fr 350px;
gap: 20px;
margin-top: 20px;
}
.main-content {
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
padding: 25px;
display: flex;
flex-direction: column;
gap: 25px;
}
.sidebar {
background: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.card {
background: var(--light);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
h2 {
color: var(--primary);
margin-bottom: 15px;
font-size: 1.5rem;
border-bottom: 2px solid var(--secondary);
padding-bottom: 8px;
}
h3 {
color: var(--secondary);
margin-bottom: 12px;
font-size: 1.2rem;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
input, select, button {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
}
button {
background: var(--primary);
color: white;
border: none;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
margin-top: 10px;
}
button:hover {
background: var(--secondary);
transform: translateY(-2px);
}
button.secondary {
background: var(--secondary);
}
button.success {
background: var(--success);
}
button.warning {
background: var(--warning);
color: var(--dark);
}
button.danger {
background: var(--danger);
}
.rates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
margin-top: 10px;
}
.rate-card {
background: white;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
text-align: center;
}
.rate-type {
font-weight: 600;
color: var(--primary);
}
.rate-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--success);
}
.posts-container {
margin-top: 15px;
}
.post-entry {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.post-number {
font-weight: 600;
min-width: 30px;
}
.post-type {
flex: 1;
}
.results {
background: white;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
display: none;
}
.results.active {
display: block;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.result-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.result-row:last-child {
border-bottom: none;
font-weight: 700;
font-size: 1.1rem;
color: var(--primary);
}
.player-stats {
margin-top: 15px;
}
.stat-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #ddd;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-active {
background: var(--success);
}
.status-inactive {
background: var(--danger);
}
.notification {
padding: 12px;
border-radius: 5px;
margin-bottom: 15px;
display: none;
}
.notification.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
display: block;
}
.notification.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
display: block;
}
.notification.warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
display: block;
}
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
}
.tab.active {
border-bottom: 3px solid var(--primary);
font-weight: 600;
color: var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
@media (max-width: 768px) {
.app-container {
grid-template-columns: 1fr;
}
.rates-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🏦 Банк ролевых игр - RUSFF.ME</h1>
<p class="tagline">Автоматический учет денег за посты с проверкой заполнения банка</p>
</header>
<div class="app-container">
<div class="main-content">
<div class="tabs">
<div class="tab active" data-tab="auto">Автоматический режим</div>
<div class="tab" data-tab="manual">Ручной ввод</div>
<div class="tab" data-tab="stats">Статистика</div>
</div>
<div class="tab-content active" id="auto-tab">
<div class="card">
<h2>Автоматическое пополнение счета</h2>
<div class="notification warning" id="auto-notification">
⚠️ Автоматическая проверка rusff.me временно недоступна. Используйте ручной ввод.
</div>
<div class="form-group">
<label for="player-name">Имя игрока:</label>
<input type="text" id="player-name" placeholder="Введите имя персонажа">
</div>
<div class="form-group">
<label for="posts-count">Количество постов для проверки:</label>
<input type="number" id="posts-count" min="1" max="50" value="5">
</div>
<button id="check-bank-btn" class="success">🔍 Проверить заполнение банка</button>
</div>
<div class="card" id="posts-section" style="display: none;">
<h2>Типы постов</h2>
<p>Укажите тип для каждого поста:</p>
<div class="rates-grid">
<div class="rate-card">
<div class="rate-type">Обычный</div>
<div class="rate-value">10 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Развитие</div>
<div class="rate-value">15 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Админский</div>
<div class="rate-value">20 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Массовый</div>
<div class="rate-value">12 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Отыгровка</div>
<div class="rate-value">8 монет</div>
</div>
</div>
<div class="posts-container" id="posts-container">
<!-- Посты будут добавлены здесь динамически -->
</div>
<button id="calculate-btn" class="success">💰 Рассчитать заработок</button>
</div>
</div>
<div class="tab-content" id="manual-tab">
<div class="card">
<h2>Ручной ввод данных</h2>
<div class="form-group">
<label for="manual-player-name">Имя игрока:</label>
<input type="text" id="manual-player-name" placeholder="Введите имя персонажа">
</div>
<div class="form-group">
<label for="current-balance">Текущий баланс:</label>
<input type="number" id="current-balance" min="0" value="0">
</div>
<div class="form-group">
<label for="manual-posts-count">Количество постов:</label>
<input type="number" id="manual-posts-count" min="1" max="50" value="1">
</div>
<button id="generate-posts-btn" class="secondary">📝 Создать форму для постов</button>
</div>
<div class="card" id="manual-posts-section" style="display: none;">
<h2>Типы постов</h2>
<p>Укажите тип для каждого поста:</p>
<div class="posts-container" id="manual-posts-container">
<!-- Посты будут добавлены здесь динамически -->
</div>
<button id="manual-calculate-btn" class="success">💰 Рассчитать заработок</button>
</div>
</div>
<div class="tab-content" id="stats-tab">
<div class="card">
<h2>Статистика игроков</h2>
<div class="form-group">
<label for="search-player">Поиск игрока:</label>
<input type="text" id="search-player" placeholder="Введите имя для поиска">
</div>
<button id="search-btn" class="secondary">🔍 Найти игрока</button>
<div class="player-stats" id="player-stats">
<!-- Статистика будет отображена здесь -->
</div>
</div>
<div class="card">
<h2>Тарифы по типам постов</h2>
<div class="rates-grid">
<div class="rate-card">
<div class="rate-type">Обычный</div>
<div class="rate-value">10 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Развитие</div>
<div class="rate-value">15 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Админский</div>
<div class="rate-value">20 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Массовый</div>
<div class="rate-value">12 монет</div>
</div>
<div class="rate-card">
<div class="rate-type">Отыгровка</div>
<div class="rate-value">8 монет</div>
</div>
</div>
</div>
</div>
<div class="results" id="results">
<h2>Результаты операции</h2>
<div class="result-row">
<span>Игрок:</span>
<span id="result-player">-</span>
</div>
<div class="result-row">
<span>Дата операции:</span>
<span id="result-date">-</span>
</div>
<div class="result-row">
<span>Количество постов:</span>
<span id="result-posts">-</span>
</div>
<div class="result-row">
<span>Начислено:</span>
<span id="result-earned">-</span>
</div>
<div class="result-row">
<span>Предыдущий баланс:</span>
<span id="result-old-balance">-</span>
</div>
<div class="result-row">
<span>Новый баланс:</span>
<span id="result-new-balance">-</span>
</div>
</div>
</div>
<div class="sidebar">
<div class="card">
<h2>Активные игроки</h2>
<div id="active-players">
<!-- Список активных игроков будет здесь -->
</div>
</div>
<div class="card">
<h2>Последние операции</h2>
<div id="recent-transactions">
<!-- Последние транзакции будут здесь -->
</div>
</div>
<div class="card">
<h2>Быстрые действия</h2>
<button id="reset-data-btn" class="warning">🔄 Сбросить все данные</button>
<button id="export-data-btn" class="secondary">📤 Экспорт данных</button>
<button id="import-data-btn" class="secondary">📥 Импорт данных</button>
</div>
</div>
</div>
</div><script>
// Тарифы за посты
const rates = {
"обычный": 10,
"развитие": 15,
"админский": 20,
"массовый": 12,
"отыгровка": 8
};// Данные игроков
let players = JSON.parse(localStorage.getItem('roleplayPlayers')) || {};
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
initializeTabs();
updateActivePlayers();
updateRecentTransactions();
// Обработчики для автоматического режима
document.getElementById('check-bank-btn').addEventListener('click', checkBankFill);
document.getElementById('calculate-btn').addEventListener('click', calculateEarnings);
// Обработчики для ручного режима
document.getElementById('generate-posts-btn').addEventListener('click', generateManualPosts);
document.getElementById('manual-calculate-btn').addEventListener('click', calculateManualEarnings);
// Обработчики для статистики
document.getElementById('search-btn').addEventListener('click', searchPlayer);
// Обработчики быстрых действий
document.getElementById('reset-data-btn').addEventListener('click', resetData);
document.getElementById('export-data-btn').addEventListener('click', exportData);
document.getElementById('import-data-btn').addEventListener('click', importData);
});
// Инициализация вкладок
function initializeTabs() {
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', function() {
// Убираем активный класс со всех вкладок и контента
tabs.forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
// Добавляем активный класс к выбранной вкладке
this.classList.add('active');
const tabId = this.getAttribute('data-tab');
document.getElementById(`${tabId}-tab`).classList.add('active');
});
});
}
// Проверка заполнения банка (имитация)
function checkBankFill() {
const playerName = document.getElementById('player-name').value.trim();
const postsCount = parseInt(document.getElementById('posts-count').value);
if (!playerName) {
showNotification('Введите имя игрока', 'error');
return;
}
if (isNaN(postsCount) || postsCount < 1) {
showNotification('Введите корректное количество постов', 'error');
return;
}
// Имитация проверки банка
showNotification('🔍 Проверяем заполнение банка на RUSFF.ME...', 'warning');
setTimeout(() => {
// В реальном приложении здесь был бы запрос к API или парсинг страницы
showNotification('✅ Банк заполнен корректно! Можно вводить посты.', 'success');
// Показываем секцию с постами
document.getElementById('posts-section').style.display = 'block';
// Генерируем поля для ввода постов
generatePostInputs(postsCount, 'posts-container');
}, 1500);
}
// Генерация полей для ввода постов
function generatePostInputs(count, containerId) {
const container = document.getElementById(containerId);
container.innerHTML = '';
for (let i = 1; i <= count; i++) {
const postEntry = document.createElement('div');
postEntry.className = 'post-entry';
postEntry.innerHTML = `
<div class="post-number">${i}.</div>
<select class="post-type" name="post-type-${i}">
<option value="">Выберите тип поста</option>
${Object.keys(rates).map(type =>
`<option value="${type}">${type} (${rates[type]} монет)</option>`
).join('')}
</select>
`;
container.appendChild(postEntry);
}
}
// Расчет заработка в автоматическом режиме
function calculateEarnings() {
const playerName = document.getElementById('player-name').value.trim();
const postSelects = document.querySelectorAll('#posts-container .post-type');
if (!playerName) {
showNotification('Введите имя игрока', 'error');
return;
}
// Проверяем, что все посты имеют выбранный тип
let allSelected = true;
let totalEarned = 0;
postSelects.forEach((select, index) => {
if (!select.value) {
allSelected = false;
select.style.borderColor = 'var(--danger)';
} else {
select.style.borderColor = '';
totalEarned += rates[select.value];
}
});
if (!allSelected) {
showNotification('Выберите тип для всех постов', 'error');
return;
}
// Получаем или создаем данные игрока
if (!players[playerName]) {
players[playerName] = {
balance: 0,
totalEarned: 0,
transactions: [],
lastUpdate: new Date().toISOString()
};
}
const oldBalance = players[playerName].balance;
const newBalance = oldBalance + totalEarned;
// Обновляем данные игрока
players[playerName].balance = newBalance;
players[playerName].totalEarned += totalEarned;
players[playerName].lastUpdate = new Date().toISOString();
// Добавляем транзакцию
players[playerName].transactions.push({
date: new Date().toISOString(),
posts: Array.from(postSelects).map(select => select.value),
earned: totalEarned,
oldBalance: oldBalance,
newBalance: newBalance
});
// Сохраняем данные
localStorage.setItem('roleplayPlayers', JSON.stringify(players));
// Показываем результаты
showResults(playerName, postSelects.length, totalEarned, oldBalance, newBalance);
// Обновляем боковые панели
updateActivePlayers();
updateRecentTransactions();
showNotification('✅ Баланс успешно обновлен!', 'success');
}
// Генерация постов для ручного режима
function generateManualPosts() {
const playerName = document.getElementById('manual-player-name').value.trim();
const postsCount = parseInt(document.getElementById('manual-posts-count').value);
if (!playerName) {
showNotification('Введите имя игрока', 'error');
return;
}
if (isNaN(postsCount) || postsCount < 1) {
showNotification('Введите корректное количество постов', 'error');
return;
}
// Показываем секцию с постами
document.getElementById('manual-posts-section').style.display = 'block';
// Генерируем поля для ввода постов
generatePostInputs(postsCount, 'manual-posts-container');
}
// Расчет заработка в ручном режиме
function calculateManualEarnings() {
const playerName = document.getElementById('manual-player-name').value.trim();
const currentBalance = parseFloat(document.getElementById('current-balance').value) || 0;
const postSelects = document.querySelectorAll('#manual-posts-container .post-type');
if (!playerName) {
showNotification('Введите имя игрока', 'error');
return;
}
// Проверяем, что все посты имеют выбранный тип
let allSelected = true;
let totalEarned = 0;
postSelects.forEach((select, index) => {
if (!select.value) {
allSelected = false;
select.style.borderColor = 'var(--danger)';
} else {
select.style.borderColor = '';
totalEarned += rates[select.value];
}
});
if (!allSelected) {
showNotification('Выберите тип для всех постов', 'error');
return;
}
// Получаем или создаем данные игрока
if (!players[playerName]) {
players[playerName] = {
balance: currentBalance,
totalEarned: 0,
transactions: [],
lastUpdate: new Date().toISOString()
};
}
const oldBalance = players[playerName].balance;
const newBalance = oldBalance + totalEarned;
// Обновляем данные игрока
players[playerName].balance = newBalance;
players[playerName].totalEarned += totalEarned;
players[playerName].lastUpdate = new Date().toISOString();
// Добавляем транзакцию
players[playerName].transactions.push({
date: new Date().toISOString(),
posts: Array.from(postSelects).map(select => select.value),
earned: totalEarned,
oldBalance: oldBalance,
newBalance: newBalance
});
// Сохраняем данные
localStorage.setItem('roleplayPlayers', JSON.stringify(players));
// Показываем результаты
showResults(playerName, postSelects.length, totalEarned, oldBalance, newBalance);
// Обновляем боковые панели
updateActivePlayers();
updateRecentTransactions();
showNotification('✅ Баланс успешно обновлен!', 'success');
}
// Поиск игрока в статистике
function searchPlayer() {
const searchName = document.getElementById('search-player').value.trim().toLowerCase();
const statsContainer = document.getElementById('player-stats');
if (!searchName) {
statsContainer.innerHTML = '<p>Введите имя игрока для поиска</p>';
return;
}
// Ищем игрока
const playerKeys = Object.keys(players).filter(key =>
key.toLowerCase().includes(searchName)
);
if (playerKeys.length === 0) {
statsContainer.innerHTML = '<p>Игрок не найден</p>';
return;
}
let statsHTML = '';
playerKeys.forEach(playerName => {
const player = players[playerName];
const lastUpdate = new Date(player.lastUpdate).toLocaleString('ru-RU');
statsHTML += `
<div class="card" style="margin-top: 15px;">
<h3>${playerName}</h3>
<div class="stat-item">
<span>Текущий баланс:</span>
<span>${player.balance} монет</span>
</div>
<div class="stat-item">
<span>Всего заработано:</span>
<span>${player.totalEarned} монет</span>
</div>
<div class="stat-item">
<span>Последнее обновление:</span>
<span>${lastUpdate}</span>
</div>
<div class="stat-item">
<span>Количество транзакций:</span>
<span>${player.transactions ? player.transactions.length : 0}</span>
</div>
</div>
`;
});
statsContainer.innerHTML = statsHTML;
}
// Показать результаты операции
function showResults(playerName, postsCount, earned, oldBalance, newBalance) {
const results = document.getElementById('results');
const now = new Date();
document.getElementById('result-player').textContent = playerName;
document.getElementById('result-date').textContent = now.toLocaleString('ru-RU');
document.getElementById('result-posts').textContent = postsCount;
document.getElementById('result-earned').textContent = `${earned} монет`;
document.getElementById('result-old-balance').textContent = `${oldBalance} монет`;
document.getElementById('result-new-balance').textContent = `${newBalance} монет`;
results.classList.add('active');
}
// Обновление списка активных игроков
function updateActivePlayers() {
const container = document.getElementById('active-players');
const playerKeys = Object.keys(players);
if (playerKeys.length === 0) {
container.innerHTML = '<p>Нет данных об игроках</p>';
return;
}
// Сортируем по дате последнего обновления (новые сверху)
playerKeys.sort((a, b) =>
new Date(players[b].lastUpdate) - new Date(players[a].lastUpdate)
);
let playersHTML = '';
playerKeys.slice(0, 5).forEach(playerName => {
const player = players[playerName];
const lastUpdate = new Date(player.lastUpdate);
const now = new Date();
const daysDiff = Math.floor((now - lastUpdate) / (1000 * 60 * 60 * 24));
const isActive = daysDiff < 7; // Активен, если обновлялся менее недели назад
playersHTML += `
<div class="stat-item">
<span>
<span class="status-indicator ${isActive ? 'status-active' : 'status-inactive'}"></span>
${playerName}
</span>
<span>${player.balance} монет</span>
</div>
`;
});
container.innerHTML = playersHTML;
}
// Обновление последних транзакций
function updateRecentTransactions() {
const container = document.getElementById('recent-transactions');
// Собираем все транзакции из всех игроков
let allTransactions = [];
Object.keys(players).forEach(playerName => {
if (players[playerName].transactions) {
players[playerName].transactions.forEach(transaction => {
allTransactions.push({
player: playerName,
...transaction
});
});
}
});
// Сортируем по дате (новые сверху)
allTransactions.sort((a, b) => new Date(b.date) - new Date(a.date));
if (allTransactions.length === 0) {
container.innerHTML = '<p>Нет транзакций</p>';
return;
}
let transactionsHTML = '';
allTransactions.slice(0, 5).forEach(transaction => {
const date = new Date(transaction.date).toLocaleDateString('ru-RU');
transactionsHTML += `
<div class="stat-item">
<span>${transaction.player}</span>
<span>+${transaction.earned}</span>
</div>
<div style="font-size: 0.8rem; color: #666; margin-bottom: 8px;">
${date}
</div>
`;
});
container.innerHTML = transactionsHTML;
}
// Сброс всех данных
function resetData() {
if (confirm('Вы уверены, что хотите удалить все данные? Это действие нельзя отменить.')) {
players = {};
localStorage.removeItem('roleplayPlayers');
updateActivePlayers();
updateRecentTransactions();
showNotification('Все данные были удалены', 'warning');
}
}
// Экспорт данных
function exportData() {
const dataStr = JSON.stringify(players, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'roleplay_bank_data.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showNotification('Данные успешно экспортированы', 'success');
}
// Импорт данных
function importData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = e => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function(event) {
try {
const importedData = JSON.parse(event.target.result);
players = importedData;
localStorage.setItem('roleplayPlayers', JSON.stringify(players));
updateActivePlayers();
updateRecentTransactions();
showNotification('Данные успешно импортированы', 'success');
} catch (error) {
showNotification('Ошибка при импорте данных: неверный формат файла', 'error');
}
};
reader.readAsText(file);
};
input.click();
}
// Показать уведомление
function showNotification(message, type) {
// Создаем уведомление, если его нет
let notification = document.querySelector('.notification');
if (!notification) {
notification = document.createElement('div');
notification.className = 'notification';
document.querySelector('.main-content').prepend(notification);
}
notification.textContent = message;
notification.className = `notification ${type}`;
// Автоматически скрываем через 5 секунд
setTimeout(() => {
notification.style.display = 'none';
}, 5000);
}
</script>
</body>
</html>[/html]