feat: improve admin user management with filters and pagination
This commit is contained in:
@@ -654,6 +654,200 @@
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-users-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-toolbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1.2fr) repeat(4, minmax(130px, 1fr)) minmax(84px, 0.6fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-filter label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-filter input,
|
||||||
|
.admin-users-filter select {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-filter input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-filter-reset {
|
||||||
|
align-self: end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-filter-reset .btn {
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-stat-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 130px;
|
||||||
|
border: 1px dashed var(--glass-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-empty-state i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(40, 50, 80, 0.92) !important;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-user-status-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 66px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-user-status-tag.status-active {
|
||||||
|
color: #22c55e;
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
border-color: rgba(34, 197, 94, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-user-status-tag.status-banned {
|
||||||
|
color: #ef4444;
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
border-color: rgba(239, 68, 68, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-user-status-tag.status-unverified {
|
||||||
|
color: #f59e0b;
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
border-color: rgba(245, 158, 11, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-user-status-tag.status-download_blocked {
|
||||||
|
color: #f97316;
|
||||||
|
background: rgba(249, 115, 22, 0.14);
|
||||||
|
border-color: rgba(249, 115, 22, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-pagination-info {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-pagination-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-pagination-actions .btn {
|
||||||
|
min-width: 76px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-users-page-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 78px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-hit {
|
||||||
|
background: rgba(250, 204, 21, 0.28);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 移动端适配 */
|
/* 移动端适配 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -917,6 +1111,39 @@
|
|||||||
.admin-log-pager .btn {
|
.admin-log-pager .btn {
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
}
|
}
|
||||||
|
.admin-users-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.admin-users-toolbar {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.admin-users-filter-search {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.admin-users-filter-reset {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.admin-users-filter-reset .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.admin-users-pagination {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.admin-users-pagination-info {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.admin-users-pagination-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.admin-users-pagination-actions .btn {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.admin-users-page-indicator {
|
||||||
|
min-width: 64px;
|
||||||
|
}
|
||||||
.admin-users-table {
|
.admin-users-table {
|
||||||
min-width: 700px !important;
|
min-width: 700px !important;
|
||||||
}
|
}
|
||||||
@@ -972,6 +1199,9 @@
|
|||||||
.admin-log-actions {
|
.admin-log-actions {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.admin-users-toolbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
.admin-health-summary {
|
.admin-health-summary {
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
}
|
}
|
||||||
@@ -993,6 +1223,13 @@
|
|||||||
.admin-users-table td:nth-child(7) {
|
.admin-users-table td:nth-child(7) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.admin-users-pagination-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.admin-users-pagination-actions .btn {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 列表视图在超小屏幕隐藏文件大小列 */
|
/* 列表视图在超小屏幕隐藏文件大小列 */
|
||||||
.file-list th:nth-child(2),
|
.file-list th:nth-child(2),
|
||||||
@@ -3275,115 +3512,222 @@
|
|||||||
<!-- ========== 用户标签页 ========== -->
|
<!-- ========== 用户标签页 ========== -->
|
||||||
<div v-show="adminTab === 'users'">
|
<div v-show="adminTab === 'users'">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 style="margin-bottom: 20px;">用户管理</h3>
|
<div class="admin-users-header">
|
||||||
<div class="admin-users-table-wrap" style="overflow-x: auto;">
|
<h3 style="margin: 0;">用户管理</h3>
|
||||||
<table class="admin-users-table" style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">
|
<button class="btn btn-secondary" @click="loadUsers" :disabled="adminUsersLoading" style="min-width: 92px;">
|
||||||
<thead>
|
<i :class="adminUsersLoading ? 'fas fa-spinner fa-spin' : 'fas fa-rotate'"></i> 刷新
|
||||||
<tr style="background: rgba(255,255,255,0.05);">
|
</button>
|
||||||
<th style="padding: 10px; text-align: left; width: 4%;">ID</th>
|
|
||||||
<th style="padding: 10px; text-align: left; width: 9%;">用户名</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 9%;">角色</th>
|
|
||||||
<th style="padding: 10px; text-align: left; width: 14%;">邮箱</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 8%;">存储权限</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 8%;">当前存储</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 11%;">存储配额</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 11%;">下载流量</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 7%;">状态</th>
|
|
||||||
<th style="padding: 10px; text-align: center; width: 19%;">操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="u in adminUsers" :key="u.id" style="border-bottom: 1px solid #eee;">
|
|
||||||
<td style="padding: 10px;">{{ u.id }}</td>
|
|
||||||
<td style="padding: 10px; overflow: hidden;">
|
|
||||||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.username">
|
|
||||||
{{ u.username }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
|
||||||
<span v-if="u.is_admin" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
|
|
||||||
<i class="fas fa-crown"></i> 管理员
|
|
||||||
</span>
|
|
||||||
<span v-else style="background: rgba(255,255,255,0.1); color: var(--text-secondary); padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
|
|
||||||
<i class="fas fa-user"></i> 用户
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.email">{{ u.email }}</td>
|
|
||||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
|
||||||
<span v-if="u.storage_permission === 'local_only'" style="background: #667eea; color: white; padding: 3px 8px; border-radius: 4px;">仅本地</span>
|
|
||||||
<span v-else-if="u.storage_permission === 'oss_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅OSS</span>
|
|
||||||
<span v-else style="background: #22c55e; color: white; padding: 3px 8px; border-radius: 4px;">用户选择</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
|
||||||
<span v-if="u.current_storage_type === 'local'" style="color: #667eea;">
|
|
||||||
<i class="fas fa-hard-drive"></i> 本地
|
|
||||||
</span>
|
|
||||||
<span v-else style="color: #6c757d;">
|
|
||||||
<i class="fas fa-cloud"></i> OSS
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
|
||||||
<div v-if="u.current_storage_type === 'local'">
|
|
||||||
<div>{{ formatBytes(u.local_storage_used) }} / {{ formatBytes(u.local_storage_quota) }}</div>
|
|
||||||
<div style="font-size: 11px; color: var(--text-muted);">
|
|
||||||
{{ getAdminUserQuotaPercentage(u) }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div>{{ formatBytes(u.storage_used || 0) }} / {{ formatBytes(u.oss_storage_quota) }}</div>
|
|
||||||
<div style="font-size: 11px; color: var(--text-muted);">
|
|
||||||
{{ getAdminUserOssQuotaPercentage(u) }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
|
||||||
<div v-if="u.download_traffic_quota >= 0">
|
|
||||||
<div>{{ formatBytes(u.download_traffic_used || 0) }} / {{ formatBytes(u.download_traffic_quota) }}</div>
|
|
||||||
<div style="font-size: 11px; color: var(--text-muted);">
|
|
||||||
{{ u.download_traffic_quota === 0 ? '已禁用下载' : (getAdminUserDownloadQuotaPercentage(u) + '%') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div>{{ formatBytes(u.download_traffic_used || 0) }} / 不限</div>
|
|
||||||
<div style="font-size: 11px; color: var(--text-muted);">
|
|
||||||
不限
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">
|
|
||||||
{{ getDownloadResetCycleText(u.download_traffic_reset_cycle || 'none') }}
|
|
||||||
</div>
|
|
||||||
<div v-if="u.download_traffic_quota_expires_at" style="font-size: 11px; color: #f59e0b; margin-top: 2px;">
|
|
||||||
到期: {{ formatDate(u.download_traffic_quota_expires_at) }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center;">
|
|
||||||
<span v-if="u.is_banned" style="color: #ef4444; font-weight: 600;">已封禁</span>
|
|
||||||
<span v-else-if="!u.is_verified" style="color: #f59e0b; font-weight: 600;">未激活</span>
|
|
||||||
<span v-else style="color: #22c55e;">正常</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center;">
|
|
||||||
<div class="admin-user-actions" style="display: flex; gap: 3px; justify-content: center; flex-wrap: wrap;">
|
|
||||||
<button class="btn admin-user-action-btn" style="background: #667eea; color: white; font-size: 11px; padding: 5px 10px;" @click="openEditStorageModal(u)" title="存储设置">
|
|
||||||
<i class="fas fa-database"></i> 存储
|
|
||||||
</button>
|
|
||||||
<button v-if="!u.is_banned" class="btn admin-user-action-btn" style="background: #f59e0b; color: #000; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, true)">
|
|
||||||
<i class="fas fa-ban"></i> 封禁
|
|
||||||
</button>
|
|
||||||
<button v-else class="btn admin-user-action-btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)">
|
|
||||||
<i class="fas fa-check"></i> 解封
|
|
||||||
</button>
|
|
||||||
<button v-if="u.oss_config_source !== 'none'" class="btn admin-user-action-btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
|
|
||||||
<i class="fas fa-folder-open"></i> 文件
|
|
||||||
</button>
|
|
||||||
<button class="btn admin-user-action-btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-users-toolbar">
|
||||||
|
<div class="admin-users-filter admin-users-filter-search">
|
||||||
|
<label>搜索</label>
|
||||||
|
<input type="text" v-model.trim="adminUserFilters.keyword" @input="adminUsersPage = 1" placeholder="ID / 用户名 / 邮箱">
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-filter">
|
||||||
|
<label>角色</label>
|
||||||
|
<select v-model="adminUserFilters.role" @change="adminUsersPage = 1">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="admin">管理员</option>
|
||||||
|
<option value="user">普通用户</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-filter">
|
||||||
|
<label>状态</label>
|
||||||
|
<select v-model="adminUserFilters.status" @change="adminUsersPage = 1">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="active">正常</option>
|
||||||
|
<option value="banned">已封禁</option>
|
||||||
|
<option value="unverified">未激活</option>
|
||||||
|
<option value="download_blocked">下载受限</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-filter">
|
||||||
|
<label>存储</label>
|
||||||
|
<select v-model="adminUserFilters.storage" @change="adminUsersPage = 1">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="local">当前本地</option>
|
||||||
|
<option value="oss">当前OSS</option>
|
||||||
|
<option value="local_only">仅本地</option>
|
||||||
|
<option value="oss_only">仅OSS</option>
|
||||||
|
<option value="user_choice">用户选择</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-filter">
|
||||||
|
<label>排序</label>
|
||||||
|
<select v-model="adminUserFilters.sort" @change="adminUsersPage = 1">
|
||||||
|
<option value="created_desc">注册时间(新到旧)</option>
|
||||||
|
<option value="created_asc">注册时间(旧到新)</option>
|
||||||
|
<option value="username_asc">用户名(A-Z)</option>
|
||||||
|
<option value="username_desc">用户名(Z-A)</option>
|
||||||
|
<option value="storage_usage_desc">存储使用率(高到低)</option>
|
||||||
|
<option value="download_usage_desc">下载流量使用率(高到低)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-filter admin-users-filter-page-size">
|
||||||
|
<label>每页</label>
|
||||||
|
<select v-model.number="adminUsersPageSize" @change="adminUsersPage = 1">
|
||||||
|
<option :value="20">20</option>
|
||||||
|
<option :value="50">50</option>
|
||||||
|
<option :value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-filter admin-users-filter-reset">
|
||||||
|
<button class="btn btn-secondary" @click="resetAdminUserFilters">
|
||||||
|
<i class="fas fa-filter-circle-xmark"></i> 重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-users-stats">
|
||||||
|
<span class="admin-users-stat-chip">总用户 {{ adminUsers.length }}</span>
|
||||||
|
<span class="admin-users-stat-chip">筛选后 {{ adminUsersFilteredCount }}</span>
|
||||||
|
<span class="admin-users-stat-chip">正常 {{ adminUserStats.active }}</span>
|
||||||
|
<span class="admin-users-stat-chip">封禁 {{ adminUserStats.banned }}</span>
|
||||||
|
<span class="admin-users-stat-chip">未激活 {{ adminUserStats.unverified }}</span>
|
||||||
|
<span class="admin-users-stat-chip">下载受限 {{ adminUserStats.download_blocked }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="adminUsersLoading" class="admin-users-empty-state">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<span>正在加载用户数据...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="adminUsersFilteredCount > 0" class="admin-users-table-wrap" style="overflow-x: auto;">
|
||||||
|
<table class="admin-users-table" style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 960px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: rgba(255,255,255,0.05);">
|
||||||
|
<th style="padding: 10px; text-align: left; width: 5%;">ID</th>
|
||||||
|
<th style="padding: 10px; text-align: left; width: 11%;">用户名</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 9%;">角色</th>
|
||||||
|
<th style="padding: 10px; text-align: left; width: 15%;">邮箱</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 9%;">存储权限</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 8%;">当前存储</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 12%;">存储配额</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 12%;">下载流量</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 8%;">状态</th>
|
||||||
|
<th style="padding: 10px; text-align: center; width: 11%;">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="u in adminUsersPaged" :key="u.id" style="border-bottom: 1px solid #eee;">
|
||||||
|
<td style="padding: 10px;">{{ u.id }}</td>
|
||||||
|
<td style="padding: 10px; overflow: hidden;">
|
||||||
|
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.username" v-html="getHighlightedText(u.username, adminUserFilters.keyword)"></div>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||||
|
<span v-if="u.is_admin" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
|
||||||
|
<i class="fas fa-crown"></i> 管理员
|
||||||
|
</span>
|
||||||
|
<span v-else style="background: rgba(255,255,255,0.1); color: var(--text-secondary); padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
|
||||||
|
<i class="fas fa-user"></i> 用户
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.email || '-'" v-html="getHighlightedText(u.email || '-', adminUserFilters.keyword)"></td>
|
||||||
|
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||||
|
<span v-if="u.storage_permission === 'local_only'" style="background: #667eea; color: white; padding: 3px 8px; border-radius: 4px;">仅本地</span>
|
||||||
|
<span v-else-if="u.storage_permission === 'oss_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅OSS</span>
|
||||||
|
<span v-else style="background: #22c55e; color: white; padding: 3px 8px; border-radius: 4px;">用户选择</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||||
|
<span v-if="u.current_storage_type === 'local'" style="color: #667eea;">
|
||||||
|
<i class="fas fa-hard-drive"></i> 本地
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: #6c757d;">
|
||||||
|
<i class="fas fa-cloud"></i> OSS
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||||
|
<div v-if="u.current_storage_type === 'local'">
|
||||||
|
<div>{{ formatBytes(u.local_storage_used) }} / {{ formatBytes(u.local_storage_quota) }}</div>
|
||||||
|
<div style="font-size: 11px; color: var(--text-muted);">
|
||||||
|
{{ getAdminUserQuotaPercentage(u) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div>{{ formatBytes(u.storage_used || 0) }} / {{ formatBytes(u.oss_storage_quota) }}</div>
|
||||||
|
<div style="font-size: 11px; color: var(--text-muted);">
|
||||||
|
{{ getAdminUserOssQuotaPercentage(u) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||||
|
<div v-if="u.download_traffic_quota >= 0">
|
||||||
|
<div>{{ formatBytes(u.download_traffic_used || 0) }} / {{ formatBytes(u.download_traffic_quota) }}</div>
|
||||||
|
<div style="font-size: 11px; color: var(--text-muted);">
|
||||||
|
{{ u.download_traffic_quota === 0 ? '已禁用下载' : (getAdminUserDownloadQuotaPercentage(u) + '%') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div>{{ formatBytes(u.download_traffic_used || 0) }} / 不限</div>
|
||||||
|
<div style="font-size: 11px; color: var(--text-muted);">
|
||||||
|
不限
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">
|
||||||
|
{{ getDownloadResetCycleText(u.download_traffic_reset_cycle || 'none') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="u.download_traffic_quota_expires_at" style="font-size: 11px; color: #f59e0b; margin-top: 2px;">
|
||||||
|
到期: {{ formatDate(u.download_traffic_quota_expires_at) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; text-align: center;">
|
||||||
|
<span class="admin-user-status-tag" :class="'status-' + getAdminUserStatusTag(u)">
|
||||||
|
{{ getAdminUserStatusLabel(u) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; text-align: center;">
|
||||||
|
<div class="admin-user-actions" style="display: flex; gap: 3px; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<button class="btn admin-user-action-btn" style="background: #667eea; color: white; font-size: 11px; padding: 5px 10px;" @click="openEditStorageModal(u)" title="存储设置">
|
||||||
|
<i class="fas fa-database"></i> 存储
|
||||||
|
</button>
|
||||||
|
<button v-if="!u.is_banned" class="btn admin-user-action-btn" style="background: #f59e0b; color: #000; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, true)">
|
||||||
|
<i class="fas fa-ban"></i> 封禁
|
||||||
|
</button>
|
||||||
|
<button v-else class="btn admin-user-action-btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)">
|
||||||
|
<i class="fas fa-check"></i> 解封
|
||||||
|
</button>
|
||||||
|
<button v-if="u.oss_config_source !== 'none'" class="btn admin-user-action-btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
|
||||||
|
<i class="fas fa-folder-open"></i> 文件
|
||||||
|
</button>
|
||||||
|
<button class="btn admin-user-action-btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-users-pagination" v-if="adminUsersFilteredCount > 0">
|
||||||
|
<div class="admin-users-pagination-info">
|
||||||
|
显示 {{ adminUsersPageStart }}-{{ adminUsersPageEnd }} 条,共 {{ adminUsersFilteredCount }} 条(总 {{ adminUsers.length }} 条)
|
||||||
|
</div>
|
||||||
|
<div class="admin-users-pagination-actions">
|
||||||
|
<button class="btn btn-secondary" @click="setAdminUsersPage(1)" :disabled="adminUsersCurrentPage <= 1">
|
||||||
|
首页
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" @click="setAdminUsersPage(adminUsersCurrentPage - 1)" :disabled="adminUsersCurrentPage <= 1">
|
||||||
|
<i class="fas fa-chevron-left"></i> 上一页
|
||||||
|
</button>
|
||||||
|
<span class="admin-users-page-indicator">{{ adminUsersCurrentPage }} / {{ adminUsersTotalPages }}</span>
|
||||||
|
<button class="btn btn-secondary" @click="setAdminUsersPage(adminUsersCurrentPage + 1)" :disabled="adminUsersCurrentPage >= adminUsersTotalPages">
|
||||||
|
下一页 <i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" @click="setAdminUsersPage(adminUsersTotalPages)" :disabled="adminUsersCurrentPage >= adminUsersTotalPages">
|
||||||
|
末页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="admin-users-empty-state">
|
||||||
|
<i class="fas fa-users-slash"></i>
|
||||||
|
<span v-if="adminUsers.length > 0">没有符合当前筛选条件的用户</span>
|
||||||
|
<span v-else>暂无用户数据</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- 用户标签页结束 -->
|
</div><!-- 用户标签页结束 -->
|
||||||
</div><!-- 管理员视图结束 -->
|
</div><!-- 管理员视图结束 -->
|
||||||
@@ -5420,6 +5764,62 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-users-toolbar,
|
||||||
|
body.enterprise-netdisk .admin-users-pagination,
|
||||||
|
body.enterprise-netdisk .admin-users-empty-state {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-users-filter input,
|
||||||
|
body.enterprise-netdisk .admin-users-filter select {
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-users-stat-chip {
|
||||||
|
background: var(--bg-card-hover);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-users-table thead th {
|
||||||
|
background: var(--bg-card-hover) !important;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-user-status-tag.status-active {
|
||||||
|
color: #15803d;
|
||||||
|
background: #dcfce7;
|
||||||
|
border-color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-user-status-tag.status-banned {
|
||||||
|
color: #b91c1c;
|
||||||
|
background: #fee2e2;
|
||||||
|
border-color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-user-status-tag.status-unverified {
|
||||||
|
color: #b45309;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-color: #fcd34d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-user-status-tag.status-download_blocked {
|
||||||
|
color: #c2410c;
|
||||||
|
background: #ffedd5;
|
||||||
|
border-color: #fdba74;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.enterprise-netdisk .admin-search-hit {
|
||||||
|
background: #fde68a;
|
||||||
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
body.enterprise-netdisk .admin-log-row {
|
body.enterprise-netdisk .admin-log-row {
|
||||||
border-bottom: 1px solid var(--glass-border);
|
border-bottom: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|||||||
210
frontend/app.js
210
frontend/app.js
@@ -141,6 +141,16 @@ createApp({
|
|||||||
|
|
||||||
// 管理员
|
// 管理员
|
||||||
adminUsers: [],
|
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,
|
showResetPwdModal: false,
|
||||||
resetPwdUser: {},
|
resetPwdUser: {},
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
@@ -559,6 +569,132 @@ createApp({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return list;
|
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() {
|
async loadUsers() {
|
||||||
|
this.adminUsersLoading = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.apiBase}/api/admin/users`);
|
const response = await axios.get(`${this.apiBase}/api/admin/users`);
|
||||||
|
|
||||||
if (response.data.success) {
|
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) {
|
} catch (error) {
|
||||||
console.error('加载用户列表失败:', error);
|
console.error('加载用户列表失败:', error);
|
||||||
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
|
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) {
|
formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -3920,6 +4125,9 @@ handleDragLeave(e) {
|
|||||||
adminTab(newTab) {
|
adminTab(newTab) {
|
||||||
if (this.isLoggedIn && this.user?.is_admin) {
|
if (this.isLoggedIn && this.user?.is_admin) {
|
||||||
localStorage.setItem('adminTab', newTab);
|
localStorage.setItem('adminTab', newTab);
|
||||||
|
if (newTab === 'users') {
|
||||||
|
this.loadUsers();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user