feat: improve admin user management with filters and pagination
This commit is contained in:
210
frontend/app.js
210
frontend/app.js
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user