410 lines
13 KiB
HTML
410 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>МракоЗвон — управление</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||
background: #0f0f0f;
|
||
color: #fff;
|
||
padding: 16px;
|
||
margin: 0;
|
||
}
|
||
.container {
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
}
|
||
.card {
|
||
background: #1e1e1e;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
h2 {
|
||
font-size: 22px;
|
||
color: #6ab3f3;
|
||
}
|
||
h3 {
|
||
font-size: 18px;
|
||
margin-bottom: 12px;
|
||
color: #aaa;
|
||
}
|
||
.total {
|
||
background: #2d7a2d;
|
||
padding: 6px 14px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
.record-card {
|
||
background: #2a2a2a;
|
||
border-radius: 12px;
|
||
padding: 14px 16px;
|
||
margin-bottom: 10px;
|
||
border-left: 4px solid;
|
||
}
|
||
.record-card.phone {
|
||
border-left-color: #2d7a2d;
|
||
}
|
||
.record-card.qr {
|
||
border-left-color: #6ab3f3;
|
||
}
|
||
.record-card.both {
|
||
border-left-color: #f3b33d;
|
||
}
|
||
.phone-number {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
font-family: monospace;
|
||
color: #fff;
|
||
}
|
||
.qr-data {
|
||
font-size: 12px;
|
||
font-family: monospace;
|
||
color: #888;
|
||
word-break: break-all;
|
||
}
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
margin-right: 8px;
|
||
}
|
||
.badge-phone {
|
||
background: #2d7a2d;
|
||
color: #fff;
|
||
}
|
||
.badge-qr {
|
||
background: #6ab3f3;
|
||
color: #000;
|
||
}
|
||
.badge-both {
|
||
background: #f3b33d;
|
||
color: #000;
|
||
}
|
||
.meta {
|
||
font-size: 11px;
|
||
color: #888;
|
||
margin-top: 6px;
|
||
}
|
||
.btn-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
.show-qr-btn {
|
||
background: #6ab3f3;
|
||
border: none;
|
||
color: #000;
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
.delete-btn {
|
||
background: #dc2626;
|
||
border: none;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
.show-qr-btn:active {
|
||
background: #4a8bc0;
|
||
}
|
||
.delete-btn:active {
|
||
background: #b91c1c;
|
||
}
|
||
.loading {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #aaa;
|
||
}
|
||
.empty {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #666;
|
||
}
|
||
.tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.tab-btn {
|
||
flex: 1;
|
||
background: #2d2d2d;
|
||
border: none;
|
||
color: white;
|
||
padding: 12px;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
.tab-btn.active {
|
||
background: #2d7a2d;
|
||
}
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
</style>
|
||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="card">
|
||
<div class="header">
|
||
<h2>🌑 МракоЗвон</h2>
|
||
<div class="total" id="totalCount">Загрузка...</div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="showTab('all')">📋 Все</button>
|
||
<button class="tab-btn" onclick="showTab('phones')">📞 Номера</button>
|
||
<button class="tab-btn" onclick="showTab('qrs')">🃏 Карты</button>
|
||
<button class="tab-btn" onclick="showTab('both')">🔗 Номер+Карта</button>
|
||
</div>
|
||
|
||
<div id="tab-all" class="tab-content active">
|
||
<div id="list-all"></div>
|
||
</div>
|
||
<div id="tab-phones" class="tab-content">
|
||
<div id="list-phones"></div>
|
||
</div>
|
||
<div id="tab-qrs" class="tab-content">
|
||
<div id="list-qrs"></div>
|
||
</div>
|
||
<div id="tab-both" class="tab-content">
|
||
<div id="list-both"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const tg = window.Telegram.WebApp;
|
||
tg.expand();
|
||
|
||
let currentData = [];
|
||
|
||
async function loadData() {
|
||
try {
|
||
const response = await fetch('/get_records');
|
||
const data = await response.json();
|
||
currentData = data.records || [];
|
||
updateCounters();
|
||
renderAll();
|
||
renderPhones();
|
||
renderQRs();
|
||
renderBoth();
|
||
} catch (e) {
|
||
console.error(e);
|
||
document.getElementById('list-all').innerHTML = '<div class="empty">⚠️ Ошибка загрузки</div>';
|
||
}
|
||
}
|
||
|
||
function updateCounters() {
|
||
const total = currentData.length;
|
||
const phones = currentData.filter(r => r.type === 'phone').length;
|
||
const qrs = currentData.filter(r => r.type === 'qr').length;
|
||
const both = currentData.filter(r => r.type === 'both').length;
|
||
document.getElementById('totalCount').innerHTML = `📊 ${total} | 📞 ${phones} | 🃏 ${qrs} | 🔗 ${both}`;
|
||
}
|
||
|
||
function getTypeBadge(type) {
|
||
if (type === 'phone') return '<span class="badge badge-phone">📞 Только номер</span>';
|
||
if (type === 'qr') return '<span class="badge badge-qr">🃏 Только карта</span>';
|
||
return '<span class="badge badge-both">🔗 Номер + карта</span>';
|
||
}
|
||
|
||
function getRecordHTML(record) {
|
||
const created = new Date(record.created_at).toLocaleString('ru');
|
||
const used = record.used_at ? new Date(record.used_at).toLocaleString('ru') : '—';
|
||
const badge = getTypeBadge(record.type);
|
||
|
||
let content = '';
|
||
if (record.type === 'phone' || record.type === 'both') {
|
||
content += `<div class="phone-number">📱 ${record.phone}</div>`;
|
||
}
|
||
if (record.type === 'qr' || record.type === 'both') {
|
||
const shortData = record.qr_data ? record.qr_data.slice(0, 50) + (record.qr_data.length > 50 ? '...' : '') : '—';
|
||
content += `<div class="qr-data">🃏 Данные карты: ${shortData}</div>`;
|
||
}
|
||
|
||
const showQRBtn = (record.type === 'qr' || record.type === 'both') && record.qr_code ?
|
||
`<button class="show-qr-btn" data-id="${record.id}">🖼️ Показать QR</button>` : '';
|
||
|
||
return `
|
||
<div class="record-card ${record.type}">
|
||
${badge}
|
||
${content}
|
||
<div class="meta">➕ Добавлен: ${created}</div>
|
||
<div class="meta">🔄 Использован: ${record.usage_count} раз, последний: ${used}</div>
|
||
<div class="btn-group">
|
||
${showQRBtn}
|
||
<button class="delete-btn" data-id="${record.id}">🗑 Удалить</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderAll() {
|
||
const container = document.getElementById('list-all');
|
||
if (currentData.length === 0) {
|
||
container.innerHTML = '<div class="empty">📭 Нет записей</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = currentData.map(r => getRecordHTML(r)).join('');
|
||
attachEvents();
|
||
}
|
||
|
||
function renderPhones() {
|
||
const container = document.getElementById('list-phones');
|
||
const phones = currentData.filter(r => r.type === 'phone');
|
||
if (phones.length === 0) {
|
||
container.innerHTML = '<div class="empty">📭 Нет номеров</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = phones.map(r => getRecordHTML(r)).join('');
|
||
attachEvents();
|
||
}
|
||
|
||
function renderQRs() {
|
||
const container = document.getElementById('list-qrs');
|
||
const qrs = currentData.filter(r => r.type === 'qr');
|
||
if (qrs.length === 0) {
|
||
container.innerHTML = '<div class="empty">🃏 Нет бонусных карт</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = qrs.map(r => getRecordHTML(r)).join('');
|
||
attachEvents();
|
||
}
|
||
|
||
function renderBoth() {
|
||
const container = document.getElementById('list-both');
|
||
const both = currentData.filter(r => r.type === 'both');
|
||
if (both.length === 0) {
|
||
container.innerHTML = '<div class="empty">🔗 Нет связок номер+карта</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = both.map(r => getRecordHTML(r)).join('');
|
||
attachEvents();
|
||
}
|
||
|
||
async function showQRModal(recordId) {
|
||
try {
|
||
const response = await fetch(`/get_qr_code/${recordId}`, {
|
||
headers: {
|
||
'X-TG-Init-Data': tg.initData
|
||
}
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.qr_code) {
|
||
const modal = document.createElement('div');
|
||
modal.style.position = 'fixed';
|
||
modal.style.top = '0';
|
||
modal.style.left = '0';
|
||
modal.style.width = '100%';
|
||
modal.style.height = '100%';
|
||
modal.style.background = 'rgba(0,0,0,0.95)';
|
||
modal.style.display = 'flex';
|
||
modal.style.justifyContent = 'center';
|
||
modal.style.alignItems = 'center';
|
||
modal.style.zIndex = '1000';
|
||
modal.style.cursor = 'pointer';
|
||
|
||
const img = document.createElement('img');
|
||
img.src = data.qr_code;
|
||
img.style.maxWidth = '85%';
|
||
img.style.maxHeight = '85%';
|
||
img.style.borderRadius = '12px';
|
||
|
||
modal.onclick = () => modal.remove();
|
||
modal.appendChild(img);
|
||
document.body.appendChild(modal);
|
||
} else {
|
||
alert('QR-код отсутствует');
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
alert('Ошибка загрузки QR');
|
||
}
|
||
}
|
||
|
||
async function deleteRecord(recordId) {
|
||
if (!confirm('Удалить запись?')) return;
|
||
|
||
try {
|
||
const response = await fetch('/delete_record', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({id: recordId})
|
||
});
|
||
if (response.ok) {
|
||
loadData();
|
||
} else {
|
||
alert('Ошибка при удалении');
|
||
}
|
||
} catch (e) {
|
||
alert('Ошибка');
|
||
}
|
||
}
|
||
|
||
function attachEvents() {
|
||
document.querySelectorAll('.show-qr-btn').forEach(btn => {
|
||
btn.removeEventListener('click', btn._handler);
|
||
btn._handler = () => showQRModal(btn.dataset.id);
|
||
btn.addEventListener('click', btn._handler);
|
||
});
|
||
|
||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||
btn.removeEventListener('click', btn._handler);
|
||
btn._handler = () => deleteRecord(btn.dataset.id);
|
||
btn.addEventListener('click', btn._handler);
|
||
});
|
||
}
|
||
|
||
function showTab(tabName) {
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||
|
||
if (tabName === 'all') {
|
||
document.querySelector('.tab-btn:first-child').classList.add('active');
|
||
document.getElementById('tab-all').classList.add('active');
|
||
} else if (tabName === 'phones') {
|
||
document.querySelectorAll('.tab-btn')[1].classList.add('active');
|
||
document.getElementById('tab-phones').classList.add('active');
|
||
} else if (tabName === 'qrs') {
|
||
document.querySelectorAll('.tab-btn')[2].classList.add('active');
|
||
document.getElementById('tab-qrs').classList.add('active');
|
||
} else if (tabName === 'both') {
|
||
document.querySelectorAll('.tab-btn')[3].classList.add('active');
|
||
document.getElementById('tab-both').classList.add('active');
|
||
}
|
||
}
|
||
|
||
loadData();
|
||
</script>
|
||
</body>
|
||
</html> |