Add admin link IDs, tabs for extend/traffic, and pagination
This commit is contained in:
+77
-11
@@ -64,20 +64,62 @@ async def admin_dashboard(request: Request):
|
||||
return RedirectResponse("/admin/login", status_code=303)
|
||||
|
||||
settings = get_settings()
|
||||
tab = (request.query_params.get("type") or "extend").strip().lower()
|
||||
if tab not in ("extend", "traffic", "all"):
|
||||
tab = "extend"
|
||||
|
||||
try:
|
||||
page = max(1, int(request.query_params.get("page") or 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
per_page = 10
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
if tab == "all":
|
||||
count_sql = "SELECT COUNT(*) AS c FROM guest_links"
|
||||
list_sql = """
|
||||
SELECT l.*,
|
||||
(SELECT COUNT(*) FROM guest_claims c WHERE c.link_id = l.id) AS claims
|
||||
FROM guest_links l
|
||||
ORDER BY l.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
SELECT l.*,
|
||||
(SELECT COUNT(*) FROM guest_claims c WHERE c.link_id = l.id) AS claims
|
||||
FROM guest_links l
|
||||
ORDER BY l.id DESC
|
||||
count_args: tuple = ()
|
||||
list_args: tuple = (per_page, offset)
|
||||
else:
|
||||
count_sql = "SELECT COUNT(*) AS c FROM guest_links WHERE link_type = ?"
|
||||
list_sql = """
|
||||
SELECT l.*,
|
||||
(SELECT COUNT(*) FROM guest_claims c WHERE c.link_id = l.id) AS claims
|
||||
FROM guest_links l
|
||||
WHERE l.link_type = ?
|
||||
ORDER BY l.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
)
|
||||
count_args = (tab,)
|
||||
list_args = (tab, per_page, offset)
|
||||
|
||||
cursor = await db.execute(count_sql, count_args)
|
||||
total = int((await cursor.fetchone())["c"])
|
||||
cursor = await db.execute(list_sql, list_args)
|
||||
links = [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT link_type, COUNT(*) AS c FROM guest_links GROUP BY link_type"
|
||||
)
|
||||
counts = {"extend": 0, "traffic": 0, "all": 0}
|
||||
for row in await cursor.fetchall():
|
||||
counts[row["link_type"]] = int(row["c"])
|
||||
counts["all"] += int(row["c"])
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
total_pages = max(1, (total + per_page - 1) // per_page)
|
||||
if page > total_pages:
|
||||
page = total_pages
|
||||
|
||||
site_base = public_base_url(request)
|
||||
return templates.TemplateResponse(
|
||||
"admin/dashboard.html",
|
||||
@@ -87,6 +129,12 @@ async def admin_dashboard(request: Request):
|
||||
"links": links,
|
||||
"site_base": site_base,
|
||||
"flash": request.query_params.get("flash"),
|
||||
"tab": tab,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total": total,
|
||||
"total_pages": total_pages,
|
||||
"counts": counts,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -159,11 +207,19 @@ async def create_link(
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
return RedirectResponse("/admin?flash=created", status_code=303)
|
||||
return RedirectResponse(
|
||||
f"/admin?type={link_type}&flash=created", status_code=303
|
||||
)
|
||||
|
||||
|
||||
@router.post("/links/{link_id}/toggle")
|
||||
async def toggle_link(link_id: int, _admin: str = Depends(get_current_admin)):
|
||||
async def toggle_link(
|
||||
request: Request,
|
||||
link_id: int,
|
||||
_admin: str = Depends(get_current_admin),
|
||||
):
|
||||
tab = (request.query_params.get("type") or "extend").strip()
|
||||
page = request.query_params.get("page") or "1"
|
||||
db = await get_db()
|
||||
try:
|
||||
await db.execute(
|
||||
@@ -173,18 +229,28 @@ async def toggle_link(link_id: int, _admin: str = Depends(get_current_admin)):
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
return RedirectResponse("/admin?flash=toggled", status_code=303)
|
||||
return RedirectResponse(
|
||||
f"/admin?type={tab}&page={page}&flash=toggled", status_code=303
|
||||
)
|
||||
|
||||
|
||||
@router.post("/links/{link_id}/delete")
|
||||
async def delete_link(link_id: int, _admin: str = Depends(get_current_admin)):
|
||||
async def delete_link(
|
||||
request: Request,
|
||||
link_id: int,
|
||||
_admin: str = Depends(get_current_admin),
|
||||
):
|
||||
tab = (request.query_params.get("type") or "extend").strip()
|
||||
page = request.query_params.get("page") or "1"
|
||||
db = await get_db()
|
||||
try:
|
||||
await db.execute("DELETE FROM guest_links WHERE id = ?", (link_id,))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
return RedirectResponse("/admin?flash=deleted", status_code=303)
|
||||
return RedirectResponse(
|
||||
f"/admin?type={tab}&page={page}&flash=deleted", status_code=303
|
||||
)
|
||||
|
||||
|
||||
@router.get("/links/{link_id}", response_class=HTMLResponse)
|
||||
|
||||
@@ -530,6 +530,78 @@ th {
|
||||
max-width: min(520px, 100%);
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 1rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
border: 1px solid var(--line);
|
||||
color: var(--mist);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.admin-tab:hover {
|
||||
color: var(--fog);
|
||||
border-color: rgba(212, 255, 63, 0.35);
|
||||
}
|
||||
|
||||
.admin-tab.is-active {
|
||||
color: var(--ink);
|
||||
background: var(--signal);
|
||||
border-color: var(--signal);
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.id-pill {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-weight: 600;
|
||||
color: var(--signal);
|
||||
}
|
||||
|
||||
.list-meta {
|
||||
margin: 0 0 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.pager .btn {
|
||||
padding: 0.4rem 0.7rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.pager-current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
padding: 0.4rem 0.7rem;
|
||||
background: rgba(212, 255, 63, 0.15);
|
||||
color: var(--signal);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.login-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
<label>
|
||||
Направление
|
||||
<select name="link_type" required>
|
||||
<option value="extend">Продление VPN (дни)</option>
|
||||
<option value="traffic">Докупка трафика (ГБ)</option>
|
||||
<option value="extend" {% if tab == "extend" %}selected{% endif %}>Продление VPN (дни)</option>
|
||||
<option value="traffic" {% if tab == "traffic" %}selected{% endif %}>Докупка трафика (ГБ)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -49,7 +49,7 @@
|
||||
<label>
|
||||
Трафик тарифа (ГБ)
|
||||
<input type="number" name="plan_traffic_gb" min="0" value="100" />
|
||||
<span class="muted" style="font-size:0.75rem;">Только продление: лимит станет ровно столько (и сбросится счётчик). Пример: тариф 49₽ → 100</span>
|
||||
<span class="muted" style="font-size:0.75rem;">Только продление: лимит станет ровно столько (и сбросится счётчик)</span>
|
||||
</label>
|
||||
<label>
|
||||
Срок бонус-трафика (дней)
|
||||
@@ -77,10 +77,27 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="admin-tabs">
|
||||
<a class="admin-tab {% if tab == 'extend' %}is-active{% endif %}" href="/admin?type=extend">
|
||||
Продление <span class="tab-count">{{ counts.extend }}</span>
|
||||
</a>
|
||||
<a class="admin-tab {% if tab == 'traffic' %}is-active{% endif %}" href="/admin?type=traffic">
|
||||
Трафик <span class="tab-count">{{ counts.traffic }}</span>
|
||||
</a>
|
||||
<a class="admin-tab {% if tab == 'all' %}is-active{% endif %}" href="/admin?type=all">
|
||||
Все <span class="tab-count">{{ counts.all }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="muted list-meta">
|
||||
Показано {{ links|length }} из {{ total }} · страница {{ page }} / {{ total_pages }}
|
||||
</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Тип</th>
|
||||
<th>Заголовок</th>
|
||||
<th>Значение</th>
|
||||
@@ -93,6 +110,7 @@
|
||||
<tbody>
|
||||
{% for link in links %}
|
||||
<tr>
|
||||
<td><span class="id-pill">#{{ link.id }}</span></td>
|
||||
<td>
|
||||
{% if link.link_type == "extend" %}
|
||||
<span class="badge badge-extend">Продление</span>
|
||||
@@ -127,10 +145,10 @@
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<a class="btn btn-ghost" href="/admin/links/{{ link.id }}">История</a>
|
||||
<form method="post" action="/admin/links/{{ link.id }}/toggle">
|
||||
<form method="post" action="/admin/links/{{ link.id }}/toggle?type={{ tab }}&page={{ page }}">
|
||||
<button class="btn btn-ghost" type="submit">{% if link.is_active %}Выкл{% else %}Вкл{% endif %}</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/links/{{ link.id }}/delete" onsubmit="return confirm('Удалить ссылку?')">
|
||||
<form method="post" action="/admin/links/{{ link.id }}/delete?type={{ tab }}&page={{ page }}" onsubmit="return confirm('Удалить ссылку #{{ link.id }}?')">
|
||||
<button class="btn btn-danger" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -138,12 +156,32 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="muted">Пока нет ссылок — создайте первую выше.</td>
|
||||
<td colspan="8" class="muted">В этом разделе пока нет ссылок.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pager">
|
||||
{% if page > 1 %}
|
||||
<a class="btn btn-ghost" href="/admin?type={{ tab }}&page={{ page - 1 }}">← Назад</a>
|
||||
{% endif %}
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span class="pager-current">{{ p }}</span>
|
||||
{% elif p == 1 or p == total_pages or (p >= page - 2 and p <= page + 2) %}
|
||||
<a class="btn btn-ghost" href="/admin?type={{ tab }}&page={{ p }}">{{ p }}</a>
|
||||
{% elif p == page - 3 or p == page + 3 %}
|
||||
<span class="muted">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page < total_pages %}
|
||||
<a class="btn btn-ghost" href="/admin?type={{ tab }}&page={{ page + 1 }}">Вперёд →</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script src="/static/js/admin.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="panel-wrap">
|
||||
<header class="panel-header">
|
||||
<div>
|
||||
<h1>{{ link.title }}</h1>
|
||||
<h1>#{{ link.id }} · {{ link.title }}</h1>
|
||||
<div class="link-copy-row">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user