228 lines
12 KiB
HTML
228 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_pagination.html" import render_pagination %}
|
|
|
|
{% set active_page = "admin" %}
|
|
{% block title %}管理台{% endblock %}
|
|
|
|
{% block content %}
|
|
<div>
|
|
<h1 class="text-3xl font-extrabold">管理台</h1>
|
|
<p class="mt-2 text-sm text-textSub">管理用户账号、注册状态和预测记录。</p>
|
|
</div>
|
|
|
|
<section class="mt-6 rounded-xl border border-line bg-white p-5 shadow-panel">
|
|
<h2 class="font-extrabold">用户注册</h2>
|
|
<form method="post" action="{{ url_for('main.update_registration_setting') }}" class="mt-3 flex items-center gap-3">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<label class="text-sm">
|
|
<input type="checkbox" name="allow_registration" {{ 'checked' if registration_allowed }}>
|
|
允许自助注册
|
|
</label>
|
|
<button class="ui-btn ui-btn-sm ui-btn-primary">保存</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="mt-6 rounded-xl border border-line bg-white p-5 shadow-panel">
|
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
<div>
|
|
<h2 class="font-extrabold">用户管理</h2>
|
|
<p class="mt-1 text-sm text-textSub">查看用户状态,发送重置通知或撤销受信设备。</p>
|
|
</div>
|
|
<form method="get" class="flex w-full gap-2 sm:w-auto">
|
|
<input
|
|
name="q"
|
|
value="{{ user_search }}"
|
|
class="min-w-0 flex-1 rounded-lg border border-line px-3 py-2 text-sm sm:w-64"
|
|
placeholder="搜索显示名或邮箱"
|
|
>
|
|
<button class="ui-btn ui-btn-sm ui-btn-secondary">搜索</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="mt-4 space-y-3 lg:hidden">
|
|
{% for user in user_pagination.items %}
|
|
<article class="rounded-lg border border-line bg-slate-50 p-4">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
<h3 class="truncate font-bold">{{ user.username }}</h3>
|
|
<p class="mt-1 break-all text-xs text-textSub">{{ user.email or '未设置邮箱' }}</p>
|
|
</div>
|
|
<span class="shrink-0 rounded-full px-2 py-1 text-xs font-bold {{ 'bg-successSoft text-successText' if user.is_active_account else 'bg-dangerSoft text-dangerText' }}">
|
|
{{ '已启用' if user.is_active_account else '已停用' }}
|
|
</span>
|
|
</div>
|
|
<dl class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
|
<div><dt class="text-xs text-textSub">邮箱状态</dt><dd class="mt-1 font-semibold">{{ '已验证' if user.email_verified_at else '未验证' }}</dd></div>
|
|
<div><dt class="text-xs text-textSub">受信设备</dt><dd class="mt-1 font-semibold">{{ active_device_counts.get(user.id, 0) }} 台</dd></div>
|
|
<div class="col-span-2"><dt class="text-xs text-textSub">注册时间</dt><dd class="mt-1 text-textMain">{{ format_datetime(user.created_at) }}</dd></div>
|
|
</dl>
|
|
<div class="mt-4 grid gap-2 sm:grid-cols-3">
|
|
{% if user.email %}
|
|
<form method="post" action="{{ url_for('main.admin_password_reset', user_id=user.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_page" value="{{ user_pagination.page }}">
|
|
<input type="hidden" name="q" value="{{ user_search }}">
|
|
<button class="ui-btn ui-btn-compact ui-btn-secondary w-full">重置通知</button>
|
|
</form>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('main.admin_revoke_trusted_devices', user_id=user.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_page" value="{{ user_pagination.page }}">
|
|
<input type="hidden" name="q" value="{{ user_search }}">
|
|
<button class="ui-btn ui-btn-compact ui-btn-secondary w-full">撤销设备</button>
|
|
</form>
|
|
<form method="post" action="{{ url_for('main.admin_update_account_status', user_id=user.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_page" value="{{ user_pagination.page }}">
|
|
<input type="hidden" name="q" value="{{ user_search }}">
|
|
<input type="hidden" name="is_active" value="{{ 'false' if user.is_active_account else 'true' }}">
|
|
<button class="ui-btn ui-btn-compact w-full {{ 'ui-btn-secondary' if user.is_active_account else 'ui-btn-primary' }}">{{ '停用账号' if user.is_active_account else '启用账号' }}</button>
|
|
</form>
|
|
</div>
|
|
</article>
|
|
{% else %}
|
|
<p class="rounded-lg border border-dashed border-line px-3 py-8 text-center text-sm text-textSub">未找到匹配用户。</p>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="mt-4 hidden overflow-x-auto lg:block">
|
|
<table class="min-w-full text-sm">
|
|
<thead class="bg-slate-50 text-left text-xs font-bold uppercase tracking-[.08em] text-textSub">
|
|
<tr>
|
|
<th class="px-3 py-3">用户</th>
|
|
<th class="px-3 py-3">邮箱状态</th>
|
|
<th class="px-3 py-3">账号状态</th>
|
|
<th class="px-3 py-3">受信设备</th>
|
|
<th class="px-3 py-3">注册时间</th>
|
|
<th class="px-3 py-3">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-line">
|
|
{% for user in user_pagination.items %}
|
|
<tr>
|
|
<td class="px-3 py-3">
|
|
<div class="font-bold">{{ user.username }}</div>
|
|
<div class="mt-1 text-xs text-textSub">{{ user.email or '未设置邮箱' }}</div>
|
|
</td>
|
|
<td class="px-3 py-3">
|
|
<span class="rounded-full px-2 py-1 text-xs font-bold {{ 'bg-successSoft text-successText' if user.email_verified_at else 'bg-warnSoft text-warnText' }}">
|
|
{{ '已验证' if user.email_verified_at else '未验证' }}
|
|
</span>
|
|
</td>
|
|
<td class="px-3 py-3">
|
|
<span class="rounded-full px-2 py-1 text-xs font-bold {{ 'bg-successSoft text-successText' if user.is_active_account else 'bg-dangerSoft text-dangerText' }}">
|
|
{{ '已启用' if user.is_active_account else '已停用' }}
|
|
</span>
|
|
</td>
|
|
<td class="px-3 py-3 font-semibold">{{ active_device_counts.get(user.id, 0) }}</td>
|
|
<td class="whitespace-nowrap px-3 py-3 text-textSub">{{ format_datetime(user.created_at) }}</td>
|
|
<td class="px-3 py-3">
|
|
<div class="flex min-w-[260px] flex-wrap gap-2">
|
|
{% if user.email %}
|
|
<form method="post" action="{{ url_for('main.admin_password_reset', user_id=user.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_page" value="{{ user_pagination.page }}">
|
|
<input type="hidden" name="q" value="{{ user_search }}">
|
|
<button class="ui-btn ui-btn-compact ui-btn-secondary">重置通知</button>
|
|
</form>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('main.admin_revoke_trusted_devices', user_id=user.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_page" value="{{ user_pagination.page }}">
|
|
<input type="hidden" name="q" value="{{ user_search }}">
|
|
<button class="ui-btn ui-btn-compact ui-btn-secondary">撤销设备</button>
|
|
</form>
|
|
<form method="post" action="{{ url_for('main.admin_update_account_status', user_id=user.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_page" value="{{ user_pagination.page }}">
|
|
<input type="hidden" name="q" value="{{ user_search }}">
|
|
<input type="hidden" name="is_active" value="{{ 'false' if user.is_active_account else 'true' }}">
|
|
<button class="ui-btn ui-btn-compact {{ 'ui-btn-secondary' if user.is_active_account else 'ui-btn-primary' }}">
|
|
{{ '停用账号' if user.is_active_account else '启用账号' }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="6" class="px-3 py-8 text-center text-sm text-textSub">未找到匹配用户。</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% if user_pagination.total %}
|
|
<nav class="flex flex-col gap-3 border-t border-line px-1 pt-4 text-sm sm:flex-row sm:items-center sm:justify-between" aria-label="用户分页">
|
|
<span class="text-textSub">共 {{ user_pagination.total }} 位用户,第 {{ user_pagination.page }} / {{ user_pagination.pages }} 页</span>
|
|
<div class="flex gap-2">
|
|
<a
|
|
class="ui-btn ui-btn-sm ui-btn-secondary {{ 'pointer-events-none opacity-50' if not user_pagination.has_prev }}"
|
|
href="{{ url_for('main.admin_dashboard', user_page=user_pagination.prev_num if user_pagination.has_prev else user_pagination.page, q=user_search) }}"
|
|
>上一页</a>
|
|
<a
|
|
class="ui-btn ui-btn-sm ui-btn-secondary {{ 'pointer-events-none opacity-50' if not user_pagination.has_next }}"
|
|
href="{{ url_for('main.admin_dashboard', user_page=user_pagination.next_num if user_pagination.has_next else user_pagination.page, q=user_search) }}"
|
|
>下一页</a>
|
|
</div>
|
|
</nav>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<section class="mt-6 rounded-xl border border-line bg-white p-5 shadow-panel">
|
|
<h2 class="font-extrabold">上传记录</h2>
|
|
<div class="mt-4 space-y-3 lg:hidden">
|
|
{% for record in records %}
|
|
<article class="rounded-lg border border-line bg-slate-50 p-4">
|
|
<h3 class="break-all font-bold">{{ record.original_filename }}</h3>
|
|
<dl class="mt-3 grid gap-2 text-sm">
|
|
<div class="flex justify-between gap-4"><dt class="text-textSub">用户</dt><dd class="font-semibold">{{ record.user.username }}</dd></div>
|
|
<div class="flex justify-between gap-4"><dt class="text-textSub">上传时间</dt><dd class="text-right">{{ format_datetime(record.upload_time) }}</dd></div>
|
|
</dl>
|
|
<div class="mt-4 grid grid-cols-2 gap-2">
|
|
<a class="ui-btn ui-btn-compact ui-btn-secondary w-full" href="{{ url_for('main.download_file', record_id=record.id, file_type='original') }}" data-download-action data-download-pending="下载中...">
|
|
<span class="material-symbols-outlined text-lg">description</span>
|
|
原始文件
|
|
</a>
|
|
<a class="ui-btn ui-btn-compact ui-btn-primary w-full" href="{{ url_for('main.download_file', record_id=record.id, file_type='prediction') }}" data-download-action data-download-pending="下载中...">
|
|
<span class="material-symbols-outlined text-lg">download</span>
|
|
预测结果
|
|
</a>
|
|
</div>
|
|
</article>
|
|
{% else %}
|
|
<p class="rounded-lg border border-dashed border-line px-3 py-8 text-center text-sm text-textSub">暂无上传记录。</p>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="mt-4 hidden overflow-x-auto lg:block">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b border-line text-left">
|
|
<th class="p-2">用户</th>
|
|
<th class="p-2">文件</th>
|
|
<th class="p-2">时间</th>
|
|
<th class="p-2">下载</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for record in records %}
|
|
<tr class="border-b border-line">
|
|
<td class="p-2">{{ record.user.username }}</td>
|
|
<td class="p-2">{{ record.original_filename }}</td>
|
|
<td class="p-2">{{ format_datetime(record.upload_time) }}</td>
|
|
<td class="p-2">
|
|
<div class="flex min-w-[190px] gap-2">
|
|
<a class="ui-btn ui-btn-compact ui-btn-secondary" href="{{ url_for('main.download_file', record_id=record.id, file_type='original') }}" data-download-action data-download-pending="下载中...">原始文件</a>
|
|
<a class="ui-btn ui-btn-compact ui-btn-primary" href="{{ url_for('main.download_file', record_id=record.id, file_type='prediction') }}" data-download-action data-download-pending="下载中...">预测结果</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ render_pagination(pagination, 'main.admin_dashboard') }}
|
|
</section>
|
|
{% endblock %}
|