355 lines
14 KiB
JavaScript
355 lines
14 KiB
JavaScript
|
|
Router.register('/settings', async () => {
|
|||
|
|
const me = await API.me().catch(()=>({}));
|
|||
|
|
const users = (me && me.role === 'superadmin') ? await API.adminUsers().catch(()=>({list:[]})) : {list:[]};
|
|||
|
|
const userList = (users.list||[]).map(u=>`<li><span>${u.username}</span><span class="badge">${u.role}</span></li>`).join('') || '<li>暂无用户</li>';
|
|||
|
|
const html = `<div class="grid cols-2">
|
|||
|
|
<div class="card">
|
|||
|
|
<div style="font-weight:600;margin-bottom:8px">外观设置</div>
|
|||
|
|
<div class="field"><label>主题</label>
|
|||
|
|
<select class="input" id="theme-select">
|
|||
|
|
<option value="dark">深色</option>
|
|||
|
|
<option value="light">浅色</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin-top:12px">
|
|||
|
|
<button class="btn" id="refresh-page-btn" style="width:100%">🔄 刷新页面</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="card">
|
|||
|
|
<div style="font-weight:600;margin-bottom:16px">账户设置</div>
|
|||
|
|
<div style="margin-bottom:16px">
|
|||
|
|
<div style="font-weight:500;margin-bottom:8px">当前用户</div>
|
|||
|
|
<div id="user-card" class="badge">${(me && me.username) ? me.username : '未登录'}</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--border)">
|
|||
|
|
<div style="font-weight:500;margin-bottom:12px">头像设置</div>
|
|||
|
|
<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px">
|
|||
|
|
<img id="preview-avatar" src="${(me && me.avatar) ? me.avatar : './assets/user-avatar.svg'}" alt="头像预览" style="width:64px;height:64px;border-radius:50%;object-fit:cover;border:2px solid var(--border)" />
|
|||
|
|
<div style="flex:1">
|
|||
|
|
<div class="field" style="margin-bottom:8px">
|
|||
|
|
<label>选择头像图片</label>
|
|||
|
|
<input type="file" id="avatar-file" accept="image/*" class="input" style="padding:6px" />
|
|||
|
|
</div>
|
|||
|
|
<div style="font-size:12px;color:var(--text-2)">支持 JPG、PNG、GIF 等图片格式,建议尺寸 200x200 像素</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="actions">
|
|||
|
|
<button class="btn" id="upload-avatar-btn">上传头像</button>
|
|||
|
|
<button class="btn btn-secondary" id="reset-avatar-btn">恢复默认</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
${(me && me.role === 'superadmin') ? `
|
|||
|
|
<div class="card">
|
|||
|
|
<div style="font-weight:600;margin-bottom:16px">用户管理</div>
|
|||
|
|
|
|||
|
|
<div style="margin-bottom:24px;padding-bottom:24px;border-bottom:1px solid var(--border)">
|
|||
|
|
<div style="font-weight:500;margin-bottom:12px">添加新用户</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>用户名</label>
|
|||
|
|
<input id="new-username" class="input" placeholder="输入用户名" />
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>密码</label>
|
|||
|
|
<input id="new-password" type="password" class="input" placeholder="输入密码" />
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>角色</label>
|
|||
|
|
<select id="new-role" class="input">
|
|||
|
|
<option value="admin">管理员 (admin)</option>
|
|||
|
|
<option value="superadmin">超级管理员 (superadmin)</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="actions"><button id="add-user-btn" class="btn">添加用户</button></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style="margin-bottom:24px;padding-bottom:24px;border-bottom:1px solid var(--border)">
|
|||
|
|
<div style="font-weight:500;margin-bottom:12px">修改用户密码</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>选择用户</label>
|
|||
|
|
<select id="reset-user" class="input">${(users.list||[]).map(u=>`<option value="${u.username}">${u.username}</option>`).join('')}</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>新密码</label>
|
|||
|
|
<input id="reset-pass" type="password" class="input" />
|
|||
|
|
</div>
|
|||
|
|
<div class="actions"><button id="change-btn" class="btn">修改密码</button></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style="font-weight:500;margin-bottom:12px">超级管理员工具</div>
|
|||
|
|
<div style="margin-top:12px"></div>
|
|||
|
|
<div class="grid cols-2" style="margin-top:8px">
|
|||
|
|
<div class="card">
|
|||
|
|
<div style="font-weight:600;margin-bottom:8px">清空上传数据</div>
|
|||
|
|
<div class="actions" style="flex-wrap:wrap;gap:6px">
|
|||
|
|
<button class="btn" data-clear="mac">清空MAC与批次</button>
|
|||
|
|
<button class="btn" data-clear="stats">清空良/不良统计</button>
|
|||
|
|
<button class="btn" data-clear="defects">清空不良明细</button>
|
|||
|
|
<button class="btn" data-clear="shipments">清空发货记录</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="card">
|
|||
|
|
<div style="font-weight:600;margin-bottom:8px">清空扩展采集</div>
|
|||
|
|
<div class="actions" style="flex-wrap:wrap;gap:6px">
|
|||
|
|
<button class="btn" data-clear="devices">清空设备状态</button>
|
|||
|
|
<button class="btn" data-clear="environment">清空环境参数</button>
|
|||
|
|
<button class="btn" data-clear="personnel">清空人员信息</button>
|
|||
|
|
<button class="btn" data-clear="qa">清空质检报告</button>
|
|||
|
|
<button class="btn" data-clear="production">清空生产时间</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin-top:12px"></div>
|
|||
|
|
<div class="grid cols-2" style="margin-top:8px">
|
|||
|
|
<div class="card">
|
|||
|
|
<div style="font-weight:600;margin-bottom:8px">数据概览</div>
|
|||
|
|
<div class="grid cols-2">
|
|||
|
|
<div>
|
|||
|
|
<div class="badge" id="overview-stats">良/不良统计:加载中</div>
|
|||
|
|
<div class="badge" id="overview-defects">不良明细:加载中</div>
|
|||
|
|
<div class="badge" id="overview-mac">MAC与批次:加载中</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="badge" id="overview-shipments">发货记录:加载中</div>
|
|||
|
|
<div class="badge" id="overview-devices">设备状态:加载中</div>
|
|||
|
|
<div class="badge" id="overview-personnel">人员信息:加载中</div>
|
|||
|
|
<div class="badge" id="overview-qa">质检报告:加载中</div>
|
|||
|
|
<div class="badge" id="overview-production">生产时间:加载中</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin-top:12px"><div style="font-weight:600;margin-bottom:8px">用户列表</div><ul class="list">${userList}</ul></div>
|
|||
|
|
</div>
|
|||
|
|
` : ''}
|
|||
|
|
</div>`;
|
|||
|
|
setTimeout(() => {
|
|||
|
|
// 主题切换
|
|||
|
|
const themeSelect = document.getElementById('theme-select');
|
|||
|
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|||
|
|
if(themeSelect) {
|
|||
|
|
themeSelect.value = savedTheme;
|
|||
|
|
themeSelect.addEventListener('change', (e) => {
|
|||
|
|
const theme = e.target.value;
|
|||
|
|
localStorage.setItem('theme', theme);
|
|||
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|||
|
|
API.toast(`已切换到${theme === 'dark' ? '深色' : '浅色'}主题`);
|
|||
|
|
|
|||
|
|
// 触发自定义事件,通知其他组件主题已更改
|
|||
|
|
window.dispatchEvent(new CustomEvent('themeChanged', { detail: { theme } }));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 刷新页面按钮
|
|||
|
|
const refreshBtn = document.getElementById('refresh-page-btn');
|
|||
|
|
refreshBtn?.addEventListener('click', () => {
|
|||
|
|
location.reload();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 头像文件选择预览
|
|||
|
|
const avatarFileInput = document.getElementById('avatar-file');
|
|||
|
|
const previewAvatar = document.getElementById('preview-avatar');
|
|||
|
|
|
|||
|
|
avatarFileInput?.addEventListener('change', (e) => {
|
|||
|
|
const file = e.target.files?.[0];
|
|||
|
|
if (file) {
|
|||
|
|
// 验证文件类型
|
|||
|
|
if (!file.type.startsWith('image/')) {
|
|||
|
|
API.toast('请选择图片文件');
|
|||
|
|
avatarFileInput.value = '';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证文件大小(限制2MB)
|
|||
|
|
if (file.size > 2 * 1024 * 1024) {
|
|||
|
|
API.toast('图片大小不能超过2MB');
|
|||
|
|
avatarFileInput.value = '';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 预览图片
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
previewAvatar.src = e.target.result;
|
|||
|
|
};
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 上传头像
|
|||
|
|
const uploadAvatarBtn = document.getElementById('upload-avatar-btn');
|
|||
|
|
uploadAvatarBtn?.addEventListener('click', async () => {
|
|||
|
|
const file = avatarFileInput?.files?.[0];
|
|||
|
|
|
|||
|
|
if (!file) {
|
|||
|
|
return API.toast('请先选择头像图片');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uploadAvatarBtn.disabled = true;
|
|||
|
|
try {
|
|||
|
|
const formData = new FormData();
|
|||
|
|
formData.append('avatar', file);
|
|||
|
|
|
|||
|
|
const res = await fetch('/api/user/upload-avatar', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: formData,
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
if (res.ok && data.ok) {
|
|||
|
|
API.toast('头像上传成功');
|
|||
|
|
// 更新顶部用户头像显示
|
|||
|
|
const avatarImg = document.getElementById('user-avatar-img');
|
|||
|
|
if (avatarImg && data.avatar_url) {
|
|||
|
|
avatarImg.src = data.avatar_url;
|
|||
|
|
}
|
|||
|
|
// 如果有全局更新函数,调用它
|
|||
|
|
if (window.updateUserDisplay) {
|
|||
|
|
const updatedUser = await API.me().catch(() => null);
|
|||
|
|
window.updateUserDisplay(updatedUser);
|
|||
|
|
}
|
|||
|
|
// 清空文件选择
|
|||
|
|
avatarFileInput.value = '';
|
|||
|
|
} else {
|
|||
|
|
API.toast(data.error || '上传失败');
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
API.toast('上传失败:' + e.message);
|
|||
|
|
} finally {
|
|||
|
|
uploadAvatarBtn.disabled = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 恢复默认头像
|
|||
|
|
const resetAvatarBtn = document.getElementById('reset-avatar-btn');
|
|||
|
|
resetAvatarBtn?.addEventListener('click', async () => {
|
|||
|
|
resetAvatarBtn.disabled = true;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/user/reset-avatar', {
|
|||
|
|
method: 'POST',
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
if (res.ok && data.ok) {
|
|||
|
|
API.toast('已恢复默认头像');
|
|||
|
|
previewAvatar.src = './assets/user-avatar.svg';
|
|||
|
|
const avatarImg = document.getElementById('user-avatar-img');
|
|||
|
|
if (avatarImg) {
|
|||
|
|
avatarImg.src = './assets/user-avatar.svg';
|
|||
|
|
}
|
|||
|
|
if (avatarFileInput) avatarFileInput.value = '';
|
|||
|
|
// 如果有全局更新函数,调用它
|
|||
|
|
if (window.updateUserDisplay) {
|
|||
|
|
const updatedUser = await API.me().catch(() => null);
|
|||
|
|
window.updateUserDisplay(updatedUser);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
API.toast(data.error || '操作失败');
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
API.toast('操作失败:' + e.message);
|
|||
|
|
} finally {
|
|||
|
|
resetAvatarBtn.disabled = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 添加新用户
|
|||
|
|
const addUserBtn = document.getElementById('add-user-btn');
|
|||
|
|
addUserBtn?.addEventListener('click', async () => {
|
|||
|
|
const usernameEl = document.getElementById('new-username');
|
|||
|
|
const passwordEl = document.getElementById('new-password');
|
|||
|
|
const roleEl = document.getElementById('new-role');
|
|||
|
|
|
|||
|
|
const username = usernameEl?.value?.trim();
|
|||
|
|
const password = passwordEl?.value;
|
|||
|
|
const role = roleEl?.value || 'admin';
|
|||
|
|
|
|||
|
|
if (!username || !password) {
|
|||
|
|
return API.toast('请输入用户名和密码');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (password.length < 6) {
|
|||
|
|
return API.toast('密码长度至少6位');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addUserBtn.disabled = true;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/admin/add-user', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify({ username, password, role })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
if (res.ok && data.ok) {
|
|||
|
|
API.toast(data.message || '用户创建成功');
|
|||
|
|
// 清空输入框
|
|||
|
|
if (usernameEl) usernameEl.value = '';
|
|||
|
|
if (passwordEl) passwordEl.value = '';
|
|||
|
|
// 刷新页面以更新用户列表
|
|||
|
|
setTimeout(() => Router.navigate('/settings'), 1000);
|
|||
|
|
} else {
|
|||
|
|
API.toast(data.error || '创建失败');
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
API.toast('创建失败:' + e.message);
|
|||
|
|
} finally {
|
|||
|
|
addUserBtn.disabled = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 修改密码
|
|||
|
|
const change = document.getElementById('change-btn');
|
|||
|
|
change?.addEventListener('click', async () => {
|
|||
|
|
const uEl = document.getElementById('reset-user');
|
|||
|
|
const pEl = document.getElementById('reset-pass');
|
|||
|
|
const u = uEl ? uEl.value : '';
|
|||
|
|
const p = pEl ? pEl.value : '';
|
|||
|
|
if (!u || !p) return API.toast('请输入用户与新密码');
|
|||
|
|
change.disabled = true;
|
|||
|
|
try {
|
|||
|
|
await API.changePassword(u, p);
|
|||
|
|
API.toast('已修改');
|
|||
|
|
} catch(e) {
|
|||
|
|
API.toast('修改失败');
|
|||
|
|
} finally {
|
|||
|
|
change.disabled = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
document.querySelectorAll('button[data-clear]')?.forEach(btn => {
|
|||
|
|
btn.addEventListener('click', async () => {
|
|||
|
|
const mod = btn.getAttribute('data-clear');
|
|||
|
|
btn.disabled = true;
|
|||
|
|
try{
|
|||
|
|
await API.clearModule(mod);
|
|||
|
|
API.toast('已清空');
|
|||
|
|
}catch(e){
|
|||
|
|
API.toast('清空失败');
|
|||
|
|
}finally{
|
|||
|
|
btn.disabled = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
(async ()=>{
|
|||
|
|
const ov = await API.overview().catch(()=>null);
|
|||
|
|
const setText=(id, text)=>{const el=document.getElementById(id); if(el) el.textContent=text;}
|
|||
|
|
if (ov) {
|
|||
|
|
setText('overview-stats', `良/不良统计:${ov.stats.records} 条(良 ${ov.stats.goodTotal} / 不良 ${ov.stats.badTotal})`);
|
|||
|
|
setText('overview-defects', `不良明细:${ov.defects} 条`);
|
|||
|
|
setText('overview-mac', `MAC与批次:${ov.mac} 条`);
|
|||
|
|
setText('overview-shipments', `发货记录:${ov.shipments.records} 条(总量 ${ov.shipments.qtyTotal})`);
|
|||
|
|
setText('overview-devices', `设备状态:${ov.devices} 条`);
|
|||
|
|
setText('overview-personnel', `人员信息:${ov.personnel} 条`);
|
|||
|
|
setText('overview-qa', `质检报告:${ov.qa} 条`);
|
|||
|
|
setText('overview-production', `生产时间:${ov.production} 条`);
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
}, 0);
|
|||
|
|
return html;
|
|||
|
|
});
|