编辑页面

修改后将从当前时间重新计算过期时间

导入预览

识别字段
23
有效数据
486
需确认
14
`; } function permissionsView() { const grantRows = personPermissions.filter(item => permissionRules[item.permission]).map(item => [ item.person, people.find(p => p.name === item.person)?.role || '-', item.permission, permissionExpireText(item) ]); return `

角色权限包

${actionButton('newRole', '')}
${tablePanel(['角色', '默认说明', '人数'], roles.map(r => [r.name, r.desc, r.count]))}

个人权限覆盖

${actionButton('grantPermission', '')}
${tablePanel(['人员', '角色', '覆盖权限', '有效期'], grantRows)}
`; } function peopleView() { const rows = state.role === 'lead' ? people.filter(p => p.group === 'A组' && ['技术人员', '技术负责人'].includes(p.role)) : people; const title = state.role === 'lead' ? '本组技术人员管理' : '人员管理'; const addButton = state.role === 'lead' ? '' : actionButton('newRole', ''); return `

${title}

${addButton}
${tablePanel(['姓名', '账号', '角色', '所属组', '个人授权', '状态', '操作'], rows.map(p => [ p.name, p.account, p.role, p.group, permissionSummary(p.name), statusBadge(p.status, p.status === '启用' ? 'green' : 'red'), state.role === 'lead' ? `` : ` ` ]), true)}
`; } function logsView() { const restoreButton = versionId => { if (!can('restoreVersion')) return '无权限'; return canRestoreVersion(versionId) ? `` : '仅追溯'; }; return `

操作日志与版本追溯

${tablePanel(['操作', '版本号', '步骤', '时间', '操作人', '模块', '动作', '对象', '操作前', '操作后'], logs.map(l => [ restoreButton(l.versionId), l.versionId || '-', l.stepNo ? `第${l.stepNo}步` : '-', l.time, l.user, l.module, l.action, l.object, logValue(l.before), logValue(l.after) ]), true)}
`; } const views = { dashboard, projects: projectsView, experiments: experimentsView, formulas: formulasView, experiment: experimentView, quality: qualityView, rawQuality: rawQualityView, outQuality: outQualityView, inventory: inventoryView, stockRecords: stockRecordsView, import: importView, people: peopleView, permissions: permissionsView, logs: logsView, aiAnalysis: aiAnalysisView }; function renderRoleSwitch() { const switcher = $('#roleSwitch'); switcher.innerHTML = Object.entries(roleProfiles).map(([key, item]) => ``).join(''); switcher.value = state.role; } function render() { try { ensureViewAllowed(); renderRoleSwitch(); renderNav(); setTitle(); content.innerHTML = roleTip() + views[state.view](); bindActions(); } catch (error) { console.error('页面渲染失败', error); content.innerHTML = `

页面渲染失败

