201 lines
8.0 KiB
HTML
201 lines
8.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>设备管理 - 用户认证</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; min-height: 100vh; }
|
|
.header { background: #4f46e5; color: #fff; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
|
|
.header h1 { font-size: 18px; font-weight: 600; }
|
|
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
.user-info { font-size: 14px; opacity: 0.9; }
|
|
.btn-logout { background: rgba(255,255,255,0.2); color: #fff; border: 1px solid rgba(255,255,255,0.4); border-radius: 6px; padding: 6px 14px; font-size: 13px; cursor: pointer; }
|
|
.btn-logout:hover { background: rgba(255,255,255,0.3); }
|
|
.main { max-width: 640px; margin: 32px auto; padding: 0 16px; }
|
|
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; margin-bottom: 16px; }
|
|
.card-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: space-between; }
|
|
.card-header h2 { font-size: 16px; color: #1a1a2e; }
|
|
.device-limit { font-size: 12px; color: #888; background: #f5f5f5; padding: 2px 8px; border-radius: 10px; }
|
|
.card-body { padding: 8px 0; }
|
|
.device-item { display: flex; align-items: center; padding: 14px 20px; border-bottom: 1px solid #f9f9f9; gap: 14px; }
|
|
.device-item:last-child { border-bottom: none; }
|
|
.device-icon { font-size: 28px; flex-shrink: 0; }
|
|
.device-info { flex: 1; min-width: 0; }
|
|
.device-name { font-size: 14px; color: #1a1a2e; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.device-meta { font-size: 12px; color: #888; margin-top: 3px; }
|
|
.device-current { display: inline-block; background: #dcfce7; color: #16a34a; font-size: 11px; padding: 1px 7px; border-radius: 8px; margin-left: 6px; font-weight: 600; }
|
|
.device-actions { flex-shrink: 0; }
|
|
.btn-remove { background: #fef2f2; color: #dc2626; border: 1px solid #fca5a5; border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer; }
|
|
.btn-remove:hover { background: #fee2e2; }
|
|
.btn-remove:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.empty { text-align: center; padding: 40px 20px; color: #888; font-size: 14px; }
|
|
.loading { text-align: center; padding: 40px; color: #888; }
|
|
.msg { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; display: none; }
|
|
.msg.error { background: #fef2f2; color: #dc2626; border: 1px solid #fca5a5; display: block; }
|
|
.msg.success { background: #f0fdf4; color: #16a34a; border: 1px solid #86efac; display: block; }
|
|
.user-card { display: flex; align-items: center; gap: 12px; padding: 16px 20px; }
|
|
.avatar { width: 44px; height: 44px; border-radius: 50%; background: #e8e7ff; color: #4f46e5; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; }
|
|
.user-details h3 { font-size: 15px; color: #1a1a2e; }
|
|
.user-details p { font-size: 12px; color: #888; margin-top: 2px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>设备管理</h1>
|
|
<div class="header-right">
|
|
<span class="user-info" id="userPhone"></span>
|
|
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
|
</div>
|
|
</div>
|
|
<div class="main">
|
|
<div id="msg" class="msg"></div>
|
|
|
|
<!-- 当前用户信息 -->
|
|
<div class="card">
|
|
<div class="user-card" id="userCard">
|
|
<div class="avatar" id="avatar">?</div>
|
|
<div class="user-details">
|
|
<h3 id="displayName">加载中...</h3>
|
|
<p id="displayPhone"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 设备列表 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>已登录设备</h2>
|
|
<span class="device-limit" id="deviceCount">0 / 3 台</span>
|
|
</div>
|
|
<div class="card-body" id="deviceList">
|
|
<div class="loading" id="loadingTip">加载中...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = 'http://localhost:3000/api/auth';
|
|
const token = localStorage.getItem('auth_token');
|
|
const user = JSON.parse(localStorage.getItem('auth_user') || '{}');
|
|
|
|
if (!token) {
|
|
alert('未登录,正在跳转到登录页');
|
|
location.href = 'login.html';
|
|
}
|
|
|
|
document.getElementById('userPhone').textContent = user.phone || '';
|
|
document.getElementById('displayName').textContent = '用户 ' + (user.phone || '未知');
|
|
document.getElementById('displayPhone').textContent = user.phone ? '手机号: ' + user.phone : '';
|
|
document.getElementById('avatar').textContent = user.phone ? user.phone.slice(-2) : '?';
|
|
|
|
function showMsg(text, type) {
|
|
const el = document.getElementById('msg');
|
|
el.textContent = text;
|
|
el.className = 'msg ' + type;
|
|
el.style.display = 'block';
|
|
if (type === 'success') setTimeout(() => { el.style.display = 'none'; }, 3000);
|
|
}
|
|
|
|
function formatTime(dateStr) {
|
|
if (!dateStr) return '未知时间';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function getDeviceIcon(name) {
|
|
const n = (name || '').toLowerCase();
|
|
if (n.includes('iphone') || n.includes('ipad')) return '📱';
|
|
if (n.includes('android')) return '📱';
|
|
if (n.includes('mac') || n.includes('iphone')) return '💻';
|
|
if (n.includes('windows')) return '🖥️';
|
|
if (n.includes('linux')) return '🖥️';
|
|
return '💻';
|
|
}
|
|
|
|
async function loadDevices() {
|
|
try {
|
|
const res = await fetch(API + '/devices', {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
renderDevices(data.devices || []);
|
|
} else if (res.status === 401 || data.code === 401) {
|
|
logout('登录已过期,请重新登录');
|
|
} else {
|
|
showMsg(data.message || '加载设备列表失败', 'error');
|
|
}
|
|
} catch {
|
|
showMsg('网络错误,无法加载设备列表', 'error');
|
|
}
|
|
}
|
|
|
|
function renderDevices(devices) {
|
|
const list = document.getElementById('deviceList');
|
|
document.getElementById('deviceCount').textContent = devices.length + ' / 3 台';
|
|
|
|
if (devices.length === 0) {
|
|
list.innerHTML = '<div class="empty">暂无已登录设备</div>';
|
|
return;
|
|
}
|
|
|
|
// 当前设备标识(简单用 userId 判断)
|
|
const currentId = user.id;
|
|
list.innerHTML = devices.map(dev => {
|
|
const isCurrent = dev.userId === currentId || dev.isCurrent;
|
|
return '<div class="device-item">' +
|
|
'<div class="device-icon">' + getDeviceIcon(dev.deviceName) + '</div>' +
|
|
'<div class="device-info">' +
|
|
'<div class="device-name">' + escapeHtml(dev.deviceName || '未知设备') +
|
|
(isCurrent ? '<span class="device-current">当前设备</span>' : '') + '</div>' +
|
|
'<div class="device-meta">最后活跃: ' + formatTime(dev.lastActiveAt) + '</div>' +
|
|
'</div>' +
|
|
'<div class="device-actions">' +
|
|
(!isCurrent ? '<button class="btn-remove" onclick="removeDevice(\'' + dev.id + '\')">下线</button>' : '') +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function removeDevice(deviceId) {
|
|
if (!confirm('确定要将该设备强制下线吗?')) return;
|
|
try {
|
|
const res = await fetch(API + '/devices/' + deviceId, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
showMsg('设备已下线', 'success');
|
|
loadDevices();
|
|
} else {
|
|
showMsg(data.message || '下线失败', 'error');
|
|
}
|
|
} catch {
|
|
showMsg('网络错误', 'error');
|
|
}
|
|
}
|
|
|
|
function logout(reason) {
|
|
localStorage.removeItem('auth_token');
|
|
localStorage.removeItem('auth_user');
|
|
location.href = 'login.html';
|
|
}
|
|
|
|
document.getElementById('logoutBtn').addEventListener('click', () => {
|
|
if (confirm('确定要退出登录吗?')) logout();
|
|
});
|
|
|
|
loadDevices();
|
|
</script>
|
|
</body>
|
|
</html>
|