feat: improve admin user management with filters and pagination

This commit is contained in:
2026-02-17 20:13:32 +08:00
parent c506cf83be
commit 1eae645bfd
2 changed files with 717 additions and 109 deletions

View File

@@ -141,6 +141,16 @@ createApp({
// 管理员
adminUsers: [],
adminUsersLoading: false,
adminUsersPage: 1,
adminUsersPageSize: 20,
adminUserFilters: {
keyword: '',
role: 'all', // all/admin/user
status: 'all', // all/active/banned/unverified/download_blocked
storage: 'all', // all/local/oss/local_only/oss_only/user_choice
sort: 'created_desc' // created_desc/created_asc/username_asc/username_desc/storage_usage_desc/download_usage_desc
},
showResetPwdModal: false,
resetPwdUser: {},
newPassword: '',
@@ -559,6 +569,132 @@ createApp({
});
return list;
},
adminUsersFiltered() {
let list = Array.isArray(this.adminUsers) ? [...this.adminUsers] : [];
const keyword = (this.adminUserFilters.keyword || '').trim().toLowerCase();
if (keyword) {
list = list.filter((u) => {
const idText = String(u?.id || '');
const usernameText = String(u?.username || '').toLowerCase();
const emailText = String(u?.email || '').toLowerCase();
return idText.includes(keyword) || usernameText.includes(keyword) || emailText.includes(keyword);
});
}
if (this.adminUserFilters.role === 'admin') {
list = list.filter((u) => !!u?.is_admin);
} else if (this.adminUserFilters.role === 'user') {
list = list.filter((u) => !u?.is_admin);
}
if (this.adminUserFilters.status !== 'all') {
list = list.filter((u) => this.getAdminUserStatusTag(u) === this.adminUserFilters.status);
}
if (this.adminUserFilters.storage !== 'all') {
const selectedStorage = this.adminUserFilters.storage;
list = list.filter((u) => {
const currentStorage = u?.current_storage_type || 'oss';
const storagePermission = u?.storage_permission || 'oss_only';
if (selectedStorage === 'local' || selectedStorage === 'oss') {
return currentStorage === selectedStorage;
}
return storagePermission === selectedStorage;
});
}
const getCreatedTime = (u) => {
const raw = String(u?.created_at || '').trim();
if (!raw) return 0;
const normalized = raw.includes('T') ? raw : raw.replace(' ', 'T');
const ts = Date.parse(normalized);
return Number.isFinite(ts) ? ts : 0;
};
const getStorageUsage = (u) => {
const currentStorage = u?.current_storage_type || 'oss';
if (currentStorage === 'local') {
return this.getAdminUserQuotaPercentage(u);
}
return this.getAdminUserOssQuotaPercentage(u);
};
const getDownloadUsage = (u) => this.getAdminUserDownloadQuotaPercentage(u);
list.sort((a, b) => {
const aName = String(a?.username || '').toLowerCase();
const bName = String(b?.username || '').toLowerCase();
const aId = Number(a?.id || 0);
const bId = Number(b?.id || 0);
switch (this.adminUserFilters.sort) {
case 'created_asc':
return getCreatedTime(a) - getCreatedTime(b) || aId - bId;
case 'username_asc':
return aName.localeCompare(bName, 'zh-CN') || aId - bId;
case 'username_desc':
return bName.localeCompare(aName, 'zh-CN') || bId - aId;
case 'storage_usage_desc':
return getStorageUsage(b) - getStorageUsage(a) || bId - aId;
case 'download_usage_desc':
return getDownloadUsage(b) - getDownloadUsage(a) || bId - aId;
default:
return getCreatedTime(b) - getCreatedTime(a) || bId - aId;
}
});
return list;
},
adminUsersFilteredCount() {
return this.adminUsersFiltered.length;
},
adminUsersTotalPages() {
const size = Math.max(1, Number(this.adminUsersPageSize) || 20);
return Math.max(1, Math.ceil(this.adminUsersFilteredCount / size));
},
adminUsersCurrentPage() {
const page = Math.max(1, Number(this.adminUsersPage) || 1);
return Math.min(page, this.adminUsersTotalPages);
},
adminUsersPageStart() {
if (this.adminUsersFilteredCount <= 0) return 0;
const size = Math.max(1, Number(this.adminUsersPageSize) || 20);
return (this.adminUsersCurrentPage - 1) * size + 1;
},
adminUsersPageEnd() {
if (this.adminUsersFilteredCount <= 0) return 0;
const size = Math.max(1, Number(this.adminUsersPageSize) || 20);
return Math.min(this.adminUsersCurrentPage * size, this.adminUsersFilteredCount);
},
adminUsersPaged() {
const size = Math.max(1, Number(this.adminUsersPageSize) || 20);
const start = (this.adminUsersCurrentPage - 1) * size;
return this.adminUsersFiltered.slice(start, start + size);
},
adminUserStats() {
const stats = {
active: 0,
banned: 0,
unverified: 0,
download_blocked: 0
};
for (const u of this.adminUsersFiltered) {
const tag = this.getAdminUserStatusTag(u);
if (Object.prototype.hasOwnProperty.call(stats, tag)) {
stats[tag] += 1;
}
}
return stats;
}
},
@@ -2712,15 +2848,22 @@ handleDragLeave(e) {
// ===== 管理员功能 =====
async loadUsers() {
this.adminUsersLoading = true;
try {
const response = await axios.get(`${this.apiBase}/api/admin/users`);
if (response.data.success) {
this.adminUsers = response.data.users;
this.adminUsers = Array.isArray(response.data.users) ? response.data.users : [];
const maxPage = Math.max(1, Math.ceil(this.adminUsers.length / Math.max(1, Number(this.adminUsersPageSize) || 20)));
if (this.adminUsersPage > maxPage) {
this.adminUsersPage = maxPage;
}
}
} catch (error) {
console.error('加载用户列表失败:', error);
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
} finally {
this.adminUsersLoading = false;
}
},
@@ -3373,6 +3516,68 @@ handleDragLeave(e) {
// ===== 工具函数 =====
setAdminUsersPage(page) {
const nextPage = Number(page) || 1;
if (nextPage < 1) {
this.adminUsersPage = 1;
return;
}
const maxPage = this.adminUsersTotalPages;
this.adminUsersPage = Math.min(nextPage, maxPage);
},
resetAdminUserFilters() {
this.adminUserFilters = {
keyword: '',
role: 'all',
status: 'all',
storage: 'all',
sort: 'created_desc'
};
this.adminUsersPageSize = 20;
this.adminUsersPage = 1;
},
getAdminUserStatusTag(user) {
if (user?.is_banned) return 'banned';
if (!user?.is_verified) return 'unverified';
const quota = Number(user?.download_traffic_quota);
const used = Number(user?.download_traffic_used || 0);
if (Number.isFinite(quota) && quota >= 0 && (quota === 0 || used >= quota)) {
return 'download_blocked';
}
return 'active';
},
getAdminUserStatusLabel(user) {
const tag = this.getAdminUserStatusTag(user);
if (tag === 'banned') return '已封禁';
if (tag === 'unverified') return '未激活';
if (tag === 'download_blocked') return '下载受限';
return '正常';
},
escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
escapeRegExp(value) {
return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
getHighlightedText(value, keyword) {
const text = this.escapeHtml(value || '-');
const search = String(keyword || '').trim();
if (!search) return text;
const reg = new RegExp(this.escapeRegExp(search), 'ig');
return text.replace(reg, (match) => `<mark class="admin-search-hit">${match}</mark>`);
},
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
@@ -3920,6 +4125,9 @@ handleDragLeave(e) {
adminTab(newTab) {
if (this.isLoggedIn && this.user?.is_admin) {
localStorage.setItem('adminTab', newTab);
if (newTab === 'users') {
this.loadUsers();
}
}
}
}