当前演示数据有一处格式异常:${error.message || '未知错误'}。请返回上一步或刷新页面后重试。
`; } } function bindActions() { document.querySelectorAll('[data-jump]').forEach(btn => { btn.addEventListener('click', () => { goToView(btn.dataset.jump, { detailTab: btn.dataset.jump === 'projects' ? 'overview' : state.detailTab }); }); }); document.querySelectorAll('[data-project]').forEach(row => { row.addEventListener('click', (event) => { event.stopPropagation(); const project = findVisibleProject(row.dataset.project); if (!project) return; state.selectedProject = project; goToView('projects', { project, detailTab: 'experiment' }); }); }); document.querySelectorAll('[data-experiments]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const project = findVisibleProject(btn.dataset.experiments); if (!project) return; goToView('experiment', { project }); }); }); document.querySelectorAll('[data-assign-project]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); if (!can('assign')) return; const project = findVisibleProject(btn.dataset.assignProject); if (!project) return; state.selectedProject = project; openModal('assign'); }); }); document.querySelectorAll('[data-edit-project]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const project = findVisibleProject(btn.dataset.editProject); if (!project) return; state.selectedProject = project; openModal('editProject'); }); }); document.querySelectorAll('[data-experiment-direction-filter]').forEach(select => { select.addEventListener('change', () => { state.experimentDirectionFilter = select.value; render(); }); }); document.querySelectorAll('[data-experiment-category-filter]').forEach(select => { select.addEventListener('change', () => { state.experimentCategoryFilter = select.value; state.experimentSubcategoryFilter = '全部产品类型'; render(); }); }); document.querySelectorAll('[data-experiment-subcategory-filter]').forEach(select => { select.addEventListener('change', () => { state.experimentSubcategoryFilter = select.value; render(); }); }); document.querySelectorAll('[data-project-category-filter]').forEach(select => { select.addEventListener('change', () => { state.projectCategoryFilter = select.value; state.projectSubcategoryFilter = '全部产品类型'; render(); }); }); document.querySelectorAll('[data-project-subcategory-filter]').forEach(select => { select.addEventListener('change', () => { state.projectSubcategoryFilter = select.value; render(); }); }); document.querySelectorAll('[data-project-direction]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); state.projectDirectionFilter = btn.dataset.projectDirection || '全部产品方向'; render(); }); }); document.querySelectorAll('[data-add-experiment]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const project = findVisibleProject(btn.dataset.addExperiment); if (!project) return; state.selectedProject = project; openModal('newExperiment'); }); }); document.querySelectorAll('[data-new-experiment-from-list]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const project = visibleProjects()[0] || projects[0]; if (project) state.selectedProject = project; openModal('newExperiment'); }); }); document.querySelectorAll('[data-tech-plan]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const project = findVisibleProject(btn.dataset.techPlan); if (!project) return; state.selectedProject = project; openModal('newTechPlan'); }); }); document.querySelectorAll('[data-exp-detail]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); openModal('experimentDetail', btn.dataset.expDetail); }); }); document.querySelectorAll('[data-reuse-experiment]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); openModal('reuseExperiment', btn.dataset.reuseExperiment); }); }); document.querySelectorAll('[data-test-record]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const [projectId] = String(btn.dataset.testRecord || '').split('::'); const project = projects.find(p => p.id === projectId); if (project) state.selectedProject = project; openModal('newTest', btn.dataset.testRecord); }); }); document.querySelectorAll('[data-stock-in]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); openModal('stockIn', btn.dataset.stockIn); }); }); document.querySelectorAll('[data-stock-out]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); openModal('stockOut', btn.dataset.stockOut); }); }); document.querySelectorAll('[data-raw-out]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); openModal('rawOut', btn.dataset.rawOut); }); }); document.querySelectorAll('[data-product-in]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); openModal('productIn', btn.dataset.productIn); }); }); document.querySelectorAll('[data-tab]').forEach(tab => { tab.addEventListener('click', () => { state.detailTab = tab.dataset.tab; render(); }); }); document.querySelectorAll('[data-revoke-permission]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); const [person, permission] = String(btn.dataset.revokePermission || '').split('::'); if (!person || !permission) return; revokePermission(person, permission); }); }); document.querySelectorAll('[data-open-modal]').forEach(btn => { btn.addEventListener('click', () => openModal(btn.dataset.openModal, btn.dataset.person || '')); }); document.querySelectorAll('[data-restore-version]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); confirmRestoreVersion(btn.dataset.restoreVersion); }); }); document.querySelectorAll('[data-export-projects]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); exportProjectTable(); }); }); document.querySelectorAll('[data-export-experiments]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); exportExperimentTable(); }); }); document.querySelectorAll('[data-run-ai]').forEach(btn => { btn.addEventListener('click', (event) => { event.stopPropagation(); if (btn.dataset.runAi === 'dashboard') buildDashboardAiResult(); persistSharedData(); render(); showToast(); }); }); } function openModal(type, payload) { state.modalType = type; state.modalPayload = payload || ''; const titles = { newProject: '新增项目', editProject: '编辑项目', importProject: 'Excel导入项目表', importExperimentExcel: 'Excel导入实验数据', assign: '项目分配', newExperiment: '新增实验记录', newTechPlan: '输出技术方案', newFormula: '新增配方版本', newTest: '新增检测记录', stockIn: '原料入库', stockOut: '成品出库', rawOut: '用料出库', productIn: '成品生产入库', newRole: '新增角色', newPerson: '新增人员', editPerson: '编辑人员', grantPermission: '授予个人权限', restoreVersion: '恢复版本', experimentDetail: '实验记录详情', reuseExperiment: '复用实验记录' }; $('#modalTitle').textContent = titles[type] || '新增记录'; try { $('#modalBody').innerHTML = modalForm(type); bindModalInteractions(type); $('#modalSave').style.display = type === 'experimentDetail' || (type === 'reuseExperiment' && !projectsWithoutExperiments().length) ? 'none' : ''; } catch (error) { console.error('弹窗打开失败', error); $('#modalBody').innerHTML = `
弹窗打开失败:${error.message || '未知错误'}
`; $('#modalSave').style.display = 'none'; } $('#modal').classList.add('open'); } function bindModalInteractions(type) { if (type === 'newExperiment') { const selectedDimensions = new Set(Array.from(document.querySelectorAll('[data-dimension-value]')).map(input => input.dataset.dimensionName).filter(Boolean)); const fieldsWrap = document.querySelector('[data-dimension-fields]'); const editButton = document.querySelector('[data-edit-dimensions]'); const optionsWrap = document.querySelector('[data-dimension-options]'); const fixedWrap = document.querySelector('[data-fixed-dimensions]'); const readCurrentValues = () => { const values = new Map(); document.querySelectorAll('[data-dimension-value]').forEach(input => { values.set(input.dataset.dimensionName, input.value); }); return values; }; const renderDimensionFields = () => { if (!fieldsWrap) return; const currentValues = readCurrentValues(); fieldsWrap.innerHTML = [...selectedDimensions].map(name => ` `).join(''); }; editButton?.addEventListener('click', () => { if (optionsWrap) optionsWrap.style.display = ''; if (fixedWrap) fixedWrap.style.display = 'none'; editButton.style.display = 'none'; }); document.querySelectorAll('[data-dimension-option]').forEach(btn => { if (selectedDimensions.has(btn.dataset.dimensionOption)) btn.classList.add('active'); btn.addEventListener('click', () => { const name = btn.dataset.dimensionOption; if (!name) return; if (selectedDimensions.has(name)) { selectedDimensions.delete(name); btn.classList.remove('active'); } else { selectedDimensions.add(name); btn.classList.add('active'); } renderDimensionFields(); }); }); } if (type === 'newFormula') { const projectSelect = document.querySelector('[data-formula-project]'); const directionInput = document.querySelector('[data-formula-direction]'); const productInput = document.querySelector('[data-formula-product]'); const syncFormulaProject = () => { const project = projects.find(item => item.id === projectSelect?.value) || projects[0]; if (project && directionInput) directionInput.value = productDirectionText(project); if (project && productInput) productInput.value = projectProductName(project); }; projectSelect?.addEventListener('change', syncFormulaProject); syncFormulaProject(); } if (type === 'assign') { const groupSelect = document.querySelector('[data-assign-group]'); const ownerSelect = document.querySelector('[data-assign-owner]'); const renderOwnerOptions = () => { if (!groupSelect || !ownerSelect) return; const group = state.role === 'lead' ? 'A组' : groupSelect.value; const rows = people.filter(person => person.role === '技术人员' && person.group === group && person.status !== '停用'); const currentOwner = ownerSelect.value || state.selectedProject.owner; ownerSelect.innerHTML = rows.map(person => `${person.name}`).join(''); }; groupSelect?.addEventListener('change', renderOwnerOptions); renderOwnerOptions(); } if (type === 'newProject' || type === 'editProject') { const groupSelect = document.querySelector('[data-project-group]'); const ownerSelect = document.querySelector('[data-project-owner]'); const categorySelect = document.querySelector('[data-product-category]'); const subcategorySelect = document.querySelector('[data-product-subcategory]'); const renderProjectOwnerOptions = () => { if (!groupSelect || !ownerSelect) return; const currentOwner = ownerSelect.value || state.selectedProject?.owner; const rows = technicalOwners(groupSelect.value); ownerSelect.innerHTML = rows.map(person => `${person.name}`).join(''); }; const renderProductSubcategoryOptions = () => { if (!categorySelect || !subcategorySelect) return; const currentSubcategory = subcategorySelect.value; const direction = productDirections.find(item => item.category === categorySelect.value) || productDirections[0]; const rows = direction?.children || []; subcategorySelect.innerHTML = rows.map(child => `${child.name}`).join(''); }; groupSelect?.addEventListener('change', renderProjectOwnerOptions); categorySelect?.addEventListener('change', renderProductSubcategoryOptions); renderProjectOwnerOptions(); renderProductSubcategoryOptions(); } } function modalForm(type) { if (type === 'assign') { const groupOptions = state.role === 'lead' ? '' : `A组B组`; const selectedGroup = state.role === 'lead' ? 'A组' : (state.selectedProject.group && state.selectedProject.group !== '-' ? state.selectedProject.group : 'A组'); const technicianPool = state.role === 'lead' ? people.filter(person => person.role === '技术人员' && person.group === 'A组' && person.status !== '停用') : people.filter(person => person.role === '技术人员' && person.group === selectedGroup && person.status !== '停用'); const ownerOptions = technicianPool.map(person => `${person.name}`).join(''); return `
`; } if (type === 'newExperiment') { const project = state.selectedProject || visibleProjects()[0] || projects[0]; const fixedDimensions = fixedExperimentDimensions(project); const dimensions = [...new Set([...fixedDimensions, ...experimentDimensionOptions(project)])]; const hasFixedDimensions = fixedDimensions.length > 0; const ownerField = state.role === 'tech' ? `` : ``; return `
${ownerField}
${hasFixedDimensions ? '本项目固定实验维度' : '选择实验维度'}
${hasFixedDimensions ? `
${fixedDimensions.map(name => `${name}`).join('')}
${fixedDimensions.map(name => ``).join('')}
` : `
${dimensions.map(name => ``).join('')}
`}
`; } if (type === 'newTechPlan') { return `
`; } if (type === 'newFormula') { const projectOptions = projects.map(project => ``).join(''); return `
`; } if (type === 'newProject' || type === 'editProject') { const editing = type === 'editProject'; const project = editing ? state.selectedProject : {}; const nextProjectNo = `P202607-${String(projects.length + 101).padStart(3, '0')}`; const selectedCategory = productCategory(project); const selectedSubcategory = productSubcategory(project); const selectedGroup = project.group && project.group !== '-' ? project.group : 'A组'; const ownerRows = technicalOwners(selectedGroup); const selected = (value, target) => value === target ? ' selected' : ''; const categoryOptions = productDirections.map(direction => `${direction.category}`).join(''); const activeDirection = productDirections.find(direction => direction.category === selectedCategory) || productDirections[0] || { children: [] }; const subcategoryOptions = activeDirection.children.map(child => `${child.name}`).join(''); return `
`; } if (type === 'importProject') { return `
`; } if (type === 'importExperimentExcel') { return `
`; } if (type === 'newTest') { const [payloadProject, payloadNo] = String(state.modalPayload || '').split('::'); const record = visibleExperimentRecords().find(r => r.project === payloadProject && r.no === payloadNo); const project = projects.find(p => p.id === (record?.project || state.selectedProject.id)) || state.selectedProject; const batchNo = `Q${String(tests.length + 1).padStart(3, '0')}`; return `
`; } if (type === 'stockIn' || type === 'stockOut') { const test = tests.find(item => item.batch === state.modalPayload) || {}; const itemType = type === 'stockIn' ? '原料' : '成品'; const item = inventoryItemForTest(test, itemType); const defaultQty = type === 'stockIn' ? 100 : Math.min(Number(item.qty) || 0, 50); return `
`; } if (type === 'rawOut' || type === 'productIn') { const itemType = type === 'rawOut' ? '原料' : '成品'; const item = inventoryItems.find(row => row.name === state.modalPayload && row.type === itemType) || {}; const isNewProduct = type === 'productIn' && !item.name; const defaultQty = type === 'rawOut' ? Math.min(Number(item.qty) || 0, 50) : 100; return `
`; } if (type === 'grantPermission') { const selectedPerson = normalizePersonName(state.modalPayload) || '张工'; const personOptions = ['张工', '王工', '陈工', '王负责人', '陈负责人', '赵检'].map(name => `${name}`).join(''); const permissionOptions = Object.keys(permissionRules).map(name => ``).join(''); return `
`; } if (type === 'newPerson' || type === 'editPerson') { const editingPerson = people.find(p => p.name === state.modalPayload) || people.find(p => p.account === state.modalPayload) || {}; const selected = (value, target) => value === target ? ' selected' : ''; if (state.role === 'lead') { return `
`; } return `
`; } if (type === 'experimentDetail') { const [payloadProject, payloadNo] = String(state.modalPayload || '').split('::'); const record = visibleExperimentRecords().find(r => r.project === payloadProject && r.no === payloadNo); const project = projects.find(p => p.id === record?.project) || {}; const dimensionRows = Array.isArray(record?.dimensions) ? record.dimensions : []; return `
${record ? experimentDisplayNo(record) : ''} / ${project.name || project.product || ''}
项目编号:${record?.project || '-'} 产品方向:${record ? experimentDirectionText(record) : '-'} 产品:${projectProductName(project)}
客户来源备注:${project.customer || '-'} 实验人员:${record?.owner || '-'} 日期:${record?.date || '-'}
实验项目:${record?.item || '-'} 配方版本:${record?.formula || '-'}
结论:${record?.conclusion || '-'}
实验编号实验项目实验结果实验人员日期
${record ? experimentDisplayNo(record) : '-'}${record?.item || '-'}${record ? statusBadge(record.result) : '-'}${record?.owner || '-'}${record?.date || '-'}
${dimensionRows.length ? `
${dimensionRows.map(row => ``).join('')}
维度名称填写内容判定
${row.name}${dimensionContentText(row.value)}${statusBadge(dimensionResultText(row.value), dimensionResultText(row.value) === '不合格' ? 'red' : 'green')}
` : ''}
`; } if (type === 'reuseExperiment') { const source = findExperimentByKey(state.modalPayload); const sourceProject = experimentProject(source || {}); const targetProjects = projectsWithoutExperiments(); const targetOptions = targetProjects.map(project => ``).join(''); return `
来源实验:${source ? `${experimentDisplayNo(source)} / ${sourceProject.customer || '-'} / ${source.item || '-'}` : '-'}
实验结果:${source ? statusBadge(source.result) : '-'} 维度数量:${source ? experimentDimensionCount(source) : 0} 个
${targetProjects.length ? ` ` : '
暂无可复用的新项目。只有没有实验记录的项目才会出现在这里。
'}
`; } return `
`; } function closeModal() { $('#modal').classList.remove('open'); } function showToast() { const toast = $('#toast'); toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2200); } $('#modalClose').addEventListener('click', closeModal); $('#modalCancel').addEventListener('click', closeModal); $('#modalSave').addEventListener('click', async () => { try { const modalInputs = Array.from(document.querySelectorAll('#modalBody input, #modalBody select, #modalBody textarea')).map(el => el.value); if (state.modalType === 'importProject') { try { const result = await importProjectFile(); const hint = result.added || result.updated ? '已刷新到项目管理列表。' : '未新增项目,请检查项目编号是否已存在或字段是否正确。'; alert(`导入完成:新增 ${result.added} 条,更新 ${result.updated} 条,跳过 ${result.skipped} 条。${hint}`); } catch (error) { alert(error.message || '导入失败,请检查项目表字段。'); return; } } if (state.modalType === 'importExperimentExcel') { try { const result = await importExperimentExcelFile(); const hint = result.added ? '已刷新到实验记录列表。' : '未新增数据,请检查项目编号是否存在,或实验编号是否已导入过。'; alert(`导入完成:新增 ${result.added} 条,跳过重复 ${result.skipped} 条,待确认 ${result.unmatched} 条,无权限 ${result.denied || 0} 条。${hint}`); } catch (error) { alert(error.message || '导入失败,请检查Excel字段。'); return; } } if (state.modalType === 'newFormula') { const code = modalInputs[0] || `F${String(formulas.length + 1).padStart(3, '0')}`; const existing = formulas.find(item => item.code === code); if (existing) { alert('配方版本已存在,请修改后再保存。'); return; } const project = projects.find(item => item.id === modalInputs[2]) || state.selectedProject; formulas.unshift({ code, name: modalInputs[1] || '新配方', project: project.id, category: productCategory(project), subcategory: productSubcategory(project), productName: projectProductName(project), ratio: `树脂 ${modalInputs[5] || '-'} / 固化剂 ${modalInputs[6] || '-'} / 助剂 ${modalInputs[7] || '-'} / 溶剂 ${modalInputs[8] || '-'}`, status: modalInputs[9] || '待验证', owner: modalInputs[10] || profile().user, date: currentFollowTime().split(' ')[0], note: modalInputs[11] || '该配方比例固定保存,后续项目实验可引用该版本。' }); state.view = 'formulas'; state.filter = ''; $('#globalSearch').value = ''; addLog('配方管理', '新增配方版本', code, '无', `${modalInputs[1] || '新配方'} / ${project.id}`); } if (state.modalType === 'newProject') { const projectId = modalInputs[0] || `P202607-${String(projects.length + 101).padStart(3, '0')}`; if (projects.some(project => project.id === projectId)) { alert('项目编号已存在,请修改后再保存。'); return; } const status = modalInputs[6] || '待分配'; const newProject = { id: projectId, name: modalInputs[1] || modalInputs[4] || '新项目', category: modalInputs[2] || '环氧防腐类', subcategory: modalInputs[3] || '环氧地坪涂料', productName: modalInputs[4] || '水性涂层开发', customer: modalInputs[5] || '客户来源待补充', product: modalInputs[4] || '水性涂层开发', status, statusClass: statusClassFor(status), group: modalInputs[7] || 'A组', owner: modalInputs[8] || '未分配', priority: modalInputs[9] || '普通', progress: 0, experiments: 0, samples: 0, due: modalInputs[10] || '2026-08-20', note: modalInputs[11] || '记录项目来源、初始指标和研发背景。' }; projects.unshift(newProject); state.selectedProject = newProject; state.view = 'projects'; state.detailTab = 'overview'; state.filter = ''; $('#globalSearch').value = ''; state.lastImportProjectIds = [newProject.id]; addLog('项目管理', '新增项目', newProject.id, '-', `${newProject.customer} / ${newProject.product}`); } if (state.modalType === 'editProject') { const project = state.selectedProject; if (project) { const beforeText = `${project.name || project.product} / ${project.customer || '-'} / ${project.status || '-'}`; project.name = modalInputs[1] || project.name; project.category = modalInputs[2] || project.category; project.subcategory = modalInputs[3] || project.subcategory; project.productName = modalInputs[4] || project.productName; project.product = modalInputs[4] || project.product; project.customer = modalInputs[5] || project.customer; project.status = modalInputs[6] || project.status; project.statusClass = statusClassFor(project.status); project.group = modalInputs[7] || project.group; project.owner = modalInputs[8] || project.owner; project.priority = modalInputs[9] || project.priority; project.due = modalInputs[10] || project.due; project.note = modalInputs[11] || project.note; state.view = 'projects'; state.detailTab = 'overview'; state.filter = ''; $('#globalSearch').value = ''; addLog('项目管理', '编辑项目基础信息', project.id, beforeText, `${project.name || project.product} / ${project.customer || '-'} / ${project.status || '-'}`); } } if (state.modalType === 'assign') { if (!can('assign')) { alert('当前角色无权分配项目。'); return; } const previousOwner = state.selectedProject.owner; const previousGroup = state.selectedProject.group; if (state.role === 'lead' && state.selectedProject.group !== 'A组') { alert('技术负责人只能分配本组项目。'); return; } state.selectedProject.group = modalInputs[1] || state.selectedProject.group; state.selectedProject.owner = modalInputs[2] || state.selectedProject.owner; if (state.role === 'lead') { state.selectedProject.group = 'A组'; state.selectedProject.priority = modalInputs[3] || state.selectedProject.priority; } else { state.selectedProject.priority = modalInputs[3] || state.selectedProject.priority; } if (state.selectedProject.status === '待分配') { updateProjectStatus(state.selectedProject, '研发中'); } const beforeText = `${previousGroup}/${previousOwner}`; const afterText = `${state.selectedProject.group}/${state.selectedProject.owner}`; addLog('项目管理', '分配/修改项目人员', state.selectedProject.id, beforeText, afterText); } if (state.modalType === 'newExperiment') { const projectId = document.querySelector('[data-experiment-project]')?.value || state.selectedProject?.id; const project = projects.find(p => p.id === projectId) || state.selectedProject; if (!project || !canEditExperimentProject(project.id)) { alert('当前角色只能给自己权限范围内的项目添加实验数据。'); return; } const dimensionRows = Array.from(document.querySelectorAll('[data-dimension-value]')).map(input => ({ name: input.dataset.dimensionName, value: input.value.trim() })).filter(item => item.name); if (!dimensionRows.length) { alert('请至少选择一个实验维度。'); return; } const nextNo = `EXP-${project.id.replace(/\D/g, '').slice(-3)}-${String(experimentRecords.filter(r => r.project === project.id).length + 1).padStart(2, '0')}`; const formulaValue = document.querySelector('[data-experiment-formula]')?.value || 'F003'; const resultValue = document.querySelector('[data-experiment-result]')?.value || '研发中'; const ownerValue = state.role === 'tech' ? profile().user : (document.querySelector('[data-experiment-owner]')?.value || project.owner); const dateValue = document.querySelector('[data-experiment-date]')?.value || '2026/07/08'; const conclusionValue = document.querySelector('#modalBody textarea')?.value || ''; const newRecord = { project: project.id, no: nextNo, direction: productDirectionText(project), formula: formulaValue, item: dimensionRows.map(item => item.name).join('/'), dimensions: dimensionRows, result: resultValue, owner: ownerValue, date: dateValue.replace(/\//g, '-'), conclusion: conclusionValue || `已选择 ${dimensionRows.length} 个实验维度:${dimensionRows.map(item => item.name).join('、')}。` }; experimentRecords.push(newRecord); project.experiments += 1; if (project.status === '待分配') updateProjectStatus(project, '研发中'); state.lastImportExperimentKeys = [experimentKey(newRecord)]; state.view = 'experiments'; state.filter = ''; $('#globalSearch').value = ''; addLog('实验记录', '新增实验记录', project.id, '-', experimentDisplayNo(newRecord)); } if (state.modalType === 'reuseExperiment') { const source = findExperimentByKey(state.modalPayload); const targetId = document.querySelector('[data-reuse-target]')?.value; const targetProject = projects.find(project => project.id === targetId); if (!source) { alert('未找到来源实验记录。'); return; } if (!targetProject) { alert('请选择要复用到的新项目。'); return; } if (experimentRecords.some(record => record.project === targetProject.id)) { alert('该项目已经有实验记录,不能作为复用目标。'); return; } if (!canEditExperimentProject(targetProject.id)) { alert('当前角色无权给该项目复用实验记录。'); return; } const sourceProject = experimentProject(source); const nextNo = `EXP-${targetProject.id.replace(/\D/g, '').slice(-3)}-01`; const note = document.querySelector('[data-reuse-note]')?.value || ''; const reusedRecord = { project: targetProject.id, no: nextNo, direction: productDirectionText(targetProject), formula: source.formula, item: source.item, dimensions: Array.isArray(source.dimensions) ? source.dimensions.map(item => ({ ...item })) : [], result: source.result, owner: targetProject.owner || source.owner, date: currentFollowTime().split(' ')[0], conclusion: note || `复用 ${sourceProject.id || source.project} 的 ${experimentDisplayNo(source)} 实验记录。` }; experimentRecords.push(reusedRecord); targetProject.experiments = experimentRecords.filter(record => record.project === targetProject.id).length; if (targetProject.status === '待分配') updateProjectStatus(targetProject, '研发中'); state.lastImportExperimentKeys = [experimentKey(reusedRecord)]; state.view = 'experiments'; state.filter = ''; $('#globalSearch').value = ''; addLog('实验记录', '复用实验记录', `${source.project} -> ${targetProject.id}`, experimentDisplayNo(source), experimentDisplayNo(reusedRecord)); } if (state.modalType === 'newTechPlan') { const project = state.selectedProject; const previousStatus = project.status; updateProjectStatus(project, '方案已提交'); addLog('项目管理', '输出技术方案', project.id, previousStatus, `${modalInputs[4] || 'TS-001'} / ${modalInputs[5] || '待确认'}`); } if (state.modalType === 'newTest') { tests.unshift({ batch: modalInputs[0] || `Q${String(tests.length + 1).padStart(3, '0')}`, object: modalInputs[3] || '原料/成品批次', type: modalInputs[4] || '原料来料检验', project: modalInputs[1] || state.selectedProject.id, result: modalInputs[5] || '合格', inspector: modalInputs[6] || profile().user, reason: modalInputs[7] || '-', trend: modalInputs[8] || '合格后允许入库/出库;不合格禁止入库/出库', value: modalInputs[9] || '检测指标已录入', handling: modalInputs[10] || '合格贴合格标签并允许入库/出库;异常批次隔离、复测或返工。', history: String(modalInputs[11] || '取样检测 → 出具检测结果 → 合格入库/出库 或 不合格隔离处理').split(/→|>|,|,/).map(item => item.trim()).filter(Boolean), date: '07-07' }); addLog('检测质检', '新增检测记录', `${modalInputs[1] || state.selectedProject.id} / ${modalInputs[0] || '-'}`, '-', `${modalInputs[4] || '原料来料检验'} / ${modalInputs[5] || '合格'}`); } if (state.modalType === 'stockIn' || state.modalType === 'stockOut') { if (!can('stockManage')) { alert('当前角色无权操作库存。'); return; } const test = tests.find(item => item.batch === (modalInputs[0] || state.modalPayload)); if (!test || !testPassed(test)) { alert('只有检测合格的批次才能进行库存操作。'); return; } const isIn = state.modalType === 'stockIn'; const itemType = isIn ? '原料' : '成品'; const item = inventoryItemForTest(test, itemType); const qty = Math.max(0, Number(modalInputs[3]) || 0); if (!qty) { alert('请输入有效数量。'); return; } if (!isIn && Number(item.qty) < qty) { alert('当前库存不足,无法出库。'); return; } const beforeQty = Number(item.qty) || 0; item.unit = modalInputs[4] || item.unit || 'kg'; item.location = isIn ? (modalInputs[5] || item.location || '原料库') : item.location; item.qty = isIn ? beforeQty + qty : beforeQty - qty; item.lastBatch = test.batch; inventoryRecords.unshift({ time: currentFollowTime(), type: isIn ? '原料入库' : '成品出库', itemType, movement: isIn ? '入库' : '出库', item: item.name, batch: test.batch, qty, unit: item.unit, before: beforeQty, after: item.qty, operator: profile().user, note: modalInputs[6] || (isIn ? '原料检测合格,贴合格标签后入库。' : '成品出库检测合格,扣减库存并允许出库。') }); state.view = 'stockRecords'; addLog('库存管理', isIn ? '原料入库' : '成品出库', item.name, `${beforeQty}${item.unit}`, `${item.qty}${item.unit}`); } if (state.modalType === 'rawOut' || state.modalType === 'productIn') { if (!can('stockManage')) { alert('当前角色无权操作库存。'); return; } const isProductIn = state.modalType === 'productIn'; const itemType = isProductIn ? '成品' : '原料'; const movement = isProductIn ? '入库' : '出库'; const itemName = modalInputs[0] || state.modalPayload || (isProductIn ? '新成品型号' : ''); if (isProductIn && itemName === '新成品型号') { alert('请填写实际成品名称。'); return; } let item = inventoryItems.find(row => row.name === itemName && row.type === itemType); if (!item && isProductIn) { item = { name: itemName, type: itemType, qty: 0, unit: modalInputs[4] || 'kg', location: modalInputs[5] || '成品库', lastBatch: modalInputs[2] || '生产入库' }; inventoryItems.unshift(item); } if (!item) { alert('未找到对应库存物料。'); return; } const qty = Math.max(0, Number(modalInputs[3]) || 0); if (!qty) { alert('请输入有效数量。'); return; } if (!isProductIn && Number(item.qty) < qty) { alert('当前原料库存不足,无法出库。'); return; } const beforeQty = Number(item.qty) || 0; item.unit = modalInputs[4] || item.unit || 'kg'; if (isProductIn) item.location = modalInputs[5] || item.location || '成品库'; item.qty = isProductIn ? beforeQty + qty : beforeQty - qty; item.lastBatch = modalInputs[2] || (isProductIn ? '生产入库' : '用料出库'); inventoryRecords.unshift({ time: currentFollowTime(), type: `${itemType}${movement}`, itemType, movement, item: item.name, batch: item.lastBatch, qty, unit: item.unit, before: beforeQty, after: item.qty, operator: profile().user, note: modalInputs[6] || (isProductIn ? '成品生产完成后入库。' : '原料用于研发或生产领用。') }); state.view = 'stockRecords'; addLog('库存管理', `${itemType}${movement}`, item.name, `${beforeQty}${item.unit}`, `${item.qty}${item.unit}`); } if (state.modalType === 'newPerson') { const personRole = state.role === 'lead' ? '技术人员' : (modalInputs[2] || '技术人员'); const personGroup = state.role === 'lead' ? 'A组' : (modalInputs[3] || 'A组'); people.unshift({ name: modalInputs[0] || '新员工', account: modalInputs[1] || 'newuser', role: personRole, group: personGroup, phone: modalInputs[4] || '138****0000', status: modalInputs[5] || '启用' }); addLog('人员管理', state.role === 'lead' ? '新增本组技术人员' : '新增人员', modalInputs[0] || '新员工', '-', `${personRole} / ${personGroup}`); } if (state.modalType === 'editPerson') { const target = people.find(p => p.name === state.modalPayload) || people.find(p => p.account === state.modalPayload) || people.find(p => p.account === modalInputs[1]) || people.find(p => p.name === modalInputs[0]); if (target) { if (state.role === 'lead' && (target.group !== 'A组' || !['技术人员', '技术负责人'].includes(target.role))) { alert('技术负责人只能维护本组技术人员。'); return; } const before = `${target.role}/${target.group}/${target.status}`; target.name = modalInputs[0] || target.name; target.account = modalInputs[1] || target.account; target.role = state.role === 'lead' ? '技术人员' : (modalInputs[2] || target.role); target.group = state.role === 'lead' ? 'A组' : (modalInputs[3] || target.group); target.phone = modalInputs[4] || target.phone; target.status = modalInputs[5] || target.status; addLog('人员管理', '编辑人员', target.name, before, `${target.role}/${target.group}/${target.status}`); } } if (state.modalType === 'grantPermission') { const person = normalizePersonName(modalInputs[0] || '张工'); const permission = modalInputs[1] || '查看操作日志'; const expires = modalInputs[2] || '长期'; const reason = modalInputs[3] || '项目需要'; const existing = personPermissions.find(item => normalizePersonName(item.person) === person && item.permission === permission); const payload = { person, permission, expires, reason, grantedBy: profile().user, time: currentFollowTime() }; if (existing) { const before = permissionExpireText(existing); Object.assign(existing, payload); addLog('人员管理', '更新个人权限', person, before, `${permission} / ${expires}`); } else { personPermissions.unshift(payload); addLog('人员管理', '授予个人权限', person, '-', `${permission} / ${expires}`); } } persistSharedData(); closeModal(); showToast(); render(); } catch (error) { console.error('保存演示数据失败', error); alert(`保存失败:${error.message || '请检查填写内容后重试。'}`); } }); $('#modal').addEventListener('click', (event) => { if (event.target.id === 'modal') closeModal(); }); $('#globalSearch').addEventListener('input', (event) => { state.filter = event.target.value; if (state.view !== 'projects') { goToView('projects', { filter: state.filter }); } else { render(); } $('#globalSearch').value = state.filter; $('#globalSearch').focus(); }); $('#backBtn').addEventListener('click', goBack); $('#printBtn').addEventListener('click', () => window.print()); $('#roleSwitch').addEventListener('change', (event) => { loadSharedData(); state.selectedProject = projects.find(p => p.id === state.selectedProject?.id) || projects[0]; state.role = event.target.value; state.filter = ''; state.history = []; $('#globalSearch').value = ''; ensureViewAllowed(); render(); showToast(); }); render(); 取消
访问码: uiipbpb8