ERP/frontend/js/components/settings.js
2025-12-08 11:20:28 +08:00

485 lines
21 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 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>
<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">
支持 JPG、PNG、GIF动图、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>
<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>
<!-- 修改用户工厂 -->
<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">
<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;
}
// 验证文件大小限制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 factoryEl = document.getElementById('new-factory');
const roleEl = document.getElementById('new-role');
const username = usernameEl?.value?.trim();
const password = passwordEl?.value;
const factory = factoryEl?.value?.trim();
const role = roleEl?.value || 'admin';
if (!username || !password) {
return API.toast('请输入用户名和密码');
}
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',
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 = '';
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;
}
});
// 更新用户工厂
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;
});