feat: admin user management supports multi-identity bind/unbind
- users list: identity_count column (backend subquery) - user detail: returns identities array - POST /users/:id/identities, DELETE /users/:id/identities/:identityId (owner protection, dup checks, keep >=1, audit logs) - users page: identity column in table + manage section in edit modal - API docs updated
This commit is contained in:
@@ -14,12 +14,13 @@ const UsersPage = {
|
||||
const srcLabel = { netease:'网易', skin:'皮肤站' };
|
||||
ct.innerHTML = `
|
||||
<div class="card table-wrap"><table><thead><tr>
|
||||
<th>#</th><th>用户名</th><th>邮箱</th><th>游戏名</th><th>游戏UID</th><th>来源</th><th>角色</th><th>状态</th><th>时间</th><th>操作</th>
|
||||
<th>#</th><th>用户名</th><th>邮箱</th><th>游戏名</th><th>游戏UID</th><th>来源</th><th>身份</th><th>角色</th><th>状态</th><th>时间</th><th>操作</th>
|
||||
</tr></thead><tbody>${users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td><td>${U.esc(u.username)}</td><td>${U.esc(u.email)}</td>
|
||||
<td>${U.esc(u.game_name)}</td><td>${U.esc(u.game_uid)}</td>
|
||||
<td>${srcLabel[u.source] || u.source || '网易'}</td>
|
||||
<td>${(u.identity_count||0) > 1 ? `<span class="badge bg-processing">${u.identity_count} 个</span>` : (u.identity_count||0) === 1 ? '<span class="badge bg-admin">1</span>' : '<span class="tm ts">-</span>'}</td>
|
||||
<td>${U.badge(u.role,'role')}</td>
|
||||
<td>${u.active ? '<span class="badge bg-resolved">启用</span>' : '<span class="badge bg-rejected">禁用</span>'}</td>
|
||||
<td class="ts">${U.date(u.created_at)}</td>
|
||||
@@ -79,9 +80,10 @@ const UsersPage = {
|
||||
},
|
||||
|
||||
async loadUserServers(id, username, email, gameName, gameUid, role, active, source) {
|
||||
let groups = [], userServers = [];
|
||||
let groups = [], userServers = [], identities = [];
|
||||
try { groups = await API.get('/polls/groups'); } catch {}
|
||||
try { userServers = await API.get('/users/'+id+'/servers'); } catch {}
|
||||
try { identities = await API.get('/users/'+id).then(u => u.identities || []); } catch {}
|
||||
const checks = groups.map(g => {
|
||||
const key = g.group_name+'::'+g.server_name;
|
||||
const has = userServers.some(s => s.group_name===g.group_name && s.server_name===g.server_name);
|
||||
@@ -96,6 +98,23 @@ const UsersPage = {
|
||||
<div class="grid-2"><div class="form-group"><label>游戏名</label><input id="eu-gn" value="${gameName}"></div><div class="form-group"><label>游戏UID</label><input id="eu-gu" value="${gameUid}"></div></div>
|
||||
<div class="form-group"><label>管辖子服务器</label><div style="max-height:120px;overflow-y:auto;border:1px solid var(--g200);border-radius:var(--r);padding:8px">${checks||'<span class="tm">无可用服务器(先在投票→分组管理中添加)</span>'}</div></div>
|
||||
<div class="grid-2"><div class="form-group"><label>新密码(留空不变)</label><input type="password" id="eu-p"></div><div class="form-group"><label>状态</label><select id="eu-a"><option value="1" ${active?'selected':''}>启用</option><option value="0" ${!active?'selected':''}>禁用</option></select></div></div>
|
||||
<div class="form-group"><label>已绑定身份 (多来源)</label>
|
||||
<div style="border:1px solid var(--g200);border-radius:var(--r);padding:8px;margin-bottom:6px" id="identities-box">
|
||||
${identities.length===0?'<span class="tm">暂无身份记录</span>':identities.map(it=>`
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:4px 0">
|
||||
<span class="badge bg-${it.source==='netease'?'processing':'admin'}">${it.source==='netease'?'网易端':'皮肤站'}</span>
|
||||
<span>${U.esc(it.game_name)}${it.game_uid?' (UID: '+U.esc(it.game_uid)+')':''}</span>
|
||||
<span class="tm ts">${U.date(it.created_at)}</span>
|
||||
${identities.length>1?`<button type="button" class="btn btn-d btn-sm" onclick="UsersPage.removeIdentity(${id},${it.id})"><i class="fas fa-unlink"></i></button>`:'<span class="tm ts">主身份</span>'}
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<select id="eu-ident-source" style="padding:6px;border:1px solid var(--g300);border-radius:var(--r);font-size:12px"><option value="netease">网易端</option><option value="skin">皮肤站</option></select>
|
||||
<input id="eu-ident-name" placeholder="游戏名" style="flex:1;padding:6px;border:1px solid var(--g300);border-radius:var(--r);font-size:12px">
|
||||
<input id="eu-ident-uid" placeholder="UID(网易必填)" style="flex:1;padding:6px;border:1px solid var(--g300);border-radius:var(--r);font-size:12px">
|
||||
<button type="button" class="btn btn-p btn-sm" onclick="UsersPage.addIdentity(${id})"><i class="fas fa-plus"></i> 绑定</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">取消</button><button type="submit" class="btn btn-p">保存</button></div>
|
||||
</form>
|
||||
`);
|
||||
@@ -112,5 +131,47 @@ const UsersPage = {
|
||||
const pw = document.getElementById('eu-p').value; if (pw) data.password = pw;
|
||||
try { await API.put('/users/'+id, data); App.closeModal(); this.load(); } catch (ex) { alert(ex.message); }
|
||||
};
|
||||
},
|
||||
|
||||
async addIdentity(userId) {
|
||||
const source = document.getElementById('eu-ident-source').value;
|
||||
const name = document.getElementById('eu-ident-name').value.trim();
|
||||
const uid = document.getElementById('eu-ident-uid').value.trim();
|
||||
if (!name) return alert('请填写游戏名');
|
||||
try {
|
||||
await API.post(`/users/${userId}/identities`, { source, game_name: name, game_uid: uid });
|
||||
alert('绑定成功');
|
||||
// 刷新弹窗中的身份列表
|
||||
const u = await API.get('/users/'+userId);
|
||||
const box = document.getElementById('identities-box');
|
||||
const list = u.identities || [];
|
||||
box.innerHTML = list.length===0 ? '<span class="tm">暂无身份记录</span>' : list.map(it=>`
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:4px 0">
|
||||
<span class="badge bg-${it.source==='netease'?'processing':'admin'}">${it.source==='netease'?'网易端':'皮肤站'}</span>
|
||||
<span>${U.esc(it.game_name)}${it.game_uid?' (UID: '+U.esc(it.game_uid)+')':''}</span>
|
||||
<span class="tm ts">${U.date(it.created_at)}</span>
|
||||
${list.length>1?`<button type="button" class="btn btn-d btn-sm" onclick="UsersPage.removeIdentity(${userId},${it.id})"><i class="fas fa-unlink"></i></button>`:'<span class="tm ts">主身份</span>'}
|
||||
</div>`).join('');
|
||||
document.getElementById('eu-ident-name').value = '';
|
||||
document.getElementById('eu-ident-uid').value = '';
|
||||
} catch (ex) { alert(ex.message); }
|
||||
},
|
||||
|
||||
async removeIdentity(userId, identityId) {
|
||||
if (!await U.confirm('确认解绑该身份?')) return;
|
||||
try {
|
||||
await API.delete(`/users/${userId}/identities/${identityId}`);
|
||||
alert('已解绑');
|
||||
const u = await API.get('/users/'+userId);
|
||||
const box = document.getElementById('identities-box');
|
||||
const list = u.identities || [];
|
||||
box.innerHTML = list.length===0 ? '<span class="tm">暂无身份记录</span>' : list.map(it=>`
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:4px 0">
|
||||
<span class="badge bg-${it.source==='netease'?'processing':'admin'}">${it.source==='netease'?'网易端':'皮肤站'}</span>
|
||||
<span>${U.esc(it.game_name)}${it.game_uid?' (UID: '+U.esc(it.game_uid)+')':''}</span>
|
||||
<span class="tm ts">${U.date(it.created_at)}</span>
|
||||
${list.length>1?`<button type="button" class="btn btn-d btn-sm" onclick="UsersPage.removeIdentity(${userId},${it.id})"><i class="fas fa-unlink"></i></button>`:'<span class="tm ts">主身份</span>'}
|
||||
</div>`).join('');
|
||||
} catch (ex) { alert(ex.message); }
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user