feat: add server-side admin user pagination and align traffic report accounting

This commit is contained in:
2026-02-17 20:30:02 +08:00
parent 1eae645bfd
commit aed5dfdcb2
4 changed files with 378 additions and 154 deletions

View File

@@ -144,6 +144,15 @@ createApp({
adminUsersLoading: false,
adminUsersPage: 1,
adminUsersPageSize: 20,
adminUsersTotalCount: 0,
adminUsersTotalPages: 1,
adminUsersGlobalCount: 0,
adminUserStats: {
active: 0,
banned: 0,
unverified: 0,
download_blocked: 0
},
adminUserFilters: {
keyword: '',
role: 'all', // all/admin/user
@@ -571,91 +580,8 @@ 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));
return Math.max(0, Number(this.adminUsersTotalCount) || 0);
},
adminUsersCurrentPage() {
@@ -673,28 +599,6 @@ createApp({
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;
}
},
@@ -2847,17 +2751,91 @@ handleDragLeave(e) {
// ===== 管理员功能 =====
async loadUsers() {
calculateAdminUserStats(users = []) {
const stats = {
active: 0,
banned: 0,
unverified: 0,
download_blocked: 0
};
for (const user of users) {
const statusTag = this.getAdminUserStatusTag(user);
if (Object.prototype.hasOwnProperty.call(stats, statusTag)) {
stats[statusTag] += 1;
}
}
return stats;
},
buildAdminUsersQueryParams() {
const params = {
paged: 1,
page: Math.max(1, Number(this.adminUsersPage) || 1),
pageSize: Math.max(1, Number(this.adminUsersPageSize) || 20),
sort: this.adminUserFilters.sort || 'created_desc'
};
const keyword = String(this.adminUserFilters.keyword || '').trim();
if (keyword) params.keyword = keyword;
if (this.adminUserFilters.role && this.adminUserFilters.role !== 'all') {
params.role = this.adminUserFilters.role;
}
if (this.adminUserFilters.status && this.adminUserFilters.status !== 'all') {
params.status = this.adminUserFilters.status;
}
if (this.adminUserFilters.storage && this.adminUserFilters.storage !== 'all') {
params.storage = this.adminUserFilters.storage;
}
return params;
},
async loadUsers(options = {}) {
const resetPage = options && options.resetPage === true;
if (resetPage) {
this.adminUsersPage = 1;
}
this.adminUsersLoading = true;
try {
const response = await axios.get(`${this.apiBase}/api/admin/users`);
const response = await axios.get(`${this.apiBase}/api/admin/users`, {
params: this.buildAdminUsersQueryParams()
});
if (response.data.success) {
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;
}
const rows = Array.isArray(response.data.users) ? response.data.users : [];
this.adminUsers = rows;
const rawTotal = Number(response.data?.pagination?.total);
const totalCount = Number.isFinite(rawTotal) && rawTotal >= 0
? Math.floor(rawTotal)
: rows.length;
this.adminUsersTotalCount = totalCount;
const rawTotalPages = Number(response.data?.pagination?.totalPages);
this.adminUsersTotalPages = Number.isFinite(rawTotalPages) && rawTotalPages > 0
? Math.floor(rawTotalPages)
: Math.max(1, Math.ceil(totalCount / Math.max(1, Number(this.adminUsersPageSize) || 20)));
const rawPage = Number(response.data?.pagination?.page);
const nextPage = Number.isFinite(rawPage) && rawPage > 0
? Math.floor(rawPage)
: this.adminUsersPage;
this.adminUsersPage = Math.min(Math.max(1, nextPage), this.adminUsersTotalPages);
const summary = response.data?.summary || {};
const rawGlobalTotal = Number(summary.global_total);
this.adminUsersGlobalCount = Number.isFinite(rawGlobalTotal) && rawGlobalTotal >= 0
? Math.floor(rawGlobalTotal)
: this.adminUsersTotalCount;
const fallbackStats = this.calculateAdminUserStats(rows);
this.adminUserStats = {
active: Number.isFinite(Number(summary.active)) ? Math.max(0, Math.floor(Number(summary.active))) : fallbackStats.active,
banned: Number.isFinite(Number(summary.banned)) ? Math.max(0, Math.floor(Number(summary.banned))) : fallbackStats.banned,
unverified: Number.isFinite(Number(summary.unverified)) ? Math.max(0, Math.floor(Number(summary.unverified))) : fallbackStats.unverified,
download_blocked: Number.isFinite(Number(summary.download_blocked)) ? Math.max(0, Math.floor(Number(summary.download_blocked))) : fallbackStats.download_blocked
};
}
} catch (error) {
console.error('加载用户列表失败:', error);
@@ -3519,11 +3497,37 @@ handleDragLeave(e) {
setAdminUsersPage(page) {
const nextPage = Number(page) || 1;
if (nextPage < 1) {
this.adminUsersPage = 1;
if (this.adminUsersPage !== 1) {
this.adminUsersPage = 1;
this.loadUsers();
}
return;
}
const maxPage = this.adminUsersTotalPages;
this.adminUsersPage = Math.min(nextPage, maxPage);
const targetPage = Math.min(nextPage, maxPage);
if (targetPage === this.adminUsersPage) return;
this.adminUsersPage = targetPage;
this.loadUsers();
},
triggerAdminUsersKeywordSearch() {
this.adminUsersPage = 1;
if (!this._debouncedAdminUsersQuery) {
this._debouncedAdminUsersQuery = this.debounce(() => {
this.loadUsers();
}, 260);
}
this._debouncedAdminUsersQuery();
},
handleAdminUsersFilterChange() {
this.adminUsersPage = 1;
this.loadUsers();
},
handleAdminUsersPageSizeChange() {
this.adminUsersPage = 1;
this.loadUsers();
},
resetAdminUserFilters() {
@@ -3536,6 +3540,7 @@ handleDragLeave(e) {
};
this.adminUsersPageSize = 20;
this.adminUsersPage = 1;
this.loadUsers();
},
getAdminUserStatusTag(user) {