需测试[0.2.7.3][ci]
This commit is contained in:
@@ -29,6 +29,13 @@
|
||||
.image-item .info { padding: 10px; font-size: 12px; color: #888; text-align: center; }
|
||||
|
||||
.no-data { text-align: center; color: #999; padding: 40px; }
|
||||
.form-group { margin-bottom: 14px; }
|
||||
.form-group label { display:block; margin-bottom: 6px; font-weight: 600; color: #333; }
|
||||
.form-group input { width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; box-sizing: border-box; }
|
||||
.btn { padding: 10px 14px; border: none; border-radius: 10px; cursor: pointer; background: #4f46e5; color: #fff; }
|
||||
.msg { margin-top: 10px; font-size: 13px; }
|
||||
.msg.error { color: #b91c1c; }
|
||||
.msg.success { color: #166534; }
|
||||
|
||||
/* 图片放大模态框 */
|
||||
.image-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: none; align-items: center; justify-content: center; z-index: 2000; }
|
||||
@@ -65,6 +72,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if permission_name != "管理员" and not profile_user.manage_key %}
|
||||
<div class="profile-card">
|
||||
<div class="profile-header">
|
||||
<div class="profile-info">
|
||||
<h2>修改密码</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form id="pwdForm">
|
||||
<div class="form-group">
|
||||
<label for="newPassword">新密码</label>
|
||||
<input type="password" id="newPassword" autocomplete="new-password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">确认密码</label>
|
||||
<input type="password" id="confirmPassword" autocomplete="new-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn">保存</button>
|
||||
<div id="pwdMsg" class="msg"></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-title">我的提交</div>
|
||||
{% if achievements %}
|
||||
<div class="image-grid">
|
||||
@@ -128,6 +157,54 @@
|
||||
const modal = document.getElementById('imageModal');
|
||||
if (event.target == modal) closeModal();
|
||||
}
|
||||
|
||||
const pwdForm = document.getElementById('pwdForm');
|
||||
if (pwdForm) {
|
||||
pwdForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const msg = document.getElementById('pwdMsg');
|
||||
msg.textContent = '';
|
||||
msg.className = 'msg';
|
||||
|
||||
const pwd = (document.getElementById('newPassword').value || '').trim();
|
||||
const cpwd = (document.getElementById('confirmPassword').value || '').trim();
|
||||
if (pwd !== cpwd) {
|
||||
msg.textContent = '密码和确认密码不匹配';
|
||||
msg.className = 'msg error';
|
||||
return;
|
||||
}
|
||||
if (pwd.length < 6) {
|
||||
msg.textContent = '密码长度至少为6位';
|
||||
msg.className = 'msg error';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
const resp = await fetch(`/elastic/users/{{ profile_user.user_id }}/update/`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrftoken || ''
|
||||
},
|
||||
body: JSON.stringify({ password: pwd })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (resp.ok && data.status === 'success') {
|
||||
msg.textContent = '修改成功';
|
||||
msg.className = 'msg success';
|
||||
document.getElementById('newPassword').value = '';
|
||||
document.getElementById('confirmPassword').value = '';
|
||||
} else {
|
||||
msg.textContent = data.message || '操作失败';
|
||||
msg.className = 'msg error';
|
||||
}
|
||||
} catch (err) {
|
||||
msg.textContent = '操作失败';
|
||||
msg.className = 'msg error';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -134,6 +134,13 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.search-container select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-container button {
|
||||
padding: 8px 15px;
|
||||
background: #4f46e5;
|
||||
@@ -262,8 +269,10 @@
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" id="searchInput" placeholder="搜索用户名...">
|
||||
<select id="keyFilter"></select>
|
||||
<button id="searchBtn">搜索</button>
|
||||
<button id="resetBtn">重置</button>
|
||||
<button id="clearKeyBtn">清空Key</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
@@ -272,6 +281,7 @@
|
||||
<tr>
|
||||
<th>用户ID</th>
|
||||
<th>用户名</th>
|
||||
<th>Key</th>
|
||||
<th>权限</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@@ -364,11 +374,12 @@
|
||||
}
|
||||
|
||||
// 获取所有用户
|
||||
async function loadUsers(searchTerm = '') {
|
||||
async function loadUsers(searchTerm = '', key = '') {
|
||||
try {
|
||||
const url = searchTerm ?
|
||||
`/elastic/users/?search=${encodeURIComponent(searchTerm)}` :
|
||||
'/elastic/users/';
|
||||
const params = new URLSearchParams();
|
||||
if ((searchTerm || '').trim()) params.set('search', (searchTerm || '').trim());
|
||||
if ((key || '').trim()) params.set('key', (key || '').trim());
|
||||
const url = params.toString() ? `/elastic/users/?${params.toString()}` : '/elastic/users/';
|
||||
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
@@ -385,10 +396,13 @@
|
||||
|
||||
// 根据权限值显示权限名称
|
||||
const permissionText = Number(user.permission) === 0 ? '管理员' : '普通用户';
|
||||
const keys = Array.isArray(user.key) ? user.key : (user.key ? [user.key] : []);
|
||||
const keysText = keys.map(k => String(k || '').trim()).filter(Boolean).join('、') || '-';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${user.user_id}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${keysText}</td>
|
||||
<td>${permissionText}</td>
|
||||
<td class="action-buttons">
|
||||
<button class="btn btn-success edit-btn" data-user='${JSON.stringify(user)}'>编辑</button>
|
||||
@@ -407,6 +421,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function initKeyFilter() {
|
||||
const select = document.getElementById('keyFilter');
|
||||
if (!select) return;
|
||||
select.innerHTML = '<option value="">全部Key</option>';
|
||||
try {
|
||||
const resp = await fetch('/elastic/keys-for-filter/', { credentials: 'same-origin' });
|
||||
const data = await resp.json();
|
||||
if (data.status !== 'success') return;
|
||||
const keys = data.data || [];
|
||||
keys.forEach(k => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = String(k || '').trim();
|
||||
opt.textContent = String(k || '').trim();
|
||||
if (opt.value) select.appendChild(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
select.addEventListener('change', () => {
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
loadUsers(searchTerm, select.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 打开添加用户模态框
|
||||
function openAddModal() {
|
||||
document.getElementById('modalTitle').textContent = '添加用户';
|
||||
@@ -558,7 +595,8 @@
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', function() {
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
loadUsers(searchTerm);
|
||||
const key = (document.getElementById('keyFilter') || {}).value || '';
|
||||
loadUsers(searchTerm, key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -566,7 +604,19 @@
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function() {
|
||||
document.getElementById('searchInput').value = '';
|
||||
loadUsers();
|
||||
const select = document.getElementById('keyFilter');
|
||||
if (select) select.value = '';
|
||||
loadUsers('', '');
|
||||
});
|
||||
}
|
||||
|
||||
const clearKeyBtn = document.getElementById('clearKeyBtn');
|
||||
if (clearKeyBtn) {
|
||||
clearKeyBtn.addEventListener('click', function() {
|
||||
const select = document.getElementById('keyFilter');
|
||||
if (select) select.value = '';
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
loadUsers(searchTerm, '');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -609,6 +659,7 @@
|
||||
|
||||
// 页面加载时获取用户列表
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initKeyFilter();
|
||||
const selfForm = document.getElementById('selfPwdForm');
|
||||
if (selfForm) {
|
||||
selfForm.addEventListener('submit', async (e) => {
|
||||
@@ -635,7 +686,8 @@
|
||||
}
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
if (tbody) {
|
||||
loadUsers();
|
||||
const select = document.getElementById('keyFilter');
|
||||
loadUsers('', select ? select.value : '');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -654,4 +706,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -444,6 +444,7 @@ def get_users(request):
|
||||
is_admin = int(request.session.get("permission", 1)) == 0
|
||||
requester = get_user_by_id(uid) or {}
|
||||
mgr_keys = set(requester.get("manage_key") or [])
|
||||
key_q = (request.GET.get("key") or "").strip()
|
||||
q = (request.GET.get("search") or "").strip()
|
||||
users = get_all_users()
|
||||
if is_admin:
|
||||
@@ -455,6 +456,15 @@ def get_users(request):
|
||||
filtered = [u for u in users if match_manage(u)]
|
||||
else:
|
||||
filtered = [u for u in users if str(u.get("user_id")) == str(uid)]
|
||||
if key_q:
|
||||
k = str(key_q).strip()
|
||||
def match_key(user):
|
||||
try:
|
||||
ukeys = {str(x).strip() for x in (user.get("key") or []) if str(x).strip()}
|
||||
except Exception:
|
||||
ukeys = set()
|
||||
return k in ukeys
|
||||
filtered = [u for u in filtered if match_key(u)]
|
||||
if q:
|
||||
filtered = [u for u in filtered if q in str(u.get("username", ""))]
|
||||
return JsonResponse({"status": "success", "data": filtered})
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% if is_admin or has_manage_key %}
|
||||
<a href="{% url 'elastic:manage_page' %}" onclick="return handleNavClick(this, '/elastic/manage/');">数据管理</a>
|
||||
{% endif %}
|
||||
{% if is_admin %}
|
||||
{% if is_admin or has_manage_key %}
|
||||
<a href="{% url 'elastic:user_manage' %}" onclick="return handleNavClick(this, '/elastic/user_manage/');">用户管理</a>
|
||||
{% endif %}
|
||||
<a href="/accounts/profile/">个人中心</a>
|
||||
|
||||
Reference in New Issue
Block a user