ERP/frontend/js/components/settings.js

485 lines
21 KiB
JavaScript
Raw Normal View History

Router.register('/settings', async () => {
const me = await API.me().catch(()=>({}));
const users = (me && me.role === 'superadmin') ? await API.adminUsers().catch(()=>({list:[]})) : {list:[]};
2025-11-24 08:21:13 +00:00
const userList = (users.list||[]).map(u=>`<li style="display:flex;justify-content:space-between;align-items:center"><div><span style="margin-right:8px">${u.username}</span><span class="badge">${u.role}</span>${u.factory ? `<span class="badge" style="margin-left:8px;background:var(--info)">${u.factory}</span>` : ''}</div><button class="btn btn-secondary" data-delete-user="${u.username}" style="padding:4px 8px;font-size:12px">删除</button></li>`).join('') || '<li>暂无用户</li>';
const html = `
<div class="page-container">
<div class="page-header">
<h1>系统设置</h1>
</div>
<!-- 账户设置区域 -->
<div class="card" style="margin-bottom:24px">
<h2 style="font-size:18px;font-weight:600;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid var(--border)">👤 账户设置</h2>
<div style="margin-bottom:24px">
<div style="font-weight:500;margin-bottom:8px;color:var(--text-2)">当前登录用户</div>
2025-11-24 08:21:13 +00:00
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<div id="user-card" class="badge" style="font-size:16px;padding:8px 16px">${(me && me.username) ? me.username : '未登录'}</div>
${(me && me.factory) ? `<div class="badge" style="font-size:14px;padding:6px 12px;background:var(--info)">🏭 ${me.factory}</div>` : ''}
</div>
</div>
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:16px;font-size:16px">头像设置</div>
<div style="display:flex;align-items:flex-start;gap:24px">
<div style="flex-shrink:0">
<img id="preview-avatar" src="${(me && me.avatar) ? me.avatar : './assets/user-avatar.svg'}" alt="头像预览" style="width:100px;height:100px;border-radius:50%;object-fit:cover;border:3px solid var(--border);box-shadow:0 2px 8px rgba(0,0,0,0.1)" />
</div>
<div style="flex:1">
<div class="field" style="margin-bottom:12px">
<label style="font-weight:500">选择头像图片</label>
<input type="file" id="avatar-file" accept="image/*" class="input" style="padding:8px" />
</div>
<div style="font-size:13px;color:var(--text-2);margin-bottom:16px;line-height:1.5">
支持 JPGPNGGIF动图WEBP 等图片格式<br/>
最大 5MB建议尺寸 200x200 像素
</div>
<div class="actions" style="gap:12px">
<button class="btn btn-primary" id="upload-avatar-btn">上传头像</button>
<button class="btn btn-secondary" id="reset-avatar-btn">恢复默认</button>
</div>
</div>
</div>
</div>
</div>
${(me && me.role === 'superadmin') ? `
<!-- 用户管理区域 -->
<div class="card" style="margin-bottom:24px">
<h2 style="font-size:18px;font-weight:600;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid var(--border)">👥 用户管理</h2>
<div class="grid cols-2" style="gap:24px;margin-bottom:24px">
<!-- 添加新用户 -->
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:16px;font-size:15px"> 添加新用户</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="输入密码至少6位" />
</div>
2025-11-24 08:21:13 +00:00
<div class="field">
<label>所属工厂</label>
<input id="new-factory" 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 btn-primary" style="width:100%">添加用户</button>
</div>
</div>
<!-- 修改用户密码 -->
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:16px;font-size:15px">🔑 修改用户密码</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" placeholder="输入新密码" />
</div>
<div class="actions">
<button id="change-btn" class="btn btn-primary" style="width:100%">修改密码</button>
</div>
</div>
</div>
2025-11-24 08:21:13 +00:00
<!-- 修改用户工厂 -->
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border);margin-bottom:24px">
<div style="font-weight:600;margin-bottom:16px;font-size:15px">🏭 修改用户所属工厂</div>
<div class="grid cols-2" style="gap:16px">
<div class="field">
<label>选择用户</label>
<select id="factory-user" class="input">${(users.list||[]).map(u=>`<option value="${u.username}" data-factory="${u.factory || ''}">${u.username}${u.factory ? ` (${u.factory})` : ''}</option>`).join('')}</select>
</div>
<div class="field">
<label>新工厂</label>
<input id="factory-name" class="input" placeholder="输入新的工厂名称" value="${(users.list||[])[0]?.factory || ''}" />
</div>
</div>
<div class="actions">
<button id="update-factory-btn" class="btn btn-primary" style="width:100%">更新工厂</button>
</div>
</div>
<!-- 用户列表 -->
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:12px;font-size:15px">📋 用户列表</div>
<ul class="list">${userList}</ul>
</div>
</div>
<!-- 系统配置区域 -->
<div class="card" style="margin-bottom:24px">
<h2 style="font-size:18px;font-weight:600;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid var(--border)"> 系统配置</h2>
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:12px;font-size:15px">💧 水印设置</div>
<div style="display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg);border:1px solid var(--border);border-radius:6px">
2025-12-08 03:20:28 +00:00
<label class="custom-checkbox" style="flex:1;cursor:pointer;display:flex;align-items:center;gap:12px">
<input type="checkbox" id="watermark-toggle" ${localStorage.getItem('watermarkEnabled') !== 'false' ? 'checked' : ''} /><span class="checkmark"></span>
<span style="font-size:15px;font-weight:500">启用页面水印显示用户名和时间戳</span>
</label>
</div>
<div style="margin-top:12px;font-size:13px;color:var(--text-2);line-height:1.5">
💡 水印用于防止数据泄露和责任追溯建议保持启用状态
</div>
</div>
</div>
<!-- 数据管理区域 -->
<div class="card" style="margin-bottom:24px">
<h2 style="font-size:18px;font-weight:600;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid var(--border)">📊 数据管理</h2>
<!-- 数据概览 -->
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border);margin-bottom:20px">
<div style="font-weight:600;margin-bottom:16px;font-size:15px">📈 数据概览</div>
<div class="grid cols-2" style="gap:12px">
<div style="display:flex;flex-direction:column;gap:8px">
<div class="badge" id="overview-stats" style="padding:10px">/不良统计加载中</div>
<div class="badge" id="overview-defects" style="padding:10px">不良明细加载中</div>
<div class="badge" id="overview-mac" style="padding:10px">MAC与批次加载中</div>
<div class="badge" id="overview-shipments" style="padding:10px">发货记录加载中</div>
</div>
<div style="display:flex;flex-direction:column;gap:8px">
<div class="badge" id="overview-devices" style="padding:10px">设备状态加载中</div>
<div class="badge" id="overview-personnel" style="padding:10px">人员信息加载中</div>
<div class="badge" id="overview-qa" style="padding:10px">质检报告加载中</div>
<div class="badge" id="overview-production" style="padding:10px">生产时间加载中</div>
</div>
</div>
</div>
<!-- 数据清空操作 -->
<div class="grid cols-2" style="gap:20px">
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:12px;font-size:15px;color:var(--danger)">🗑 清空上传数据</div>
<div class="actions" style="flex-wrap:wrap;gap:8px">
<button class="btn btn-secondary" data-clear="mac" style="flex:1;min-width:140px">清空MAC与批次</button>
<button class="btn btn-secondary" data-clear="stats" style="flex:1;min-width:140px">清空良/不良统计</button>
<button class="btn btn-secondary" data-clear="defects" style="flex:1;min-width:140px">清空不良明细</button>
<button class="btn btn-secondary" data-clear="shipments" style="flex:1;min-width:140px">清空发货记录</button>
</div>
</div>
<div style="padding:20px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:12px;font-size:15px;color:var(--danger)">🗑 清空扩展采集</div>
<div class="actions" style="flex-wrap:wrap;gap:8px">
<button class="btn btn-secondary" data-clear="devices" style="flex:1;min-width:140px">清空设备状态</button>
<button class="btn btn-secondary" data-clear="environment" style="flex:1;min-width:140px">清空环境参数</button>
<button class="btn btn-secondary" data-clear="personnel" style="flex:1;min-width:140px">清空人员信息</button>
<button class="btn btn-secondary" data-clear="qa" style="flex:1;min-width:140px">清空质检报告</button>
<button class="btn btn-secondary" data-clear="production" style="flex:1;min-width:140px">清空生产时间</button>
</div>
</div>
</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;
}
2025-11-22 12:40:46 +00:00
// 验证文件大小限制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');
2025-11-24 08:21:13 +00:00
const factoryEl = document.getElementById('new-factory');
const roleEl = document.getElementById('new-role');
const username = usernameEl?.value?.trim();
const password = passwordEl?.value;
2025-11-24 08:21:13 +00:00
const factory = factoryEl?.value?.trim();
const role = roleEl?.value || 'admin';
if (!username || !password) {
return API.toast('请输入用户名和密码');
}
2025-11-24 08:21:13 +00:00
if (!factory) {
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',
2025-11-24 08:21:13 +00:00
body: JSON.stringify({ username, password, factory, role })
});
const data = await res.json();
if (res.ok && data.ok) {
API.toast(data.message || '用户创建成功');
// 清空输入框
if (usernameEl) usernameEl.value = '';
if (passwordEl) passwordEl.value = '';
2025-11-24 08:21:13 +00:00
if (factoryEl) factoryEl.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;
}
});
2025-11-24 08:21:13 +00:00
// 更新用户工厂
const factoryUserSelect = document.getElementById('factory-user');
const factoryNameInput = document.getElementById('factory-name');
// 当选择用户时,自动填充当前工厂
factoryUserSelect?.addEventListener('change', () => {
const selectedOption = factoryUserSelect.options[factoryUserSelect.selectedIndex];
const currentFactory = selectedOption.getAttribute('data-factory') || '';
if (factoryNameInput) {
factoryNameInput.value = currentFactory;
}
});
const updateFactoryBtn = document.getElementById('update-factory-btn');
updateFactoryBtn?.addEventListener('click', async () => {
const username = factoryUserSelect?.value;
const factory = factoryNameInput?.value?.trim();
if (!username) {
return API.toast('请选择用户');
}
if (!factory) {
return API.toast('请输入工厂名称');
}
updateFactoryBtn.disabled = true;
try {
const res = await fetch('/api/admin/update-user-factory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, factory })
});
const data = await res.json();
if (res.ok && data.ok) {
API.toast(data.message || '工厂更新成功');
// 刷新页面以更新用户列表
setTimeout(() => Router.navigate('/settings'), 1000);
} else {
API.toast(data.error || '更新失败');
}
} catch(e) {
API.toast('更新失败:' + e.message);
} finally {
updateFactoryBtn.disabled = false;
}
});
// 水印开关
const watermarkToggle = document.getElementById('watermark-toggle');
watermarkToggle?.addEventListener('change', (e) => {
const enabled = e.target.checked;
if (window.toggleWatermark) {
window.toggleWatermark(enabled);
API.toast(enabled ? '水印已启用' : '水印已关闭');
}
});
// 删除用户
document.querySelectorAll('button[data-delete-user]')?.forEach(btn => {
btn.addEventListener('click', async () => {
const username = btn.getAttribute('data-delete-user');
if (!confirm(`确定要删除用户 "${username}" 吗?此操作不可恢复。`)) {
return;
}
btn.disabled = true;
try {
await API.deleteUser(username);
API.toast('用户已删除');
// 刷新页面以更新用户列表
setTimeout(() => Router.navigate('/settings'), 1000);
} catch(e) {
API.toast('删除失败');
} finally {
btn.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;
});