feat: improve responsive admin experience

This commit is contained in:
2026-08-04 16:47:14 +08:00
parent c71b1351d6
commit 631a362501
20 changed files with 522 additions and 801 deletions
+186 -24
View File
@@ -7,7 +7,7 @@
{% block content %}
<div>
<h1 class="text-3xl font-extrabold">管理台</h1>
<p class="mt-2 text-sm text-textSub">管理注册状态、密码重置和所有预测记录。</p>
<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">
@@ -23,43 +23,205 @@
</section>
<section class="mt-6 rounded-xl border border-line bg-white p-5 shadow-panel">
<h2 class="font-extrabold">用户密码重置</h2>
<p class="mt-1 text-sm text-textSub">向用户已验证邮箱发送重置通知。</p>
<div class="mt-4 overflow-x-auto">
<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>
<th class="p-2">文件</th>
<th class="p-2">时间</th>
<th class="p-2">下载</th>
</tr>
</thead>
<tbody>
{% for user in password_reset_users %}
{% for record in records %}
<tr class="border-b border-line">
<td class="p-2">{{ user.username }}</td>
<td class="p-2">{{ user.email or '未设置' }}</td>
<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">
{% 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() }}">
<button class="ui-btn ui-btn-sm ui-btn-secondary">发送重置通知</button>
</form>
{% endif %}
<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>
</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 overflow-x-auto">
<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></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></tr>{% endfor %}</tbody>
</table>
</div>
{{ render_pagination(pagination, 'main.admin_dashboard') }}
</section>
{% endblock %}