ERP/frontend/js/components/settings.js
2025-11-22 20:40:46 +08:00

321 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: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动图、WEBP 等图片格式,最大 5MB建议尺寸 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 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;
}
// 验证文件大小限制5MB
if (file.size > 5 * 1024 * 1024) {
API.toast('图片大小不能超过5MB');
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;
});