2026-01-12 00:53:31 +08:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
{% block title %}RBAC 权限中心 - AI 视界{% endblock %}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
{% block content %}
|
2026-01-17 23:15:58 +08:00
|
|
|
<div class="w-full h-full overflow-hidden flex flex-col p-6 lg:p-10">
|
|
|
|
|
<div class="max-w-7xl w-full mx-auto flex flex-col h-full space-y-6">
|
|
|
|
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="flex items-center justify-between shrink-0">
|
2026-01-12 00:53:31 +08:00
|
|
|
<div class="flex items-center gap-4">
|
2026-01-17 23:15:58 +08:00
|
|
|
<div
|
|
|
|
|
class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-200">
|
2026-01-12 00:53:31 +08:00
|
|
|
<i data-lucide="shield-check" class="w-7 h-7"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-01-17 23:15:58 +08:00
|
|
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight">权限控制中心</h1>
|
|
|
|
|
<p class="text-slate-400 text-sm font-medium">RBAC Role-Based Access Control</p>
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-17 23:15:58 +08:00
|
|
|
<!-- Tabs Switcher -->
|
|
|
|
|
<div class="bg-slate-100 p-1.5 rounded-xl flex font-bold text-sm">
|
|
|
|
|
<button onclick="switchTab('roles')" id="tab-roles"
|
|
|
|
|
class="px-6 py-2.5 rounded-lg bg-white text-slate-900 shadow-sm transition-all">角色定义</button>
|
|
|
|
|
<button onclick="switchTab('users')" id="tab-users"
|
|
|
|
|
class="px-6 py-2.5 rounded-lg text-slate-500 hover:text-slate-900 transition-all">用户授权</button>
|
|
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
<!-- Tab 1: Roles Management -->
|
|
|
|
|
<div id="view-roles" class="flex-1 flex gap-6 min-h-0 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
|
|
|
<!-- Left: Roles List -->
|
|
|
|
|
<div class="w-1/3 flex flex-col gap-4">
|
|
|
|
|
<div
|
|
|
|
|
class="bg-white rounded-3xl shadow-xl border border-slate-100 flex-1 flex flex-col overflow-hidden">
|
|
|
|
|
<div class="p-6 border-b border-slate-50 flex justify-between items-center bg-slate-50/50">
|
|
|
|
|
<h3 class="font-black text-slate-800">角色列表</h3>
|
|
|
|
|
<button onclick="createNewRole()"
|
|
|
|
|
class="p-2 bg-indigo-50 text-indigo-600 rounded-lg hover:bg-indigo-100 transition-colors">
|
|
|
|
|
<i data-lucide="plus" class="w-5 h-5"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="rolesList" class="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
|
|
|
|
|
<!-- Dynamic Roles -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Right: Role Editor -->
|
|
|
|
|
<div
|
|
|
|
|
class="w-2/3 bg-white rounded-3xl shadow-xl border border-slate-100 flex flex-col overflow-hidden relative">
|
|
|
|
|
<!-- Overlay for no selection -->
|
|
|
|
|
<div id="roleEditorEmpty"
|
|
|
|
|
class="absolute inset-0 bg-white z-10 flex flex-col items-center justify-center text-slate-300">
|
|
|
|
|
<i data-lucide="shield" class="w-16 h-16 mb-4 opacity-50"></i>
|
|
|
|
|
<p class="font-bold">请选择左侧角色或创建新角色</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="p-8 h-full overflow-y-auto custom-scrollbar">
|
|
|
|
|
<div class="flex items-center justify-between mb-8">
|
|
|
|
|
<h2 id="editorTitle" class="text-xl font-black text-slate-900">编辑角色</h2>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button id="btnDeleteRole"
|
|
|
|
|
class="hidden px-4 py-2 text-rose-500 bg-rose-50 hover:bg-rose-100 rounded-xl font-bold text-xs transition-colors">
|
|
|
|
|
删除角色
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick="saveRole()"
|
|
|
|
|
class="px-6 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-200 transition-all">
|
|
|
|
|
保存配置 (Save)
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-6">
|
2026-01-12 00:53:31 +08:00
|
|
|
<input type="hidden" id="editRoleId">
|
2026-01-17 23:15:58 +08:00
|
|
|
<div class="grid grid-cols-2 gap-6">
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">角色名称</label>
|
|
|
|
|
<input type="text" id="editRoleName" placeholder="如: 运营专员"
|
|
|
|
|
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-bold outline-none focus:border-indigo-500 transition-all">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">描述备注</label>
|
|
|
|
|
<input type="text" id="editRoleDesc" placeholder="简要描述职能"
|
|
|
|
|
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-medium outline-none focus:border-indigo-500 transition-all">
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
<div class="space-y-4">
|
|
|
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">权限分配
|
|
|
|
|
(Permissions)</label>
|
|
|
|
|
<div id="permissionsMatrix" class="grid grid-cols-2 gap-6">
|
|
|
|
|
<!-- Dynamic Permissions Groups -->
|
|
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-17 23:15:58 +08:00
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
<!-- Tab 2: Users Authorization -->
|
|
|
|
|
<div id="view-users"
|
|
|
|
|
class="hidden flex-1 flex flex-col min-h-0 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
|
|
|
<div class="bg-white rounded-3xl shadow-xl border border-slate-100 flex-1 flex flex-col overflow-hidden">
|
|
|
|
|
<!-- Toolbar -->
|
|
|
|
|
<div class="p-6 border-b border-slate-50 flex items-center justify-between bg-slate-50/30">
|
|
|
|
|
<div class="relative w-96">
|
|
|
|
|
<i data-lucide="search"
|
|
|
|
|
class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
|
|
|
|
|
<input type="text" id="userSearch" onkeyup="if(event.key === 'Enter') searchUsers()"
|
|
|
|
|
placeholder="搜索用户手机号..."
|
|
|
|
|
class="w-full pl-10 pr-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all">
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
2026-01-17 23:15:58 +08:00
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<button onclick="searchUsers()"
|
|
|
|
|
class="px-5 py-3 bg-slate-900 text-white rounded-xl font-bold text-sm hover:bg-slate-800 transition-all shadow-lg">查询</button>
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
<!-- Table -->
|
|
|
|
|
<div class="flex-1 overflow-auto">
|
|
|
|
|
<table class="w-full text-left border-collapse">
|
|
|
|
|
<thead
|
|
|
|
|
class="sticky top-0 bg-white z-10 after:content-[''] after:absolute after:bottom-0 after:left-0 after:right-0 after:border-b after:border-slate-100">
|
|
|
|
|
<tr>
|
|
|
|
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">用户
|
|
|
|
|
ID</th>
|
|
|
|
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
|
|
|
|
手机号</th>
|
|
|
|
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
|
|
|
|
当前角色</th>
|
|
|
|
|
<th
|
|
|
|
|
class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">
|
|
|
|
|
操作 (分配角色)</th>
|
2026-01-12 00:53:31 +08:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
2026-01-17 23:15:58 +08:00
|
|
|
<tbody id="usersTableBody">
|
|
|
|
|
<!-- Dynamic Users -->
|
2026-01-12 00:53:31 +08:00
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
|
|
|
<!-- Pagination -->
|
|
|
|
|
<div class="p-4 border-t border-slate-50 flex items-center justify-between bg-slate-50/50">
|
|
|
|
|
<span class="text-xs font-bold text-slate-400" id="pageInfo">共 0 条数据</span>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<button onclick="changePage(-1)"
|
|
|
|
|
class="p-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50"><i
|
|
|
|
|
data-lucide="chevron-left" class="w-4 h-4"></i></button>
|
|
|
|
|
<button onclick="changePage(1)"
|
|
|
|
|
class="p-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50"><i
|
|
|
|
|
data-lucide="chevron-right" class="w-4 h-4"></i></button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-17 23:15:58 +08:00
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block scripts %}
|
|
|
|
|
<script>
|
2026-01-17 23:15:58 +08:00
|
|
|
// State
|
|
|
|
|
let currentTab = 'roles';
|
2026-01-12 00:53:31 +08:00
|
|
|
let allRoles = [];
|
2026-01-17 23:15:58 +08:00
|
|
|
let allPermissions = [];
|
|
|
|
|
let currentRole = null;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
let userPage = 1;
|
|
|
|
|
let userTotalPages = 1;
|
|
|
|
|
|
|
|
|
|
// --- Tab Logic ---
|
|
|
|
|
function switchTab(tab) {
|
|
|
|
|
currentTab = tab;
|
|
|
|
|
document.querySelectorAll('[id^="tab-"]').forEach(el => {
|
|
|
|
|
if (el.id === `tab-${tab}`) {
|
|
|
|
|
el.classList.remove('bg-white', 'text-slate-500');
|
|
|
|
|
el.classList.add('bg-white', 'text-slate-900', 'shadow-sm'); // Actually active doesn't have bg-gray-100, just white + shadow
|
|
|
|
|
} else {
|
|
|
|
|
el.classList.add('text-slate-500');
|
|
|
|
|
el.classList.remove('bg-white', 'text-slate-900', 'shadow-sm');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('view-roles').classList.toggle('hidden', tab !== 'roles');
|
|
|
|
|
document.getElementById('view-users').classList.toggle('hidden', tab !== 'users');
|
|
|
|
|
|
|
|
|
|
if (tab === 'users') loadUsers();
|
2026-01-12 00:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
// --- Logic: Roles ---
|
|
|
|
|
async function initRolesView() {
|
|
|
|
|
const [pRes, rRes] = await Promise.all([
|
|
|
|
|
fetch('/api/admin/permissions').then(r => r.json()),
|
|
|
|
|
fetch('/api/admin/roles').then(r => r.json())
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
allPermissions = pRes.permissions;
|
|
|
|
|
allRoles = rRes.roles;
|
|
|
|
|
renderRolesList();
|
|
|
|
|
renderPermissionsMatrix(); // With no checked
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderRolesList() {
|
|
|
|
|
const container = document.getElementById('rolesList');
|
|
|
|
|
container.innerHTML = allRoles.map(role => `
|
|
|
|
|
<div onclick='selectRole(${role.id})'
|
|
|
|
|
class="p-4 rounded-2xl cursor-pointer border-2 transition-all group hover:bg-slate-50
|
|
|
|
|
${currentRole && currentRole.id === role.id ? 'border-indigo-600 bg-indigo-50/30' : 'border-transparent bg-slate-50'}">
|
|
|
|
|
<div class="flex items-center justify-between mb-1">
|
|
|
|
|
<span class="font-bold text-slate-800">${role.name}</span>
|
|
|
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-slate-300 ${currentRole && currentRole.id === role.id ? 'text-indigo-600' : ''}"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-xs text-slate-400 truncate">${role.description || '无描述'}</div>
|
|
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
`).join('');
|
|
|
|
|
lucide.createIcons();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
function selectRole(id) {
|
|
|
|
|
currentRole = allRoles.find(r => r.id === id);
|
|
|
|
|
renderRolesList(); // Refresh active state
|
|
|
|
|
|
|
|
|
|
document.getElementById('roleEditorEmpty').classList.add('hidden');
|
|
|
|
|
document.getElementById('editorTitle').innerText = '编辑角色';
|
|
|
|
|
document.getElementById('editRoleId').value = currentRole.id;
|
|
|
|
|
document.getElementById('editRoleName').value = currentRole.name;
|
|
|
|
|
document.getElementById('editRoleDesc').value = currentRole.description || '';
|
|
|
|
|
|
|
|
|
|
// Delete button
|
|
|
|
|
const delBtn = document.getElementById('btnDeleteRole');
|
|
|
|
|
if (currentRole.name === '超级管理员') {
|
|
|
|
|
delBtn.classList.add('hidden');
|
|
|
|
|
} else {
|
|
|
|
|
delBtn.classList.remove('hidden');
|
|
|
|
|
delBtn.onclick = () => deleteRole(currentRole.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check permissions
|
|
|
|
|
document.querySelectorAll('.perm-check').forEach(cb => {
|
|
|
|
|
cb.checked = currentRole.permissions.includes(cb.value);
|
2026-01-12 00:53:31 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
function createNewRole() {
|
|
|
|
|
currentRole = { id: '', name: '', description: '', permissions: [] };
|
|
|
|
|
renderRolesList(); // Clear active selection visual
|
2026-01-12 00:53:31 +08:00
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
document.getElementById('roleEditorEmpty').classList.add('hidden');
|
|
|
|
|
document.getElementById('editorTitle').innerText = '创建新角色';
|
2026-01-12 00:53:31 +08:00
|
|
|
document.getElementById('editRoleId').value = '';
|
2026-01-17 23:15:58 +08:00
|
|
|
document.getElementById('editRoleName').value = '';
|
|
|
|
|
document.getElementById('editRoleDesc').value = '';
|
|
|
|
|
document.getElementById('btnDeleteRole').classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.perm-check').forEach(cb => cb.checked = false);
|
|
|
|
|
document.getElementById('editRoleName').focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPermissionsMatrix() {
|
|
|
|
|
// Simple grouping logic based on name prefix
|
|
|
|
|
const groups = {};
|
|
|
|
|
allPermissions.forEach(p => {
|
|
|
|
|
const prefix = p.name.split('_')[0]; // manage, view, etc.
|
|
|
|
|
let groupName = '其他权限';
|
|
|
|
|
if (p.name.includes('user')) groupName = '用户管理';
|
|
|
|
|
else if (p.name.includes('role') || p.name.includes('rbac')) groupName = '角色权限';
|
|
|
|
|
else if (p.name.includes('dict') || p.name.includes('system') || p.name.includes('notif')) groupName = '系统配置';
|
|
|
|
|
else if (p.name.includes('log')) groupName = '日志审计';
|
|
|
|
|
else if (p.name.includes('order')) groupName = '订单财务';
|
|
|
|
|
|
|
|
|
|
if (!groups[groupName]) groups[groupName] = [];
|
|
|
|
|
groups[groupName].push(p);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById('permissionsMatrix');
|
|
|
|
|
container.innerHTML = Object.keys(groups).map(gName => `
|
|
|
|
|
<div class="bg-slate-50 p-5 rounded-2xl border border-slate-100">
|
|
|
|
|
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4 border-b border-slate-200 pb-2">${gName}</h4>
|
|
|
|
|
<div class="grid grid-cols-1 gap-3">
|
|
|
|
|
${groups[gName].map(p => `
|
|
|
|
|
<label class="flex items-center justify-between cursor-pointer group">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="text-sm font-bold text-slate-700">${p.description || p.name}</div>
|
|
|
|
|
<div class="text-[10px] text-slate-400 font-mono">${p.name}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="relative inline-flex items-center cursor-pointer">
|
|
|
|
|
<input type="checkbox" value="${p.name}" class="perm-check sr-only peer">
|
|
|
|
|
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none ring-4 ring-indigo-500/10 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveRole() {
|
|
|
|
|
const id = document.getElementById('editRoleId').value;
|
|
|
|
|
const name = document.getElementById('editRoleName').value;
|
|
|
|
|
const description = document.getElementById('editRoleDesc').value;
|
|
|
|
|
const permissions = Array.from(document.querySelectorAll('.perm-check:checked')).map(cb => cb.value);
|
|
|
|
|
|
|
|
|
|
if (!name) return showToast('角色名不能为空', 'warning');
|
|
|
|
|
|
|
|
|
|
const r = await fetch('/api/admin/roles', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ id, name, description, permissions })
|
|
|
|
|
});
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
|
|
|
|
if (d.message) {
|
|
|
|
|
showToast('✅ 保存成功', 'success');
|
|
|
|
|
// Reload roles but keep editing
|
|
|
|
|
const rRes = await fetch('/api/admin/roles').then(r => r.json());
|
|
|
|
|
allRoles = rRes.roles;
|
|
|
|
|
|
|
|
|
|
// If created new, find it
|
|
|
|
|
if (!id) {
|
|
|
|
|
const newRole = allRoles.find(r => r.name === name);
|
|
|
|
|
if (newRole) selectRole(newRole.id);
|
|
|
|
|
else renderRolesList();
|
|
|
|
|
} else {
|
|
|
|
|
renderRolesList();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteRole(id) {
|
2026-01-17 23:15:58 +08:00
|
|
|
if (!confirm('确定删除此角色吗?')) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
const r = await fetch('/api/admin/roles/delete', {
|
2026-01-17 23:15:58 +08:00
|
|
|
method: 'POST', body: JSON.stringify({ id }), headers: { 'Content-Type': 'application/json' }
|
2026-01-12 00:53:31 +08:00
|
|
|
});
|
|
|
|
|
const d = await r.json();
|
2026-01-17 23:15:58 +08:00
|
|
|
if (d.message) {
|
|
|
|
|
showToast('已删除', 'success');
|
|
|
|
|
const rRes = await fetch('/api/admin/roles').then(r => r.json());
|
|
|
|
|
allRoles = rRes.roles;
|
|
|
|
|
createNewRole(); // Reset view
|
|
|
|
|
renderRolesList();
|
2026-01-12 00:53:31 +08:00
|
|
|
} else {
|
|
|
|
|
showToast(d.error, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
// --- Logic: Users ---
|
2026-01-12 00:53:31 +08:00
|
|
|
async function loadUsers() {
|
2026-01-17 23:15:58 +08:00
|
|
|
const q = document.getElementById('userSearch').value;
|
|
|
|
|
const r = await fetch(`/api/admin/users?page=${userPage}&per_page=10&q=${encodeURIComponent(q)}`);
|
2026-01-12 00:53:31 +08:00
|
|
|
const d = await r.json();
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
|
|
|
userTotalPages = d.pages;
|
|
|
|
|
document.getElementById('pageInfo').innerText = `第 ${d.current_page} / ${d.pages} 页 (共 ${d.total} 人)`;
|
|
|
|
|
|
|
|
|
|
const tbody = document.getElementById('usersTableBody');
|
|
|
|
|
tbody.innerHTML = d.users.map(user => `
|
|
|
|
|
<tr class="border-b border-slate-50 hover:bg-slate-50/50 ${user.is_banned ? 'opacity-50' : ''}">
|
|
|
|
|
<td class="px-8 py-4 text-xs font-mono text-slate-400">${user.id}</td>
|
|
|
|
|
<td class="px-8 py-4 text-sm font-bold ${user.is_banned ? 'text-rose-500' : 'text-slate-700'}">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
${user.phone}
|
|
|
|
|
${user.is_banned ? '<span class="px-1.5 py-0.5 bg-rose-50 text-rose-500 text-[9px] font-black rounded uppercase border border-rose-100/50">BANNED</span>' : ''}
|
|
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
</td>
|
|
|
|
|
<td class="px-8 py-4">
|
2026-01-17 23:15:58 +08:00
|
|
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold ${user.role === '未分配' ? 'bg-slate-100 text-slate-500' : 'bg-indigo-50 text-indigo-600'}">
|
|
|
|
|
${user.role === '未分配' ? '<i data-lucide="alert-circle" class="w-3 h-3"></i>' : '<i data-lucide="shield-check" class="w-3 h-3"></i>'}
|
|
|
|
|
${user.role}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-8 py-4 text-right">
|
|
|
|
|
<div class="flex items-center justify-end gap-3">
|
|
|
|
|
<button onclick="toggleBan(${user.id})" class="px-3 py-1.5 rounded-lg text-[10px] font-black uppercase transition-all ${user.is_banned ? 'bg-emerald-50 text-emerald-600 hover:bg-emerald-100' : 'bg-rose-50 text-rose-500 hover:bg-rose-100'}">
|
|
|
|
|
${user.is_banned ? '解封 (Unban)' : '封禁 (Ban)'}
|
|
|
|
|
</button>
|
|
|
|
|
<select onchange="assignUserRole(${user.id}, this.value)" class="bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-xs font-bold outline-none hover:border-indigo-400 focus:border-indigo-500 transition-colors cursor-pointer">
|
|
|
|
|
<option value="">${user.role_id ? '变更角色...' : '分配角色...'}</option>
|
|
|
|
|
${allRoles.map(role => `<option value="${role.id}" ${user.role_id === role.id ? 'selected hidden' : ''}>${role.name}</option>`).join('')}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2026-01-12 00:53:31 +08:00
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
2026-01-17 23:15:58 +08:00
|
|
|
lucide.createIcons();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function searchUsers() {
|
|
|
|
|
userPage = 1;
|
|
|
|
|
loadUsers();
|
2026-01-12 00:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
function changePage(delta) {
|
|
|
|
|
const newPage = userPage + delta;
|
|
|
|
|
if (newPage >= 1 && newPage <= userTotalPages) {
|
|
|
|
|
userPage = newPage;
|
|
|
|
|
loadUsers();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function assignUserRole(uid, rid) {
|
|
|
|
|
if (!rid) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
const r = await fetch('/api/admin/users/assign', {
|
|
|
|
|
method: 'POST',
|
2026-01-17 23:15:58 +08:00
|
|
|
body: JSON.stringify({ user_id: uid, role_id: rid }),
|
|
|
|
|
headers: { 'Content-Type': 'application/json' }
|
2026-01-12 00:53:31 +08:00
|
|
|
});
|
|
|
|
|
const d = await r.json();
|
2026-01-17 23:15:58 +08:00
|
|
|
if (d.message) {
|
|
|
|
|
showToast('✅ 授权成功', 'success');
|
|
|
|
|
loadUsers(); // Refresh
|
2026-01-12 00:53:31 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 23:15:58 +08:00
|
|
|
async function toggleBan(uid) {
|
|
|
|
|
const r = await fetch('/api/admin/users/toggle_ban', {
|
2026-01-12 00:53:31 +08:00
|
|
|
method: 'POST',
|
2026-01-17 23:15:58 +08:00
|
|
|
body: JSON.stringify({ user_id: uid }),
|
|
|
|
|
headers: { 'Content-Type': 'application/json' }
|
2026-01-12 00:53:31 +08:00
|
|
|
});
|
|
|
|
|
const d = await r.json();
|
2026-01-17 23:15:58 +08:00
|
|
|
if (d.message) {
|
2026-01-12 00:53:31 +08:00
|
|
|
showToast(d.message, 'success');
|
|
|
|
|
loadUsers();
|
|
|
|
|
} else {
|
|
|
|
|
showToast(d.error, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
|
|
|
// Init
|
|
|
|
|
initRolesView();
|
2026-01-12 00:53:31 +08:00
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|