// 蝶雅采购智能体平台 - 前端逻辑 let charts = {}; // 页面缓存:切换菜单时不销毁页面状态 var CACHE_VERSION = 78; if (!window._pageCache) window._pageCache = {}; if (!window._pageDataCache) window._pageDataCache = {}; if (!window._pageReloaders) window._pageReloaders = {}; if (window._cacheVersion !== CACHE_VERSION) { window._pageCache = {}; window._pageDataCache = {}; window._pageReloaders = {}; window._cacheVersion = CACHE_VERSION; } // 全局点击关闭自动补全下拉 if (!window._autoCompleteInit) { window._autoCompleteInit = true; document.addEventListener('click', function(e) { var dropdowns = document.querySelectorAll('.autocomplete-dropdown'); dropdowns.forEach(function(dd) { var wrap = dd.closest('.autocomplete-wrap'); if (wrap && !wrap.contains(e.target)) { dd.style.display = 'none'; } }); }); } const PAGE_LOADERS = { 'dashboard': loadDashboard, 'data_detail': loadDataPage, 'data_summary': loadDataSummaryPage, 'data_outbound': loadDataPage, 'products': loadProductsPage, 'brand_management': loadBrandManagementPage, 'users': loadUsersPage, 'logs': loadLogsPage, 'login_records': loadLoginRecordsPage, 'config': loadConfigPage, 'permissions': loadPermissionsPage, 'warehouse': loadWarehousePage, 'inventory': loadInventoryPage, 'inventory_finished': loadInventoryPage, 'inventory_packaging': loadInventoryPage, 'suppliers': loadSuppliersPage, 'personnel': loadPersonnelPage, 'planning': loadPlanningPage, 'purchase': loadPurchasePage, 'purchase_ready': loadPurchaseReadyPage, 'orders': loadOrdersPage, 'orders_domestic': loadDomesticOrdersPage, 'orders_overseas': loadOverseasOrdersPage, 'orders_packaging': loadPackagingOrdersPage, 'orders_finished': loadFinishedProductsPage, 'bom': loadBomPage, 'knowledge': loadKnowledgePage, 'feedback': loadFeedbackPage, 'contract_templates': loadContractTemplatesPage, 'product_order': loadProductOrderPage, 'product_new': loadProductNewPage, 'product_packaging': loadProductPackagingPage, 'product_category': loadProductCategoryPage, 'collaboration': function(container) { container.innerHTML = '
🤝

采销协同

请选择左侧子菜单

'; }, 'collaboration_inventory_dashboard': loadInventoryBoardPage, 'collaboration_key_sku_forecast': loadKeySkuForecastPage, 'collaboration_sku_health_index': loadSkuHealthIndexPage, 'collaboration_slow_moving_sku': loadSlowMovingSkuPage, 'collaboration_sales_management': loadSalesManagementPage, 'spot': function(container) { container.innerHTML = '
📦

现货模式

请选择左侧子菜单

'; }, 'spot_dashboard': function(container) { loadSpotSubPage(container, '现货看板'); }, 'spot_application': loadSpotApplicationPage, 'spot_confirmation': loadSpotConfirmationPage, 'spot_sales': loadSpotSalesPage, 'ai_analysis': loadAiAnalysisPage }; // ─── 动销管理页面 ─── var salesManagementPage = 1; var salesManagementSearch = ''; var salesManagementColVisible = {}; var salesManagementColOrder = []; var salesManagementColPending = {}; var salesManagementColDragOrder = []; var salesManagementColumnFilters = {}; var salesManagementFilterMenu = null; var SALES_MANAGEMENT_COLUMNS = [ '批次', '货品编码', '货品名称', '确权人', '确权平台', '库存区间', '建档时间', '盘盈盘亏', '差异合计', '生产日期', '过期时间', '成本价', '总库存', '可发库存', '可支撑天数', '30天销量', '仓库存合计', '29青鸟海外仓', '27启程仓', '26海外抖音仓', '25海外拼多多保税仓', '1蝶雅仓', '2青鸟仓', '4百橙仓', '5东升仓', '6CK仓', '9天猫菜鸟仓', '10唯品会海外仓', '京东海外11仓', '京东海外12仓', '京东13仓', '京东14仓', '16蝶雅南阳仓', '分类', '戴总确认意见', '运营核实更改', '运营核对备注' ]; function loadSalesManagementPage(container) { salesManagementInitColumns(); container.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;'; container.innerHTML = '
' + '' + '
'; salesManagementPage = 1; salesManagementSearch = ''; salesManagementLoad(); } function salesManagementLoad() { var pane = document.getElementById('salesManagementPane'); if (!pane) return; pane.innerHTML = '
'; var url = '/api/sales_management/list?page=' + salesManagementPage + '&size=50&search=' + encodeURIComponent(salesManagementSearch); fetch(url, {method:'POST', headers:{'X-Requested-With':'XMLHttpRequest','Content-Type':'application/json'}, body:JSON.stringify({filters:salesManagementColumnFilters})}).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function(res) { if (res.code !== 0) throw new Error(res.msg || '加载失败'); salesManagementRender(pane, res.data || [], res.total || 0); }).catch(function(e) { pane.innerHTML = '
加载失败:' + escHtml(String(e.message || e)) + '
'; }); } function salesManagementDownloadTemplate() { window.location.href = '/api/sales_management/template'; } function salesManagementImport() { var input = document.getElementById('salesManagementImportFile'); if (!input || !input.files.length) { showCenteredAlert('提示', '请选择Excel文件'); return; } var form = new FormData(); form.append('file', input.files[0]); fetch('/api/sales_management/import', {method:'POST', body:form}).then(function(r){return r.json();}).then(function(res) { if (res.code !== 0) throw new Error(res.msg || '导入失败'); showCenteredAlert('提示', res.msg || '导入成功'); salesManagementLoad(); }).catch(function(e){ showCenteredAlert('导入失败', e.message || '导入失败'); }); } function salesManagementOpenExport() { var old = document.getElementById('salesManagementExportModal'); if (old) old.remove(); var fields = SALES_MANAGEMENT_COLUMNS.map(function(c, i) { return ''; }).join(''); var overlay = document.createElement('div'); overlay.id = 'salesManagementExportModal'; overlay.className = 'modal-overlay'; overlay.innerHTML = ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); } function salesManagementExport() { var selected = Array.prototype.slice.call(document.querySelectorAll('#salesManagementExportFields input:checked')).map(function(e){return e.value;}); if (!selected.length) { showCenteredAlert('提示', '请至少选择一个导出字段'); return; } var url = '/api/sales_management/export?fields=' + encodeURIComponent(selected.join(',')); fetch(url).then(function(r){if(!r.ok)return r.json().then(function(x){throw new Error(x.msg||'导出失败')});return r.blob();}).then(function(blob){var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='动销管理导出.xlsx';document.body.appendChild(a);a.click();a.remove();var m=document.getElementById('salesManagementExportModal');if(m)m.remove();}).catch(function(e){showCenteredAlert('导出失败',e.message||'导出失败');}); } function salesManagementInitColumns() { var saved = null; try { saved = JSON.parse(localStorage.getItem('salesManagementColumns') || 'null'); } catch (e) {} salesManagementColOrder = Array.isArray(saved && saved.order) ? saved.order.slice() : SALES_MANAGEMENT_COLUMNS.slice(); salesManagementColOrder = salesManagementColOrder.filter(function(c, i, a) { return SALES_MANAGEMENT_COLUMNS.indexOf(c) >= 0 && a.indexOf(c) === i; }); SALES_MANAGEMENT_COLUMNS.forEach(function(c) { if (salesManagementColOrder.indexOf(c) < 0) salesManagementColOrder.push(c); }); salesManagementColVisible = saved && saved.visible ? saved.visible : {}; SALES_MANAGEMENT_COLUMNS.forEach(function(c) { if (salesManagementColVisible[c] === undefined) salesManagementColVisible[c] = true; }); } function salesManagementVisibleColumns() { return (salesManagementColOrder.length ? salesManagementColOrder : SALES_MANAGEMENT_COLUMNS).filter(function(c) { return salesManagementColVisible[c] !== false; }); } function salesManagementSaveColumns() { localStorage.setItem('salesManagementColumns', JSON.stringify({order:salesManagementColOrder, visible:salesManagementColVisible})); } function salesManagementOpenColumns() { var old=document.getElementById('salesManagementColumnsModal'); if(old)old.remove(); salesManagementColPending = Object.assign({}, salesManagementColVisible); salesManagementColDragOrder = salesManagementColOrder.slice(); var overlay=document.createElement('div'); overlay.id='salesManagementColumnsModal'; overlay.className='modal-overlay'; var items=salesManagementColDragOrder.map(function(c){return '';}).join(''); overlay.innerHTML=''; overlay.onclick=function(e){if(e.target===overlay){}}; document.body.appendChild(overlay); salesManagementBindColumnDrag(); salesManagementLoadSchemes(); } function salesManagementBindColumnDrag(){var grid=document.getElementById('salesManagementColumnGrid');if(!grid)return;var dragged=null;grid.querySelectorAll('label').forEach(function(el){el.addEventListener('dragstart',function(e){dragged=this;e.dataTransfer.setData('text/plain',this.dataset.field);this.style.opacity='.4'});el.addEventListener('dragend',function(){this.style.opacity='1'});el.addEventListener('dragover',function(e){e.preventDefault()});el.addEventListener('drop',function(e){e.preventDefault();if(!dragged||dragged===this)return;var els=Array.from(grid.querySelectorAll('label')),a=els.indexOf(dragged),b=els.indexOf(this);if(a'+escHtml(s.scheme_name)+'';}).join('')||'暂无保存方案';});} function salesManagementLoadScheme(id){fetch('/api/planning/schemes?page=sales_management').then(function(r){return r.json()}).then(function(res){var s=(res.data||[]).find(function(x){return x.id==id});if(!s)return;var parsed=JSON.parse(s.columns_json),grid=document.getElementById('salesManagementColumnGrid');salesManagementColDragOrder=parsed.map(function(x){return x.field});salesManagementColDragOrder.forEach(function(c){salesManagementColPending[c]=parsed.find(function(x){return x.field===c}).visible!==false});grid.innerHTML=salesManagementColDragOrder.map(function(c){return ''}).join('');salesManagementBindColumnDrag();});} function salesManagementDetail(id) { var detailStyle = document.getElementById('salesManagementDetailStyle'); if (!detailStyle) { detailStyle = document.createElement('style'); detailStyle.id = 'salesManagementDetailStyle'; detailStyle.textContent = '.sales-detail-table{width:100%;border-collapse:separate;border-spacing:0;border:1px solid #d9d9d9;border-radius:8px;overflow:hidden;font-size:13px}.sales-detail-table td{border-right:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8;padding:10px 12px;vertical-align:top}.sales-detail-table tr:last-child td{border-bottom:0}.sales-detail-table td:last-child{border-right:0}.sales-detail-label{width:16%;min-width:105px;background:#f7f9fc;color:#555;font-weight:600;white-space:nowrap}.sales-detail-value{width:34%;color:#333;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.sales-detail-table tr td:nth-child(3){border-left:1px solid #e8e8e8}@media(max-width:700px){.sales-detail-table{font-size:12px}.sales-detail-label{min-width:78px;width:22%;white-space:normal}.sales-detail-value{width:28%;padding:8px}.sales-detail-table td{padding:8px}}'; document.head.appendChild(detailStyle); } fetch('/api/sales_management/detail/' + id).then(function(r){return r.json();}).then(function(res) { if (res.code !== 0) { showCenteredAlert('提示', res.msg || '加载失败'); return; } var ov = document.createElement('div'); ov.id = 'salesManagementDetailModal'; ov.className = 'modal-overlay'; ov.onclick = function(e) { if (e.target === ov) {} }; var rows = ''; for (var i = 0; i < SALES_MANAGEMENT_COLUMNS.length; i += 2) { var leftField = SALES_MANAGEMENT_COLUMNS[i]; var leftValue = res.data[leftField]; rows += '' + (i + 1) + '. ' + escHtml(leftField) + '' + escHtml(leftValue === null || leftValue === undefined || leftValue === '' ? '-' : String(leftValue)) + ''; if (i + 1 < SALES_MANAGEMENT_COLUMNS.length) { var rightField = SALES_MANAGEMENT_COLUMNS[i + 1]; var rightValue = res.data[rightField]; rows += '' + (i + 2) + '. ' + escHtml(rightField) + '' + escHtml(rightValue === null || rightValue === undefined || rightValue === '' ? '-' : String(rightValue)) + ''; } else { rows += ''; } rows += ''; } ov.innerHTML = ''; document.body.appendChild(ov); }); } function salesManagementDelete(id){showCenteredConfirm('确认删除','确定删除这条动销管理记录吗?').then(function(ok){if(!ok)return;fetch('/api/sales_management/delete/'+id,{method:'POST'}).then(function(r){return r.json()}).then(function(res){if(res.code===0)salesManagementLoad();else showCenteredAlert('删除失败',res.msg||'删除失败')});});} function salesManagementOpenColumnFilter(field, event) { event = event || window.event; event.stopPropagation(); if (salesManagementFilterMenu) salesManagementFilterMenu.remove(); var th = event.currentTarget.closest('th'); var rect = th.getBoundingClientRect(); var menu = document.createElement('div'); menu.id = 'salesManagementFilterMenu'; menu.style.cssText = 'position:fixed;z-index:20000;top:' + (rect.bottom + 4) + 'px;left:' + Math.min(Math.max(8, rect.left), window.innerWidth - 310) + 'px;width:290px;padding:12px;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.16)'; var title = document.createElement('div'); title.style.cssText = 'font-weight:600;margin-bottom:8px'; title.textContent = '筛选:' + field; var search = document.createElement('input'); search.id = 'salesManagementFilterSearch'; search.className = 'form-input'; search.autocomplete = 'off'; search.placeholder = '搜索选项'; search.style.cssText = 'width:100%;box-sizing:border-box;margin-bottom:8px'; var selectAll = document.createElement('label'); selectAll.style.cssText = 'display:flex;align-items:center;gap:5px;font-size:12px;margin-bottom:5px;cursor:pointer'; selectAll.innerHTML = ' 全选'; var list = document.createElement('div'); list.id = 'salesManagementFilterList'; list.style.cssText = 'max-height:230px;overflow:auto;border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0;padding:5px 0;text-align:center;color:#999;font-size:12px'; var actions = document.createElement('div'); actions.style.cssText = 'display:flex;justify-content:flex-end;gap:6px;margin-top:10px'; var clear = document.createElement('button'); clear.className = 'btn btn-sm'; clear.textContent = '清除'; clear.onclick = function() { salesManagementClearColumnFilter(field); }; var confirm = document.createElement('button'); confirm.className = 'btn btn-sm btn-primary'; confirm.textContent = '确定'; confirm.onclick = function() { salesManagementApplyColumnFilter(field); }; actions.appendChild(clear); actions.appendChild(confirm); menu.appendChild(title); menu.appendChild(search); menu.appendChild(selectAll); menu.appendChild(list); menu.appendChild(actions); document.body.appendChild(menu); salesManagementFilterMenu = menu; var params = '?field=' + encodeURIComponent(field); if (salesManagementSearch) params += '&search=' + encodeURIComponent(salesManagementSearch); fetch('/api/sales_management/column-values' + params, {headers:{'X-Requested-With':'XMLHttpRequest'}}) .then(function(r){return r.json();}).then(function(res){ if (res.code !== 0) { list.textContent = res.msg || '加载失败'; return; } var values = res.data || []; if (!values.length) { list.textContent = '暂无数据'; return; } list.style.textAlign = 'left'; list.innerHTML = values.map(function(value) { var checked = (salesManagementColumnFilters[field] || []).indexOf(value) >= 0; return ''; }).join(''); var all = list.querySelectorAll('.sales-management-filter-cb'); var selected = list.querySelectorAll('.sales-management-filter-cb:checked'); var allCb = selectAll.querySelector('input'); allCb.checked = all.length === selected.length; allCb.onchange = function(){list.querySelectorAll('.sales-management-filter-cb').forEach(function(cb){cb.checked=allCb.checked;});}; search.oninput = function(){var q=search.value.toLowerCase();list.querySelectorAll('.sales-management-filter-option').forEach(function(el){el.style.display=el.textContent.toLowerCase().indexOf(q)>=0?'flex':'none';});}; }).catch(function(){ list.textContent = '加载失败'; }); } function salesManagementApplyColumnFilter(field) { var values = []; document.querySelectorAll('#salesManagementFilterList .sales-management-filter-cb:checked').forEach(function(cb){ values.push(cb.value); }); if (values.length) salesManagementColumnFilters[field] = values; else delete salesManagementColumnFilters[field]; if (salesManagementFilterMenu) { salesManagementFilterMenu.remove(); salesManagementFilterMenu = null; } salesManagementPage = 1; salesManagementLoad(); } function salesManagementClearColumnFilter(field) { delete salesManagementColumnFilters[field]; if (salesManagementFilterMenu) { salesManagementFilterMenu.remove(); salesManagementFilterMenu = null; } salesManagementPage = 1; salesManagementLoad(); } function salesManagementResetAllFilters() { salesManagementSearch = ''; salesManagementColumnFilters = {}; salesManagementPage = 1; if (salesManagementFilterMenu) { salesManagementFilterMenu.remove(); salesManagementFilterMenu = null; } salesManagementLoad(); } function salesManagementRender(pane, rows, total) { var visibleColumns = salesManagementVisibleColumns(); var body = rows.map(function(row) { var cells = visibleColumns.map(function(c) { var value = row[c]; if (value === null || value === undefined || value === '') value = '-'; return '' + escHtml(String(value)) + ''; }).join(''); return '' + cells + ' '; }).join(''); var head = visibleColumns.map(function(c) { var active = !!salesManagementColumnFilters[c]; return '' + escHtml(c) + ' ▾'; }).join('') + '操作'; if (!body) body = '暂无30天无动销商品'; var pages = Math.max(1, Math.ceil(total / 50)); var pagination = '
' + '' + '第 ' + salesManagementPage + ' / ' + pages + ' 页,共 ' + total + ' 条' + '
'; pane.innerHTML = '
' + '

30天无动销列表

共 ' + total + ' 条' + '
' + '' + '' + '' + '' + '' + '' + '
' + '
' + head + '' + body + '
' + pagination + '
'; } function loadSpotSubPage(container, title) { container.innerHTML = '
📦

' + escHtml(title) + '

功能页面建设中

'; } function loadSpotSalesPage(container) { container.innerHTML = `
序号商家编码货品名称平台类目首批成本价 库存采购在途昨日销量 前日销量7天销量14天销量 30天销量总销量销售状态销量趋势 首批入库时间负责运营
加载中...
`; window._spotSalesPage = 1; loadSpotSalesList(); } function loadSpotSalesList() { var body = document.getElementById('spotSalesRows'); if (!body) return; var page = window._spotSalesPage || 1; var searchEl = document.getElementById('spotSalesSearch'); var search = searchEl ? searchEl.value.trim() : ''; var url = '/api/spot/sales/list?page=' + page + '&size=20&search=' + encodeURIComponent(search); fetch(url, {headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){ return r.json(); }).then(function(res) { if (res.code !== 0) { body.innerHTML = '' + escHtml(res.msg || '加载失败') + ''; return; } var rows = res.data || []; var total = res.total || 0; var meta = document.getElementById('spotSalesMeta'); var totalEl = document.getElementById('spotSalesTotal'); if (meta) meta.textContent = '销量数据截至 ' + (res.latest_sales_date || '—'); if (totalEl) totalEl.textContent = '共 ' + total + ' 个现货商品'; if (!rows.length) { body.innerHTML = '暂无现货商品'; } else { body.innerHTML = rows.map(function(row) { var statusMuted = row['销售状态'] === '暂无销量' ? ' spot-sales-status-muted' : ''; var rowIndex = (page - 1) * (res.size || 20) + rows.indexOf(row) + 1; return '' + '' + rowIndex + '' + '' + escHtml(row['商家编码'] || '') + '' + '' + escHtml(row['货品名称'] || '') + '' + '' + escHtml(row['平台'] || '') + '' + '' + escHtml(row['类目'] || '') + '' + '' + spotSalesNumber(row['首批成本价'], 4) + '' + '' + spotSalesNumber(row['库存']) + '' + '' + spotSalesNumber(row['采购在途']) + '' + '' + spotSalesNumber(row['昨日销量']) + '' + '' + spotSalesNumber(row['前日销量']) + '' + '' + spotSalesNumber(row['7天销量']) + '' + '' + spotSalesNumber(row['14天销量']) + '' + '' + spotSalesNumber(row['30天销量']) + '' + '' + spotSalesNumber(row['总销量']) + '' + '' + escHtml(row['销售状态'] || '') + '' + '' + escHtml(row['销量趋势'] || '') + '' + '' + escHtml(row['首批入库时间'] || '') + '' + '' + escHtml(row['负责运营'] || '') + '' + ''; }).join(''); } renderSpotSalesPagination(total, res.size || 20, res.page || page); }).catch(function(){ body.innerHTML = '网络异常,请稍后重试'; }); } function spotSalesNumber(value, decimals) { if (value === null || value === undefined || value === '') return '0'; var n = Number(value); if (!isFinite(n)) return escHtml(value); return n.toLocaleString('zh-CN', {minimumFractionDigits: decimals || 0, maximumFractionDigits: decimals || 0}); } function renderSpotSalesPagination(total, size, page) { var el = document.getElementById('spotSalesPagination'); if (!el) return; var pages = Math.ceil(total / size), html = ''; if (pages <= 1) { el.innerHTML = ''; return; } html += ''; for (var i = Math.max(1, page - 2); i <= Math.min(pages, page + 2); i++) html += ''; html += ''; el.innerHTML = html; } var _spotApplicationRowsCache = {}; function openSpotApplicationDetail(id) { var row = _spotApplicationRowsCache[id]; if (!row) return; var old = document.getElementById('spotApplicationDetailModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'spotApplicationDetailModal'; overlay.className = 'spot-app-modal-overlay'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var image = row.image_path ? '
产品图片
' : '—'; var link = row.product_link ? '查看产品链接' : '—'; var items = [ ['申请人', row.applicant], ['日期', row.application_date], ['部门', row.department], ['品牌', row.brand], ['产品名称', row.product_name], ['厂家', row.manufacturer], ['产品链接', link], ['寄样数量', row.sample_quantity], ['条形码', row.barcode], ['状态', row.status || '待处理'], ['原因', row.reason], ['收件信息', row.recipient_info] ]; var html = '

现货申请详情

'; items.forEach(function(item) { html += '
' + escHtml(item[0]) + '
' + (item[0] === '产品链接' ? item[1] : escHtml(item[1] || '')) + '
'; }); html += '
图片
' + image + '
'; overlay.innerHTML = html; document.body.appendChild(overlay); } function deleteSpotApplication(id) { showCenteredConfirm('确认', '确认删除这条现货申请记录吗?删除后不可恢复。').then(function(ok) { if (!ok) return; fetch('/api/spot/application/delete', {method:'POST', headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}, body:JSON.stringify({id:id})}) .then(function(r){return r.json();}) .then(function(res){ if (res.code === 0) { delete _spotApplicationRowsCache[id]; loadSpotApplicationList(); alert('删除成功'); } else alert(res.msg || '删除失败'); }).catch(function(){ alert('网络异常,请稍后重试'); }); }); } function loadSpotApplicationPage(container) { container.innerHTML = `
申请人日期部门品牌产品名称厂家图片产品链接寄样数量条形码收件信息状态原因操作
加载中...
`; window._spotAppPage = 1; loadSpotApplicationList(); } function loadSpotApplicationList() { var body = document.getElementById('spotAppRows'); if (!body) return; var page = window._spotAppPage || 1; var searchEl = document.getElementById('spotAppSearch'); var search = searchEl ? searchEl.value.trim() : ''; var url = '/api/spot/application/list?page=' + page + '&size=20&search=' + encodeURIComponent(search); fetch(url, {headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){ return r.json(); }).then(function(res) { if (res.code !== 0) { body.innerHTML = '' + escHtml(res.msg || '加载失败') + ''; return; } var data = res.data; document.getElementById('spotAppTotal').textContent = '共 ' + data.total + ' 条申请'; if (!data.rows.length) { body.innerHTML = '暂无现货申请'; } else { body.innerHTML = data.rows.map(function(row) { _spotApplicationRowsCache[row.id] = row; var image = row.image_path ? '产品图片' : '—'; var link = row.product_link ? '查看链接' : '—'; var actions = ''; if (window._hasPagePerm('spot_application', 'delete')) actions += ' '; return '' + escHtml(row.applicant) + '' + escHtml(row.application_date) + '' + escHtml(row.department || '') + '' + escHtml(row.brand) + '' + escHtml(row.product_name) + '' + escHtml(row.manufacturer) + '' + image + '' + link + '' + escHtml(row.sample_quantity) + '' + escHtml(row.barcode) + '' + escHtml(row.recipient_info) + '' + escHtml(row.status || '待处理') + '' + escHtml(row.reason || '') + '' + actions + ''; }).join(''); } renderSpotApplicationPagination(data.total, data.size, data.page); }).catch(function(){ body.innerHTML = '网络异常,请稍后重试'; }); } function renderSpotApplicationPagination(total, size, page) { var el = document.getElementById('spotAppPagination'); if (!el) return; var pages = Math.ceil(total / size), html = ''; if (pages <= 1) { el.innerHTML = ''; return; } html += ''; for (var i = Math.max(1, page - 2); i <= Math.min(pages, page + 2); i++) html += ''; html += ''; el.innerHTML = html; } function openSpotApplicationModal() { var old = document.getElementById('spotApplicationModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'spotApplicationModal'; overlay.className = 'spot-app-modal-overlay'; overlay.innerHTML = '

新增现货申请

'; document.body.appendChild(overlay); document.getElementById('spotApplicationForm').addEventListener('submit', function(e) { e.preventDefault(); var form = this, button = form.querySelector('button[type="submit"]'); button.disabled = true; fetch('/api/spot/application/create', {method:'POST', body:new FormData(form), headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res) { if (res.code === 0) { document.getElementById('spotApplicationModal').remove(); loadSpotApplicationList(); alert('申请已添加'); } else { alert(res.msg || '保存失败'); button.disabled = false; } }).catch(function(){ alert('网络异常,请稍后重试'); button.disabled = false; }); }); } function previewSpotApplicationImage(path) { var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;z-index:20000;background:rgba(0,0,0,.75);display:flex;align-items:center;justify-content:center;'; overlay.innerHTML = '产品图片预览'; document.body.appendChild(overlay); } function loadSpotConfirmationPage(container) { var fields = [ {key:'application_date', label:'日期'}, {key:'brand', label:'品牌'}, {key:'product_name', label:'产品名称'}, {key:'image_path', label:'图片'}, {key:'barcode', label:'条形码'}, {key:'sample_quantity', label:'寄样数量'}, {key:'product_link', label:'产品链接'}, {key:'department', label:'部门'}, {key:'applicant', label:'申请人'}, {key:'manufacturer', label:'厂家'}, {key:'recipient_info', label:'收件信息'}, {key:'sample_status', label:'样品状态'}, {key:'sample_remark', label:'样品备注'}, {key:'oa_approval_no', label:'OA审批单号'}, {key:'express_tracking_no', label:'寄样快递单号'}, {key:'arrival_date', label:'到样日期'}, {key:'remark', label:'备注'}, {key:'receiving_department', label:'领样部门'}, {key:'recipient', label:'领样人'}, {key:'sample_confirmed', label:'样品确认'}, {key:'sample_confirmation_reason', label:'样品确认原因'}, {key:'sample_location', label:'样品位置'}, {key:'product_code', label:'产品编码'}, {key:'final_product_name', label:'货品名称(最终名称)'}, {key:'platform', label:'平台'} ]; var headerHtml = fields.map(function(field) { return '' + escHtml(field.label) + ''; }).join(''); container.innerHTML = `
${headerHtml}
加载中...
`; window._spotConfirmFields = fields; window._spotConfirmPage = 1; loadSpotConfirmationList(); } function loadSpotConfirmationList() { var body = document.getElementById('spotConfirmRows'); if (!body) return; var page = window._spotConfirmPage || 1; var searchEl = document.getElementById('spotConfirmSearch'); var search = searchEl ? searchEl.value.trim() : ''; var url = '/api/spot/confirmation/list?page=' + page + '&size=20&search=' + encodeURIComponent(search); fetch(url, {headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){ return r.json(); }).then(function(res) { if (res.code !== 0) { body.innerHTML = '' + escHtml(res.msg || '加载失败') + ''; return; } var data = res.data; var fields = window._spotConfirmFields || []; var totalEl = document.getElementById('spotConfirmTotal'); if (totalEl) totalEl.textContent = '共 ' + data.total + ' 条确认记录'; if (!data.rows.length) { body.innerHTML = '暂无现货确认记录'; } else { body.innerHTML = data.rows.map(function(row) { return '' + fields.map(function(field) { var value = row[field.key] === null || row[field.key] === undefined ? '' : row[field.key]; if (field.key === 'image_path') { return value ? '产品图片' : '—'; } if (field.key === 'product_link') { return value ? '查看链接' : '—'; } var cls = ['application_date','arrival_date','barcode','sample_quantity','oa_approval_no','express_tracking_no','product_code'].indexOf(field.key) >= 0 ? ' class="spot-confirm-nowrap"' : ''; return '' + escHtml(value) + ''; }).join('') + ''; }).join(''); } renderSpotConfirmationPagination(data.total, data.size, data.page); }).catch(function(){ body.innerHTML = '网络异常,请稍后重试'; }); } function renderSpotConfirmationPagination(total, size, page) { var el = document.getElementById('spotConfirmPagination'); if (!el) return; var pages = Math.ceil(total / size), html = ''; if (pages <= 1) { el.innerHTML = ''; return; } html += ''; for (var i = Math.max(1, page - 2); i <= Math.min(pages, page + 2); i++) html += ''; html += ''; el.innerHTML = html; } // ─── 新品下单页面(占位) ─── function loadProductOrderPage(container) { container.innerHTML = '
🛒

新品下单

功能建设中,敬请期待

'; } // ─── AI 分析页面(问答式) ─── var AI_SUGGEST_QUESTIONS = [ '当前库存最多的 10 个 SKU 是哪些?', '最近 7 天出库量 TOP10 是哪些?', '库存中呆滞(超过30天无出库)的 SKU 有哪些?', '生产在途量最多的 10 个 SKU 是哪些?', '最近 30 天各品牌出库数量排行', '当前库存总额是多少?' ]; // 初始空状态 HTML(重置时恢复) function aiEmptyHtml() { return '
' + '
🧠
' + '
用自然语言向 AI 提问
' + '
例如:当前库存最多的 10 个 SKU 是哪些?
' + '
试试这些问题:
' + '
' + AI_SUGGEST_QUESTIONS.map(function(q) { return ''; }).join('') + '
' + '
'; } // 重置:清空聊天记录,回到初始界面 function aiReset() { var body = document.getElementById('aiChatBody'); if (body) body.innerHTML = aiEmptyHtml(); var input = document.getElementById('aiQuestionInput'); if (input) input.value = ''; window._aiHistory = []; } // 动态计算 AI 页面可用高度(撑满主内容区,输入框固定在视口底部) function aiFitHeight() { var container = document.getElementById('page-container-ai_analysis'); if (!container) return; var mainContent = document.getElementById('mainContent'); if (!mainContent) return; var rect = mainContent.getBoundingClientRect(); // main-content 底部 padding 约 24px,留出空间避免出现滚动条 var available = window.innerHeight - rect.top - 24; if (available < 320) available = 320; container.style.height = available + 'px'; } function loadAiAnalysisPage(container) { // 撑满主内容区(动态测量,输入框固定底部) container.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;'; container.id = 'page-container-ai_analysis'; container.innerHTML = '' + '' + '
' + '
' + aiEmptyHtml() + '
' + '
' + '' + '' + '' + '
' + '
' + ''; // 历史记录改为弹窗形式:点击按钮弹出弹窗,点击记录展开内容弹窗 // 首次渲染后测量高度 + 窗口尺寸变化时重新测量 setTimeout(aiFitHeight, 50); if (!window._aiFitBound) { window._aiFitBound = true; window.addEventListener('resize', aiFitHeight); } } // 添加一条聊天气泡 function aiAppendBubble(role, html) { var body = document.getElementById('aiChatBody'); if (!body) return; var empty = document.getElementById('aiChatEmpty'); if (empty) empty.style.display = 'none'; var div = document.createElement('div'); div.style.cssText = 'display:flex;' + (role === 'user' ? 'justify-content:flex-end' : 'justify-content:flex-start'); div.innerHTML = '
' + html + '
'; body.appendChild(div); body.scrollTop = body.scrollHeight; return div; } // 渲染 AI 结果表格 function aiRenderTable(columns, rows, sql) { if (!rows || !rows.length) return '
查询无数据
'; var thead = columns.map(function(c) { return '' + escHtml(c) + ''; }).join(''); var tbody = rows.slice(0, 100).map(function(r) { return '' + columns.map(function(c) { var v = r[c]; if (v === null || v === undefined) v = ''; var s = String(v); if (s.length > 60) s = s.slice(0, 60) + '…'; return '' + escHtml(s) + ''; }).join('') + ''; }).join(''); return '
' + '' + thead + '' + tbody + '
' + (sql ? '
SQL:' + escHtml(sql) + '
' : ''); } // 发送问题 function aiSend() { var input = document.getElementById('aiQuestionInput'); if (!input) return; var q = input.value.trim(); if (!q) return; aiAsk(q); input.value = ''; } // ─── 历史记录 ─── // 打开历史记录弹窗 function aiToggleHistory() { var existing = document.getElementById('aiHistoryOverlay'); if (existing) { existing.remove(); return; } var overlay = document.createElement('div'); overlay.id = 'aiHistoryOverlay'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; // 只能通过按钮关闭 overlay.innerHTML = '' + '
' + '
' + '📜 我的分析历史' + '仅保存当前账号的记录' + '
' + '' + '' + '
' + '
' + '
' + '
'; document.body.appendChild(overlay); aiLoadHistory(); } // 加载当前账号的历史记录 function aiLoadHistory() { var list = document.getElementById('aiHistoryList'); if (!list) return; list.innerHTML = '
加载中…
'; fetch('/api/ai/history', { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(function(r) { return r.json(); }) .then(function(res) { var rows = (res && res.data) || []; if (!rows.length) { list.innerHTML = '
暂无历史记录,提问后自动保存
'; return; } window._aiHistoryRows = rows; list.innerHTML = rows.map(function(h) { var q = escHtml(h.question || ''); var s = escHtml((h.summary || '').slice(0, 80)); var t = escHtml(h.created_at || ''); var ds = h.datasource ? ' [' + escHtml(h.datasource) + ']' : ''; return '
' + '
🔍 ' + q + ds + '
' + '
' + t + '
' + (s ? '
' + s + '
' : '') + '
'; }).join(''); }) .catch(function() { list.innerHTML = '
历史记录加载失败
'; }); } // 点击历史记录:展开弹窗显示记录内容 function aiShowHistoryDetail(id) { var rows = window._aiHistoryRows || []; var rec = null; for (var i = 0; i < rows.length; i++) { if (rows[i].id === id) { rec = rows[i]; break; } } if (!rec) return; var content = {}; try { content = JSON.parse(rec.content || '{}') || {}; } catch (e) { content = {}; } var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.55);z-index:100000;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; // 只能通过按钮关闭 var bodyHtml = ''; if (content.type === 'table' && content.columns && content.rows && content.rows.length) { bodyHtml = aiRenderTable(content.columns, content.rows, null); } else if (content.summary) { bodyHtml = '
' + escHtml(content.summary) + '
'; } else { bodyHtml = '
该记录暂无内容
'; } overlay.innerHTML = '' + '
' + '
' + '📄 记录内容' + '' + '
' + '
' + '
🔍 ' + escHtml(rec.question || '') + '
' + '
提问时间:' + escHtml(rec.created_at || '') + '
' + '
' + '
' + bodyHtml + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(overlay); } // 记录详情弹窗中重新提问:关闭所有历史弹窗后发起提问 function aiReaskFromDetail(q) { var overlays = document.querySelectorAll('.modal-overlay'); for (var i = 0; i < overlays.length; i++) overlays[i].remove(); aiAsk(q); } // 清空当前账号的历史记录 function aiClearHistory() { var list = document.getElementById('aiHistoryList'); if (!list) return; if (!confirm('确定清空当前账号的全部 AI 分析历史记录吗?')) return; fetch('/api/ai/history', { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(function(r) { return r.json(); }) .then(function(res) { window._aiHistoryRows = []; list.innerHTML = '
暂无历史记录,提问后自动保存
'; }) .catch(function() { alert('清空失败,请重试'); }); } function aiAsk(question) { if (!question) return; aiAppendBubble('user', escHtml(question)); var pending = aiAppendBubble('ai', '🤔 思考中…'); var btn = document.getElementById('aiSendBtn'); if (btn) btn.disabled = true; // 携带最近对话历史(最多 8 轮),便于追问“这个数据怎么算的”等上下文问题 if (!window._aiHistory) window._aiHistory = []; var history = window._aiHistory.slice(-8); fetch('/api/ai/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: question, history: history }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 200) { if (pending) pending.querySelector('div').innerHTML = '' + escHtml(res.msg || '分析失败') + ''; return; } var d = res.data || {}; var html = ''; if (d.summary) html += '
' + escHtml(d.summary) + '
'; if (d.type === 'table') { html += aiRenderTable(d.columns || [], d.rows || [], d.sql); } if (pending) pending.querySelector('div').innerHTML = html || '(无结果)'; // 记录历史(含本次回答摘要,供后续追问上下文) window._aiHistory.push({role: 'user', content: question}); window._aiHistory.push({role: 'assistant', content: (d.summary || '') + (d.sql ? ' SQL:' + d.sql : '')}); if (window._aiHistory.length > 40) window._aiHistory = window._aiHistory.slice(-40); }) .catch(function() { if (pending) pending.querySelector('div').innerHTML = '网络错误,请重试'; }) .finally(function() { if (btn) btn.disabled = false; }); } // ─── 新品成品管理页面 ─── // 列配置:与《新品管理模板表》表头一致(18列),三个图片列统一放在最右侧 var NEW_PRODUCT_COLUMNS = [ {field: 'seq', label: '序号', width: '60px'}, {field: 'category1', label: '一级类目', width: '110px'}, {field: 'category2', label: '二级类目', width: '110px'}, {field: 'category3', label: '三级类目', width: '140px'}, {field: 'product_type', label: '类别', width: '90px'}, {field: 'brand', label: '品牌', width: '110px'}, {field: 'product_code', label: '产品编码', width: '160px'}, {field: 'product_name', label: '产品名称', width: '220px'}, {field: 'barcode', label: '条形码', width: '130px'}, {field: 'capacity', label: '容量', width: '110px'}, {field: 'remark', label: '成品备注', width: '140px'}, {field: 'total_cost', label: '产品总成本', width: '110px'}, {field: 'total_payment', label: '产品总货款', width: '110px'}, {field: 'effect_img', label: '产品效果图(内外包)', width: '110px'}, {field: 'upgrade_img', label: '升级前的', width: '110px'}, {field: 'confirm_img', label: '确版内料图凭证', width: '110px'}, {field: 'quote_inner_img', label: '内料报价单', width: '110px'}, {field: 'quote_pack_img', label: '包材报价单', width: '110px'}, {field: 'procurement_planner', label: '采购计划', width: '100px'}, {field: 'product_dev', label: '产品开发', width: '100px'}, {field: 'package_dev', label: '包材开发', width: '100px'}, {field: 'designer', label: '设计师', width: '90px'}, {field: 'operator', label: '负责运营', width: '100px'} ]; // 列表中的图片列(渲染为缩略图,点击放大预览) var NEW_LIST_IMAGE_FIELDS = ['effect_img', 'upgrade_img', 'confirm_img', 'quote_inner_img', 'quote_pack_img']; function newListImgCell(url) { if (!url) return '—'; var urls = String(url).split(',').filter(function(s) { return s; }); if (!urls.length) return '—'; return '' + urls.map(function(u) { return ''; }).join('') + ''; } // ─── 新品管理 字段设置(拖拽排序 + 显隐) ─── // 实际生效的字段显隐状态(seq 序号列固定不参与) if (!window.newColVisible) { window.newColVisible = {}; NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field !== 'seq') window.newColVisible[c.field] = true; }); } // 弹窗临时显隐状态 if (!window._newPendingVisible) { window._newPendingVisible = {}; NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field !== 'seq') window._newPendingVisible[c.field] = true; }); } // 拖拽后的字段顺序(不含 seq,seq 固定最前) if (!window._newColDragOrder) { window._newColDragOrder = NEW_PRODUCT_COLUMNS.filter(function(c) { return c.field !== 'seq'; }).map(function(c) { return c.field; }); } // 当前生效的列顺序(seq 固定最前) function getNewColOrder() { var order = ['seq']; window._newColDragOrder.forEach(function(f) { order.push(f); }); NEW_PRODUCT_COLUMNS.forEach(function(c) { if (order.indexOf(c.field) === -1) order.push(c.field); }); return order; } function newColDef(field) { for (var i = 0; i < NEW_PRODUCT_COLUMNS.length; i++) { if (NEW_PRODUCT_COLUMNS[i].field === field) return NEW_PRODUCT_COLUMNS[i]; } return null; } function renderNewColCheckboxes() { var grid = document.getElementById('newColGrid'); if (!grid) return; grid.innerHTML = ''; window._newColDragOrder.forEach(function(field) { var col = newColDef(field); if (!col) return; var checked = window._newPendingVisible[field] ? 'checked' : ''; var label = document.createElement('label'); label.draggable = true; label.dataset.field = field; label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px'; label.innerHTML = '⠿ ' + col.label; label.addEventListener('dragstart', function(e) { e.dataTransfer.setData('text/plain', field); this.style.opacity = '0.4'; }); label.addEventListener('dragend', function() { this.style.opacity = '1'; document.querySelectorAll('#newColGrid label').forEach(function(el) { el.style.border = ''; }); }); label.addEventListener('dragover', function(e) { e.preventDefault(); document.querySelectorAll('#newColGrid label').forEach(function(el) { el.style.border = ''; }); this.style.border = '2px dashed var(--primary)'; }); label.addEventListener('dragleave', function() { this.style.border = ''; }); label.addEventListener('drop', function(e) { e.preventDefault(); document.querySelectorAll('#newColGrid label').forEach(function(el) { el.style.border = ''; }); var fromField = e.dataTransfer.getData('text/plain'); var toField = this.dataset.field; if (fromField === toField) return; var order = window._newColDragOrder; var fromIdx = order.indexOf(fromField); var toIdx = order.indexOf(toField); if (fromIdx === -1 || toIdx === -1) return; order.splice(fromIdx, 1); order.splice(toIdx, 0, fromField); renderNewColCheckboxes(); }); grid.appendChild(label); }); } function openNewColModal() { // 基于当前生效顺序重建拖拽顺序 window._newColDragOrder = getNewColOrder().filter(function(f) { return f !== 'seq'; }); NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field === 'seq') return; window._newPendingVisible[c.field] = window.newColVisible[c.field]; }); var old = document.getElementById('newColModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'newColModal'; overlay.innerHTML = '' + ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); renderNewColCheckboxes(); var allCb = document.getElementById('new-cb-all'); if (allCb) allCb.checked = window._newColDragOrder.every(function(f) { return window._newPendingVisible[f]; }); refreshNewSchemeList(); } function closeNewColModal() { var m = document.getElementById('newColModal'); if (m) m.remove(); } function confirmNewCol() { NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field === 'seq') return; window.newColVisible[c.field] = window._newPendingVisible[c.field]; }); closeNewColModal(); if (window._pageCache) delete window._pageCache['product_new']; loadPage('product_new'); } // ─── 新品管理 列方案管理(方案按账号隔离,仅本账号可见) ─── // 构建当前新品列配置JSON(顺序+显隐,基于弹窗内临时状态) function buildNewColumnsJSON() { var cols = []; getNewColOrder().forEach(function(field) { if (field === 'seq') return; var col = newColDef(field); if (col) { cols.push({ field: col.field, label: col.label, visible: window._newPendingVisible[field] !== undefined ? window._newPendingVisible[field] : true }); } }); return JSON.stringify(cols); } // 保存方案 window.saveNewScheme = function() { var input = document.getElementById('newSchemeNameInput'); if (!input) return; var schemeName = input.value.trim(); if (!schemeName) { alert('请输入方案名称'); input.focus(); return; } fetch('/api/planning/scheme/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ scheme_name: schemeName, columns_json: buildNewColumnsJSON(), page: 'product_new', is_default: 0 }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { input.value = ''; refreshNewSchemeList(); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败,请检查网络'); }); }; // 加载方案到弹窗(预览勾选,不关闭弹窗) window.loadNewScheme = function(schemeId) { if (schemeId === undefined || schemeId === null) return; fetch('/api/planning/schemes?page=product_new') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var target = null; for (var i = 0; i < schemes.length; i++) { if (schemes[i].id == schemeId) { target = schemes[i]; break; } } if (!target) { alert('方案不存在或已被删除'); refreshNewSchemeList(); return; } var cols; try { cols = JSON.parse(target.columns_json); } catch(e) { alert('方案数据异常'); return; } if (!cols || !Array.isArray(cols) || cols.length === 0) { alert('方案数据为空'); return; } // 更新弹窗临时状态(勾选+拖拽顺序) var newOrder = []; var pendingVisible = {}; cols.forEach(function(c) { if (c && c.field && c.field !== 'seq') { newOrder.push(c.field); pendingVisible[c.field] = c.visible !== false; } }); NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field === 'seq') return; if (pendingVisible[c.field] === undefined) { newOrder.push(c.field); pendingVisible[c.field] = true; } }); window._newColDragOrder = newOrder.slice(); NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field === 'seq') return; window._newPendingVisible[c.field] = pendingVisible[c.field]; }); renderNewColCheckboxes(); var allCb = document.getElementById('new-cb-all'); if (allCb) allCb.checked = window._newColDragOrder.every(function(f) { return window._newPendingVisible[f]; }); }) .catch(function(err) { console.error('loadNewScheme error:', err); }); }; // 删除方案 window.deleteNewScheme = function(schemeId) { showCenteredConfirm('确认', '确定要删除此方案吗?').then(function(ok) { if (!ok) return; fetch('/api/planning/scheme/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) refreshNewSchemeList(); else alert(res.msg || '删除失败'); }); }); }; // 设为默认方案 window.setDefaultNewScheme = function(schemeId) { fetch('/api/planning/scheme/set_default', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId, page: 'product_new'}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadNewScheme(schemeId); refreshNewSchemeList(); } else { alert(res.msg || '设置失败'); } }) .catch(function(err) { console.error('setDefaultNewScheme error:', err); }); }; // 刷新方案列表UI function refreshNewSchemeList() { var container = document.getElementById('newSchemeList'); if (!container) return; fetch('/api/planning/schemes?page=product_new') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; if (schemes.length === 0) { container.innerHTML = '暂无保存方案'; return; } var html = ''; schemes.forEach(function(s) { var isDefault = s.is_default == 1; html += ''; html += '' + escHtml(s.scheme_name) + ''; if (isDefault) html += '★默认'; html += '📌'; html += '✕'; html += ''; }); container.innerHTML = html; }); } // 页面初始化时应用默认方案(首次进入,防止循环) function loadDefaultNewScheme() { if (window._newDefaultApplied) return; window._newDefaultApplied = true; fetch('/api/planning/schemes?page=product_new') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var defaultScheme = null; schemes.forEach(function(s) { if (s.is_default == 1) defaultScheme = s; }); if (!defaultScheme) return; var cols; try { cols = JSON.parse(defaultScheme.columns_json); } catch(e) { return; } if (!cols || !Array.isArray(cols) || cols.length === 0) return; // 应用默认方案到列表生效状态 var newOrder = []; var visible = {}; cols.forEach(function(c) { if (c && c.field && c.field !== 'seq') { newOrder.push(c.field); visible[c.field] = c.visible !== false; } }); NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field === 'seq') return; if (newOrder.indexOf(c.field) === -1) { newOrder.push(c.field); visible[c.field] = true; } }); window._newColDragOrder = newOrder.slice(); NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field === 'seq') return; window.newColVisible[c.field] = visible[c.field]; }); if (window._pageCache) delete window._pageCache['product_new']; loadPage('product_new'); }); } // 重置列表为默认状态 window.resetNewColumns = function() { window.newColVisible = {}; NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field !== 'seq') window.newColVisible[c.field] = true; }); window._newColDragOrder = NEW_PRODUCT_COLUMNS.filter(function(c) { return c.field !== 'seq'; }).map(function(c) { return c.field; }); window._newPendingVisible = {}; NEW_PRODUCT_COLUMNS.forEach(function(c) { if (c.field !== 'seq') window._newPendingVisible[c.field] = true; }); if (window._pageCache) delete window._pageCache['product_new']; loadPage('product_new'); }; function toggleNewCol(field) { window._newPendingVisible[field] = !window._newPendingVisible[field]; var cb = document.getElementById('new-cb-' + field); if (cb) cb.checked = window._newPendingVisible[field]; } function toggleNewColPendingAll(checked) { window._newColDragOrder.forEach(function(f) { window._newPendingVisible[f] = checked; var cb = document.getElementById('new-cb-' + f); if (cb) cb.checked = checked; }); } function filterNewColCheckboxes() { var kw = (document.getElementById('newColSearchInput') ? document.getElementById('newColSearchInput').value : '').trim(); document.querySelectorAll('#newColGrid label').forEach(function(label) { if (!kw) { label.style.display = ''; } else { label.style.display = (label.textContent || '').indexOf(kw) >= 0 ? '' : 'none'; } }); } function newRowCellHtml(field, r, i) { if (field === 'seq') return '' + (i + 1) + ''; if (NEW_LIST_IMAGE_FIELDS.indexOf(field) >= 0) return newListImgCell(r[field]); var val = escHtml(r[field] || '—'); if (field === 'product_code') return '' + val + ''; if (field === 'product_name') return '' + val + ''; // 产品总成本/产品总货款保留3位小数 if (field === 'total_cost' || field === 'total_payment') { var num = parseFloat(r[field]); val = escHtml(isNaN(num) ? '—' : num.toFixed(3)); } return '' + val + ''; } function loadProductNewPage(container) { var theadHtml = getNewColOrder().map(function(field) { if (field !== 'seq' && !window.newColVisible[field]) return ''; var c = newColDef(field); if (!c) return ''; return '' + c.label + ''; }).join(''); container.innerHTML = `
共 - 条
${theadHtml}
操作
加载中...
`; loadNewProducts(); applyNewActionState(); restoreAdjustableTableColumnWidths('newTable', 'new_products'); enableAdjustableTableColumnResize('newTable', 'new_products'); // 页面初始化时应用默认方案(仅首次) loadDefaultNewScheme(); } // ─── 新品列表数据 ─── function resetNewFilters() { ['newFilterCodeName', 'newFilterSupplier'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); loadNewProducts(); } function loadNewProducts() { var tbody = document.getElementById('newTbody'); var codeName = (document.getElementById('newFilterCodeName') ? document.getElementById('newFilterCodeName').value : '').trim(); var supplier = (document.getElementById('newFilterSupplier') ? document.getElementById('newFilterSupplier').value : '').trim(); var url = '/api/new_products/list'; var params = []; if (codeName) params.push('search=' + encodeURIComponent(codeName)); if (supplier) params.push('supplier=' + encodeURIComponent(supplier)); if (params.length) url += '?' + params.join('&'); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { var totalEl = document.getElementById('newTotal'); if (totalEl) totalEl.textContent = res.total || 0; if (!tbody) return; if (res.code !== 0) { tbody.innerHTML = '加载失败:' + escHtml(res.msg || '未知错误') + ''; return; } var rows = res.data || []; window._newRowsCache = rows; var visibleColCount = getNewColOrder().filter(function(f) { return f === 'seq' || window.newColVisible[f]; }).length + 1; if (!rows.length) { tbody.innerHTML = '暂无新品数据'; return; } tbody.innerHTML = rows.map(function(r, i) { var cells = getNewColOrder().map(function(field) { if (field !== 'seq' && !window.newColVisible[field]) return ''; return newRowCellHtml(field, r, i); }).join(''); return '' + cells + '
' + ' ' + ' ' + ' ' + '' + '
' + ''; }).join(''); applyNewActionState(); }) .catch(function() { if (tbody) tbody.innerHTML = '网络错误'; }); } // ─── 新品同步:成品、包材和 BOM 同步到产品信息管理 ─── function syncNewProduct(id) { window._newSyncPending = window._newSyncPending || {}; if (window._newSyncPending[id]) return; var rows = window._newRowsCache || []; var row = null; for (var i = 0; i < rows.length; i++) { if (rows[i].id == id) { row = rows[i]; break; } } if (!row) { showCenteredAlert('提示', '未找到该条数据,请刷新后重试'); return; } if (!(row.product_code || '').trim() || !(row.product_name || '').trim()) { showCenteredAlert('提示', '产品编码和产品名称不能为空,无法同步'); return; } var message = '确认同步新品 [' + escHtml(row.product_code) + '] ' + escHtml(row.product_name) + '?' + '
成品信息将同步到成品基础信息表,物料信息将同步到包材基础信息表,并同步更新物料清单。'; showCenteredConfirm('确认同步', message).then(function(confirmed) { if (!confirmed) return; var button = document.getElementById('newSyncBtn-' + id); window._newSyncPending[id] = true; if (button) { button.disabled = true; button.dataset.originalText = button.innerHTML; button.innerHTML = '同步中...'; } return fetch('/api/new_products/sync', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_id: id}) }) .then(function(response) { return response.json(); }) .then(function(res) { if (res.code !== 0) throw new Error(res.msg || '同步失败'); var data = res.data || {}; if (window._pageCache) { delete window._pageCache.products; delete window._pageCache.bom; } showCenteredAlert( '同步成功', '✅ 成品信息已同步;包材共 ' + (data.materials_total || 0) + ' 条,BOM 共 ' + (data.bom_total || 0) + ' 条。' ); }) .catch(function(error) { showCenteredAlert('同步失败', '❌ ' + escHtml(error.message || '同步失败')); }) .finally(function() { delete window._newSyncPending[id]; var currentButton = document.getElementById('newSyncBtn-' + id); if (currentButton) { currentButton.disabled = false; currentButton.innerHTML = currentButton.dataset.originalText || '🔄 同步'; } }); }); } // ─── 新品操作列折叠/展开 ─── function isNewActionCollapsed() { return localStorage.getItem('newProductsActionsCollapsed') === 'true'; } function applyNewActionState() { var table = document.getElementById('newTable'); if (!table) return; var collapsed = isNewActionCollapsed(); table.classList.toggle('order-actions-collapsed', collapsed); var actionWidth = collapsed ? 34 : 250; var widthCols = table.querySelectorAll('colgroup col'); if (widthCols.length) { var actionCol = widthCols[widthCols.length - 1]; actionCol.style.width = actionWidth + 'px'; actionCol.style.minWidth = actionWidth + 'px'; actionCol.style.maxWidth = actionWidth + 'px'; } var head = table.querySelector('thead tr:last-child th:last-child'); if (head) { head.classList.add('order-action-head'); head.style.setProperty('width', actionWidth + 'px', 'important'); head.style.setProperty('min-width', actionWidth + 'px', 'important'); head.style.setProperty('max-width', actionWidth + 'px', 'important'); } table.querySelectorAll('tbody tr td:last-child').forEach(function(cell) { cell.classList.add('order-action-cell'); cell.style.setProperty('width', actionWidth + 'px', 'important'); cell.style.setProperty('min-width', actionWidth + 'px', 'important'); cell.style.setProperty('max-width', actionWidth + 'px', 'important'); var buttons = cell.querySelector('.order-action-buttons'); if (buttons) buttons.style.setProperty('display', collapsed ? 'none' : 'flex', 'important'); }); var toggle = table.querySelector('.order-action-toggle'); if (toggle) { toggle.textContent = collapsed ? '«' : '»'; toggle.title = collapsed ? '展开操作列' : '折叠操作列'; } } window.toggleNewActionColumn = function() { localStorage.setItem('newProductsActionsCollapsed', String(!isNewActionCollapsed())); applyNewActionState(); }; // ─── 新品新增/编辑弹窗 ─── var NEW_FORM_FIELDS = [ {field: 'category1', label: '一级类目', type: 'cat1'}, {field: 'category2', label: '二级类目', type: 'cat2'}, {field: 'category3', label: '三级类目', type: 'cat3'}, {field: 'product_type', label: '类别', type: 'select', options: ['成品', '赠品', '现货']}, {field: 'brand', label: '品牌', type: 'brand_select'}, {field: 'product_code', label: '产品编码', type: 'text'}, {field: 'product_name', label: '产品名称', type: 'text'}, {field: 'barcode', label: '条形码', type: 'text'}, {field: 'capacity', label: '容量', type: 'text'}, {field: 'remark', label: '成品备注', type: 'text'}, {field: 'total_cost', label: '产品总成本', type: 'text'}, {field: 'total_payment', label: '产品总货款', type: 'text'}, {field: 'procurement_planner', label: '采购计划', type: 'text', addReadonly: true}, {field: 'product_dev', label: '产品开发', type: 'text'}, {field: 'package_dev', label: '包材开发', type: 'text'}, {field: 'designer', label: '设计师', type: 'text'}, {field: 'operator', label: '负责运营', type: 'text'} ]; // 图片上传字段(放在弹窗最下边) var NEW_IMAGE_FIELDS = [ {field: 'effect_img', label: '产品效果图(内外包)', max: 1}, {field: 'upgrade_img', label: '升级前的(内料图)', max: 1}, {field: 'confirm_img', label: '确版内料图凭证', max: 1}, {field: 'quote_inner_img', label: '内料报价单', max: 1}, {field: 'quote_pack_img', label: '包材报价单', max: 1} ]; // ─── 三级类目模糊搜索 ─── function newCatSearch(kw) { var box = document.getElementById('newCatSuggest'); if (!box) return; kw = (kw || '').trim(); if (!kw) { box.style.display = 'none'; return; } fetch('/api/new_products/category_search?q=' + encodeURIComponent(kw)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { box.style.display = 'none'; return; } var items = res.data || []; if (!items.length) { box.innerHTML = '
未找到匹配的三级类目
'; box.style.display = 'block'; return; } box.innerHTML = items.map(function(it) { return '
' + '' + escHtml(it.category3) + '' + ' (' + escHtml(it.category1) + ' / ' + escHtml(it.category2) + ')' + '
'; }).join(''); box.style.display = 'block'; }) .catch(function() { box.style.display = 'none'; }); } function newPickCategory(c1, c2, c3) { document.getElementById('newF_category1').value = c1; document.getElementById('newF_category2').value = c2; document.getElementById('newF_category3').value = c3; var box = document.getElementById('newCatSuggest'); if (box) box.style.display = 'none'; } // 生成单个图片上传槽位 HTML(点击/拖拽/粘贴通用) function newImgSlotHtml(field, i) { var sfx = '_' + i; return '
' + '' + '' + '
📷 点击上传图片
' + '
📋 点击此处,Ctrl+V 粘贴图片
' + '' + '' + '' + '
'; } // ─── 图片上传(点击/拖拽/粘贴通用) ─── function newDoUpload(field, slot, file) { if (!file) return; if (file.size > 10 * 1024 * 1024) { showCenteredAlert('提示', '图片大小不能超过10MB'); return; } var fd = new FormData(); fd.append('file', file); fetch('/api/new_products/upload_image', { method: 'POST', body: fd }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data && res.data.url) { var slotSuffix = slot ? '_' + slot : ''; document.getElementById('newF_' + field + slotSuffix).value = res.data.url; var img = document.getElementById('newImgPreview_' + field + slotSuffix); if (img) { img.src = res.data.url; img.style.display = 'block'; } var ph = document.getElementById('newImgPlaceholder_' + field + slotSuffix); if (ph) ph.style.display = 'none'; var hint = document.getElementById('newImgHint_' + field + slotSuffix); if (hint) hint.style.display = 'block'; var rm = document.getElementById('newImgRemove_' + field + slotSuffix); if (rm) rm.style.display = 'inline-block'; } else { showCenteredAlert('提示', res.msg || '上传失败'); } }) .catch(function() { showCenteredAlert('提示', '网络错误,上传失败'); }); } function newUploadImage(field, slot, input) { var file = input.files && input.files[0]; newDoUpload(field, slot, file); input.value = ''; } // 记录最近点击/拖入的图片槽位(粘贴上传优先定位到该槽位) window._newActiveSlot = null; function newSetActiveSlot(field, slot) { window._newActiveSlot = {field: field, slot: slot}; } // 拖拽图片到槽位上传 function newHandleDrop(e, field, slot) { var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (file) newDoUpload(field, slot, file); } // 点击“粘贴”区域:仅激活槽位并提示,不弹文件选择框 function newPasteSlot(field, slot) { newSetActiveSlot(field, slot); var btn = document.getElementById('newPasteBtn_' + field + '_' + slot); if (btn) { btn.style.borderColor = '#52c41a'; btn.style.background = '#f6ffed'; btn.textContent = '✅ 已就绪,请按 Ctrl+V 粘贴图片'; btn.focus && btn.focus(); setTimeout(function() { btn.style.borderColor = '#b37feb'; btn.style.background = '#f9f0ff'; btn.textContent = '📋 点击此处,Ctrl+V 粘贴图片'; }, 3000); } } // 粘贴图片上传(新品弹窗打开时生效) document.addEventListener('paste', function(e) { var overlay = document.getElementById('newModal'); if (!overlay || overlay.style.display !== 'flex') return; var items = e.clipboardData && e.clipboardData.items; if (!items) return; var file = null; for (var i = 0; i < items.length; i++) { if (items[i].kind === 'file' && items[i].type.indexOf('image') === 0) { file = items[i].getAsFile(); break; } } if (!file) return; e.preventDefault(); // 优先上传到当前激活槽位(若为空),否则找第一个空槽位 var target = null; if (window._newActiveSlot) { var hid = document.getElementById('newF_' + window._newActiveSlot.field + '_' + window._newActiveSlot.slot); if (hid && !hid.value) target = window._newActiveSlot; } if (!target) { for (var j = 0; j < NEW_IMAGE_FIELDS.length && !target; j++) { var f = NEW_IMAGE_FIELDS[j]; var max = f.max || 1; for (var k = 1; k <= max; k++) { var el = document.getElementById('newF_' + f.field + '_' + k); if (el && !el.value) { target = {field: f.field, slot: k}; break; } } } } if (target) newDoUpload(target.field, target.slot, file); else showCenteredAlert('提示', '图片槽位已满,请先删除部分图片'); }); function loadNewBrands(preset, callback) { if (typeof preset === 'function') { callback = preset; preset = undefined; } fetch('/api/new_products/brands') .then(function(r) { return r.json(); }) .then(function(res) { var sel = document.getElementById('newF_brand'); if (!sel || res.code !== 0) { if (callback) callback(); return; } var cur = sel.value; sel.innerHTML = '' + (res.data || []).map(function(b) { return ''; }).join(''); sel.value = (preset !== undefined && preset !== null) ? preset : cur; if (callback) callback(); }) .catch(function() { if (callback) callback(); }); } function showNewAdd() { var overlay = document.getElementById('newModal'); if (!overlay) { overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'newModal'; overlay.innerHTML = '' + ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); } var grid = document.getElementById('newFormGrid'); var imgGrid = document.getElementById('newImageGrid'); if (imgGrid) { imgGrid.style.gridTemplateColumns = '1fr 1fr'; imgGrid.style.columnGap = '16px'; } grid.innerHTML = NEW_FORM_FIELDS.map(function(f) { var input = ''; if (f.type === 'cat1' || f.type === 'cat2') { input = ''; } else if (f.type === 'cat3') { input = '
' + '' + '' + '
'; } else if (f.type === 'brand_select') { input = ''; } else if (f.type === 'select') { input = ''; } else if (f.addReadonly) { input = ''; } else { input = ''; } return '
' + input + '
'; }).join(''); // 图片上传区域:前三项各占一格,报价单(内料+包材并排各半宽)占一格,共4格2×2对齐 imgGrid.innerHTML = (function() { var html = ''; ['effect_img', 'upgrade_img', 'confirm_img'].forEach(function(field) { var f = null; for (var i = 0; i < NEW_IMAGE_FIELDS.length; i++) { if (NEW_IMAGE_FIELDS[i].field === field) { f = NEW_IMAGE_FIELDS[i]; break; } } if (!f) return; html += '
' + '' + newImgSlotHtml(f.field, 1) + '
'; }); html += '
' + '
' + '
' + newImgSlotHtml('quote_inner_img', 1) + '
' + '
' + newImgSlotHtml('quote_pack_img', 1) + '
' + '
' + '
'; return html; })(); document.getElementById('newModalTitle').textContent = '新增新品'; delete overlay.dataset.editId; overlay.style.display = 'flex'; loadNewBrands(); } function showNewEdit(id) { var rows = window._newRowsCache || []; var row = null; if (Array.isArray(rows)) { for (var i = 0; i < rows.length; i++) if (rows[i].id == id) { row = rows[i]; break; } } if (!row) { showCenteredAlert('提示', '未找到该条数据,请刷新后重试'); return; } showNewAdd(); var overlay = document.getElementById('newModal'); var imageGrid = document.getElementById('newImageGrid'); if (imageGrid) { imageGrid.style.gridTemplateColumns = '1fr 1fr'; imageGrid.style.columnGap = '16px'; } document.getElementById('newModalTitle').textContent = '编辑新品'; overlay.dataset.editId = id; // 编辑弹窗:采购计划改为可人工编辑(海外品由人工维护) var plannerEl = document.getElementById('newF_procurement_planner'); if (plannerEl) { plannerEl.readOnly = false; plannerEl.style.background = ''; plannerEl.style.color = ''; plannerEl.placeholder = '请输入采购计划'; } NEW_FORM_FIELDS.forEach(function(f) { var el = document.getElementById('newF_' + f.field); if (el && f.type !== 'brand_select') el.value = row[f.field] || ''; }); // 品牌:等品牌下拉异步加载完成后回填(避免赋值时选项尚未生成导致丢失) loadNewBrands(row.brand || '', function() { var sel = document.getElementById('newF_brand'); if (sel && row.brand) sel.value = row.brand; }); // 回填图片预览 NEW_IMAGE_FIELDS.forEach(function(f) { var urls = (row[f.field] || '').split(',').filter(function(s) { return s; }); var max = f.max || 1; for (var i = 1; i <= max; i++) { var sfx = '_' + i; var url = urls[i - 1] || ''; var hidden = document.getElementById('newF_' + f.field + sfx); if (hidden) hidden.value = url; if (url) { var img = document.getElementById('newImgPreview_' + f.field + sfx); if (img) { img.src = url; img.style.display = 'block'; } var ph = document.getElementById('newImgPlaceholder_' + f.field + sfx); if (ph) ph.style.display = 'none'; var hint = document.getElementById('newImgHint_' + f.field + sfx); if (hint) hint.style.display = 'block'; var rm = document.getElementById('newImgRemove_' + f.field + sfx); if (rm) rm.style.display = 'inline-block'; } } }); } // ─── 删除已上传的图片凭证 ─── function newRemoveImage(field, slot) { var sfx = slot ? '_' + slot : ''; var hidden = document.getElementById('newF_' + field + sfx); if (hidden) hidden.value = ''; var img = document.getElementById('newImgPreview_' + field + sfx); if (img) { img.src = ''; img.style.display = 'none'; } var ph = document.getElementById('newImgPlaceholder_' + field + sfx); if (ph) ph.style.display = 'block'; var hint = document.getElementById('newImgHint_' + field + sfx); if (hint) hint.style.display = 'none'; var rm = document.getElementById('newImgRemove_' + field + sfx); if (rm) rm.style.display = 'none'; } function closeNewModal() { var overlay = document.getElementById('newModal'); if (overlay) { overlay.style.display = 'none'; delete overlay.dataset.editId; } } function saveNewData() { var overlay = document.getElementById('newModal'); var payload = {}; NEW_FORM_FIELDS.forEach(function(f) { var el = document.getElementById('newF_' + f.field); payload[f.field] = el ? el.value.trim() : ''; }); // 图片字段(隐藏域存上传后的URL,多图用逗号拼接) NEW_IMAGE_FIELDS.forEach(function(f) { var max = f.max || 1; var urls = []; for (var i = 1; i <= max; i++) { var el = document.getElementById('newF_' + f.field + '_' + i); var v = el ? el.value.trim() : ''; if (v) urls.push(v); } payload[f.field] = urls.join(','); }); if (!payload.product_name && !payload.product_code) { showCenteredAlert('提示', '产品名称或产品编码至少填写一项'); return; } var editId = overlay ? overlay.dataset.editId : null; var url = editId ? '/api/new_products/update' : '/api/new_products/create'; if (editId) payload.id = parseInt(editId); fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { closeNewModal(); loadNewProducts(); showCenteredAlert('提示', editId ? '保存成功' : '新增成功'); } else { showCenteredAlert('提示', res.msg || '保存失败'); } }) .catch(function() { showCenteredAlert('提示', '网络错误,保存失败'); }); } // ─── 新品删除确认 ─── function showNewDelete(id, name) { var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'newDelModal'; overlay.innerHTML = '' + ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); } function closeNewDelModal() { var overlay = document.getElementById('newDelModal'); if (overlay) overlay.remove(); } function doNewDelete(id) { fetch('/api/new_products/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }) .then(function(r) { return r.json(); }) .then(function(res) { closeNewDelModal(); if (res.code === 0) { loadNewProducts(); showCenteredAlert('提示', '删除成功'); } else { showCenteredAlert('提示', res.msg || '删除失败'); } }) .catch(function() { closeNewDelModal(); showCenteredAlert('提示', '网络错误,删除失败'); }); } // ─── 新品BOM弹窗(独立物料清单,可编辑,默认3行空数据+添加一行) ─── var NEW_BOM_FIELDS = [ {key: 'material_code', label: '物料编码', width: '120px'}, {key: 'material_name', label: '物料名称', width: '160px'}, {key: 'usage_quantity', label: '用量', width: '76px', defaultValue: '1'}, {key: 'supplier_code', label: '供应商编码', width: '110px'}, {key: 'supplier', label: '供应商', width: '120px'}, {key: 'lead_time', label: '货期/天', width: '70px'}, {key: 'tax_rate', label: '税点', width: '64px'}, {key: 'tax_included', label: '是否含税', width: '84px'}, {key: 'unit_price', label: '单价/元', width: '90px'}, {key: 'quantity', label: '数量', width: '90px'}, {key: 'amount', label: '货款/元', width: '100px'}, {key: 'payment_method', label: '结款方式', width: '100px'}, {key: 'can_order', label: '是否可下单', width: '90px'}, {key: 'remark', label: '下单备注', width: '280px'}, {key: 'common_code', label: '通用物料编码', width: '120px'}, {key: 'common_name', label: '通用物料名称', width: '140px'}, {key: 'order_date', label: '下单日期', width: '120px'} ]; var NEW_EXPORT_FIELDS = NEW_PRODUCT_COLUMNS.filter(function(c) { return c.field !== 'seq'; }).map(function(c) { return {field: c.field, label: c.label}; }); var NEW_PACKAGING_EXPORT_FIELDS = [ {field: 'finished_product_code', label: '成品编码'}, {field: 'finished_product_name', label: '成品名称'} ].concat(NEW_BOM_FIELDS.map(function(f) { return {field: f.key, label: f.label}; })); function newBomTaxIncludedValue(value) { var raw = String(value == null ? '' : value).trim().replace('%', ''); var num = parseFloat(raw); return !isNaN(num) && num !== 0 ? '是' : '否'; } function newBomUpdateTaxIncluded(idx) { var tax = document.getElementById('nb-tax_rate-' + idx); var included = document.getElementById('nb-tax_included-' + idx); if (included) included.value = newBomTaxIncludedValue(tax ? tax.value : ''); } function newExportPageKey(type) { return type === 'product' ? 'new_products_export' : 'new_packaging_export'; } function newExportDefs(type) { return type === 'product' ? NEW_EXPORT_FIELDS : NEW_PACKAGING_EXPORT_FIELDS; } function newExportTitle(type) { return type === 'product' ? '新品成品管理导出' : '新品包材管理导出'; } function newExportFilters(type) { if (type === 'product') { return { search: (document.getElementById('newFilterCodeName')?.value || '').trim(), supplier: (document.getElementById('newFilterSupplier')?.value || '').trim() }; } return newPackagingFilterValues(); } function newExportRenderSchemeList(type) { var box = document.getElementById('newExportSchemeList'); if (!box) return; fetch('/api/planning/schemes?page=' + encodeURIComponent(newExportPageKey(type))) .then(function(r) { return r.json(); }) .then(function(res) { var schemes = res.data || []; if (!schemes.length) { box.innerHTML = '暂无保存方案'; return; } box.innerHTML = schemes.map(function(s) { var star = s.is_default == 1 ? '★默认' : ''; return '' + '' + escHtml(s.scheme_name) + '' + star + '📌' + '✕'; }).join(''); }); } function newExportSelectedFields() { return Array.prototype.map.call(document.querySelectorAll('#newExportFieldList input:checked'), function(el) { return el.value; }); } function newExportBuildColumns(type) { var defs = newExportDefs(type); var selected = newExportSelectedFields(); return JSON.stringify(defs.map(function(d) { return {field: d.field, label: d.label, visible: selected.indexOf(d.field) >= 0}; })); } function newExportSaveScheme(type) { var input = document.getElementById('newExportSchemeName'); var name = input ? input.value.trim() : ''; if (!name) { showCenteredAlert('提示', '请输入导出方案名称'); return; } var selected = newExportSelectedFields(); if (!selected.length) { showCenteredAlert('提示', '请至少选择一个导出字段'); return; } fetch('/api/planning/scheme/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({scheme_name: name, columns_json: newExportBuildColumns(type), page: newExportPageKey(type), is_default: 0}) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code === 0) { input.value = ''; newExportRenderSchemeList(type); showCenteredAlert('提示', '导出方案已保存'); } else showCenteredAlert('提示', res.msg || '保存失败'); }).catch(function() { showCenteredAlert('提示', '保存失败,请检查网络'); }); } function newExportLoadScheme(type, id) { fetch('/api/planning/schemes?page=' + encodeURIComponent(newExportPageKey(type))) .then(function(r) { return r.json(); }).then(function(res) { var target = (res.data || []).find(function(s) { return s.id == id; }); if (!target) return; newExportApplyScheme(target); }); } function newExportApplyScheme(target) { var cols; try { cols = JSON.parse(target.columns_json); } catch (e) { showCenteredAlert('提示', '方案数据异常'); return; } var visible = {}; (cols || []).forEach(function(c) { visible[c.field] = c.visible !== false; }); document.querySelectorAll('#newExportFieldList input').forEach(function(el) { el.checked = visible[el.value] !== false; }); } function newExportLoadDefaultScheme(type) { fetch('/api/planning/schemes?page=' + encodeURIComponent(newExportPageKey(type))) .then(function(r) { return r.json(); }).then(function(res) { var target = (res.data || []).find(function(s) { return Number(s.is_default) === 1; }); if (target) newExportApplyScheme(target); }); } function newExportSetDefault(type, id) { fetch('/api/planning/scheme/set_default', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, page: newExportPageKey(type)}) }).then(function() { newExportRenderSchemeList(type); }); } function newExportDeleteScheme(type, id) { showCenteredConfirm('确认', '确定删除这个导出方案吗?').then(function(ok) { if (!ok) return; fetch('/api/planning/scheme/delete', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id})}) .then(function() { newExportRenderSchemeList(type); }); }); } function openNewExportModal(type) { var old = document.getElementById('newExportModal'); if (old) old.remove(); var defs = newExportDefs(type); var fields = defs.map(function(d) { return ''; }).join(''); var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'newExportModal'; overlay.innerHTML = ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); newExportRenderSchemeList(type); newExportLoadDefaultScheme(type); } function newExportDownload(type) { var fields = newExportSelectedFields(); if (!fields.length) { showCenteredAlert('提示', '请至少选择一个导出字段'); return; } var filters = newExportFilters(type); var endpoint = type === 'product' ? '/api/new_products/export' : '/api/new_products/packaging/export'; fetch(endpoint, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(Object.assign({fields: fields}, filters))}) .then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.msg || '导出失败'); }); return r.blob(); }).then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = type === 'product' ? '新品成品管理.xlsx' : '新品包材管理.xlsx'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); var modal = document.getElementById('newExportModal'); if (modal) modal.remove(); }).catch(function(e) { showCenteredAlert('导出失败', e.message || '导出失败'); }); } var newPackagingPage = {page: 1, size: 50}; function loadProductPackagingPage(container) { container.innerHTML = `
共 - 条
${NEW_BOM_FIELDS.map(function(f) { return ''; }).join('')}
序号 成品编码 成品名称' + f.label + '
加载中...
`; resetNewPackagingFilters(false); loadNewPackaging(1); } function newPackagingFilterValues() { return { product: (document.getElementById('newPkgProduct')?.value || '').trim(), material: (document.getElementById('newPkgMaterial')?.value || '').trim(), supplier: (document.getElementById('newPkgSupplier')?.value || '').trim() }; } function resetNewPackagingFilters(load) { ['newPkgProduct', 'newPkgMaterial', 'newPkgSupplier'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); if (load !== false) loadNewPackaging(1); } function loadNewPackaging(page) { newPackagingPage.page = page || 1; var f = newPackagingFilterValues(); var params = ['page=' + newPackagingPage.page, 'size=' + newPackagingPage.size]; if (f.product) params.push('product=' + encodeURIComponent(f.product)); if (f.material) params.push('material=' + encodeURIComponent(f.material)); if (f.supplier) params.push('supplier=' + encodeURIComponent(f.supplier)); fetch('/api/new_products/packaging?' + params.join('&')) .then(function(r) { return r.json(); }) .then(function(res) { var tbody = document.getElementById('newPkgTbody'); if (!tbody) return; if (res.code !== 0) { tbody.innerHTML = '' + escHtml(res.msg || '加载失败') + ''; return; } var data = res.data || {}; var rows = data.rows || []; var total = Number(data.total || 0); var totalEl = document.getElementById('newPkgTotal'); if (totalEl) totalEl.textContent = total; if (!rows.length) { tbody.innerHTML = '暂无新品包材数据'; } else { tbody.innerHTML = rows.map(function(r, i) { var cells = '' + ((newPackagingPage.page - 1) * newPackagingPage.size + i + 1) + ''; cells += '' + escHtml(r.finished_product_code || '—') + ''; cells += '' + escHtml(r.finished_product_name || '—') + ''; cells += NEW_BOM_FIELDS.map(function(f) { var value = r[f.key]; if (value === null || value === undefined || value === '') value = '—'; return '' + escHtml(String(value)) + ''; }).join(''); return '' + cells + ''; }).join(''); } renderNewPackagingPagination(total); }) .catch(function() { var tbody = document.getElementById('newPkgTbody'); if (tbody) tbody.innerHTML = '网络错误'; }); } function renderNewPackagingPagination(total) { var el = document.getElementById('newPkgPagination'); if (!el) return; var pages = Math.max(1, Math.ceil(total / newPackagingPage.size)); if (pages <= 1) { el.innerHTML = ''; return; } var p = newPackagingPage.page; el.innerHTML = ' ' + '第 ' + p + ' / ' + pages + ' 页' + ''; } function newBomRowHtml(m, idx) { var INPUT_STYLE = 'box-sizing:border-box;height:36px;padding:4px 8px;font-size:13px;border:1px solid #d9d9d9;border-radius:4px;width:100%;'; var TEXTAREA_STYLE = 'box-sizing:border-box;height:36px;min-height:36px;padding:4px 8px;font-size:13px;border:1px solid #d9d9d9;border-radius:4px;resize:none;overflow:hidden;white-space:pre;font-family:inherit;line-height:1.5;width:100%;'; var MULTILINE_KEYS = {material_code:1, material_name:1, payment_method:1, remark:1, common_code:1, common_name:1}; var tds = NEW_BOM_FIELDS.map(function(f) { var val = m && m[f.key] != null ? m[f.key] : (f.defaultValue !== undefined ? f.defaultValue : ''); var valAttr = escHtml(val); var input; if (f.key === 'amount') { input = ''; } else if (f.key === 'supplier_code') { // 供应商编码:只读,由供应商自动匹配 input = ''; } else if (f.key === 'supplier') { // 供应商:模糊搜索下拉 input = '
' + '' + '' + '
'; } else if (f.key === 'tax_rate') { // 税点:输入框自带 % 后缀 input = '
' + '' + '%' + '
'; } else if (f.key === 'tax_included') { input = ''; } else if (f.key === 'lead_time') { input = ''; } else if (f.key === 'unit_price' || f.key === 'quantity') { input = ''; } else if (f.key === 'can_order') { var canOrderValue = String(val == null ? '' : val); input = ''; } else if (f.key === 'order_date') { input = ''; } else if (MULTILINE_KEYS[f.key]) { // 多行文本:宽度跟随列宽填充,行高固定,内容超高时内部滚动 input = ''; } else { input = ''; } var frozenClass = (f.key === 'material_code' || f.key === 'material_name') ? ' new-bom-frozen' : ''; return '' + input + ''; }).join(''); return '' + '' + (idx + 1) + '' + tds + '✕' + ''; } // ─── 新品BOM:列宽按每列最宽内容自适应(含表头),输入框填满列宽 ─── function newBomTextWidth(s) { // 估算文本像素宽度:ASCII ~7.5px/字符,中文 ~13px/字符(font-size 13px) var px = 0; s = String(s == null ? '' : s); for (var i = 0; i < s.length; i++) { px += s.charCodeAt(i) > 255 ? 13 : 7.5; } return px; } function newBomFitColWidths() { var tbody = document.getElementById('newBomTbody'); if (!tbody) return; var table = tbody.closest('table'); var colMax = {}; // 以字段配置和表头为最小宽度,再按该列最长的实际内容扩展。 NEW_BOM_FIELDS.forEach(function(f) { colMax[f.key] = Math.max(parseInt(f.width, 10) || 0, newBomTextWidth(f.label) + 30); }); Array.prototype.forEach.call(tbody.querySelectorAll('tr'), function(tr) { Array.prototype.forEach.call(tr.querySelectorAll('td[data-col]'), function(td) { var key = td.getAttribute('data-col'); if (!key || !colMax[key]) return; var control = td.querySelector('input,textarea,select'); var value = control ? control.value : td.textContent; var longestLine = String(value == null ? '' : value).split('\n').reduce(function(longest, line) { return newBomTextWidth(line) > newBomTextWidth(longest) ? line : longest; }, ''); var width = newBomTextWidth(longestLine) + 34; if (width > colMax[key]) colMax[key] = width; }); }); Object.keys(colMax).forEach(function(key) { var width = colMax[key]; Array.prototype.forEach.call(table.querySelectorAll('[data-col="' + key + '"]'), function(cell) { cell.style.width = width + 'px'; cell.style.minWidth = width + 'px'; }); }); newBomUpdateFrozenColumns(table, colMax); } // 序号、物料编码、物料名称三列固定,横向滚动时保持可见。 function newBomUpdateFrozenColumns(table, colWidths) { if (!table) return; var serialWidth = 44; var codeWidth = colWidths.material_code || 120; Array.prototype.forEach.call(table.querySelectorAll('.new-bom-frozen-serial'), function(cell) { cell.style.left = '0px'; cell.style.width = serialWidth + 'px'; cell.style.minWidth = serialWidth + 'px'; }); Array.prototype.forEach.call(table.querySelectorAll('[data-col="material_code"]'), function(cell) { cell.style.left = serialWidth + 'px'; }); Array.prototype.forEach.call(table.querySelectorAll('[data-col="material_name"]'), function(cell) { cell.style.left = (serialWidth + codeWidth) + 'px'; }); } // ─── 新品BOM:回车保存 / 失焦保存 / 换行自适应 / 供应商模糊搜索 ─── function newBomKeySave(e, el) { if (e.key !== 'Enter') return; if (el.tagName === 'TEXTAREA' && e.shiftKey) return; // Shift+Enter 换行 e.preventDefault(); e.stopPropagation(); if (e.repeat || e.isComposing) return; // 长按回车/输入法确认不重复触发保存 newBomAutoSave(el); } function newBomAutoResize(el) { if (!el) return; el.style.height = 'auto'; el.style.height = (el.scrollHeight + 2) + 'px'; } function newBomAutoSave(el) { if (!el) return; var modal = document.getElementById('newBomModal'); // 点击底部保存按钮时,mousedown 先于输入框 blur;此时由手动保存统一提交,避免并发请求。 if (modal && modal.dataset.manualSavePending === '1') return; var cur = el.value; var orig = el.getAttribute('data-orig'); if (orig === null) orig = ''; if (cur === orig) return; // 无变化不保存 el.setAttribute('data-orig', cur); saveNewBom(true, el); } function newBomPositionSupplierDropdown(input, dd) { if (!input || !dd) return; var rect = input.getBoundingClientRect(); var modal = document.getElementById('newBomModal'); var modalRect = modal ? modal.getBoundingClientRect() : {top: 8, bottom: window.innerHeight - 8}; var spaceBelow = modalRect.bottom - rect.bottom - 12; var spaceAbove = rect.top - modalRect.top - 12; var openAbove = spaceBelow < 140 && spaceAbove > spaceBelow; var maxHeight = Math.max(80, Math.min(180, (openAbove ? spaceAbove : spaceBelow) - 8)); dd.style.position = 'fixed'; dd.style.left = rect.left + 'px'; dd.style.width = Math.max(rect.width, 220) + 'px'; dd.style.right = 'auto'; dd.style.maxHeight = maxHeight + 'px'; if (openAbove) { dd.style.top = 'auto'; dd.style.bottom = (window.innerHeight - rect.top + 4) + 'px'; } else { dd.style.top = (rect.bottom + 4) + 'px'; dd.style.bottom = 'auto'; } } function newBomSupSearch(input, idx) { var dd = document.getElementById('nb-sup-dd-' + idx); if (!dd) return; var val = input.value.trim(); if (!val) { dd.style.display = 'none'; return; } clearTimeout(input._nbSupTimer); input._nbSupTimer = setTimeout(function() { fetch('/api/suppliers/list?search=' + encodeURIComponent(val) + '&size=50') .then(function(r) { return r.json(); }) .then(function(res) { var rows = (res.data && res.data.rows) || []; // 过滤:仅按供应商名称/简称/合同简称/编码匹配,排除 production_type 误命中 var v = val.toLowerCase(); rows = rows.filter(function(s) { return (s.supplier_name || '').toLowerCase().indexOf(v) >= 0 || (s.short_name || '').toLowerCase().indexOf(v) >= 0 || (s.contract_short_name || '').toLowerCase().indexOf(v) >= 0 || (s.supplier_code || '').toLowerCase().indexOf(v) >= 0; }); if (!rows.length) { dd.style.display = 'none'; return; } dd.innerHTML = rows.map(function(s, i) { return '
' + '' + escHtml(s.supplier_name || '') + '' + (s.supplier_code ? ' ' + escHtml(s.supplier_code) + '' : '') + (s.short_name ? ' ' + escHtml(s.short_name) + '' : '') + '
'; }).join(''); newBomPositionSupplierDropdown(input, dd); dd.style.display = 'block'; }) .catch(function() { dd.style.display = 'none'; }); }, 150); } function newBomSupKey(e, input, idx) { if (e.key !== 'Enter') return; if (e.shiftKey) return; // Shift+Enter 换行 e.preventDefault(); e.stopPropagation(); if (e.repeat || e.isComposing) return; // 长按回车/输入法确认不重复触发 var dd = document.getElementById('nb-sup-dd-' + idx); if (dd && dd.style.display !== 'none') { var first = dd.querySelector('.nb-sup-dd-item'); if (first) { newBomSupPick(first, idx); return; } } newBomAutoSave(input); } window.newBomSupPick = function(el, idx) { var code = el.getAttribute('data-code') || ''; var name = el.getAttribute('data-name') || ''; var sup = document.getElementById('nb-supplier-' + idx); var supCode = document.getElementById('nb-supplier_code-' + idx); if (sup) sup.value = name; if (supCode) supCode.value = code; var dd = document.getElementById('nb-sup-dd-' + idx); if (dd) dd.style.display = 'none'; if (sup) { newBomAutoSave(sup); } if (supCode) {} newBomFitColWidths(); }; // 全局:点击下拉选项选中 / 点击其他区域关闭下拉 document.addEventListener('click', function(e) { var item = e.target.closest('.nb-sup-dd-item'); if (item) { var dd = item.closest('[id^="nb-sup-dd-"]'); var idx = dd ? dd.id.replace('nb-sup-dd-', '') : null; if (idx !== null) newBomSupPick(item, parseInt(idx, 10)); e.preventDefault(); return; } document.querySelectorAll('[id^="nb-sup-dd-"]').forEach(function(d) { d.style.display = 'none'; }); }); function newBomCalcAmount(idx) { var up = document.getElementById('nb-unit_price-' + idx); var q = document.getElementById('nb-quantity-' + idx); var amt = document.getElementById('nb-amount-' + idx); if (!up || !q || !amt) return; var p = parseFloat(up.value) || 0; var n = parseFloat(q.value) || 0; amt.value = (p * n).toFixed(2); newBomFitColWidths(); } function newBomDelRow(idx) { var tr = document.querySelector('#newBomTbody tr[data-idx="' + idx + '"]'); if (tr) tr.remove(); // 重新编号 var tbody = document.getElementById('newBomTbody'); if (tbody) { Array.prototype.forEach.call(tbody.querySelectorAll('tr'), function(tr2, i) { tr2.dataset.idx = i; var firstTd = tr2.querySelector('td'); if (firstTd) firstTd.textContent = i + 1; }); } } function newBomAddRow() { var tbody = document.getElementById('newBomTbody'); if (!tbody) return; var idx = tbody.querySelectorAll('tr').length; tbody.insertAdjacentHTML('beforeend', newBomRowHtml({}, idx)); // 初始化 data-orig + 列宽自适应(行高固定) var tr = tbody.querySelector('tr[data-idx="' + idx + '"]'); if (tr) { Array.prototype.forEach.call(tr.querySelectorAll('input,textarea'), function(el) { el.setAttribute('data-orig', el.value); }); newBomFitColWidths(); } } function newBomPrepareManualSave() { var modal = document.getElementById('newBomModal'); if (!modal) return; modal.dataset.manualSavePending = '1'; clearTimeout(modal._manualSavePendingTimer); modal._manualSavePendingTimer = setTimeout(function() { if (modal) delete modal.dataset.manualSavePending; }, 500); } function showNewBomSaveAlert(title, message) { // BOM 主弹窗 z-index=99999,保存结果必须显示在它上面;固定 id 同时避免重复叠加。 showCenteredAlert(title, message, {id: 'newBomSaveAlert', zIndex: 100001}); } function saveNewBom(silent, triggerEl) { var modal = document.getElementById('newBomModal'); var pid = modal ? modal.dataset.pid : null; if (!pid) return; if (!silent) delete modal.dataset.manualSavePending; // 同一时间只允许一个保存请求。自动保存若在请求期间触发,排队到当前请求完成后执行。 if (modal.dataset.saving === '1') { if (silent) modal._queuedAutoSaveTrigger = triggerEl || modal; return; } modal.dataset.saving = '1'; var saveBtn = document.getElementById('newBomSaveBtn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.dataset.originalText = saveBtn.dataset.originalText || saveBtn.innerHTML; saveBtn.innerHTML = '保存中...'; } var tbody = document.getElementById('newBomTbody'); var rows = []; if (tbody) { Array.prototype.forEach.call(tbody.querySelectorAll('tr'), function(tr) { var item = {}; NEW_BOM_FIELDS.forEach(function(f) { var el = document.getElementById('nb-' + f.key + '-' + tr.dataset.idx); item[f.key] = el ? el.value.trim() : ''; }); rows.push(item); }); } fetch('/api/new_products/bom/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_id: parseInt(pid), rows: rows}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { if (silent && triggerEl) { triggerEl.style.borderColor = '#52c41a'; setTimeout(function() { triggerEl.style.borderColor = ''; }, 800); } else { // 手动保存成功:自动关闭 BOM 弹窗并刷新列表(总成本/总货款已同步更新) var m = document.getElementById('newBomModal'); if (m) m.remove(); if (window._pageCache) delete window._pageCache['product_new']; loadNewProducts(); showNewBomSaveAlert('提示', '保存成功'); } } else { showNewBomSaveAlert('提示', res.msg || '保存失败'); } }) .catch(function() { showNewBomSaveAlert('提示', '网络错误,保存失败'); }) .finally(function() { if (!modal) return; delete modal.dataset.saving; var btn = document.getElementById('newBomSaveBtn'); if (btn) { btn.disabled = false; btn.innerHTML = btn.dataset.originalText || '💾 保存'; } var queuedTrigger = modal._queuedAutoSaveTrigger; delete modal._queuedAutoSaveTrigger; if (queuedTrigger && document.body.contains(modal)) { setTimeout(function() { saveNewBom(true, queuedTrigger === modal ? null : queuedTrigger); }, 0); } }); } function showNewBom(id) { var existingModal = document.getElementById('newBomModal'); if (existingModal) { existingModal.focus({preventScroll: true}); return; } var rows = window._newRowsCache || []; var row = null; if (Array.isArray(rows)) { for (var i = 0; i < rows.length; i++) if (rows[i].id == id) { row = rows[i]; break; } } if (!row) { showCenteredAlert('提示', '未找到该条数据,请刷新后重试'); return; } var code = row.product_code || ''; var name = row.product_name || ''; var old = document.getElementById('newBomModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'newBomModal'; overlay.tabIndex = -1; overlay.dataset.pid = id; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; // 接管焦点,避免点击列表中的 BOM 按钮后,回车再次激活底层按钮造成弹窗反复重建闪烁。 overlay.addEventListener('keydown', function(e) { if (e.key === 'Enter' && e.target === overlay) { e.preventDefault(); e.stopPropagation(); } }); var theadHtml = NEW_BOM_FIELDS.map(function(f) { return '' + f.label + ''; }).join(''); overlay.innerHTML = '' + '' + ''; if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur(); document.body.appendChild(overlay); // 下一帧把键盘焦点放到 BOM 弹层本身,彻底断开底层 BOM 按钮的回车激活行为。 setTimeout(function() { if (document.body.contains(overlay)) overlay.focus({preventScroll: true}); }, 0); fetch('/api/new_products/bom?product_id=' + encodeURIComponent(id)) .then(function(r) { return r.json(); }) .then(function(res) { var tbody = document.getElementById('newBomTbody'); if (!tbody) return; if (res.code !== 0) { tbody.innerHTML = '' + escHtml(res.msg || '获取失败') + ''; return; } var materials = res.data || []; // 不足 3 行时补空行到 3 行(保存后再次打开仍保持默认 3 行) var list = materials.slice(); while (list.length < 3) list.push({}); tbody.innerHTML = list.map(function(m, i) { return newBomRowHtml(m, i); }).join(''); // 初始化 data-orig(用于判断是否有变化)+ 列宽自适应(行高固定) Array.prototype.forEach.call(tbody.querySelectorAll('input,textarea'), function(el) { el.setAttribute('data-orig', el.value); }); newBomFitColWidths(); }) .catch(function() { var tbody = document.getElementById('newBomTbody'); if (tbody) tbody.innerHTML = '网络错误'; }); } // ─── 新品图片预览(点击放大) ─── function previewNewImage(url) { if (!url) return; var old = document.getElementById('newImgPreviewModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'newImgPreviewModal'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.65);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var modal = document.createElement('div'); modal.className = 'modal'; modal.style.cssText = 'width:auto;max-width:92vw;background:transparent;box-shadow:none;border:none'; var closeBar = document.createElement('div'); closeBar.style.cssText = 'text-align:right;margin-bottom:6px'; var closeButton = document.createElement('button'); closeButton.className = 'btn btn-sm'; closeButton.type = 'button'; closeButton.textContent = '✕ 关闭'; closeButton.onclick = function() { overlay.remove(); }; closeBar.appendChild(closeButton); var image = document.createElement('img'); image.alt = '图片预览'; image.src = url; image.style.cssText = 'max-width:90vw;max-height:80vh;border-radius:8px;background:#fff;box-shadow:0 8px 32px rgba(0,0,0,0.4)'; image.onerror = function() { image.style.display = 'none'; var error = document.createElement('div'); error.style.cssText = 'padding:32px 48px;border-radius:8px;background:#fff;color:#f5222d;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,0.4)'; error.textContent = '图片加载失败,请检查图片文件是否存在'; modal.appendChild(error); }; modal.appendChild(closeBar); modal.appendChild(image); overlay.appendChild(modal); document.body.appendChild(overlay); } // ─── 品牌信息管理页面 ─── function loadBrandManagementPage(container) { container.innerHTML = `
共 - 个品牌
序号品牌分类更新时间操作
加载中...
`; window._brandManagePage = 1; loadBrandManagementData(1); } function resetBrandManagement() { document.getElementById('brandManageSearch').value=''; document.getElementById('brandManageCategory').value=''; loadBrandManagementData(1); } function loadBrandManagementData(page) { var body=document.getElementById('brandManageTbody'); if(!body) return; window._brandManagePage=page||1; var search=(document.getElementById('brandManageSearch').value||'').trim(), category=document.getElementById('brandManageCategory').value||''; fetch('/api/brand-management/list?page='+window._brandManagePage+'&size=10&search='+encodeURIComponent(search)+'&category='+encodeURIComponent(category)).then(function(r){return r.json();}).then(function(res){ if(res.code!==0){body.innerHTML=''+escHtml(res.msg||'加载失败')+'';return;} var rows=res.data||[], total=document.getElementById('brandManageTotal'); if(total) total.textContent=res.total||0; if(!rows.length){body.innerHTML='暂无品牌';document.getElementById('brandManagePagination').innerHTML='';return;} body.innerHTML=rows.map(function(r,i){var cat=r.category||'';var cls=cat==='国内'?'brand-category-domestic':(cat==='海外'?'brand-category-overseas':'');var select='';return ''+((window._brandManagePage-1)*10+i+1)+''+escHtml(r.brand)+''+select+''+escHtml(r.updated_at||'-')+'';}).join(''); renderBrandManagementPagination(res.total||0,10,window._brandManagePage); }).catch(function(){body.innerHTML='网络异常,请稍后重试';}); } function renderBrandManagementPagination(total,size,page){var el=document.getElementById('brandManagePagination');if(!el)return;var pages=Math.max(1,Math.ceil(total/size)),html='';for(var i=1;i<=pages;i++){if(pages>8&&i>2&&i1){if(i===3||i===pages-2)html+='…';continue;}html+='';}html+='第 '+page+'/'+pages+' 页';el.innerHTML=html;} function showBrandAdd(){showBrandModal('新增品牌','','');} function showBrandEdit(brand){fetch('/api/brand-management/list?search='+encodeURIComponent(brand)+'&size=10').then(function(r){return r.json();}).then(function(res){var row=(res.data||[]).find(function(x){return x.brand===brand;});showBrandModal('编辑品牌',brand,row?row.category:'');}).catch(function(){showBrandModal('编辑品牌',brand,'');});} function showBrandModal(title,brand,category){var old=document.getElementById('brandManageModal');if(old)old.remove();var overlay=document.createElement('div');overlay.id='brandManageModal';overlay.className='modal-overlay';overlay.style.display='flex';overlay.onclick=function(e){if(e.target===overlay){}};overlay.innerHTML='';document.body.appendChild(overlay);document.getElementById('brandManageModalCategory').value=category||'';document.getElementById('brandManageName').focus();} function submitBrandModal(oldBrand){var name=(document.getElementById('brandManageName').value||'').trim(),category=document.getElementById('brandManageModalCategory').value||'';if(!name){alert('品牌名称不能为空');return;}var isEdit=!!oldBrand;fetch(isEdit?'/api/brand-management/update':'/api/brand-management/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(isEdit?{old_brand:oldBrand,new_brand:name,category:category}:{brand:name,category:category})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){alert(res.msg||'保存失败');return;}document.getElementById('brandManageModal').remove();loadBrandManagementData(window._brandManagePage||1);}).catch(function(){alert('网络异常,保存失败');});} function saveBrandCategory(brand,category){fetch('/api/brand-management/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({old_brand:brand,new_brand:brand,category:category})}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){alert(res.msg||'保存失败');}else{loadBrandManagementData(window._brandManagePage||1);}}).catch(function(){alert('网络异常,保存失败');});} // ─── 类目管理页面 ─── function loadProductCategoryPage(container) { container.innerHTML = `
共 - 条
序号 一级类目 二级类目 三级类目 四级类目 操作
加载中...
`; loadCategoryData(1); } function resetCategoryFilter() { var el = document.getElementById('catSearch'); if (el) el.value = ''; // 重置时清除已有排序,恢复默认顺序 catSortField = ''; catSortDir = 'asc'; catPage = 1; loadCategoryData(1); } // ─── 新增类目弹窗 ─── function showCategoryAdd() { var overlay = document.getElementById('catModal'); if (!overlay) { overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'catModal'; overlay.innerHTML = ` `; document.body.appendChild(overlay); } document.getElementById('catModalTitle').textContent = '新增类目'; document.getElementById('catC1').value = ''; document.getElementById('catC2').value = ''; document.getElementById('catC3').value = ''; document.getElementById('catC4').value = ''; overlay.style.display = 'flex'; } // ─── 编辑类目弹窗 ─── function showCategoryEdit(id, c1, c2, c3, c4) { var overlay = document.getElementById('catModal'); if (!overlay) { showCategoryAdd(); overlay = document.getElementById('catModal'); } document.getElementById('catModalTitle').textContent = '编辑类目'; document.getElementById('catC1').value = c1 || ''; document.getElementById('catC2').value = c2 || ''; document.getElementById('catC3').value = c3 || ''; document.getElementById('catC4').value = c4 || ''; overlay.dataset.editId = id; overlay.style.display = 'flex'; } function closeCategoryModal() { var overlay = document.getElementById('catModal'); if (overlay) { overlay.style.display = 'none'; delete overlay.dataset.editId; } } function saveCategoryData() { var overlay = document.getElementById('catModal'); var c1 = (document.getElementById('catC1').value || '').trim(); var c2 = (document.getElementById('catC2').value || '').trim(); var c3 = (document.getElementById('catC3').value || '').trim(); var c4 = (document.getElementById('catC4').value || '').trim(); if (!c1) { showCenteredAlert('提示', '一级类目不能为空'); return; } var editId = overlay.dataset.editId; var url = editId ? '/api/product_categories/update' : '/api/product_categories/create'; var payload = { category1: c1, category2: c2, category3: c3, category4: c4 }; if (editId) payload.id = parseInt(editId); fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { closeCategoryModal(); loadCategoryData(1); showCenteredAlert('提示', editId ? '保存成功' : '新增成功'); } else { showCenteredAlert('提示', res.msg || '保存失败'); } }) .catch(function() { showCenteredAlert('提示', '网络错误,保存失败'); }); } // ─── 类目列表排序/分页状态 ─── var catSortField = ''; var catSortDir = 'asc'; var catRowsCache = []; var catPage = 1; var CAT_PAGE_SIZE = 20; function sortCatTable(field) { if (catSortField === field) { catSortDir = catSortDir === 'asc' ? 'desc' : 'asc'; } else { catSortField = field; catSortDir = 'asc'; } catPage = 1; renderCatRows(); } function goCatPage(p) { var totalPages = Math.max(1, Math.ceil(catRowsCache.length / CAT_PAGE_SIZE)); if (p < 1) p = 1; if (p > totalPages) p = totalPages; catPage = p; renderCatRows(); } function renderCatPagination() { var el = document.getElementById('catPagination'); if (!el) return; var total = catRowsCache.length; var totalPages = Math.max(1, Math.ceil(total / CAT_PAGE_SIZE)); if (totalPages <= 1) { el.style.display = 'none'; el.innerHTML = ''; return; } el.style.display = 'flex'; var html = ''; for (var p = 1; p <= totalPages; p++) { html += ''; } html += ''; html += '共 ' + total + ' 条 / ' + totalPages + ' 页'; el.innerHTML = html; } function renderCatRows() { var tbody = document.getElementById('catTbody'); if (!tbody) return; var rows = catRowsCache.slice(); if (catSortField) { var f = catSortField; var dir = catSortDir === 'asc' ? 1 : -1; rows.sort(function(a, b) { var va = String(a[f] || ''); var vb = String(b[f] || ''); var r = va.localeCompare(vb, 'zh'); return r * dir; }); } // 更新排序箭头 ['c1', 'c2', 'c3', 'c4'].forEach(function(field) { var arrow = document.getElementById('catSortArrow' + field.toUpperCase()); if (arrow) { if (catSortField === field) { arrow.textContent = catSortDir === 'asc' ? '▲' : '▼'; arrow.style.color = '#333333'; arrow.style.fontWeight = '700'; arrow.style.fontSize = ''; } else { // 未排序时也显示浅色▲▼,提示该列可点击排序 arrow.textContent = '▲▼'; arrow.style.color = '#c0c4cc'; arrow.style.fontWeight = '400'; arrow.style.fontSize = '10px'; } } }); if (!rows.length) { tbody.innerHTML = '暂无类目数据'; var pg = document.getElementById('catPagination'); if (pg) { pg.style.display = 'none'; pg.innerHTML = ''; } return; } // 分页:每页最多 20 条 var totalPages = Math.max(1, Math.ceil(rows.length / CAT_PAGE_SIZE)); if (catPage > totalPages) catPage = totalPages; var startIdx = (catPage - 1) * CAT_PAGE_SIZE; var pageRows = rows.slice(startIdx, startIdx + CAT_PAGE_SIZE); tbody.innerHTML = pageRows.map(function(r, i) { return '' + '' + (startIdx + i + 1) + '' + '' + escHtml(r.c1 || '—') + '' + '' + escHtml(r.c2 || '—') + '' + '' + escHtml(r.c3 || '—') + '' + '' + escHtml(r.c4 || '—') + '' + '' + ' ' + '' + '' + ''; }).join(''); renderCatPagination(); } // ─── 删除类目确认弹窗 ─── function showCategoryDelete(id, c1, c2, c3, c4) { var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'catDelModal'; overlay.innerHTML = [ '' ].join(''); overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); var info = [c1, c2, c3, c4].filter(function(x) { return x; }).join(' / '); document.getElementById('catDelInfo').textContent = info || '(空类目)'; } function closeCatDelModal() { var overlay = document.getElementById('catDelModal'); if (overlay) overlay.remove(); } function doDeleteCategory(id) { fetch('/api/product_categories/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }) .then(function(r) { return r.json(); }) .then(function(res) { closeCatDelModal(); if (res.code === 0) { loadCategoryData(1); showCenteredAlert('提示', '删除成功'); } else { showCenteredAlert('提示', res.msg || '删除失败'); } }) .catch(function() { closeCatDelModal(); showCenteredAlert('提示', '网络错误,删除失败'); }); } function loadCategoryData(page) { var search = (document.getElementById('catSearch')?.value || '').trim(); var url = '/api/product_categories/list'; if (search) url += '?search=' + encodeURIComponent(search); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { var tb = document.getElementById('catTbody'); if (tb) tb.innerHTML = '加载失败:' + escHtml(res.msg || '未知错误') + ''; return; } var totalEl = document.getElementById('catTotal'); if (totalEl) totalEl.textContent = res.total || 0; // 扁平化:一级/二级/三级逐行展示,与表格文件一致 var rows = []; (res.data || []).forEach(function(g) { g.children.forEach(function(c2) { c2.items.forEach(function(it) { rows.push({ id: it.id, c1: it.category1, c2: it.category2, c3: it.category3, c4: it.category4 }); }); }); }); // 搜索过滤 if (search) { var kw = search.toLowerCase(); rows = rows.filter(function(r) { return (r.c1 + r.c2 + r.c3 + r.c4).toLowerCase().indexOf(kw) >= 0; }); var cnt = document.getElementById('catTotal'); if (cnt) cnt.textContent = rows.length; } var tbody = document.getElementById('catTbody'); if (!tbody) return; if (!rows.length) { tbody.innerHTML = '暂无类目数据'; return; } catRowsCache = rows; catPage = 1; renderCatRows(); }) .catch(function() { var tbody = document.getElementById('catTbody'); if (tbody) tbody.innerHTML = '网络错误'; }); } // ─── 知识库页面 ─── // ─── 知识库页面 ─── function loadKnowledgePage(container) { renderKnowledgePage(container); } function kbRenderMarkdown(content) { var source = String(content || ''); if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') { return '
' + escHtml(source) + '
'; } return DOMPurify.sanitize(marked.parse(source), { USE_PROFILES: { html: true }, FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'], FORBID_ATTR: ['style'] }); } function renderKnowledgePage(container) { // 配置 marked if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true }); } container.innerHTML = `
📖 知识库
📁 资料库
🧑 画像
🔬 分析
⏳
正在加载知识库...
`; kbLoadIndex(); kbLoadDatalib(); kbLoadProfile(); } // 标签页切换 window._kbActiveTab = 'wiki'; function kbSwitchTab(tab) { window._kbActiveTab = tab; ['wiki','datalib','profile'].forEach(function(t) { document.getElementById('kbTab' + t.charAt(0).toUpperCase() + t.slice(1)).classList.toggle('active', t === tab); document.getElementById('kbPane' + t.charAt(0).toUpperCase() + t.slice(1)).style.display = t === tab ? '' : 'none'; }); // 切到知识库时回到第一级默认页面 if (tab === 'wiki') { var mainCard = document.getElementById('kbMainCard'); var articleCard = document.getElementById('kbArticleCard'); if (mainCard) mainCard.style.display = ''; if (articleCard) articleCard.style.display = 'none'; // 如果当前不是卡片视图(比如打开了子主题),刷新回卡片首页 if (!mainCard || !mainCard.querySelector('.kb-topics')) { kbLoadIndex(); } } } function kbLoadIndex() { var el = document.getElementById('kbContent'); el.innerHTML = '
⏳
正在加载知识库...
'; // 同时获取索引和操作日志 Promise.all([ fetch('/api/knowledge/index').then(function(r) { return r.json(); }), fetch('/api/knowledge/log').then(function(r) { return r.json(); }) ]) .then(function(results) { var idxRes = results[0]; var logRes = results[1]; if (idxRes.code !== 0) { el.innerHTML = '
❌ 加载失败
'; return; } // 解析操作日志,分页显示最近更新 var recentHtml = ''; var logContent = logRes.data || ''; if (logContent) { var logLines = logContent.split('\n'); var rawEntries = []; var currentEntry = null; logLines.forEach(function(line) { if (line.match(/^## \[/)) { if (currentEntry) rawEntries.push(currentEntry); var dateMatch = line.match(/^## \[(\d{4}-\d{2}-\d{2})\]/); var dateStr = dateMatch ? dateMatch[1] : ''; var text = line.replace(/^## \[\S+\]\s*/, '').trim(); currentEntry = { date: dateStr, text: text, children: [] }; } else if (currentEntry && line.trim().startsWith('- ')) { currentEntry.children.push(line.trim()); } }); if (currentEntry) rawEntries.push(currentEntry); // 按日期分组(每天合并为一条更新) var dayMap = {}; rawEntries.forEach(function(e) { if (!e.date) return; if (!dayMap[e.date]) { dayMap[e.date] = { date: e.date, actions: [] }; } dayMap[e.date].actions.push(e.text); }); var dayEntries = Object.keys(dayMap).sort().map(function(d) { return dayMap[d]; }); // 分页:每页5条(天) var RECENT_PER_PAGE = 5; if (!window._kbRecentPage) window._kbRecentPage = 1; var totalEntries = dayEntries.length; var totalPages = Math.max(1, Math.ceil(totalEntries / RECENT_PER_PAGE)); var page = Math.min(Math.max(1, window._kbRecentPage || 1), totalPages); window._kbRecentTotalPages = totalPages; // 从后往前取(最新的排前面) var startIdx = Math.max(0, totalEntries - page * RECENT_PER_PAGE); var endIdx = Math.max(0, totalEntries - (page - 1) * RECENT_PER_PAGE); var pageEntries = dayEntries.slice(startIdx, endIdx).reverse(); recentHtml = '
' + '
' + '🕐 最近更新' + (totalPages > 1 ? ' 第' + page + '/' + totalPages + '页' : '') + '' + '▼ 收起' + '
' + '
' + '
'; pageEntries.forEach(function(e, idx) { var dateId = 'kbDate_' + e.date.replace(/-/g, '_'); recentHtml += '
'; recentHtml += '
'; recentHtml += '▶'; recentHtml += '📅 ' + escHtml(e.date) + ''; recentHtml += '(' + e.actions.length + ' 项操作)'; recentHtml += '
'; recentHtml += '
'; }); recentHtml += '
'; // 分页控件 if (totalPages > 1) { recentHtml += '
'; recentHtml += ''; recentHtml += ''; for (var p = 1; p <= totalPages; p++) { var active = p === page; recentHtml += ''; } recentHtml += ''; recentHtml += ''; recentHtml += '
'; } recentHtml += '
'; } var topics = idxRes.data.topics || []; var html = recentHtml + '
'; var hasAny = false; topics.forEach(function(topic) { var hasSubtopics = topic.subtopics && topic.subtopics.length > 0; var totalCount = topic.articles.length; if (hasSubtopics) { topic.subtopics.forEach(function(sub) { totalCount += sub.articles.length; }); } html += '
'; html += '
📂 ' + escHtml(topic.name) + ' (' + totalCount + '篇)
'; html += '
'; // 渲染子主题(作为可点击的文件夹,点击展开) if (hasSubtopics) { topic.subtopics.forEach(function(sub) { if (sub.articles.length > 0) { html += ''; html += '
📁 ' + escHtml(sub.name) + '
'; html += '' + sub.articles.length + '篇 ›'; html += '
'; hasAny = true; } }); } // 渲染直属文章(不在子主题内的) topic.articles.forEach(function(art) { hasAny = true; html += ''; html += '
📄 ' + escHtml(art.title) + ''; if (art.summary && art.summary !== '—') { html += '' + escHtml(art.summary) + ''; } html += '
'; html += ''; html += '
'; }); if (!hasSubtopics && topic.articles.length === 0) { html += '
暂无文章
'; } html += '
'; }); html += '
'; if (!hasAny) { html = recentHtml + '
📭
知识库暂无内容
让 OpenClaw 摄入资料后,文章会自动出现在这里
'; } // 缓存索引数据用于子主题展开 window._kbIndexData = idxRes.data; el.innerHTML = html; }) .catch(function() { el.innerHTML = '
❌ 加载失败,请检查网络连接
'; }); } // 打开子主题(如训练记录) window.kbOpenSubTopic = function(topicName, subTopicName, page) { if (!page) page = 1; var idxData = window._kbIndexData; if (!idxData) { kbLoadIndex(); return; } var topic = null; (idxData.topics || []).forEach(function(t) { if (t.name === topicName) topic = t; }); if (!topic) return; var sub = null; (topic.subtopics || []).forEach(function(s) { if (s.name === subTopicName) sub = s; }); if (!sub || !sub.articles || sub.articles.length === 0) return; // 训练记录:最新在前 + 每页10条 var isTraining = subTopicName === '训练记录'; var articles = sub.articles; if (isTraining) { // 按日期倒序排列(最新在前) articles = articles.slice().sort(function(a, b) { var da = a.updated || '0'; var db = b.updated || '0'; return db.localeCompare(da); }); } var pageSize = isTraining ? 18 : 999; var totalPages = Math.ceil(articles.length / pageSize); if (page > totalPages) page = totalPages; var startIdx = (page - 1) * pageSize; var pageItems = articles.slice(startIdx, startIdx + pageSize); var el = document.getElementById('kbContent'); var html = '
'; html += '← 返回 ' + escHtml(topicName) + ''; html += '

📁 ' + escHtml(subTopicName) + ' (' + articles.length + '篇)

'; html += '
'; html += '
'; pageItems.forEach(function(art) { html += ''; html += '
📄 ' + escHtml(art.title) + ''; if (art.summary && art.summary !== '—') { html += '' + escHtml(art.summary) + ''; } html += '
'; html += ''; html += '
'; }); html += '
'; // 分页 if (isTraining && totalPages > 1) { html += '
'; if (page > 1) html += ''; html += '第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) html += ''; html += '
'; } el.innerHTML = html; }; function kbOpenArticle(path) { var listCard = document.getElementById('kbMainCard'); listCard.style.display = 'none'; var articleCard = document.getElementById('kbArticleCard'); articleCard.style.display = ''; var contentDiv = document.getElementById('kbArticleContent'); contentDiv.innerHTML = '
⏳
加载文章...
'; document.getElementById('kbArticleTitle').textContent = decodeURIComponent(path); fetch('/api/knowledge/article?path=' + encodeURIComponent(path)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { contentDiv.innerHTML = '
❌ ' + (res.msg || '加载失败') + '
'; return; } var data = res.data; document.getElementById('kbArticleTitle').textContent = data.path; document.getElementById('kbArticleMeta').textContent = '更新于 ' + data.updated; contentDiv.innerHTML = '
' + kbRenderMarkdown(data.content) + '
'; // 外链在新窗口打开 contentDiv.querySelectorAll('a').forEach(function(a) { if (a.getAttribute('href') && !a.getAttribute('href').startsWith('#')) { a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener'); } }); }) .catch(function() { contentDiv.innerHTML = '
❌ 加载失败
'; }); } // 加载资料库 function kbLoadDatalib() { var el = document.getElementById('kbPaneDatalib'); fetch('/api/knowledge/datalib') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data) { el.innerHTML = '
暂无数据
'; return; } var data = res.data; var html = ''; var sections = [ {key:'我的注视', icon:'👁️'}, {key:'我的技巧', icon:'🧰'}, {key:'我的目标', icon:'🎯'}, {key:'我的偏好', icon:'⚙️'}, {key:'我的疑问', icon:'❓'} ]; sections.forEach(function(s) { var content = data[s.key] || ''; html += '
'; html += '
' + s.icon + ' ' + escHtml(s.key) + '
'; html += '
'; html += kbRenderMarkdown(content); html += '
'; }); el.innerHTML = html; }) .catch(function() { el.innerHTML = '
❌ 加载失败
'; }); } // 加载画像 function kbLoadProfile() { var el = document.getElementById('kbPaneProfile'); fetch('/api/knowledge/profile') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { el.innerHTML = '
❌ 加载失败
'; return; } var data = res.data || {}; var html = ''; // 画像概览 if (data.atoms) { var atoms = data.atoms; html += '
'; html += '
🧑 个人画像概览
'; html += '
'; html += ''; html += ''; html += ''; var goals = atoms.目标 || []; html += ''; html += '
当前关注' + escHtml(atoms.注视 || '-') + '
技能' + (Array.isArray(atoms.技能) ? atoms.技能.join('、') : '-') + '
偏好' + (atoms.偏好 ? '语言:' + (atoms.偏好.语言||'中文') : '-') + '
目标' + (goals.length > 0 ? goals.map(function(g) { return g.名称 + '(' + g.状态 + ')'; }).join(';') : '-') + '
'; } // 画像 Markdown if (data.profile) { html += '
📝 画像详情
'; html += '
'; html += kbRenderMarkdown(data.profile); html += '
'; } el.innerHTML = html || '
暂无画像数据
'; }) .catch(function() { el.innerHTML = '
❌ 加载失败
'; }); } function kbBackToList() { document.getElementById('kbArticleCard').style.display = 'none'; document.getElementById('kbMainCard').style.display = ''; } function kbToggleRecent() { var body = document.getElementById('kbRecentBody'); var toggle = document.getElementById('kbRecentToggle'); if (!body || !toggle) return; var isHidden = body.style.display === 'none'; body.style.display = isHidden ? 'block' : 'none'; toggle.textContent = isHidden ? '▼ 收起' : '▶ 展开'; } function kbToggleDateActions(dateId) { var body = document.getElementById(dateId); var arrow = document.getElementById(dateId + '_arrow'); if (!body || !arrow) return; var isHidden = body.style.display === 'none'; body.style.display = isHidden ? 'block' : 'none'; arrow.style.transform = isHidden ? 'rotate(90deg)' : 'rotate(0deg)'; } function kbRecentGoPage(page) { window._kbRecentPage = page; kbLoadIndex(); } function kbSearch() { var q = document.getElementById('kbSearchInput').value.trim(); if (!q) { kbSwitchTab('wiki'); kbLoadIndex(); return; } kbSwitchTab('wiki'); var el = document.getElementById('kbContent'); el.innerHTML = '
🔍 搜索中...
'; fetch('/api/knowledge/search?q=' + encodeURIComponent(q)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || res.count === 0) { el.innerHTML = '
🔍
未找到相关结果
试试其他关键词
← 返回列表
'; return; } var html = '
找到 ' + res.count + ' 篇相关文章← 返回列表
'; res.data.forEach(function(item) { html += ''; html += '
📄 ' + escHtml(item.title) + '' + escHtml(item.path) + '
'; html += '
'; }); el.innerHTML = html; }); } // ─── 计划预测 - 冻结列配置 ─── var planFrozen = true; var planFrozenFields = ['product_type', 'product_code', 'product_name']; // 三个Tab各自独立的冻结列设置 if (!window._planViewFrozenFields) { window._planViewFrozenFields = { 'default': ['product_type', 'product_code', 'product_name'], 'cloud': ['product_type', 'product_code', 'product_name'], 'external': ['product_type', 'product_code', 'product_name'] }; } var FROZEN_WIDTHS = { 'product_type': 75, 'product_code': 130, 'product_name': 210, '云仓库存': 90, '外仓库存': 90, '云仓昨日销量': 90, '外仓昨日销量': 90, '云仓3天平均销量': 100, '外仓3天平均销量': 100, '云仓7天平均销量': 100, '外仓7天平均销量': 100, '云仓14天平均销量': 100, '外仓14天平均销量': 100, '云仓日销量预测': 100, '外仓日销量预测': 100 }; function loadPage(page) { const mainContent = document.getElementById('mainContent'); // 更新导航高亮 document.querySelectorAll('.nav-item, .nav-sub-item').forEach(el => el.classList.remove('active')); document.querySelector(`.nav-item[data-page="${page}"]`)?.classList.add('active'); document.querySelector(`.nav-sub-item[data-page="${page}"]`)?.classList.add('active'); // 自动展开子菜单 document.querySelectorAll('.nav-submenu').forEach(m => m.classList.remove('open')); document.querySelectorAll('.nav-arrow').forEach(a => a.classList.remove('rotated')); // 更新库存页面类型变量(成品/包材) if (page === 'inventory_finished' || page === 'inventory_packaging') { _invProductType = page === 'inventory_finished' ? '成品' : '包材'; } var activeSub = document.querySelector(`.nav-sub-item[data-page="${page}"]`); if (activeSub) { var submenu = activeSub.closest('.nav-submenu'); if (submenu) { submenu.classList.add('open'); var arrow = submenu.closest('.nav-group')?.querySelector('.nav-arrow'); if (arrow) arrow.classList.add('rotated'); } } // 从 DOM 中移除所有已缓存的页面容器(保持内存缓存), // 避免跨页面重复 ID 导致 getElementById 定位到错误的元素 var cachedContainers = mainContent.querySelectorAll('.page-container'); cachedContainers.forEach(function(el) { el.remove(); }); // 如果已有缓存,重新附加到 DOM 并显示 if (window._pageCache[page]) { var cached = window._pageCache[page]; mainContent.appendChild(cached); cached.style.display = ''; // 产品基础表/供应商管理:每次切回时清除筛选框记忆并刷新 if (page === 'products') { ['prodCodeName','prodSupplier','prodTypeFilter'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); if (typeof loadProducts === 'function') loadProducts(1); } if (page === 'suppliers') { ['supCodeName'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); if (typeof loadSuppliers === 'function') loadSuppliers(1); } // 先销毁原有 Chart 实例(canvas detach 后 chart 可能状态错乱) if (typeof Chart !== 'undefined') { const canvases = cached.querySelectorAll('canvas'); canvases.forEach(function(c) { try { var chart = Chart.getChart(c); if (chart) { chart.destroy(); } } catch(e) {} }); } // 等浏览器布局完成后重新绘制 if (window._pageDataCache && window._pageDataCache[page]) { var reloader = window._pageReloaders[page]; if (reloader) { requestAnimationFrame(function() { setTimeout(function() { reloader(window._pageDataCache[page]); }, 50); }); } } // AI 分析页:切回时重新测量高度,保证输入框始终固定在底部 if (page === 'ai_analysis' && typeof aiFitHeight === 'function') { setTimeout(aiFitHeight, 60); } return; } // 首次加载:移除初始loading占位,创建容器 const loading = document.getElementById('pageLoading'); if (loading) loading.style.display = 'none'; const container = document.createElement('div'); container.className = 'page-container'; container.id = 'page-container-' + page; mainContent.appendChild(container); // 权限检查:无查看权限则显示无权限提示 if (PAGE_LOADERS[page] && !window._canViewPage(page)) { container.innerHTML = '
🔒

无访问权限

请联系管理员配置权限

'; window._pageCache[page] = container; return; } // 调用对应加载函数 if (PAGE_LOADERS[page]) { PAGE_LOADERS[page](container); } window._pageCache[page] = container; } // ============ SPA 导航系统 ============ // 菜单页面路径 → page key 映射 var PAGE_MAP = { '/': 'dashboard', '/data/summary': 'data_summary', '/data/detail': 'data_detail', '/inventory/finished': 'inventory_finished', '/inventory/packaging': 'inventory_packaging', '/planning': 'planning', '/spot/dashboard': 'spot_dashboard', '/spot/application': 'spot_application', '/spot/confirmation': 'spot_confirmation', '/spot/sales': 'spot_sales', '/products': 'products', '/brand-management': 'brand_management', '/bom': 'bom', '/warehouse': 'warehouse', '/suppliers': 'suppliers', '/personnel': 'personnel', '/purchase': 'purchase', '/purchase/ready': 'purchase_ready', '/orders/domestic': 'orders_domestic', '/orders/overseas': 'orders_overseas', '/orders/packaging': 'orders_packaging', '/orders/finished': 'orders_finished', '/users': 'users', '/login-records': 'login_records', '/logs': 'logs', '/config': 'config', '/permissions': 'permissions', '/knowledge': 'knowledge', '/feedback': 'feedback', '/contract-templates': 'contract_templates', '/product-order': 'product_order', '/product-new': 'product_new', '/product-packaging': 'product_packaging', '/product-category': 'product_category', '/collaboration/inventory-dashboard': 'collaboration_inventory_dashboard', '/collaboration/key-sku-forecast': 'collaboration_key_sku_forecast', '/collaboration/sku-health-index': 'collaboration_sku_health_index', '/collaboration/slow-moving-sku': 'collaboration_slow_moving_sku', '/collaboration/sales-management': 'collaboration_sales_management' }; // ─── 权限工具函数 ─── // 检查当前用户是否有页面查看权限 window._canViewPage = function(pageId) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId]; if (!pagePerms && pageId.indexOf('spot_') === 0) pagePerms = perms.spot || {}; if (!pagePerms && pageId === 'product_packaging') pagePerms = perms.product_new || {}; if (!pagePerms && pageId === 'brand_management') pagePerms = perms.products || {}; pagePerms = pagePerms || {}; if (pagePerms['view'] !== undefined) return pagePerms['view'] == 1; return false; }; // 检查当前用户是否有某个字段的查看权限 window._canViewField = function(pageId, fieldName) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; if (!window._canViewPage(pageId)) return false; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || (pageId === 'brand_management' ? (perms.products || {}) : {}); var key = 'field:' + fieldName + ':view'; if (pagePerms[key] !== undefined) return pagePerms[key] == 1; return pagePerms['view'] !== 0; }; // 检查当前用户是否有某个字段的编辑权限 window._canEditField = function(pageId, fieldName) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; if (!window._canViewPage(pageId)) return false; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || {}; var key = 'field:' + fieldName + ':edit'; if (pagePerms[key] !== undefined) return pagePerms[key] == 1; return pagePerms['edit'] !== 0; }; // 检查当前用户是否有页面功能权限 window._hasPagePerm = function(pageId, op) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; if (!window._canViewPage(pageId)) return false; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || {}; if (pagePerms[op] !== undefined) return pagePerms[op] == 1; return false; // 没设时无权限(由角色级权限兜底) }; // 价格受限账号(无法查看成本价)仅允许查看计划预测-海外品 window._planOverseasOnly = !window._canViewField('planning', '成本价'); // 隐藏无权限的菜单 window._hideNoPermNavItems = function() { try { if (!window._currentUser) return; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return; // 页面→导航菜单链接映射(包括子菜单nav-sub-item和顶层菜单nav-item) var pageNavMap = [ {page:'dashboard', sel:'a.nav-item[href="/dashboard"]', sub:false}, {page:'collaboration_inventory_dashboard', sel:'a.nav-sub-item[href="/collaboration/inventory-dashboard"]', sub:true}, {page:'collaboration_key_sku_forecast', sel:'a.nav-sub-item[href="/collaboration/key-sku-forecast"]', sub:true}, {page:'collaboration_sku_health_index', sel:'a.nav-sub-item[href="/collaboration/sku-health-index"]', sub:true}, {page:'collaboration_slow_moving_sku', sel:'a.nav-sub-item[href="/collaboration/slow-moving-sku"]', sub:true}, {page:'collaboration_sales_management', sel:'a.nav-sub-item[href="/collaboration/sales-management"]', sub:true}, {page:'data_summary', sel:'a.nav-sub-item[href="/data/summary"]', sub:true}, {page:'data_detail', sel:'a.nav-sub-item[href="/data/detail"]', sub:true}, {page:'inventory_finished', sel:'a.nav-sub-item[href="/inventory/finished"]', sub:true}, {page:'inventory_packaging', sel:'a.nav-sub-item[href="/inventory/packaging"]', sub:true}, {page:'planning', sel:'a.nav-item[href="/planning"]', sub:false}, {page:'spot_dashboard', sel:'a.nav-sub-item[href="/spot/dashboard"]', sub:true}, {page:'spot_application', sel:'a.nav-sub-item[href="/spot/application"]', sub:true}, {page:'spot_confirmation', sel:'a.nav-sub-item[href="/spot/confirmation"]', sub:true}, {page:'spot_sales', sel:'a.nav-sub-item[href="/spot/sales"]', sub:true}, {page:'purchase', sel:'a.nav-sub-item[href="/purchase"]', sub:true}, {page:'purchase_ready', sel:'a.nav-sub-item[href="/purchase/ready"]', sub:true}, {page:'orders_domestic', sel:'a.nav-sub-item[href="/orders/domestic"]', sub:true}, {page:'orders_overseas', sel:'a.nav-sub-item[href="/orders/overseas"]', sub:true}, {page:'orders_packaging', sel:'a.nav-sub-item[href="/orders/packaging"]', sub:true}, {page:'orders_finished', sel:'a.nav-sub-item[href="/orders/finished"]', sub:true}, {page:'product_new', sel:'a.nav-sub-item[href="/product-new"]', sub:true}, {page:'product_packaging', sel:'a.nav-sub-item[href="/product-packaging"]', sub:true}, {page:'product_category', sel:'a.nav-sub-item[href="/product-category"]', sub:true}, {page:'brand_management', sel:'a.nav-sub-item[href="/brand-management"]', sub:true}, {page:'feedback', sel:'a.nav-item[href="/feedback"]', sub:false}, {page:'ai_analysis', sel:'a.nav-item[href="/ai-analysis"]', sub:false}, {page:'products', sel:'a.nav-sub-item[href="/products"]', sub:true}, {page:'bom', sel:'a.nav-sub-item[href="/bom"]', sub:true}, {page:'warehouse', sel:'a.nav-sub-item[href="/warehouse"]', sub:true}, {page:'suppliers', sel:'a.nav-sub-item[href="/suppliers"]', sub:true}, {page:'personnel', sel:'a.nav-sub-item[href="/personnel"]', sub:true}, {page:'knowledge', sel:'a.nav-item[href="/knowledge"]', sub:false}, {page:'contract_templates', sel:'a.nav-sub-item[href="/contract-templates"]', sub:true}, {page:'users', sel:'a.nav-sub-item[href="/users"]', sub:true}, {page:'permissions', sel:'a.nav-sub-item[href="/permissions"]', sub:true}, {page:'login_records', sel:'a.nav-sub-item[href="/login-records"]', sub:true}, {page:'logs', sel:'a.nav-sub-item[href="/logs"]', sub:true}, {page:'config', sel:'a.nav-sub-item[href="/config"]', sub:true} ]; // 收集需要检查的菜单组(子菜单用) var processedSubmenus = {}; pageNavMap.forEach(function(item) { if (!window._canViewPage(item.page)) { var el = document.querySelector(item.sel); if (el) { if (item.sub) { var submenu = el.closest('.nav-submenu'); if (submenu) { processedSubmenus[submenu.id] = (processedSubmenus[submenu.id] || 0) + 1; el.remove(); } } else { el.remove(); } } } }); // 检查并移除空的菜单组(排除已被 nav-hidden 隐藏的项) Object.keys(processedSubmenus).forEach(function(gid) { var sm = document.getElementById(gid); if (!sm) return; var visible = sm.querySelectorAll('a.nav-sub-item:not(.nav-hidden)'); if (visible.length === 0) { sm.remove(); var gKey = gid.replace('submenu-', ''); var gh = document.querySelector('.nav-group-header[data-group="' + gKey + '"]'); if (gh) gh.remove(); } }); } catch(e) {} }; // 加载当前用户权限配置(权限已由后端模板同步注入,此处仅做兜底) window._loadUserPerms = function() { if (!window._currentUser) return; if (!window._currentUser.permissions) window._currentUser.permissions = {}; }; // ─── 表格列权限过滤工具 ─── // 根据权限过滤列定义,只保留有查看权限的列 window._filterColsByPerm = function(pageId, columns, labelField) { return columns.filter(function(c) { var label = labelField ? c[labelField] : c.label || c.field || c; return window._canViewField(pageId, label); }); }; // 检查某列是否有查看权限(用于 colspan 等计算) window._colHasViewPerm = function(pageId, label) { return window._canViewField(pageId, label); }; function initNavigation() { // 加载当前用户权限 window._loadUserPerms(); // 隐藏无权限的导航菜单 try { window._hideNoPermNavItems(); } catch(e) {} // DOM可能未完全解析,延迟再试一次 setTimeout(function() { try { window._hideNoPermNavItems(); } catch(e) {} }, 300); // 成品基础信息和计划预测共用人员信息管理中的实时分组 loadPersonnelGroupsForLinkedPages(); document.addEventListener('click', function(e) { var link = e.target.closest('.nav-item, .nav-sub-item'); if (!link) return; var page = link.getAttribute('data-page'); if (!page) return; e.preventDefault(); var href = link.getAttribute('href') || '/'; history.pushState({page: page}, '', href); loadPage(page); }); // 浏览器后退/前进 window.addEventListener('popstate', function(e) { var state = e.state; if (state && state.page) { loadPage(state.page); } else { // 首次加载或状态丢失,从当前 URL 推断 var path = window.location.pathname; var p = PAGE_MAP[path]; if (p) loadPage(p); } }); } // 延迟初始化,确保 DOM 加载完成后再绑定事件 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initNavigation); } else { initNavigation(); } // ─── 工具函数 ─── function escJs(str) { return String(str).replace(/'/g, "\\'").replace(/\\/g, '\\\\').replace(/"/g, '"'); } function escHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function inlineJsString(value) { return escHtml(JSON.stringify(value === null || value === undefined ? '' : String(value))); } function truncate(str, n) { if (!str) return ''; return str.length > n ? str.substring(0, n) + '...' : str; } // 等待Chart.js加载完成 function waitChartJS() { return new Promise(function(resolve) { if (typeof Chart !== 'undefined') { resolve(); return; } var check = setInterval(function() { if (typeof Chart !== 'undefined') { clearInterval(check); resolve(); } }, 100); setTimeout(function() { clearInterval(check); resolve(); }, 10000); }); } // ─── 测试问题(意见反馈)─── function loadFeedbackPage(container) { renderFeedbackPage(container); } function renderFeedbackPage(container) { container.innerHTML = `
全部
待处理
已完成
反馈列表 (0)
👈 点击左侧反馈查看详情
`; window._fbStatus = ''; window._fbPage = 1; window._fbSort = 'default'; window._fbCategory = ''; window._fbSelected = null; loadFbList(); } // ─── 反馈列表加载(旧版,由 feedback.js 新版覆盖) ─── function loadFbList() { if (typeof loadFbActiveList === 'function') { loadFbActiveList(); return; } var status = window._fbStatus || ''; var page = window._fbPage || 1; var search = (window._fbSearch || '').trim(); var sort = window._fbSort || 'default'; var category = window._fbCategory || ''; var url = '/api/feedback/list?tab=list&page=' + page + '&size=10&status=' + status + '&sort=' + sort; if (category) url += '&category=' + encodeURIComponent(category); if (search) url += '&search=' + encodeURIComponent(search); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; document.getElementById('fbTotal').textContent = data.total; var html = ''; if (data.rows.length === 0) { html = '
暂无反馈
'; } else { data.rows.forEach(function(item) { var statusClass = item.status === 'resolved' ? 'fb-badge-resolved' : 'fb-badge-pending'; var statusText = item.status === 'resolved' ? '已完成' : '待处理'; var priorityClass = item.priority === '紧急' ? 'fb-badge-urgent' : (item.priority === '高' ? 'fb-badge-high' : (item.priority === '低' ? 'fb-badge-low' : 'fb-badge-medium')); var selected = window._fbSelected && window._fbSelected.id === item.id ? ' selected' : ''; html += '
'; html += '
' + escHtml(item.title) + '
'; html += '
'; html += '' + statusText + ''; html += '' + escHtml(item.priority) + ''; html += '' + escHtml(item.category) + ''; html += '' + escHtml(item.user_name) + ''; html += '' + (item.created_at || '') + ''; html += '
'; }); } document.getElementById('fbItems').innerHTML = html; // 分页 var totalPages = Math.ceil(data.total / data.size); var pgHtml = ''; if (totalPages > 1) { pgHtml += ''; pgHtml += ''; var start = Math.max(1, page - 2); var end = Math.min(totalPages, page + 2); for (var i = start; i <= end; i++) { pgHtml += ''; } pgHtml += ''; pgHtml += ''; } document.getElementById('fbPagination').innerHTML = pgHtml; // 选中状态恢复 if (window._fbSelected) { var found = data.rows.some(function(r) { return r.id === window._fbSelected.id; }); if (!found) window._fbSelected = null; showFbDetail(); } }); } function fbGoPage(p) { window._fbPage = p; loadFbList(); } function switchFbTab(status) { window._fbStatus = status; window._fbPage = 1; window._fbSort = 'default'; window._fbCategory = ''; window._fbSelected = null; document.getElementById('fbSort').value = 'default'; document.getElementById('fbCategory').value = ''; document.getElementById('fbSearch').value = ''; window._fbSearch = ''; document.querySelectorAll('#fbTabs .fb-tab').forEach(function(t) { t.classList.remove('active'); }); document.querySelector('#fbTabs .fb-tab[data-status="' + status + '"]').classList.add('active'); document.getElementById('fbDetail').innerHTML = '
👈 点击左侧反馈查看详情
'; loadFbList(); } // ─── 选中反馈 ─── function selectFbItem(id) { fetch('/api/feedback/detail?id=' + id) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data) return; window._fbSelected = res.data; document.querySelectorAll('.fb-item').forEach(function(el) { el.classList.remove('selected'); }); var items = document.querySelectorAll('.fb-item'); for (var i = 0; i < items.length; i++) { if (items[i].querySelector('.fb-item-title').textContent === window._fbSelected.title) { items[i].classList.add('selected'); break; } } showFbDetail(); }); } function showFbDetail() { var item = window._fbSelected; if (!item) { document.getElementById('fbDetail').innerHTML = '
👈 点击左侧反馈查看详情
'; return; } var statusClass = item.status === 'resolved' ? 'fb-badge-resolved' : (item.status === 'rejected' ? 'fb-badge-pending' : 'fb-badge-pending'); var statusText = item.status === 'resolved' ? '✅ 已完成' : (item.status === 'rejected' ? '❌ 已驳回' : '⏳ 待处理'); var priorityClass = item.priority === '紧急' ? 'fb-badge-urgent' : (item.priority === '高' ? 'fb-badge-high' : (item.priority === '低' ? 'fb-badge-low' : 'fb-badge-medium')); var attachHtml = ''; var canDelete = item.user_id === (window._currentUser && window._currentUser.id) || (window._currentUser && (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin')); if (item.attachments && item.attachments.length > 0) { attachHtml = '
'; item.attachments.forEach(function(a) { var isImage = ['png','jpg','jpeg','gif','webp','bmp','svg'].indexOf(a.type) >= 0; var delBtn = canDelete ? '🗑️' : ''; if (isImage) { var imgSrc = a.path.startsWith('/static/') ? a.path : a.path.startsWith('/') ? '/static' + a.path : '/static/' + a.path; var fname = escHtml(a.name); attachHtml += '
' + delBtn + '
'; } else { var sizeStr = a.size > 1024*1024 ? (a.size/1024/1024).toFixed(1)+'MB' : (a.size/1024).toFixed(1)+'KB'; var fileHref = a.path.startsWith('/static/') ? a.path : a.path.startsWith('/') ? '/static' + a.path : '/static/' + a.path; attachHtml += '
' + delBtn + '📎 ' + escHtml(a.name) + ' (' + sizeStr + ')
'; } }); attachHtml += '
'; } var actionsHtml = ''; var isAdmin = window._currentUser && (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin'); var isSuperAdmin = window._currentUser && window._currentUser.role === 'super_admin'; // 反馈问题(tab=list):待处理=标记完成+删除,已完成=重新打开+删除 if (window._fbMainTab === 'list') { if (isAdmin) { if (item.status === 'active') { actionsHtml += ''; } else if (item.status === 'resolved') { actionsHtml += ''; } } if (isSuperAdmin) { actionsHtml += ''; } } var resolvedInfo = ''; if (item.status === 'resolved' && item.resolved_at) { resolvedInfo = '
✅ 已于 ' + escHtml(item.resolved_at) + ' 修改完成
'; } var rejectInfo = ''; if (item.status === 'rejected' && item.reject_reason) { rejectInfo = '
❌ 驳回理由:' + escHtml(item.reject_reason) + '
'; } document.getElementById('fbDetail').innerHTML = '
' + escHtml(item.title) + '
' + statusText + '' + escHtml(item.priority) + '' + escHtml(item.category) + '' + escHtml(item.user_name) + '' + (item.created_at || '') + '
' + escHtml(item.description || '暂无详细描述') + '
' + attachHtml + '
' + actionsHtml + '
' + resolvedInfo + '
'; } function previewFbImage(src) { var imgSrc = src.startsWith('/') && !src.startsWith('/static') ? '/static' + src : src; var overlay = document.createElement('div'); overlay.className = 'fb-preview-overlay'; overlay.innerHTML = ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); } // 反馈附件图片加载失败时替换为文件链接 function fbImgError(el) { var src = el.getAttribute('data-fb-img-src') || el.src; var name = el.getAttribute('data-fb-img-name') || '文件'; var link = document.createElement('a'); link.className = 'fb-attach-item fb-attach-broken'; link.href = src; link.target = '_blank'; link.title = '图片文件不存在'; link.textContent = '📎 ' + name + ' (文件缺失)'; el.parentNode.replaceChild(link, el); } // 附件删除确认弹窗 function fbConfirmDelAttach(id, filePath) { var overlay = document.createElement('div'); overlay.className = 'fb-form-overlay'; overlay.style.zIndex = '30000'; overlay.innerHTML = '
' + '

🗑️ 确认删除附件

' + '

确认删除此附件?
删除后不可恢复。

' + '
' + '' + '' + '
'; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); } function doFbDelAttach(id, filePath) { var overlays = document.querySelectorAll('.fb-form-overlay'); overlays.forEach(function(o) { o.remove(); }); fetch('/api/feedback/delete_attachment', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, path: filePath}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { // 刷新当前反馈详情 if (window._fbSelected && window._fbSelected.id === id) { fetch('/api/feedback/detail?id=' + id) .then(function(r) { return r.json(); }) .then(function(res2) { if (res2.code === 0 && res2.data) { window._fbSelected = res2.data; showFbDetail(); } }); } if (window._fbMainTab === 'review') loadFbReviewList(); else loadFbActiveList(); } else { alert(res.msg || '操作失败'); } }); } // ─── 新增/编辑反馈表单 ─── function showFbForm(item) { var isEdit = item && item.id; var overlay = document.createElement('div'); overlay.className = 'fb-form-overlay'; overlay.id = 'fbFormOverlay'; var titleText = isEdit ? '✏️ 编辑反馈' : '📝 提交反馈'; var btnText = isEdit ? '保存修改' : '提交反馈'; overlay.innerHTML = '

' + titleText + '

📎 点击选择文件,或将文件拖拽到此处
'; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); window._fbFiles = []; // 编辑模式:预填分类和紧急度 if (isEdit) { var catSelect = document.getElementById('fbFormCategory'); var priSelect = document.getElementById('fbFormPriority'); for (var i = 0; i < catSelect.options.length; i++) { if (catSelect.options[i].value === item.category) { catSelect.selectedIndex = i; break; } } for (var i = 0; i < priSelect.options.length; i++) { if (priSelect.options[i].value === item.priority) { priSelect.selectedIndex = i; break; } } // 显示已有附件 if (item.attachments && item.attachments.length > 0) { var existingHtml = ''; item.attachments.forEach(function(a) { existingHtml += '
📎 ' + escHtml(a.name) + '
'; }); document.getElementById('fbFileList').innerHTML = '
已有附件:
' + existingHtml; } } } function closeFbForm() { var overlay = document.getElementById('fbFormOverlay'); if (overlay) overlay.remove(); window._fbFiles = null; } var FB_FILE_ID = 0; function handleFbFileSelect(e) { var files = e.target.files; for (var i = 0; i < files.length; i++) { addFbFile(files[i]); } e.target.value = ''; } function handleFbDrop(e) { e.preventDefault(); document.getElementById('fbDropzone').classList.remove('dragover'); var files = e.dataTransfer.files; for (var i = 0; i < files.length; i++) { addFbFile(files[i]); } } function addFbFile(file) { if (!window._fbFiles) window._fbFiles = []; window._fbFiles.push(file); renderFbFileList(); } function removeFbFile(idx) { window._fbFiles.splice(idx, 1); renderFbFileList(); } function renderFbFileList() { var html = ''; if (window._fbFiles) { for (var i = 0; i < window._fbFiles.length; i++) { var f = window._fbFiles[i]; var isImage = f.type && f.type.startsWith('image/'); var preview = isImage ? '' : '📄'; html += '
' + preview + ' ' + escHtml(f.name) + ' ✕
'; } } document.getElementById('fbFileList').innerHTML = html; } function submitFbForm() { var title = document.getElementById('fbFormTitle').value.trim(); if (!title) { alert('请输入标题'); return; } var desc = document.getElementById('fbFormDesc').value.trim(); var category = document.getElementById('fbFormCategory').value; var priority = document.getElementById('fbFormPriority').value; var editId = document.getElementById('fbEditId') ? document.getElementById('fbEditId').textContent.trim() : ''; var formData = new FormData(); formData.append('title', title); formData.append('description', desc); formData.append('category', category); formData.append('priority', priority); if (editId) { formData.append('id', editId); } if (window._fbFiles) { for (var i = 0; i < window._fbFiles.length; i++) { formData.append('files', window._fbFiles[i]); } } var btn = document.querySelector('#fbFormOverlay .btn-primary'); btn.disabled = true; btn.textContent = '提交中...'; var url = editId ? '/api/feedback/update' : '/api/feedback/submit'; fetch(url, { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { closeFbForm(); window._fbSelected = null; if (typeof window._fbOnSave === 'function') { var cb = window._fbOnSave; window._fbOnSave = null; cb(); } else { loadFbList(); } } else { alert(res.msg || '提交失败'); btn.disabled = false; btn.textContent = editId ? '保存修改' : '提交反馈'; } }) .catch(function() { alert('提交失败,请重试'); btn.disabled = false; btn.textContent = editId ? '保存修改' : '提交反馈'; }); } // ─── 标记完成/重新打开 ─── function resolveFbItem(id) { showCenteredConfirm('确认', '确认将此反馈标记为已完成?').then(function(ok) { if (!ok) return; fetch('/api/feedback/resolve', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadFbList(); } else { alert(res.msg || '操作失败'); } }); }); } function unresolveFbItem(id) { showCenteredConfirm('确认', '确认重新打开此反馈?').then(function(ok) { if (!ok) return; fetch('/api/feedback/unresolve', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadFbList(); } else { alert(res.msg || '操作失败'); } }); }); } // ─── 删除反馈 ─── function deleteFbItem(id) { showCenteredConfirm('确认', '确认删除此反馈?删除后不可恢复。').then(function(ok) { if (!ok) return; fetch('/api/feedback/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { window._fbSelected = null; document.getElementById('fbDetail').innerHTML = '
👈 点击左侧反馈查看详情
'; loadFbList(); } else { alert(res.msg || '操作失败'); } }); }); } // ─── 编辑反馈 ─── function editFbItem(id) { fetch('/api/feedback/detail?id=' + id) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data) return; var item = res.data; showFbForm(item); }); } // ─── 合同模板 ─── var _ctTab = 'finished'; // 当前tab:finished=成品合同 packaging=包材合同 var _ctAllTemplates = []; // 全量模板缓存 function loadContractTemplatesPage(container) { container.innerHTML = `
`; loadCtTemplates(); } function switchCtTab(tab) { _ctTab = tab; var b1 = document.getElementById('ctTabFinished'); var b2 = document.getElementById('ctTabPackaging'); if (b1) b1.className = 'ct-tab' + (tab === 'finished' ? ' active' : ''); if (b2) b2.className = 'ct-tab' + (tab === 'packaging' ? ' active' : ''); renderCtList(); } function resetCtSearch() { var searchEl = document.getElementById('ctSearchInput'); if (searchEl) searchEl.value = ''; renderCtList(); } function loadCtTemplates() { fetch('/api/contract/templates') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; _ctAllTemplates = res.data || []; renderCtList(); }); } function renderCtList() { var list = document.getElementById('ctList'); if (!list) return; var searchEl = document.getElementById('ctSearchInput'); var q = (searchEl ? searchEl.value.trim().toLowerCase() : ''); var isPackaging = _ctTab === 'packaging'; var filtered = _ctAllTemplates.filter(function(t) { // tab 过滤:包材合同=packaging+all;成品合同=domestic+overseas+all var ot = t.order_type || 'all'; var inTab = isPackaging ? (ot === 'packaging' || ot === 'all') : (ot === 'domestic' || ot === 'overseas' || ot === 'all'); if (!inTab) return false; // 搜索:供应商名/模板名称/文件名 模糊匹配 if (q) { var haystack = ((t.bound_supplier_name || '') + ' ' + (t.name || '') + ' ' + (t.file_name || '')).toLowerCase(); if (haystack.indexOf(q) < 0) return false; } return true; }); if (filtered.length === 0) { list.innerHTML = '
' + (_ctAllTemplates.length === 0 ? '暂无模板,请点击右上角上传' : '当前分类下没有匹配的模板') + '
'; return; } var html = ''; filtered.forEach(function(t) { var typeMap = {'domestic':'国内订单','overseas':'海外订单','packaging':'包材订单','all':'全部类型'}; var boundTag = ''; if (t.bound_supplier_name) { boundTag = '🔗 ' + escHtml(t.bound_supplier_name) + ''; } else { boundTag = '通用'; } html += '
'; html += '
' + escHtml(t.name) + boundTag + '
'; html += '
' + (typeMap[t.order_type] || t.order_type) + ' | 文件: ' + escHtml(t.file_name || '') + ' | ' + (t.created_at || '') + '
'; html += '
'; html += '
'; }); list.innerHTML = html; } function showCtUploadForm() { var overlay = document.createElement('div'); overlay.className = 'ct-form-overlay'; overlay.id = 'ctFormOverlay'; overlay.innerHTML = '

📤 上传合同模板

' + '' + '
' + '
'; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); // 按当前tab预选适用类型(成品tab默认国内订单,包材tab默认包材订单) var typeSel = document.getElementById('ctFormType'); if (typeSel) { typeSel.value = (_ctTab === 'packaging') ? 'packaging' : 'domestic'; } // 供应商搜索绑定 document.getElementById('ctSupplierSearch').addEventListener('input', function() { var q = this.value.trim(); var suggestions = document.getElementById('ctSupplierSuggestions'); if (q.length < 1) { suggestions.style.display = 'none'; return; } fetch('/api/suppliers/list?search=' + encodeURIComponent(q) + '&size=20') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data || !res.data.rows || res.data.rows.length === 0) { suggestions.innerHTML = '
无匹配供应商
'; suggestions.style.display = 'block'; return; } var h = ''; res.data.rows.forEach(function(s) { h += '
' + escHtml(s.supplier_name) + ' ' + escHtml(s.supplier_code || '') + '
'; }); suggestions.innerHTML = h; suggestions.style.display = 'block'; }); }); } function toggleCtUploadSupplier(val) { var area = document.getElementById('ctUploadSupplierArea'); if (area) { area.style.display = (val === 'bound') ? '' : 'none'; } } function selectCtSupplier(el) { document.getElementById('ctSupplierId').value = el.getAttribute('data-id'); document.getElementById('ctSupplierSearch').value = el.getAttribute('data-name'); document.getElementById('ctSupplierSuggestions').style.display = 'none'; document.getElementById('ctSupplierTag').innerHTML = '📌 绑定:' + escHtml(el.getAttribute('data-name')) + ''; } function clearCtSupplierBind() { document.getElementById('ctSupplierId').value = ''; document.getElementById('ctSupplierSearch').value = ''; document.getElementById('ctSupplierSuggestions').style.display = 'none'; document.getElementById('ctSupplierTag').innerHTML = ''; } function closeCtForm() { var el = document.getElementById('ctFormOverlay'); if (el) el.remove(); } function submitCtUpload() { var name = document.getElementById('ctFormName').value.trim(); var type = document.getElementById('ctFormType').value; var attr = document.getElementById('ctFormAttr').value; var supplierId = document.getElementById('ctSupplierId').value; var fileInput = document.getElementById('ctFormFile'); if (!name) { alert('请输入模板名称'); return; } if (attr === 'bound' && !supplierId) { alert('请选择要绑定的供应商'); return; } if (!fileInput.files || !fileInput.files[0]) { alert('请选择文件'); return; } var formData = new FormData(); formData.append('name', name); formData.append('order_type', type); formData.append('supplier_id', attr === 'bound' ? supplierId : ''); formData.append('file', fileInput.files[0]); var btn = document.querySelector('#ctFormOverlay .btn-primary'); btn.disabled = true; btn.textContent = '上传中...'; fetch('/api/contract/template/upload', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { closeCtForm(); loadCtTemplates(); } else { alert(res.msg || '上传失败'); btn.disabled = false; btn.textContent = '上传'; } }); } function deleteCtTemplate(id) { showCenteredConfirm('确认', '确认删除此模板?').then(function(ok) { if (!ok) return; fetch('/api/contract/template/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) loadCtTemplates(); else alert(res.msg || '操作失败'); }); }); } function downloadContractFile(url, fileName) { fetch(url) .then(function(r) { if (!r.ok) { alert('下载失败'); return; } return r.blob(); }) .then(function(blob) { if (!blob) return; var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); }) .catch(function() { // fallback: 直接打开 window.location.href = url; }); } function previewCtTemplate(id) { // 在新标签页中打开预览页面(用隐藏链接避免浏览器弹窗拦截提示) safeOpenUrl('/api/contract/template/preview_page?id=' + id); } function editCtTemplate(id) { // 获取模板数据 fetch('/api/contract/templates') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var t = null; for (var i = 0; i < res.data.length; i++) { if (res.data[i].id === id) { t = res.data[i]; break; } } if (!t) { alert('模板不存在'); return; } showCtEditDialog(t); }); } function showCtEditDialog(t) { var overlay = document.createElement('div'); overlay.className = 'ct-form-overlay'; overlay.id = 'ctEditOverlay'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var typeOptions = ['all','domestic','overseas','packaging']; var typeLabels = {'all':'全部类型','domestic':'国内订单','overseas':'海外订单','packaging':'包材订单'}; var typeSel = ''; typeOptions.forEach(function(v) { var sel = (v === t.order_type) ? ' selected' : ''; typeSel += ''; }); var isBound = t.supplier_id ? true : false; var boundSupplierName = t.bound_supplier_name || ''; var boundSupplierId = t.supplier_id || ''; overlay.innerHTML = '

✏️ 编辑合同模板

' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '' + '
' + (isBound ? '📌 绑定:' + escHtml(boundSupplierName) + '' : '') + '
' + '
' + '
当前文件:' + escHtml(t.file_name || '') + '
' + '
' + '
'; document.body.appendChild(overlay); // 供应商搜索绑定 document.getElementById('ctEditSupplierSearch').addEventListener('input', function() { var q = this.value.trim(); var suggestions = document.getElementById('ctEditSupplierSuggestions'); if (q.length < 1) { suggestions.style.display = 'none'; return; } fetch('/api/suppliers/list?search=' + encodeURIComponent(q) + '&size=20') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data || !res.data.rows || res.data.rows.length === 0) { suggestions.innerHTML = '
无匹配供应商
'; suggestions.style.display = 'block'; return; } var h = ''; res.data.rows.forEach(function(s) { h += '
' + escHtml(s.supplier_name) + ' ' + escHtml(s.supplier_code || '') + '
'; }); suggestions.innerHTML = h; suggestions.style.display = 'block'; }); }); } function toggleCtEditSupplier(val) { var area = document.getElementById('ctEditSupplierArea'); if (area) { area.style.display = (val === 'bound') ? '' : 'none'; } } function selectCtEditSupplier(el) { document.getElementById('ctEditSupplierId').value = el.getAttribute('data-id'); document.getElementById('ctEditSupplierSearch').value = el.getAttribute('data-name'); document.getElementById('ctEditSupplierSuggestions').style.display = 'none'; document.getElementById('ctEditSupplierTag').innerHTML = '📌 绑定:' + escHtml(el.getAttribute('data-name')) + ''; } function clearCtEditSupplierBind() { document.getElementById('ctEditSupplierId').value = ''; document.getElementById('ctEditSupplierSearch').value = ''; document.getElementById('ctEditSupplierSuggestions').style.display = 'none'; document.getElementById('ctEditSupplierTag').innerHTML = ''; } function closeCtEdit() { var el = document.getElementById('ctEditOverlay'); if (el) el.remove(); } function submitCtEdit(id) { var name = document.getElementById('ctEditName').value.trim(); var type = document.getElementById('ctEditType').value; var attr = document.getElementById('ctEditAttr').value; var supplierId = document.getElementById('ctEditSupplierId').value; var fileInput = document.getElementById('ctEditFile'); if (!name) { alert('请输入模板名称'); return; } if (attr === 'bound' && !supplierId) { alert('请选择要绑定的供应商'); return; } var formData = new FormData(); formData.append('id', id); formData.append('name', name); formData.append('order_type', type); formData.append('supplier_id', attr === 'bound' ? supplierId : ''); if (fileInput.files && fileInput.files[0]) { formData.append('file', fileInput.files[0]); } var btn = document.querySelector('#ctEditOverlay .btn-primary'); btn.disabled = true; btn.textContent = '保存中...'; fetch('/api/contract/template/update', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { closeCtEdit(); loadCtTemplates(); } else { alert(res.msg || '保存失败'); btn.disabled = false; btn.textContent = '保存'; } }); } // ─── 合同生成功能 ─── var _contractGenDialogOpen = false; function generateContractForOrders(orderType, orderIds) { if (_contractGenDialogOpen) return; _contractGenDialogOpen = true; var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:15000;display:flex;align-items:center;justify-content:center'; overlay.id = 'contractGenOverlay'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var box = document.createElement('div'); box.style.cssText = 'background:#fff;border-radius:12px;padding:24px;width:760px;max-width:94vw;box-shadow:0 8px 32px rgba(0,0,0,0.18)'; box.onclick = function(e) { e.stopPropagation(); }; var h2 = document.createElement('h2'); h2.textContent = '📄 生成合同'; h2.style.cssText = 'margin:0 0 4px 0;font-size:18px'; box.appendChild(h2); var typeMap = {'domestic':'国内订单','overseas':'海外订单','packaging':'包材订单'}; var infoLine = document.createElement('p'); infoLine.style.cssText = 'margin:0 0 16px 0;font-size:13px;color:#666'; infoLine.textContent = (typeMap[orderType] || orderType) + ' | 已选 ' + orderIds.length + ' 条订单'; box.appendChild(infoLine); // 加载中 var statusDiv = document.createElement('div'); statusDiv.id = 'contractGenStatus'; statusDiv.innerHTML = '
⏳ 正在分析供应商 & 模板匹配…
'; box.appendChild(statusDiv); // 已生成合同结果区(初始隐藏) var resultDiv = document.createElement('div'); resultDiv.id = 'contractGenResult'; resultDiv.style.display = 'none'; box.appendChild(resultDiv); var ba = document.createElement('div'); ba.id = 'contractGenActions'; ba.style.cssText = 'display:flex;justify-content:flex-end;gap:10px;margin-top:16px'; var cancelBtn = document.createElement('button'); cancelBtn.className = 'btn btn-outline'; cancelBtn.textContent = '取消'; cancelBtn.onclick = function() { _contractGenDialogOpen = false; var el = document.getElementById('contractGenOverlay'); if (el) el.remove(); }; ba.appendChild(cancelBtn); box.appendChild(ba); overlay.appendChild(box); document.body.appendChild(overlay); // 订单明细容器 var itemsDiv = document.createElement('div'); itemsDiv.id = 'contractGenItems'; itemsDiv.innerHTML = '
⏳ 加载订单明细…
'; box.insertBefore(itemsDiv, statusDiv); // 加载订单明细 fetch('/api/domestic/orders/by_ids', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ids: orderIds, order_type: orderType}) }).then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data && res.data.length > 0) { var ih = '
📋 合同基础信息
'; ih += '
'; ih += ''; ih += ''; var totalAmt = 0; res.data.forEach(function(item) { var qty = item['数量'] || 0; var price = item['单价'] || 0; var amt = qty * price; totalAmt += amt; ih += ''; ih += ''; ih += ''; ih += ''; ih += ''; }); ih += ''; ih += '
货品编码货品名称数量单价金额
' + escHtml(item['编码'] || '') + '' + escHtml(item['名称'] || '') + '' + qty + '' + (price.toFixed(2)) + '¥' + (amt.toFixed(2)) + '
合计¥' + (totalAmt.toFixed(2)) + '
'; itemsDiv.innerHTML = ih; } else { itemsDiv.innerHTML = ''; } }).catch(function(err) { itemsDiv.innerHTML = '
加载明细失败
'; }); // 加载供应商分组 + 模板匹配 fetch('/api/contract/supplier_groups', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({order_ids: orderIds, order_type: orderType}) }).then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data || res.data.length === 0) { statusDiv.innerHTML = '
❌ 获取分组信息失败
'; return; } var groups = res.data; var hasMissing = false; var h = '
📋 模板匹配
'; h += ''; h += ''; groups.forEach(function(g) { var templateLabel = ''; if (g.template_type === 'bound') { templateLabel = '🔗 ' + escHtml(g.bound_template.name) + ''; } else if (g.template_type === 'universal') { templateLabel = '📄 ' + escHtml(g.universal_template_name) + '(通用)'; } else { templateLabel = '❌ 无可用模板'; hasMissing = true; } h += ''; h += ''; h += ''; }); h += '
供应商产品数使用模板
' + escHtml(g.supplier) + '' + g.count + '' + templateLabel + '
'; if (hasMissing) { h += '
⚠️ 部分供应商无可用模板,请先上传对应模板
'; } statusDiv.innerHTML = h; // 生成合同按钮 if (!hasMissing) { var genBtn = document.createElement('button'); genBtn.id = 'contractGenBtn'; genBtn.className = 'btn btn-primary'; genBtn.textContent = '✅ 生成合同'; genBtn.style.marginLeft = '10px'; genBtn.onclick = function() { genBtn.disabled = true; genBtn.textContent = '⏳ 生成中…'; doGenerateContract(orderType, orderIds, overlay); }; ba.insertBefore(genBtn, cancelBtn); } }).catch(function(err) { statusDiv.innerHTML = '
❌ 加载模板匹配失败
'; }); } function doGenerateContract(orderType, orderIds, overlay) { fetch('/api/contract/generate', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({order_ids: orderIds, order_type: orderType}) }).then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert(res.msg || '生成失败'); _contractGenDialogOpen = false; var el = document.getElementById('contractGenOverlay'); if (el) el.remove(); return; } // 在当前弹窗下方追加已生成的合同 var resultDiv = document.getElementById('contractGenResult'); if (!resultDiv) return; resultDiv.style.display = ''; var th = '
📋 已生成的合同
'; th += ''; th += ''; (res.data || []).forEach(function(g) { var totalAmt = g.total_amount || 0; var amtStr = '¥' + (typeof totalAmt === 'number' ? totalAmt.toFixed(2) : parseFloat(totalAmt).toFixed(2)); var timeStr = g.generated_at || ''; var fileBtn = g.file_path ? '' : '失败'; th += ''; th += ''; th += ''; th += ''; th += ''; }); th += '
供应商合同编号金额生成时间文件
' + escHtml(g.supplier) + '' + escHtml(g.contract_no || '') + '' + amtStr + '' + timeStr + '' + fileBtn + '
'; resultDiv.innerHTML = th; // 移除生成合同按钮,取消→关闭 var genBtn = document.getElementById('contractGenBtn'); if (genBtn) genBtn.style.display = 'none'; var actions = document.getElementById('contractGenActions'); if (actions) { var btns = actions.querySelectorAll('button'); btns.forEach(function(b) { if (b.textContent === '取消' || b.textContent === '关闭') { b.textContent = '关闭'; } }); } }).catch(function() { alert('❌ 网络错误,生成合同失败'); _contractGenDialogOpen = false; var el = document.getElementById('contractGenOverlay'); if (el) el.remove(); }); } // ─── 呆滞SKU清单页面 ─── var slowSkuPage = 1, slowSkuSearch = '', slowSkuBrand = '', slowSkuPlatform = ''; function loadSlowMovingSkuPage(container) { container.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;'; container.innerHTML = '
'; slowSkuLoad(); } function slowSkuLoad() { var pane=document.getElementById('slowSkuPane'); if(!pane)return; pane.innerHTML='
'; var url='/api/slow_moving_sku/list?page='+slowSkuPage+'&size=50&search='+encodeURIComponent(slowSkuSearch)+'&brand='+encodeURIComponent(slowSkuBrand)+'&platform='+encodeURIComponent(slowSkuPlatform); fetch(url,{headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res){ if(res.code!==0){pane.innerHTML='
加载失败:'+escHtml(res.msg||'未知错误')+'
';return;} var a=document.getElementById('slowSkuInvDate'),b=document.getElementById('slowSkuSalesDate'); if(a)a.textContent=res.latest_inventory_date||'-';if(b)b.textContent=res.latest_sales_date||'-'; slowSkuRender(pane,res.data||[],res.total||0,res.brands||[],res.platforms||[]); }).catch(function(e){pane.innerHTML='
请求异常:'+escHtml(String(e))+'
';}); } function slowSkuRender(pane,rows,total,brands,platforms){ var br=''; var pf=''; var cols=['品牌','货品编号','货品名称','库存','昨日销量','7天销量','30天销量','总销量','可销天数','预测日销量','库存支撑天数','总订单数量','采购在途','生产在途','成本价','成品库存金额','单品创建时间','新老品','确权平台']; var head=cols.map(function(c){return ''+c+'';}).join(''); var body=rows.map(function(r){return ''+cols.map(function(c){var v=r[c];if(v===null||v===undefined||v==='')v='-';return ''+escHtml(String(v))+'';}).join('')+'';}).join(''); if(!body)body='暂无符合条件的呆滞SKU'; var pages=Math.max(1,Math.ceil(total/50));var pg='
第 '+slowSkuPage+' / '+pages+' 页,共 '+total+' 条
'; pane.innerHTML='

呆滞SKU清单列表

共 '+total+' 条
'+br+pf+'
'+head+''+body+'
'+pg+'
'; } // ─── SKU库存健康指数页面 ─── var skuHealthPage = 1; var skuHealthSearch = ''; var skuHealthCategory = ''; var skuHealthBrand = ''; var skuHealthStatus = ''; function loadSkuHealthIndexPage(container) { container.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;'; container.innerHTML = '
' + '' + '
'; skuHealthLoad(); } function skuHealthLoad() { var pane = document.getElementById('skuHealthPane'); if (!pane) return; pane.innerHTML = '
'; var url = '/api/sku_health_index/list?page=' + skuHealthPage + '&size=50' + '&search=' + encodeURIComponent(skuHealthSearch) + '&category=' + encodeURIComponent(skuHealthCategory) + '&brand=' + encodeURIComponent(skuHealthBrand) + '&status=' + encodeURIComponent(skuHealthStatus); fetch(url, {headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res){ if (res.code !== 0) { pane.innerHTML='
加载失败:'+escHtml(res.msg || '未知错误')+'
'; return; } var d=res.data||[]; var inv=document.getElementById('skuHealthInvDate'), sales=document.getElementById('skuHealthSalesDate'); if(inv) inv.textContent=res.latest_inventory_date||'-'; if(sales) sales.textContent=res.latest_sales_date||'-'; skuHealthRender(pane,d,res.total||0,res.categories||[],res.brands||[]); }).catch(function(e){pane.innerHTML='
请求异常:'+escHtml(String(e))+'
';}); } function skuHealthFmt(v) { return v === null || v === undefined || v === '' ? '-' : String(v); } function skuHealthRender(pane, rows, total, categories, brands) { var cat=''; var br=''; var st=''; var cols=['SKU分类','品牌','货品编号','货品名称','采购到货天数','可用库存','昨日销量','7天销量','30天销量','采购在途','预测日销量','当月需求预测汇总','昨日可销天数','7日可销天数','30日可销天数','库存可支撑天数','库存健康指数','库存是否满足','处理计划','新老品','单品创建时间','库龄']; var head=cols.map(function(c){return ''+c+'';}).join(''); var body=rows.map(function(r){ var health=Number(r['库存健康指数']); var color=health<1?'#f5222d':(health<1.5?'#fa8c16':'#52c41a'); return ''+cols.map(function(c){var v=skuHealthFmt(r[c]); if(c==='库存健康指数') v=''+v+''; if(c==='库存是否满足') v=''+v+''; return ''+(c==='处理计划'?escHtml(v):v)+'';}).join('')+''; }).join(''); if(!body) body='暂无数据'; var pages=Math.max(1,Math.ceil(total/50)); var pg='
'; pg+='第 '+skuHealthPage+' / '+pages+' 页,共 '+total+' 条
'; pane.innerHTML='

SKU库存健康指数列表

共 '+total+' 条
'+cat+br+st+'
'+head+''+body+'
'+pg+'
'; } // ─── 重点SKU预测页面 ─── // SKU基础信息列表(含可编辑字段:运营负责人/产品类型/链接) let keySkuBasicPage = 1; let keySkuBasicSearch = ''; let keySkuBasicGrade = ''; let keySkuBasicType = ''; function loadKeySkuForecastPage(container) { container.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;'; container.innerHTML = `
📋 SKU基础信息
📊 历史销量参考
`; ksfLoadBasic(); } function ksfSwitchTab(tab) { document.getElementById('ksfTabBasic').classList.toggle('active', tab === 'basic'); document.getElementById('ksfTabSales').classList.toggle('active', tab === 'sales'); document.getElementById('ksfPaneBasic').style.display = tab === 'basic' ? 'flex' : 'none'; document.getElementById('ksfPaneSales').style.display = tab === 'sales' ? 'flex' : 'none'; if (tab === 'sales') ksfLoadSales(); } function ksfRenderTabs() { var b = document.getElementById('ksfTabBasic'); var s = document.getElementById('ksfTabSales'); if (!b || !s) return; b.classList.add('active'); s.classList.remove('active'); } // ─── SKU基础信息列表 ─── function isKsfBasicActionsCollapsed() { return localStorage.getItem('keySkuForecastBasicActionsCollapsed') === 'true'; } function toggleKsfBasicActions() { localStorage.setItem('keySkuForecastBasicActionsCollapsed', String(!isKsfBasicActionsCollapsed())); var table = document.getElementById('ksfBasicTable'); if (table) ksfApplyBasicActionsState(table); } function ksfApplyBasicActionsState(table) { var collapsed = isKsfBasicActionsCollapsed(); var width = collapsed ? 34 : 76; table.classList.toggle('ksf-actions-collapsed', collapsed); var head = table.querySelector('thead th:last-child'); if (head) { head.style.width = width + 'px'; head.style.minWidth = width + 'px'; var title = head.querySelector('.ksf-action-title'); var toggle = head.querySelector('.ksf-action-toggle'); if (title) title.style.display = collapsed ? 'none' : 'inline'; if (toggle) { toggle.textContent = collapsed ? '«' : '»'; toggle.title = collapsed ? '展开操作列' : '折叠操作列'; } } table.querySelectorAll('tbody td:last-child').forEach(function(cell) { cell.style.width = width + 'px'; cell.style.minWidth = width + 'px'; cell.style.maxWidth = width + 'px'; var button = cell.querySelector('button'); if (button) button.style.display = collapsed ? 'none' : 'inline-block'; }); } function ksfLoadBasic() { var pane = document.getElementById('ksfPaneBasic'); if (!pane) return; pane.innerHTML = '
'; var url = '/api/sku_forecast/basic_list?page=' + keySkuBasicPage + '&size=50' + '&search=' + encodeURIComponent(keySkuBasicSearch) + '&grade=' + encodeURIComponent(keySkuBasicGrade) + '&sku_type=' + encodeURIComponent(keySkuBasicType); fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()) .then(res => { if (res.code !== 0) { pane.innerHTML = '
加载失败:' + (res.message || '未知错误') + '
'; return; } ksfRenderBasic(pane, res.data || [], res.total || 0); }) .catch(function(e) { pane.innerHTML = '
请求异常:' + e + '
'; }); } function ksfRenderBasic(pane, rows, total) { window._ksfBasicRows = rows || []; var totalPages = Math.max(1, Math.ceil(total / 50)); var gradeOptions = ['S级', 'A级', 'B级', 'C级', '不参与评级']; var typeOptions = ['爆品', '日销', '新品']; var gradeFilterHtml = ''; var typeFilterHtml = ''; pane.innerHTML = `

SKU基础信息列表

共 ${total} 条
${gradeFilterHtml}${typeFilterHtml}
${ksfBasicRowsHtml(rows)}
序号 货品编号 货品名称 产品分级 链接 产品ID 运营负责人 产品类型 操作
`; ksfApplyBasicActionsState(document.getElementById('ksfBasicTable')); } function ksfBasicRowsHtml(rows) { if (!rows || !rows.length) { return '暂无数据'; } return rows.map(function(r, i) { var rowNo = (keySkuBasicPage - 1) * 50 + i + 1; var gradeColor = { 'S级': '#f5222d', 'A级': '#fa8c16', 'B级': '#1890ff', 'C级': '#52c41a' }[r['产品分级']] || '#999'; var gradeHtml = '' + escHtml(r['产品分级'] || '-') + ''; var typeHtml = r['产品类型'] ? '' + escHtml(r['产品类型']) + '' : '—'; var linkHtml = r['链接'] ? '🔗 打开' : '—'; var code = String(r['货品编号'] || ''); return '' + '' + rowNo + '' + '' + escHtml(r['货品编号'] || '-') + '' + '' + escHtml(r['货品名称'] || '-') + '' + '' + gradeHtml + '' + '' + linkHtml + '' + '' + escHtml(r['产品ID'] != null ? r['产品ID'] : '-') + '' + '' + escHtml(r['运营负责人'] || '') + '' + '' + typeHtml + '' + '' + ''; }).join(''); } function ksfPaginationHtml(totalPages, curPage, fn) { if (totalPages <= 1) return ''; var html = ''; for (var i = 1; i <= totalPages; i++) { if (totalPages > 9 && i > 3 && i < totalPages - 2) { if (i === 4) html += '…'; continue; } html += ''; } return html; } function ksfBasicPage(p) { keySkuBasicPage = p; ksfLoadBasic(); } function ksfOpenEdit(code) { // 从当前行数据反查(列表已带完整行数据缓存) var rows = window._ksfBasicRows || []; var row = null; for (var i = 0; i < rows.length; i++) { if (String(rows[i]['货品编号']) === String(code)) { row = rows[i]; break; } } if (!row) return; ksfOpenEditModal(row); } // 编辑弹窗(仅可编辑:运营负责人/产品类型/链接) function ksfOpenEditModal(row) { if (document.getElementById('ksfEditOverlay')) document.getElementById('ksfEditOverlay').remove(); var overlay = document.createElement('div'); overlay.id = 'ksfEditOverlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:9999;display:flex;align-items:center;justify-content:center;'; overlay.onclick = function(e) { if (e.target === overlay) { /* 只能通过按钮关闭 */ } }; overlay.innerHTML = `
货品编号
${escHtml(row['货品编号'] || '')}
货品名称
${escHtml(row['货品名称'] || '')}
运营负责人
产品类型(爆品/日销/新品)
链接
`; document.body.appendChild(overlay); } function ksfSaveEdit(code) { var operator = document.getElementById('ksfEditOperator').value; var type = document.getElementById('ksfEditType').value; var link = document.getElementById('ksfEditLink').value; var msgEl = document.getElementById('ksfEditMsg'); var done = 0, failed = 0, total = 0; var tasks = [ { field: '运营负责人', value: operator }, { field: '产品类型', value: type }, { field: '链接', value: link } ]; total = tasks.length; tasks.forEach(function(t) { fetch('/api/sku_forecast/update_field', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product_code: code, field: t.field, value: t.value }) }) .then(r => r.json()) .then(res => { if (res.code === 0) { done++; } else { failed++; } if (done + failed === total) { if (failed === 0) { msgEl.style.color = '#52c41a'; msgEl.textContent = '保存成功'; setTimeout(function() { var ov = document.getElementById('ksfEditOverlay'); if (ov) ov.remove(); ksfLoadBasic(); }, 500); } else { msgEl.style.color = '#f5222d'; msgEl.textContent = failed + ' 项保存失败,请重试'; } } }) .catch(function() { failed++; if (done + failed === total) { msgEl.style.color = '#f5222d'; msgEl.textContent = '网络异常,保存失败'; } }); }); } // ─── 历史销量参考列表 ─── let keySkuSalesPage = 1; let keySkuSalesSearch = ''; let keySkuSalesLoaded = false; function ksfLoadSales() { var pane = document.getElementById('ksfPaneSales'); if (!pane) return; keySkuSalesLoaded = true; pane.innerHTML = '
'; var url = '/api/sku_forecast/sales_list?page=' + keySkuSalesPage + '&size=50' + '&search=' + encodeURIComponent(keySkuSalesSearch); fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()) .then(res => { if (res.code !== 0) { pane.innerHTML = '
加载失败:' + (res.message || '未知错误') + '
'; return; } ksfRenderSales(pane, res.data || [], res.total || 0); }) .catch(function(e) { pane.innerHTML = '
请求异常:' + e + '
'; }); } function ksfRenderSales(pane, rows, total) { var totalPages = Math.max(1, Math.ceil(total / 50)); pane.innerHTML = `

历史销量参考列表

共 ${total} 条
${ksfSalesRowsHtml(rows)}
序号 货品编号 货品名称 昨天销量 上月销量 本月销量 7日均销 15天均销
`; } function ksfSalesRowsHtml(rows) { if (!rows || !rows.length) { return '暂无数据'; } return rows.map(function(r, i) { var rowNo = (keySkuSalesPage - 1) * 50 + i + 1; return '' + '' + rowNo + '' + '' + escHtml(r['货品编号'] || '-') + '' + '' + escHtml(r['货品名称'] || '-') + '' + '' + Number(r['昨天销量'] || 0).toLocaleString('zh-CN') + '' + '' + Number(r['上月销量'] || 0).toLocaleString('zh-CN') + '' + '' + Number(r['本月销量'] || 0).toLocaleString('zh-CN') + '' + '' + Number(r['7日均销'] || 0).toLocaleString('zh-CN') + '' + '' + Number(r['15天均销'] || 0).toLocaleString('zh-CN') + '' + ''; }).join(''); } function ksfSalesPage(p) { keySkuSalesPage = p; ksfLoadSales(); } function loadInventoryBoardPage(container) { // 页面高度占满视口:container 作为 main-content(flex column) 子项 flex:1 撑满, // 内部 flex 列布局,四个板块从上往下依次排列(库存金额→经营效率→呆滞治理→缺货监控),内容超高时滚动 container.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0;'; container.innerHTML = `
`; var monthInput = document.getElementById('invBoardMonth'); if (monthInput) { monthInput.value = '2026-08'; monthInput.onchange = function() { reloadInventoryBoard(false); }; } fetch('/api/dashboard/inventory-board?month=' + encodeURIComponent(monthInput ? monthInput.value : ''), { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()) .then(res => { if (res.code !== 0) { document.getElementById('invBoardContent').innerHTML = '
加载失败:' + (res.message || '未知错误') + '
'; return; } renderInventoryBoard(res.data); }) .catch(function(e) { document.getElementById('invBoardContent').innerHTML = '
请求异常:' + e + '
'; }); } function inventoryBoardMonth() { var el = document.getElementById('invBoardMonth'); return (el && el.value) || new Date().toISOString().slice(0, 7); } function reloadInventoryBoard(force) { var month = inventoryBoardMonth(); var url = force ? '/api/dashboard/inventory-board/recalculate' : '/api/dashboard/inventory-board?month=' + encodeURIComponent(month); var options = {headers:{'X-Requested-With':'XMLHttpRequest'}}; if (force) { options.method = 'POST'; options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify({month:month}); } var btns = document.querySelectorAll('#invBoardContent button'); btns.forEach(function(b){b.disabled=true;}); if (!force) document.getElementById('invBoardContent').style.opacity = '0.55'; fetch(url, options).then(function(r){return r.json();}).then(function(res){ if (res.code === 0) renderInventoryBoard(res.data); else alert(res.msg || '加载失败'); }).catch(function(e){alert('请求异常:' + e);}).finally(function(){btns.forEach(function(b){b.disabled=false;}); document.getElementById('invBoardContent').style.opacity = '1';}); } function inventoryManualMonthOptions() { var options = ''; for (var year = 2025; year <= 2026; year++) { var startMonth = year === 2025 ? 1 : 1; var endMonth = year === 2026 ? 5 : 12; for (var month = startMonth; month <= endMonth; month++) { var value = year + '-' + String(month).padStart(2, '0'); options += ''; } } return options; } function openInventoryManualModal() { var groups = [ ['库存金额',[['all_amount','全部仓库成品'],['wh01_amount','蝶雅仓成品'],['wh15_amount','加工厂仓成品'],['cloud_amount','云仓成品'],['overseas_amount','海外仓成品'],['domestic_self_amount','国内平台自营仓成品']]], ['经营效率',[['inventory_total_amount','库存总金额'],['monthly_ship_cost','月销货成本'],['monthly_return_cost','月退货成本'],['monthly_refund_amount','月退款金额'],['return_rate','退货率'],['monthly_purchase_inbound','月采购入库'],['turnover_rate','月库存周转率'],['turnover_days','库存周转天数'],['active_sku','动销SKU数量(大于30)'],['active_sku_100','动销SKU数量(>100天)'],['total_sku','总SKU数量(库存>=1)'],['total_sku_200','总SKU数量(库存>=200)'],['active_rate','动销率(>30)'],['active_rate_100','动销率(>100)']]], ['呆滞治理',[['slow_amount','呆滞库存金额'],['slow_ratio','呆滞库存比例']]], ['缺货监控',[['stockout_sku_domestic','缺货SKU次数(国内)'],['stockout_sku_snowwolf','缺货SKU次数(国际-雪狼)'],['stockout_sku_lingdong','缺货SKU次数(国际-灵动)'],['stockout_sku_chunyue','缺货SKU次数(国际-春跃)'],['stockout_rate','缺货率'],['top100_stockout_rate','销量TOP100缺货率'],['new_stockout_rate','新品缺货率'],['old_stockout_rate','老品缺货率']]] ]; var moneyKeys = ['all_amount','wh01_amount','wh15_amount','cloud_amount','overseas_amount','domestic_self_amount','inventory_total_amount','monthly_ship_cost','monthly_return_cost','monthly_refund_amount','slow_amount']; var percentKeys = ['return_rate','active_rate','active_rate_100','slow_ratio','stockout_rate','top100_stockout_rate','new_stockout_rate','old_stockout_rate']; var html = ''; document.body.insertAdjacentHTML('beforeend', html); // 人工新建弹窗只初始化空白表单,不读取当前看板或已有月份数据。 } function loadInventoryManualValues(month) { var moneyKeys = ['all_amount','wh01_amount','wh15_amount','cloud_amount','overseas_amount','domestic_self_amount','inventory_total_amount','monthly_ship_cost','monthly_return_cost','monthly_refund_amount','slow_amount']; fetch('/api/dashboard/inventory-board?month=' + encodeURIComponent(month || ''), { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(function(r){ return r.json(); }) .then(function(res){ if (res.code !== 0 || !document.getElementById('invManualModal')) return; var d = res.data || {}; var elMonth = document.getElementById('invManualMonth'); if (elMonth && d.month) elMonth.value = d.month; document.querySelectorAll('[data-inv-manual]').forEach(function(el){ var v = d[el.dataset.invManual]; if (moneyKeys.indexOf(el.dataset.invManual) >= 0 && v != null) v = v / 10000; el.value = (v === null || v === undefined || v === '') ? '' : v; }); }) .catch(function(e){ /* 加载失败保持空白,不阻断录入 */ }); } function saveInventoryManual() { var monthEl = document.getElementById('invManualMonth'); var month = monthEl ? monthEl.value : inventoryBoardMonth(); if (!month) { alert('请选择月份'); return; } var values = {}; var moneyKeys = ['all_amount','wh01_amount','wh15_amount','cloud_amount','overseas_amount','domestic_self_amount','inventory_total_amount','monthly_ship_cost','monthly_return_cost','monthly_refund_amount','slow_amount']; document.querySelectorAll('[data-inv-manual]').forEach(function(el){ if (el.value.trim() !== '') { var v = parseFloat(el.value.trim()); if (isNaN(v)) { alert('请输入数字'); return; } values[el.dataset.invManual] = moneyKeys.indexOf(el.dataset.invManual) >= 0 ? v * 10000 : v; } }); fetch('/api/dashboard/inventory-board/manual',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({month:month,values:values})}).then(function(r){return r.json();}).then(function(res){if(res.code===0){alert('人工数据已保存(' + month + ')');document.getElementById('invManualModal').remove();reloadInventoryBoard(false);}else alert(res.msg||'保存失败');}).catch(function(e){alert('请求异常:'+e);}); } function renderInventoryBoard(d) { window._inventoryBoardData = d || {}; var dateEl = document.getElementById('invBoardDate'); if (dateEl && d.latest_date) dateEl.textContent = d.latest_date; // 金额统一以“万元”展示 function wan(v) { if (v === null || v === undefined) return '—'; return (v / 10000).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}); } function pct(v) { if (v === null || v === undefined) return '—'; return v.toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + '%'; } function num(v) { if (v === null || v === undefined) return '—'; return Math.round(v).toLocaleString('zh-CN'); } function has(v) { return v !== null && v !== undefined; } // 外部数据源字段定义(可编辑文本框模式):key → 展示配置 var extDef = { 'monthly_ship_cost': {label:'月销货成本', unit:'万元', icon:'📦', color:'#52c41a'}, 'monthly_return_cost': {label:'月退货成本', unit:'万元', icon:'↩️', color:'#fa8c16'}, 'monthly_refund_amount': {label:'月退款金额', unit:'万元', icon:'💸', color:'#eb2f96'}, 'monthly_purchase_inbound': {label:'月采购入库', unit:'件', icon:'🚚', color:'#13c2c2'}, 'active_sku': {label:'动销SKU数量(大于30)', unit:'个', icon:'📊', color:'#52c41a'}, 'active_sku_100': {label:'动销SKU数量(>100天)', unit:'个', icon:'📈', color:'#a0d911'}, 'total_sku': {label:'总SKU数量(库存>=1)', unit:'个', icon:'🗃️', color:'#1890ff'}, 'total_sku_200': {label:'总SKU数量(库存>=200)', unit:'个', icon:'📚', color:'#2f54eb'}, 'slow_amount': {label:'呆滞库存金额', unit:'万元', icon:'🕸️', color:'#faad14'}, 'stockout_sku_domestic': {label:'缺货SKU次数(国内)', unit:'次', icon:'🇨🇳', color:'#f5222d'}, 'stockout_sku_snowwolf': {label:'缺货SKU次数(国际-雪狼)', unit:'次', icon:'🐺', color:'#722ed1'}, 'stockout_sku_lingdong': {label:'缺货SKU次数(国际-灵动)', unit:'次', icon:'🌐', color:'#13c2c2'}, 'stockout_sku_chunyue': {label:'缺货SKU次数(国际-春跃)', unit:'次', icon:'🌸', color:'#eb2f96'} }; var extKeys = d._external_keys || Object.keys(extDef); var moneyKeys = ['monthly_ship_cost','monthly_return_cost','monthly_refund_amount','slow_amount']; // 展示值:外部字段金额以万元录入,内部计算金额以元存储 → 统一转万元显示 function fmtExternal(key, v) { if (!has(v)) return ''; if (moneyKeys.indexOf(key) >= 0) { return (v / 10000).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 4}); } return Math.round(v).toLocaleString('zh-CN'); } // 较上月环比行(所有卡片始终显示):可计算显示百分比,上月为0本月有值显示“新增”,其余显示“—” function momRow(key) { var mom = d['mom_' + key]; var prev = d['prev_' + key]; var cur = d[key]; if (mom !== null && mom !== undefined && isFinite(mom)) { var momUp = mom >= 0; return '
较上月 ' + (momUp ? '▲' : '▼') + ' ' + Math.abs(mom).toFixed(2) + '%
'; } if (prev === 0 && cur > 0) { return '
较上月 ▲ 新增
'; } return '
较上月 —
'; } // 紧凑外部可编辑卡片(输入框+保存;CALC_INFO 存在时标题右侧显示眼睛图标悬浮计算逻辑) function extCard(key) { var ext = extDef[key]; var cur = fmtExternal(key, d[key]); var html = ''; html += '
' + '' + ext.label + '' + ' (' + ext.unit + ')' + (CALC_INFO[key] ? '👁' : '') + '
'; html += '
'; html += ''; html += ''; html += '
'; // 较上月环比(始终显示) html += momRow(key); html += '
'; return html; } // 紧凑普通只读卡片(eyeKey 存在时标题右侧显示眼睛图标,悬浮查看仓库构成;momHtml 为较上月环比行) function plainCard(label, value, unit, color, note, eyeKey, momHtml) { var html = '
' + '' + label + '' + (unit ? ' (' + unit + ')' : '') + (eyeKey ? '👁' : '') + '
'; html += '
' + value + '
'; // 较上月环比(始终显示) if (momHtml) html += momHtml; if (note) html += '
' + note + '
'; html += '
'; return html; } function cardWrap(key, color, icon, inner, fill) { var base = 'background:var(--bg-card,#fff);border:1px solid var(--border,#e8e8e8);border-left:3px solid ' + color + ';border-radius:8px;padding:8px 10px;display:flex;align-items:center;gap:8px;min-width:0'; // fill:卡片高度铺满所在网格行(经营效率板块铺满用) if (fill) base += ';height:100%;box-sizing:border-box'; return '
' + '' + icon + '' + '
' + inner + '
'; } // 板块容器:标题栏 + 卡片网格(从上往下排列时高度随内容自适应,fill=true 时卡片撑满网格行) function quad(accent, title, note, cols, cardsHtml, fill) { return '
' + '
' + '' + '' + title + '' + (note ? '' + note + '' : '') + '
' + '
' + cardsHtml + '
' + '
'; } // ---- 板块1:库存金额 ---- var sec1 = quad('#1890ff', '💰 库存金额', '', 3, cardWrap('all', '#1890ff', '📦', plainCard('全部仓库成品', wan(d.all_amount), '万元', '#1890ff', null, 'all', momRow('all_amount'))) + cardWrap('wh01', '#1890ff', '🏭', plainCard('蝶雅仓成品', wan(d.wh01_amount), '万元', '#1890ff', null, 'wh01', momRow('wh01_amount'))) + cardWrap('wh15', '#722ed1', '⚙️', plainCard('加工厂仓成品', wan(d.wh15_amount), '万元', '#722ed1', null, 'wh15', momRow('wh15_amount'))) + cardWrap('cloud', '#13c2c2', '☁️', plainCard('云仓成品', wan(d.cloud_amount), '万元', '#13c2c2', null, 'cloud', momRow('cloud_amount'))) + cardWrap('overseas', '#fa8c16', '🌍', plainCard('海外仓成品', wan(d.overseas_amount), '万元', '#fa8c16', null, 'overseas', momRow('overseas_amount'))) + cardWrap('domestic_self', '#eb2f96', '🛒', plainCard('国内平台自营仓成品', wan(d.domestic_self_amount), '万元', '#eb2f96', null, 'domestic_self', momRow('domestic_self_amount'))) ); // ---- 板块2:经营效率 ---- var cards2 = [ {key:'inventory_total_amount', label:'库存总金额', value:wan(d.inventory_total_amount), unit:'万元', icon:'💰', color:'#1890ff'}, {key:'monthly_ship_cost', label:'月销货成本', value:wan(d.monthly_ship_cost), unit:'万元', icon:'📦', color:'#52c41a'}, {key:'monthly_return_cost', label:'月退货成本', value:has(d.monthly_return_cost) ? wan(d.monthly_return_cost) : '暂无数据', unit:'万元', icon:'↩️', color:'#fa8c16'}, {key:'monthly_refund_amount', label:'月退款金额', value:has(d.monthly_refund_amount) ? wan(d.monthly_refund_amount) : '暂无数据', unit:'万元', icon:'💸', color:'#eb2f96'}, {key:'return_rate', label:'退货率', value:has(d.return_rate) ? pct(d.return_rate) : '暂无数据', unit:'', icon:'📉', color:'#f5222d'}, {key:'monthly_purchase_inbound', label:'月采购入库', value:num(d.monthly_purchase_inbound), unit:'件', icon:'🚚', color:'#13c2c2'}, {key:'turnover_rate', label:'月库存周转率', value:has(d.turnover_rate) ? pct(d.turnover_rate * 100) : '—', unit:'', icon:'🔄', color:'#722ed1'}, {key:'turnover_days', label:'库存周转天数', value:has(d.turnover_days) ? d.turnover_days.toFixed(1) : '—', unit:'天', icon:'⏱️', color:'#2f54eb'}, {key:'active_sku', label:'动销SKU数量(大于30)', value:num(d.active_sku_30), unit:'个', icon:'📊', color:'#52c41a'}, {key:'active_sku_100', label:'动销SKU数量(>100天)', value:num(d.active_sku_100), unit:'个', icon:'📈', color:'#a0d911'}, {key:'total_sku', label:'总SKU数量(库存>=1)', value:num(d.total_sku), unit:'个', icon:'🗃️', color:'#1890ff'}, {key:'total_sku_200', label:'总SKU数量(库存>=200)', value:num(d.total_sku_200), unit:'个', icon:'📚', color:'#2f54eb'}, {key:'active_rate', label:'动销率(>30)', value:pct(d.active_rate), unit:'', icon:'📈', color:'#a0d911'}, {key:'active_rate_100', label:'动销率(>100)', value:pct(d.active_rate_100), unit:'', icon:'📊', color:'#13c2c2'} ]; var sec2Html = ''; cards2.forEach(function(s) { if (extKeys.indexOf(s.key) >= 0) { sec2Html += cardWrap(s.key, extDef[s.key].color, extDef[s.key].icon, extCard(s.key), true); } else { sec2Html += cardWrap(s.key, s.color, s.icon, plainCard(s.label, s.value, s.unit, s.color, null, s.key, momRow(s.key)), true); } }); var sec2 = quad('#52c41a', '📊 经营效率', '📝 可编辑为外部数据源', 3, sec2Html, true); // ---- 板块3:呆滞治理 ---- var cards3 = [ {key:'slow_amount', label:'呆滞库存金额', value:wan(d.slow_amount), unit:'万元', icon:'🕸️', color:'#faad14', eyeKey:'slow_amount'}, {key:'slow_ratio', label:'呆滞库存比例', value:pct(d.slow_ratio), unit:'', icon:'🐌', color:'#d48806', eyeKey:'slow_ratio'} ]; var sec3Html = ''; cards3.forEach(function(s) { if (extKeys.indexOf(s.key) >= 0) { sec3Html += cardWrap(s.key, extDef[s.key].color, extDef[s.key].icon, extCard(s.key)); } else { sec3Html += cardWrap(s.key, s.color, s.icon, plainCard(s.label, s.value, s.unit, s.color, null, s.eyeKey, momRow(s.key))); } }); var sec3 = quad('#faad14', '🐌 呆滞治理', '', 3, sec3Html); // ---- 板块4:缺货监控 ---- var cards4 = [ {key:'stockout_sku_domestic', label:'缺货SKU次数(国内)', value:num(d.stockout_sku_domestic), unit:'次', icon:'🇨🇳', color:'#f5222d', eyeKey:'stockout_sku_domestic'}, {key:'stockout_sku_snowwolf', label:'缺货SKU次数(国际-雪狼)', value:num(d.stockout_sku_snowwolf), unit:'次', icon:'🐺', color:'#722ed1', eyeKey:'stockout_sku_snowwolf'}, {key:'stockout_sku_lingdong', label:'缺货SKU次数(国际-灵动)', value:num(d.stockout_sku_lingdong), unit:'次', icon:'🌐', color:'#13c2c2', eyeKey:'stockout_sku_lingdong'}, {key:'stockout_sku_chunyue', label:'缺货SKU次数(国际-春跃)', value:num(d.stockout_sku_chunyue), unit:'次', icon:'🌸', color:'#eb2f96', eyeKey:'stockout_sku_chunyue'}, {key:'stockout_rate', label:'缺货率', value:pct(d.stockout_rate), unit:'', icon:'🚫', color:'#ff7a45', eyeKey:'stockout_rate'}, {key:'top100_stockout_rate', label:'销量TOP100缺货率', value:pct(d.top100_stockout_rate), unit:'', icon:'🏆', color:'#eb2f96', eyeKey:'top100_stockout_rate'}, {key:'new_stockout_rate', label:'新品缺货率', value:has(d.new_stockout_rate) ? pct(d.new_stockout_rate) : '—', unit:'', icon:'🌱', color:'#13c2c2', eyeKey:'new_stockout_rate'}, {key:'old_stockout_rate', label:'老品缺货率', value:has(d.old_stockout_rate) ? pct(d.old_stockout_rate) : '—', unit:'', icon:'🌳', color:'#8c8c8c', eyeKey:'old_stockout_rate'} ]; var sec4Html = ''; cards4.forEach(function(s) { if (extKeys.indexOf(s.key) >= 0) { sec4Html += cardWrap(s.key, extDef[s.key].color, extDef[s.key].icon, extCard(s.key)); } else { sec4Html += cardWrap(s.key, s.color, s.icon, plainCard(s.label, s.value, s.unit, s.color, null, s.eyeKey, momRow(s.key))); } }); var sec4 = quad('#f5222d', '🚨 缺货监控', '近30天有出库但最新库存日无货', 3, sec4Html); // 4 个板块从上往下依次排列(invBoardContent 为单列 grid) document.getElementById('invBoardContent').innerHTML = sec1 + sec2 + sec3 + sec4; } function saveInventoryExternal(key) { var input = document.getElementById('invExt_' + key); if (!input) return; var val = (input.value || '').trim(); var label = {'monthly_ship_cost':'月销货成本','monthly_return_cost':'月退货成本', 'monthly_refund_amount':'月退款金额','monthly_purchase_inbound':'月采购入库', 'active_sku':'动销SKU数量(大于30)','active_sku_100':'动销SKU数量(>100天)', 'total_sku':'总SKU数量(库存>=1)','total_sku_200':'总SKU数量(库存>=200)', 'slow_amount':'呆滞库存金额'}[key] || key; // 空值 = 清空外部值回退内部计算 if (val !== '' && isNaN(parseFloat(val))) { alert(label + ':请输入数字'); return; } // 金额类字段:前端录入单位为万元,后端统一存元(与内部计算口径一致) var moneyKeys = ['monthly_ship_cost','monthly_return_cost','monthly_refund_amount','slow_amount']; var submitVal = val; if (val !== '' && moneyKeys.indexOf(key) >= 0) { submitVal = String(Math.round(parseFloat(val) * 10000)); } // 月份维度人工录入:外部数据输入框也必须绑定当前月份,切换月份后只回显该月份数据。 var month = inventoryBoardMonth(); fetch('/api/dashboard/inventory-board/external', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}, body: JSON.stringify({key: key, value: submitVal, month: month}) }) .then(r => r.json()) .then(res => { if (res.code === 0) { alert(label + ' 已保存' + (val === '' ? '(已回退内部计算值)' : '')); // 重新拉取当前月份,避免保存后误显示其他月份或全局旧值 fetch('/api/dashboard/inventory-board?month=' + encodeURIComponent(month), { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()) .then(res2 => { if (res2.code === 0) renderInventoryBoard(res2.data); }); } else { alert(res.msg || '保存失败'); } }) .catch(function(e) { alert('请求异常:' + e); }); } // 库存金额板块:卡片 → 仓库构成说明(眼睛图标悬浮提示) var WH_INFO = { 'all': { title: '全部仓库成品', desc: '代表全部序号仓库数据之和', whs: '1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、20、24、25、26、27、29号仓' }, 'wh01': {title: '蝶雅仓成品', desc: '代表 1 号仓数据', whs: '1号仓(蝶雅广州新仓)'}, 'wh15': {title: '加工厂仓成品', desc: '代表 15 号仓数据', whs: '15号仓(加工厂储存仓)'}, 'cloud': {title: '云仓成品', desc: '代表以下仓库数据之和', whs: '1、2、3、4、5、6、7、8、16、24、27、29号仓'}, 'overseas': {title: '海外仓成品', desc: '代表以下仓库数据之和', whs: '9、10、11、12、25、26号仓'}, 'domestic_self': {title: '国内平台自营仓成品', desc: '代表以下仓库数据之和', whs: '13、14号仓'} }; // 经营效率板块:卡片 → 计算逻辑说明(眼睛图标悬浮提示) var CALC_INFO = { 'slow_amount': {title: '呆滞库存金额', desc: '同时符合首次入库时间大于90天、可销天数大于90天的库存金额'}, 'slow_ratio': {title: '呆滞库存比例', desc: '呆滞库存金额 ÷ 库存总金额'}, 'stockout_sku_domestic': {title: '缺货SKU次数(国内)', desc: '当月发生缺货且连续断货1天以上的国内SKU次数'}, 'stockout_sku_snowwolf': {title: '缺货SKU次数(国际-雪狼)', desc: '当月发生缺货且连续断货1天以上的国际-雪狼SKU次数'}, 'stockout_sku_lingdong': {title: '缺货SKU次数(国际-灵动)', desc: '当月发生缺货且连续断货1天以上的国际-灵动SKU次数'}, 'stockout_sku_chunyue': {title: '缺货SKU次数(国际-春跃)', desc: '当月发生缺货且连续断货1天以上的国际-春跃SKU次数'}, 'stockout_rate': {title: '缺货率', desc: '当月发生缺货的SKU数量总和 ÷ SKU总数量'}, 'top100_stockout_rate': {title: '销量TOP100缺货率', desc: '每个月销量前100名SKU发生缺货的比例'}, 'new_stockout_rate': {title: '新品缺货率', desc: '从到仓开始90天内发生断货的比例'}, 'old_stockout_rate': {title: '老品缺货率', desc: '从上架超过90天的产品发生断货的比例'}, 'inventory_total_amount': {title: '库存总金额', desc: '每个仓库的库存金额之和'}, 'monthly_ship_cost': {title: '月销货成本', desc: '上个月实际销量(已扣减退货)× 产品当前采购单价'}, 'monthly_return_cost': {title: '月退货成本', desc: '月退款金额 ÷ 10'}, 'monthly_refund_amount': {title: '月退款金额', desc: '上个月客户付款后发生退款的金额(发货前、发货后)'}, 'return_rate': {title: '退货率', desc: '月退货成本 ÷ 月销货成本'}, 'monthly_purchase_inbound': {title: '月采购入库', desc: '取自财务数据报表'}, 'turnover_rate': {title: '月库存周转率', desc: '月销货成本 ÷ 库存总金额'}, 'turnover_days': {title: '库存周转天数', desc: '当月天数 ÷ 月库存周转率'}, 'active_sku': {title: '动销SKU数量(大于30)', desc: '当月至少销售发货 30 件的 SKU 数量'}, 'active_sku_100': {title: '动销SKU数量(>100天)', desc: '当月至少销售发货 100 件的 SKU 数量'}, 'total_sku': {title: '总SKU数量(库存>=1)', desc: '月末所有在仓 SKU 库存数量(库存 ≥ 1)'}, 'total_sku_200': {title: '总SKU数量(库存>=200)', desc: '月末所有在仓 SKU 库存数量(库存 ≥ 200)'}, 'active_rate': {title: '动销率(>30)', desc: '动销SKU数量(>30)÷ 总SKU数量(库存>=1)× 100%'}, 'active_rate_100': {title: '动销率(>100)', desc: '动销SKU数量(>100)÷ 总SKU数量(库存>=1)× 100%'} }; function showWhTip(key, el) { var info = WH_INFO[key] || CALC_INFO[key]; if (!info) return; var tip = document.getElementById('whTip'); if (!tip) { tip = document.createElement('div'); tip.id = 'whTip'; tip.style.cssText = 'position:fixed;background:#fff;border:1px solid #e8e8e8;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.12);padding:10px 14px;font-size:13px;color:#333;line-height:1.7;z-index:99999;max-width:380px;pointer-events:none;white-space:normal;'; document.body.appendChild(tip); } tip.innerHTML = '
' + info.title + '
' + (info.whs ? '
' + info.desc + ':' + info.whs + '
' : '
' + info.desc + '
'); // 先临时定位测量尺寸,再居中到图标下方,避免超出视口 tip.style.display = 'block'; tip.style.left = '-9999px'; tip.style.top = '-9999px'; var w = tip.offsetWidth, h = tip.offsetHeight; var r = el.getBoundingClientRect(); var x = r.left + r.width / 2 - w / 2; var y = r.bottom + 6; if (x < 8) x = 8; if (x + w > window.innerWidth - 8) x = window.innerWidth - w - 8; if (y + h > window.innerHeight - 8) y = r.top - h - 6; tip.style.left = x + 'px'; tip.style.top = y + 'px'; } function hideWhTip() { var tip = document.getElementById('whTip'); if (tip) tip.style.display = 'none'; } function loadDashboard(container) { container.innerHTML = `
`; fetch('/api/dashboard/stats') .then(r => r.json()) .then(res => { if (res.code !== 0) return; // 缓存数据供图表重建 if (!window._pageDataCache) window._pageDataCache = {}; if (!window._pageReloaders) window._pageReloaders = {}; window._pageDataCache['dashboard'] = res.data; window._pageReloaders['dashboard'] = function(data) { renderDashboardCharts(data); }; renderDashboardCharts(res.data); }); } function renderCostBreakdown(fields, values) { fields = fields || []; values = values || {}; return '
' + fields.map(function(label) { return '
' + label + '' + Math.round(values[label] || 0).toLocaleString() + '
'; }).join('') + '
'; } function renderDashboardCharts(d) { const el = document.getElementById('dashContent'); if (!el) return; // 顶部统计卡片 var isAdmin = window._currentRole === 'admin' || window._currentRole === 'super_admin'; var costValues = { '总成本': d.total_cost, '成品成本': d.finished_cost, '包材成本': d.packaging_cost, '成品库存成本': d.finished_inventory_cost, '成品在途成本': d.finished_in_transit_cost, '包材库存成本': d.packaging_inventory_cost, '包材在途成本': d.packaging_in_transit_cost }; var statItems = [ {label:'SKU总数', value:(d.total_products || 0).toLocaleString(), sub:'', icon:'📦', color:'#1890ff'}, {label:'SPU总数', value:(d.spu_total || 0).toLocaleString(), sub:'', icon:'📦', color:'#2f54eb'}, {label:'总成本', value:renderCostBreakdown(['总成本', '成品成本', '包材成本'], costValues), sub:'', icon:'💎', color:'#eb2f96', cost:true}, {label:'成品成本', value:renderCostBreakdown(['成品成本', '成品库存成本', '成品在途成本'], costValues), sub:'', icon:'🏷️', color:'#722ed1', cost:true}, {label:'包材成本', value:renderCostBreakdown(['包材成本', '包材库存成本', '包材在途成本'], costValues), sub:'', icon:'🧰', color:'#13c2c2', cost:true} ]; var cols = Math.min(statItems.length, 5); let cards = '
'; statItems.forEach(function(s) { cards += '
'; cards += '
' + s.icon + '
'; cards += '
'; if (!s.cost) cards += '
' + s.label + (s.sub ? ' ' + s.sub + '' : '') + '
'; cards += '
' + s.value + '
'; cards += '
'; }); cards += '
'; // 第一行图表:日出库趋势 + 品牌占比 var row1 = '
'; row1 += '

📈 日出库趋势

'; row1 += '

🥧 品牌出库占比 TOP8

'; row1 += '
'; // 第二行:店铺排行 + 单品销量排行(并列纵向柱状图) var row2 = '
'; row2 += '

🏪 店铺出库排行

'; row2 += '

📦 单品销量 TOP10

'; row2 += '
'; el.innerHTML = cards + row1 + row2; // 绘制所有图表 waitChartJS().then(function() { var COLORS = ['#1890ff','#52c41a','#fa8c16','#722ed1','#13c2c2','#eb2f96','#faad14','#f5222d','#2f54eb','#a0d911']; // 日出库趋势:按月份展示,点击月份后展示每日数据 var _trendDailyData = (d.recent_trend || []).map(function(t) { return { date: t.date_str || t.date || '', qty: parseInt(t.qty) || 0 }; }); var _trendChartInstance = null; var _trendHeaderEl = null; function _aggregateByMonth(dailyData) { var months = {}; dailyData.forEach(function(d) { var monthKey = d.date.slice(0, 7); // YYYY-MM if (!months[monthKey]) months[monthKey] = { month: monthKey, qty: 0, count: 0 }; months[monthKey].qty += d.qty; months[monthKey].count++; }); var result = Object.keys(months).sort().map(function(k) { return months[k]; }); return result; } function _buildTrendChart(data, labels, labelText) { if (_trendChartInstance) { _trendChartInstance.destroy(); _trendChartInstance = null; } var canvas = document.getElementById('dashTrendChart'); if (!canvas) return; var isMonthView = /^\d{4}-\d{2}$/.test(labels[0]); _trendChartInstance = new Chart(canvas, { type: 'bar', data: { labels: labels, datasets: [ { label: labelText, data: data, backgroundColor: 'rgba(24,144,255,0.6)', borderColor: '#1890ff', borderWidth: 1, borderRadius: 4, order: 2 }, { label: '趋势线', type: 'line', data: data, borderColor: '#f5222d', borderWidth: 2, pointBackgroundColor: '#f5222d', pointBorderColor: '#fff', pointBorderWidth: 1, pointRadius: 3, pointHoverRadius: 5, fill: false, tension: 0, order: 1 } ] }, options: { responsive: true, maintainAspectRatio: false, onClick: function(e, elements) { if (elements && elements.length > 0) { var idx = elements[0].index; var label = this.data.labels[idx]; if (isMonthView && /^\d{4}-\d{2}$/.test(label)) { _showDailyByMonth(label); } } }, plugins: { legend: { display: true, position: 'top', labels: { boxWidth: 12, padding: 4, font: { size: 11 } } } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false } } } } }); } function _showMonthlyView() { if (!_trendDailyData.length) return; var monthly = _aggregateByMonth(_trendDailyData); var labels = monthly.map(function(m) { return m.month; }); var data = monthly.map(function(m) { return m.qty; }); if (_trendHeaderEl) _trendHeaderEl.innerHTML = '📈 日出库趋势'; _buildTrendChart(data, labels, '月出库量'); } function _showDailyByMonth(monthKey) { var filtered = _trendDailyData.filter(function(d) { return d.date.indexOf(monthKey) === 0; }); if (!filtered.length) return; var labels = filtered.map(function(d) { return d.date.slice(5); }); // MM-DD var data = filtered.map(function(d) { return d.qty; }); if (_trendHeaderEl) _trendHeaderEl.innerHTML = '📈 ' + monthKey + ' 日出库趋势 ← 返回月份'; _buildTrendChart(data, labels, '日出库量'); } // 注册返回函数到全局 window._trendBackToMonth = _showMonthlyView; function drawTrend() { var trendCanvas = document.getElementById('dashTrendChart'); if (!trendCanvas || !_trendDailyData.length) return; _trendHeaderEl = document.querySelector('.card-header:has(#dashTrendChart) h3') || document.querySelector('.card-header h3'); // 找最近一次匹配的标题 - 其实我们直接定位 var cardHeaders = document.querySelectorAll('.card-header h3'); for (var hi = 0; hi < cardHeaders.length; hi++) { if (cardHeaders[hi].textContent.indexOf('日出库趋势') >= 0) { _trendHeaderEl = cardHeaders[hi]; break; } } _showMonthlyView(); } function drawBrand() { var brandCanvas = document.getElementById('dashBrandChart'); if (brandCanvas && d.brand_stats && d.brand_stats.length > 0) { var topBrands = d.brand_stats.slice(0, 8); new Chart(brandCanvas, { type: 'doughnut', data: { labels: topBrands.map(function(b) { return b['品牌']; }), datasets: [{ data: topBrands.map(function(b) { return parseInt(b['总数量']) || 0; }), backgroundColor: COLORS.slice(0, topBrands.length), borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { font: { size: 11 }, boxWidth: 12, padding: 6 } }, tooltip: { callbacks: { label: function(ctx) { var total = ctx.dataset.data.reduce(function(a,b) { return a + b; }, 0); return ctx.label + ': ' + ctx.raw.toLocaleString() + ' (' + ((ctx.raw / total) * 100).toFixed(1) + '%)'; } } } }, cutout: '55%' } }); } } function drawShop() { var shopCanvas = document.getElementById('dashShopChart'); if (shopCanvas && d.shop_stats && d.shop_stats.length > 0) { var shopData = d.shop_stats.slice(0, 10); new Chart(shopCanvas, { type: 'bar', data: { labels: shopData.map(function(s) { return (s['店铺'] || '').length > 10 ? (s['店铺'] || '').substring(0, 10) + '..' : (s['店铺'] || ''); }), datasets: [{ label: '出库量', data: shopData.map(function(s) { return parseInt(s['总数量']) || 0; }), backgroundColor: 'rgba(250,140,22,0.7)', borderColor: '#fa8c16', borderWidth: 1, borderRadius: 3 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false }, ticks: { font: { size: 10 }, maxRotation: 30 } } } } }); } } function drawProductRank() { var productRankCanvas = document.getElementById('dashProductRankChart'); if (productRankCanvas && d.product_stats && d.product_stats.length > 0) { var productRankData = d.product_stats.slice(0, 10); new Chart(productRankCanvas, { type: 'bar', data: { labels: productRankData.map(function(b) { return (b['货品名称'] || b['货品编号'] || '').length > 12 ? (b['货品名称'] || b['货品编号'] || '').substring(0, 12) + '..' : (b['货品名称'] || b['货品编号'] || ''); }), datasets: [{ label: '出库量', data: productRankData.map(function(b) { return parseInt(b['总数量']) || 0; }), backgroundColor: 'rgba(24,144,255,0.7)', borderColor: '#1890ff', borderWidth: 1, borderRadius: 3 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { title: function(items) { var i = items[0].dataIndex; return productRankData[i]['货品编号'] + ' ' + (productRankData[i]['货品名称'] || ''); }, label: function(ctx) { return '出库量: ' + ctx.raw.toLocaleString(); } } } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false }, ticks: { font: { size: 10 }, maxRotation: 30 } } } } }); } } drawTrend(); drawBrand(); drawShop(); drawProductRank(); }); } // ─── 出库数据 ─── let _sumChart = null; function loadDataSummaryPage(container) { container.innerHTML = `
📦
-
总出库量 件
🏷️
-
出库SKU数 个
📅
-
出库天数 天
📊
-
日均出库 件

📈 月度出库趋势

📊 商品日出库趋势

`; // 两个Tab独立加载:汇总筛选不参与趋势图请求。 loadSummaryStats(''); loadSkuTrend(); loadSummaryData(1); // 注册切回页面时自动重新加载(防止容器移除后数据丢失) if (!window._pageReloaders) window._pageReloaders = {}; if (!window._pageDataCache) window._pageDataCache = {}; window._pageReloaders['data_summary'] = function() { loadSummaryStats(''); loadSkuTrend(); loadSummaryData(sumCurrentPage || 1); }; window._pageDataCache['data_summary'] = { _ts: Date.now() }; } function switchOutboundTab(tab) { var trendPanel = document.getElementById('outboundTrendPanel'); var summaryPanel = document.getElementById('outboundSummaryPanel'); var trendTab = document.getElementById('outboundTrendTab'); var summaryTab = document.getElementById('outboundSummaryTab'); if (!trendPanel || !summaryPanel) return; var isTrend = tab !== 'summary'; trendPanel.style.display = isTrend ? '' : 'none'; summaryPanel.style.display = isTrend ? 'none' : ''; if (trendTab) { trendTab.classList.toggle('btn-primary', isTrend); trendTab.classList.toggle('btn', !isTrend); } if (summaryTab) { summaryTab.classList.toggle('btn-primary', !isTrend); summaryTab.classList.toggle('btn', isTrend); } if (isTrend) { // 从隐藏Tab切回来后,让Chart.js按可见容器尺寸重新绘制。 setTimeout(function() { if (_sumChart) _sumChart.resize(); if (_skuTrendChart) _skuTrendChart.resize(); }, 0); } else { loadSummaryData(sumCurrentPage || 1); } } let sumCurrentPage = 1; let SUM_PAGE_SIZE = 20; let _summaryRequestSeq = 0; function loadSummaryData(page) { var requestSeq = ++_summaryRequestSeq; sumCurrentPage = page; var search = document.getElementById('sumSearch')?.value || ''; var start = document.getElementById('sumDateStart')?.value || ''; var end = document.getElementById('sumDateEnd')?.value || ''; // 数据汇总筛选只影响汇总列表,不联动趋势图表。 // 图表由趋势Tab自身的筛选条件独立加载。 // 加载明细表格 var url = '/api/data/summary?page=' + page + '&size=' + SUM_PAGE_SIZE; if (search) url += '&search=' + encodeURIComponent(search); if (start) url += '&date_from=' + start; if (end) url += '&date_to=' + end; fetch(url, {cache: 'no-store'}) .then(r => r.json()) .then(res => { if (requestSeq !== _summaryRequestSeq || res.code !== 0) return; var data = res.data; var tbody = document.getElementById('sumTbody'); if (!tbody) return; function summaryNumber(value) { if (typeof value === 'number') return isFinite(value) ? value : 0; var parsed = parseFloat(String(value == null ? '' : value).replace(/,/g, '').trim()); return isFinite(parsed) ? parsed : 0; } var totalRow = ''; if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据'; } else { var startIdx = (page - 1) * SUM_PAGE_SIZE; tbody.innerHTML = data.rows.map(function(r, i) { return '' + (startIdx + i + 1) + '' + escHtml(r.日期 || '-') + '' + escHtml(r.货品编号) + '' + escHtml(r.货品名称 || '-') + '' + escHtml(r.品牌 || '-') + '' + (r.total_qty || 0) + '' + (r.total_cost || 0).toFixed(2) + ''; }).join(''); } var totalUrl = '/api/data/summary-total'; var totalParams = []; if (search) totalParams.push('search=' + encodeURIComponent(search)); if (start) totalParams.push('date_from=' + encodeURIComponent(start)); if (end) totalParams.push('date_to=' + encodeURIComponent(end)); if (totalParams.length) totalUrl += '?' + totalParams.join('&'); fetch(totalUrl, {cache: 'no-store'}).then(function(r) { return r.json(); }).then(function(totalRes) { if (requestSeq !== _summaryRequestSeq || totalRes.code !== 0) return; var totalData = totalRes.data || {}; var totalRow = '-合计' + summaryNumber(totalData.total_qty).toLocaleString() + '' + summaryNumber(totalData.total_cost).toFixed(2) + ''; tbody.insertAdjacentHTML('afterbegin', totalRow); }); var sumCountEl = document.getElementById('sumCount'); if (sumCountEl) sumCountEl.textContent = '共 ' + (data.total || 0) + ' 条'; var pel = document.getElementById('sumPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, SUM_PAGE_SIZE, 'loadSummaryData', 'SUM_PAGE_SIZE'); }); } let _sumChartMonth = null; // 当前钻取的月份 let _skuTrendChart = null; // 商品日出库趋势图 // 商品日出库趋势(排除赠品/小样) function loadSkuTrend() { var search = document.getElementById('skuTrendSearch')?.value || ''; var start = document.getElementById('skuTrendStart')?.value || ''; var end = document.getElementById('skuTrendEnd')?.value || ''; var url = '/api/data/sku-trend'; var params = []; if (search) params.push('search=' + encodeURIComponent(search)); if (start) params.push('date_from=' + start); if (end) params.push('date_to=' + end); if (params.length > 0) url += '?' + params.join('&'); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var d = res.data || {}; var countEl = document.getElementById('skuTrendCount'); if (countEl) countEl.textContent = '合计 ' + (d.total_qty || 0).toLocaleString() + ' 件'; waitChartJS().then(function() { if (_skuTrendChart) { _skuTrendChart.destroy(); _skuTrendChart = null; } var canvas = document.getElementById('skuTrendChart'); if (!canvas) return; var trend = d.trend || []; if (trend.length === 0) { var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); return; } var labels = trend.map(function(t) { return (t['日期'] || '').slice(5); }); var values = trend.map(function(t) { return t.day_qty || 0; }); _skuTrendChart = new Chart(canvas, { type: 'line', data: { labels: labels, datasets: [{ label: '日出库量', data: values, borderColor: '#722ed1', backgroundColor: 'rgba(114,46,209,0.1)', borderWidth: 2, tension: 0.3, pointRadius: 3, pointBackgroundColor: '#722ed1', fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { title: function(items) { return items[0].label; }, label: function(ctx) { return '出库量: ' + ctx.raw.toLocaleString(); } } } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false } } } } }); }); }); } window.resetSkuTrendFilters = function() { document.getElementById('skuTrendSearch').value = ''; document.getElementById('skuTrendStart').value = ''; document.getElementById('skuTrendEnd').value = ''; loadSkuTrend(); }; function loadSummaryStats(search) { var url = '/api/data/summary-stats?group_by=month'; if (search) url += '&search=' + encodeURIComponent(search); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var d = res.data; if (!document.getElementById('sumTotalQty')) return; document.getElementById('sumTotalQty').textContent = (d.total_out_qty || 0).toLocaleString(); document.getElementById('sumTotalSku').textContent = (d.total_sku || 0).toLocaleString(); document.getElementById('sumTotalDays').textContent = (d.total_days || 0).toLocaleString(); var avg = d.total_days > 0 ? Math.round((d.total_out_qty || 0) / d.total_days) : 0; document.getElementById('sumAvgDaily').textContent = avg.toLocaleString(); // 绘制月度趋势图 waitChartJS().then(function() { if (_sumChart) { _sumChart.destroy(); _sumChart = null; } var canvas = document.getElementById('sumTrendChart'); if (!canvas || !d.trend || d.trend.length === 0) return; _sumChartMonth = null; renderTrendChart(canvas, d.trend, 'month', null, search); }); }); } function renderTrendChart(canvas, data, mode, month, search) { var labels, values, labelText; var dataKey = (mode === 'day') ? 'day_qty' : 'month_qty'; var labelKey = (mode === 'day') ? '日期' : 'month'; if (mode === 'day') { labels = data.map(function(t) { return (t[labelKey] || '').slice(5); }); labelText = month + ' 日出库趋势'; } else { labels = data.map(function(t) { return t[labelKey] || ''; }); labelText = '月度出库趋势(点击月份钻取)'; } values = data.map(function(t) { return t[dataKey] || 0; }); var barColor = mode === 'day' ? 'rgba(82,196,26,0.6)' : 'rgba(24,144,255,0.5)'; var lineColor = mode === 'day' ? '#52c41a' : '#1890ff'; _sumChart = new Chart(canvas, { type: 'bar', data: { labels: labels, datasets: [{ label: '出库量', data: values, backgroundColor: barColor, borderColor: barColor, borderWidth: 1, borderRadius: 2, order: 2 }, { label: '趋势', type: 'line', data: values, borderColor: lineColor, backgroundColor: 'transparent', borderWidth: 2, tension: 0.3, pointRadius: 4, pointBackgroundColor: '#fff', pointBorderColor: lineColor, pointBorderWidth: 2, pointHoverRadius: 6, fill: false, order: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { title: function(items) { if (mode === 'day') return month + '-' + items[0].label; return items[0].label; }, label: function(ctx) { return '出库量: ' + ctx.raw.toLocaleString(); } } } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false } } }, onClick: function(e) { if (mode === 'month') { var active = _sumChart.getElementsAtEventForMode(e, 'index', {intersect: true}); if (active.length > 0) { var idx = active[0].index; var monthLabel = _sumChart.data.labels[idx]; drillToMonth(monthLabel, search); } } else if (mode === 'day') { // 日视图点击返回月视图 var search = document.getElementById('sumSearch')?.value || ''; loadSummaryStats(search); } } } }); // 更新图表标题 var headerEl = canvas.closest('.card')?.querySelector('.card-header h3'); if (headerEl) { if (mode === 'day') { headerEl.innerHTML = '📈 ' + month + ' 日出库趋势 (点击返回月视图)'; } else { headerEl.innerHTML = '📈 月度出库趋势 (点击数据点钻取日数据)'; } } } function drillToMonth(month, search) { _sumChartMonth = month; var url = '/api/data/summary-stats?group_by=day&month=' + encodeURIComponent(month); if (search) url += '&search=' + encodeURIComponent(search); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var d = res.data; if (_sumChart) { _sumChart.destroy(); _sumChart = null; } var canvas = document.getElementById('sumTrendChart'); if (!canvas || !d.trend || d.trend.length === 0) return; renderTrendChart(canvas, d.trend, 'day', month, search); }); } window.showSkuDetail = function(productCode, dateStr, productName) { document.getElementById('skuModalTitle').textContent = productCode; document.getElementById('skuModalDate').textContent = dateStr; document.getElementById('skuModalName').textContent = productName || '-'; document.getElementById('skuDetailTbody').innerHTML = '加载中...'; document.getElementById('skuDetailSummary').textContent = ''; document.getElementById('skuModal').style.display = 'flex'; fetch('/api/data/sku-detail?product_code=' + encodeURIComponent(productCode) + '&date=' + encodeURIComponent(dateStr)) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var details = res.data.details || []; var tbody = document.getElementById('skuDetailTbody'); if (!tbody) return; if (details.length === 0) { tbody.innerHTML = '暂无明细'; } else { var SOURCE_MAP = {'wdt':'旺店通','jdbz':'京东泊紫','jdgj':'京东国际','jdjwt':'京东洁维塔','tmgj':'天猫国际','dymuto':'抖音M店','dymtuo':'抖音M店','jdsmj':'京东适美家'}; var totalQty = 0, totalCost = 0; tbody.innerHTML = details.map(function(d) { totalQty += d.qty || 0; totalCost += d.cost || 0; var srcCn = SOURCE_MAP[d.数据来源] || d.数据来源 || '-'; return '' + escHtml(d.仓库 || '-') + '' + escHtml(d.店铺 || '-') + '' + srcCn + '' + (d.qty || 0) + '' + (d.cost || 0).toFixed(2) + ''; }).join(''); document.getElementById('skuDetailSummary').textContent = '合计:' + totalQty + ' 件,' + totalCost.toFixed(2); } }); }; window.closeSkuModal = function() { document.getElementById('skuModal').style.display = 'none'; }; window.resetSummaryFilters = function() { var search = document.getElementById('sumSearch'); var start = document.getElementById('sumDateStart'); var end = document.getElementById('sumDateEnd'); if (search) search.value = ''; if (start) start.value = ''; if (end) end.value = ''; var resetButton = document.querySelector('#sumSearch')?.closest('.filter-bar')?.querySelector('button:not(.btn-primary)'); if (resetButton) { var originalText = resetButton.textContent; resetButton.textContent = '已重置'; setTimeout(function() { resetButton.textContent = originalText; }, 1200); } loadSummaryData(1); }; function loadDataPage(container) { container.innerHTML = `

出库列表

${window._hasPagePerm('data_detail', 'import') ? '' : ''}
序号日期货品编号货品名称品牌货品数量成本价金额仓库店铺数据来源
加载中...
`; var ready = false; function refresh() { if (!ready) { ready = true; loadDataPageData(1); } } refresh(); // 筛选项只在用户打开补全框时加载,避免进入子菜单时等待 5 次 DISTINCT 查询。 window._dataFiltersPromise = window._dataFiltersPromise || fetch('/api/data/dates').then(r => r.json()); window._dataFiltersPromise.then(function(res) { if (res.code !== 0) return; var d = res.data; var srcSel = document.getElementById('dataSource'); if (srcSel && d.sources) { d.sources.forEach(function(w) { var cn = SOURCE_MAP_CN[w] || w; srcSel.innerHTML += ''; }); } window.dataWarehouseOptions = d.warehouses || []; window.dataShopOptions = d.shops || []; window.dataBrandOptions = d.brands || []; }).catch(function() {}); } const SOURCE_MAP_CN = { 'wdt': '旺店通', 'jdbz': '京东泊紫', 'jdgj': '京东国际', 'jdjwt': '京东洁维塔', 'tmgj': '天猫国际', 'dymuto': '抖音M店', 'dymtuo': '抖音M店', 'jdsmj': '京东适美家' }; let dataCurrentPage = 1; let DATA_PAGE_SIZE = 20; // 模糊搜索自动补全的数据源 window.dataWarehouseOptions = []; window.dataShopOptions = []; window.dataBrandOptions = []; /** * 模糊搜索自动补全函数 * @param {HTMLInputElement} inputEl - 输入框元素 * @param {string} optionsVar - 全局选项数组变量名 */ window.dataSearchFilter = function(inputEl, optionsVar) { var val = inputEl.value.trim().toLowerCase(); var dropdownId = inputEl.id + 'Dropdown'; var dd = document.getElementById(dropdownId); if (!dd) return; var options = window[optionsVar]; if (!options || options.length === 0) { dd.style.display = 'none'; return; } // 过滤匹配的选项 var matched = []; if (!val) { // 空输入显示全部(最多50个避免卡顿) matched = options.slice(0, 50); } else { options.forEach(function(opt) { if (opt.toLowerCase().indexOf(val) !== -1) { matched.push(opt); } }); // 始终允许直接输入的值 if (matched.length === 0 && inputEl.value) { matched = [inputEl.value]; } } if (matched.length === 0) { dd.style.display = 'none'; return; } // 渲染下拉列表 var html = ''; // 始终显示「全部」选项 html += '
全部
'; matched.forEach(function(opt) { var hOpt = opt.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); html += '
' + hOpt + '
'; }); dd.innerHTML = html; dd.style.display = 'block'; // 限制列表高度 dd.style.maxHeight = '200px'; }; // ─── 出库明细导入 ─── function openOutboundImportModal() { if (!window._hasPagePerm('data_detail', 'import')) { showCenteredAlert('权限不足', '当前账号没有出库明细导入权限'); return; } var old = document.getElementById('outboundImportModal'); if (old) old.remove(); var html = ''; document.body.insertAdjacentHTML('beforeend', html); } function closeOutboundImportModal() { var m = document.getElementById('outboundImportModal'); if (m) m.remove(); } function previewOutboundImportFile() { var f = document.getElementById('outboundImportFile'); var preview = document.getElementById('outboundImportPreview'); var btn = document.getElementById('outboundImportBtn'); if (f.files && f.files.length > 0) { preview.textContent = '已选择:' + f.files[0].name + '(' + (f.files[0].size / 1024).toFixed(1) + 'KB)'; btn.disabled = false; } else { preview.textContent = ''; btn.disabled = true; } } function submitOutboundImport() { var f = document.getElementById('outboundImportFile'); if (!f.files || !f.files[0]) return; var btn = document.getElementById('outboundImportBtn'); btn.disabled = true; btn.textContent = '导入中...'; var formData = new FormData(); formData.append('file', f.files[0]); fetch('/api/data/import', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { btn.disabled = false; btn.textContent = '开始导入'; if (res.code === 0) { showCenteredAlert('导入完成', res.msg); closeOutboundImportModal(); loadDataPageData(1); } else { showCenteredAlert('导入失败', res.msg || '导入失败'); } }) .catch(function(err) { btn.disabled = false; btn.textContent = '开始导入'; showCenteredAlert('导入失败', '请求失败:' + err.message); }); } function openOutboundExportModal() { var fields = [ ['date', '日期'], ['product_code', '货品编号'], ['product_name', '货品名称'], ['brand', '品牌'], ['quantity', '货品数量'], ['unit_cost', '成本价'], ['amount', '金额'], ['warehouse', '仓库'], ['shop', '店铺'], ['source', '数据来源'] ]; var html = ''; document.body.insertAdjacentHTML('beforeend', html); } function confirmOutboundExport() { var checked = Array.from(document.querySelectorAll('#outboundExportFields input:checked')).map(function(e){ return e.value; }); var msg = document.getElementById('outboundExportMsg'); if (!checked.length) { msg.textContent = '请至少选择一个导出字段'; return; } var from = document.getElementById('outboundExportDateFrom').value; var to = document.getElementById('outboundExportDateTo').value; if (from && to && from > to) { msg.textContent = '开始日期不能晚于结束日期'; return; } var params = new URLSearchParams({date_from: from, date_to: to, fields: checked.join(',')}); var link = document.createElement('a'); link.href = '/api/data/export?' + params.toString(); link.download = '历史出库明细.xlsx'; document.body.appendChild(link); link.click(); link.remove(); document.getElementById('outboundExportModal').remove(); } function exportOutboundData() { openOutboundExportModal(); } function loadDataPageData(page) { dataCurrentPage = page; var search = document.getElementById('dataSearch')?.value || ''; var start = document.getElementById('dataDateStart')?.value || ''; var end = document.getElementById('dataDateEnd')?.value || ''; var warehouse = document.getElementById('dataWarehouse')?.value || ''; var shop = document.getElementById('dataShop')?.value || ''; var brand = document.getElementById('dataBrand')?.value || ''; var source = document.getElementById('dataSource')?.value || ''; var url = '/api/data/list?page=' + page + '&size=' + DATA_PAGE_SIZE; if (search) url += '&search=' + encodeURIComponent(search); if (start) url += '&date_from=' + start; if (end) url += '&date_to=' + end; if (warehouse) url += '&warehouse=' + encodeURIComponent(warehouse); if (shop) url += '&shop=' + encodeURIComponent(shop); if (brand) url += '&brand=' + encodeURIComponent(brand); if (source) url += '&source=' + encodeURIComponent(source); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('dataTbody'); if (!tbody) return; if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据'; } else { var startIdx = (page - 1) * DATA_PAGE_SIZE; tbody.innerHTML = data.rows.map(function(r, i) { var srcCn = SOURCE_MAP_CN[r.数据来源] || r.数据来源 || '-'; var wh = (r.数据来源 === 'wdt') ? ((r.仓库 || '').replace(/"/g, '') || '-') : '-'; var sh = (r.数据来源 === 'wdt') ? (r.店铺 || '-') : '-'; var totalCost = parseFloat(r.货品成本) || 0; var qty = parseInt(r.货品数量) || 0; // CSV 中的"货品成本"是该行总成本,不是单价 // 单价 = 总成本 / 数量,金额 = 总成本 var unitCost = (qty > 0 && totalCost > 0) ? totalCost / qty : 0; var unitCostStr = unitCost > 0 ? unitCost.toFixed(4) : '-'; var amountStr = totalCost > 0 ? totalCost.toFixed(2) : '-'; return '' + (startIdx + i + 1) + '' + escHtml(r.日期 || '-') + '' + escHtml(r.货品编号 || '-') + '' + escHtml(r.货品名称 || '-') + '' + escHtml(r.品牌 || '-') + '' + qty + '' + unitCostStr + '' + amountStr + '' + escHtml(wh) + '' + escHtml(sh) + '' + srcCn + ''; }).join(''); } var dataCountEl = document.getElementById('dataCount'); if (dataCountEl) dataCountEl.textContent = '共 ' + (data.total || 0) + ' 条'; var pel = document.getElementById('dataPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, DATA_PAGE_SIZE, 'loadDataPageData', 'DATA_PAGE_SIZE'); }); } window.searchData = function() { loadDataPageData(1); }; window.resetDataFilters = function() { document.getElementById('dataSearch').value = ''; document.getElementById('dataDateStart').value = ''; document.getElementById('dataDateEnd').value = ''; document.getElementById('dataWarehouse').value = ''; document.getElementById('dataShop').value = ''; document.getElementById('dataBrand').value = ''; document.getElementById('dataSource').value = ''; // 清除模糊搜索的 selected 标记 var wh = document.getElementById('dataWarehouse'); if (wh) wh.dataset.selected = ''; var sh = document.getElementById('dataShop'); if (sh) sh.dataset.selected = ''; var br = document.getElementById('dataBrand'); if (br) br.dataset.selected = ''; loadDataPageData(1); // 注册切回页面时自动重新加载 if (!window._pageReloaders) window._pageReloaders = {}; if (!window._pageDataCache) window._pageDataCache = {}; window._pageReloaders['data_detail'] = function() { loadDataPageData(dataCurrentPage || 1); }; window._pageDataCache['data_detail'] = { _ts: Date.now() }; }; // ─── 产品基础表 ─── // 所有产品列定义 const PROD_COLUMNS = [ {field: 'product_code', label: '货品编号', defaultShow: true, sortable: true}, {field: 'product_name', label: '货品名称', defaultShow: true, sortable: true}, {field: 'product_type', label: '货品类型', defaultShow: true, sortable: false}, {field: 'brand', label: '品牌', defaultShow: true, sortable: true}, {field: 'supplier_code', label: '供应商编码', defaultShow: true, sortable: false}, {field: 'supplier_name_match', label: '供应商名称', defaultShow: true, sortable: false}, {field: 'cost_price', label: '成本价', defaultShow: true, sortable: true}, {field: 'lead_time', label: '货期(天)', defaultShow: true, sortable: false}, {field: 'logistics_time', label: '物流时间(天)', defaultShow: true, sortable: false}, {field: 'min_order_qty', label: '最低起订量', defaultShow: true, sortable: false}, {field: 'procurement_follower', label: '采购跟单', defaultShow: true, sortable: false}, {field: 'procurement_planner', label: '采购计划', defaultShow: true, sortable: false}, {field: 'platform', label: '平台', defaultShow: true, sortable: false}, {field: 'operator', label: '运营', defaultShow: true, sortable: false}, {field: 'developer', label: '开发专员', defaultShow: true, sortable: false}, {field: 'created_at', label: '创建时间', defaultShow: true, sortable: true}, {field: 'import_date', label: '导入日期', defaultShow: true, sortable: false}, {field: 'custom_field1', label: '自定义字段1', defaultShow: true, sortable: false}, {field: 'custom_field2', label: '自定义字段2', defaultShow: true, sortable: false}, {field: 'custom_field3', label: '自定义字段3', defaultShow: true, sortable: false}, {field: 'custom_field4', label: '自定义字段4', defaultShow: true, sortable: false}, {field: 'custom_field5', label: '自定义字段5', defaultShow: true, sortable: false}, ]; // 字段显隐状态 if (!window.prodColVisible) { window.prodColVisible = {}; PROD_COLUMNS.forEach(c => { window.prodColVisible[c.field] = c.defaultShow; }); } function toggleProdCol(field) { window.prodColVisible[field] = !window.prodColVisible[field]; document.querySelectorAll(`.col-${field}`).forEach(el => { el.style.display = window.prodColVisible[field] ? '' : 'none'; }); const cb = document.getElementById('col-cb-' + field); if (cb) cb.checked = window.prodColVisible[field]; } // ─── 产品基础表 ─── let prodCurrentPage = 1; let PROD_PAGE_SIZE = 50; let prodSortBy = ''; let prodSortAsc = true; let prodActiveTab = '成品'; // '成品' | '非成品' | '回收站' function loadProductsPage(container) { container.innerHTML = `
成品基础信息表
包材基础信息表
产品回收站

成品信息列表

${window._hasPagePerm('products', 'add') ? '' : ''}
`; // 构建列头 buildProdThead(); // 添加字段设置弹窗 HTML const prodColModalHtml = ` `; if (!document.getElementById('prodColModal')) { container.insertAdjacentHTML('afterend', prodColModalHtml); } // 初始化拖拽顺序 window._prodColDragOrder = PROD_COLUMNS.map(c => c.field); window._prodColDefaultOrder = PROD_COLUMNS.map(c => c.field); if (!window._prodColPendingVisible) { window._prodColPendingVisible = {}; PROD_COLUMNS.forEach(c => { window._prodColPendingVisible[c.field] = c.defaultShow; }); } renderProdColCheckboxes(); // 清除筛选框搜索记忆 ['prodCodeName','prodSupplier','prodTypeFilter'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); // 加载 prodActiveTab = '成品'; updateProdTypeFilterOptions('成品'); applyProdTabColumns('成品'); loadProducts(1); } function applyProdTabColumns(tab) { // 回收站展示产品完整信息;基础信息表沿用各自的列显隐规则。 if (tab === '回收站') { PROD_COLUMNS.forEach(function(c) { window.prodColVisible[c.field] = true; }); } // 包材基础信息表:跳过不需要的列 // 成品基础信息表:恢复所有列显示 var hideFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer']; hideFields.forEach(function(f) { if (tab !== '回收站') window.prodColVisible[f] = tab !== '非成品'; }); // 包材tab显示成本价、货期、物流时间、最低起订量、创建时间 ['cost_price','lead_time','logistics_time','min_order_qty','created_at'].forEach(function(f) { window.prodColVisible[f] = true; }); // 字段设置checkbox状态同步 document.querySelectorAll('#prodColModal input[type="checkbox"]').forEach(function(cb) { var field = cb.id && cb.id.replace('col-cb-', ''); if (field && window.prodColVisible[field] !== undefined) cb.checked = window.prodColVisible[field]; }); } function switchProdTab(tab) { prodActiveTab = tab; document.querySelectorAll('#prodTabs .prod-tab').forEach(function(el) { el.classList.toggle('active', el.dataset.type === tab); }); document.getElementById('prodListTitle').textContent = tab === '成品' ? '成品列表' : (tab === '非成品' ? '包材列表' : '产品回收站'); var toolbar = document.getElementById('prodToolbar'); if (toolbar) toolbar.style.display = tab === '回收站' ? 'none' : 'flex'; var recycleToolbar = document.getElementById('prodRecycleToolbar'); if (recycleToolbar) recycleToolbar.style.display = tab === '回收站' ? 'flex' : 'none'; var addButton = document.getElementById('prodAddButton'); if (addButton) addButton.style.display = tab === '回收站' ? 'none' : ''; // 切页清空筛选框,互不影响 ['prodCodeName','prodSupplier','prodTypeFilter'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); // 更新货品类型下拉选项 updateProdTypeFilterOptions(tab); applyProdTabColumns(tab); // 重建表头使列显隐与当前tab一致 buildProdThead(); loadProducts(1); } function updateProdTypeFilterOptions(tab) { var sel = document.getElementById('prodTypeFilter'); if (!sel) return; var opts = ''; if (tab === '回收站') { opts = ''; } else if (tab === '成品') { // 成品基础表展示全部产品,下拉包含所有类型供筛选 opts = ''; } else { opts = ''; } sel.innerHTML = opts; } function buildProdThead() { var thead = document.getElementById('prodThead'); if (!thead) return; var theadHtml = ''; if (prodActiveTab === '回收站') { theadHtml += ''; } theadHtml += '序号'; PROD_COLUMNS.forEach(function(c) { var vis = window.prodColVisible[c.field] ? '' : ' style="display:none"'; var sortAttr = c.sortable ? ' data-sort="' + c.field + '" class="sortable"' : ''; theadHtml += '' + c.label + ''; }); theadHtml += '回收时间'; theadHtml += '' + (prodActiveTab === '回收站' ? '操作' : '操作') + ''; theadHtml += ''; thead.innerHTML = theadHtml; } function loadProducts(page) { prodCurrentPage = page; var codeName = (document.getElementById('prodCodeName')?.value || '').trim(); var supplier = (document.getElementById('prodSupplier')?.value || '').trim(); var typeFilter = document.getElementById('prodTypeFilter')?.value || ''; if (prodActiveTab === '回收站') { loadRecycleProducts(page); return; } var url = '/api/products/list?page=' + page + '&size=' + PROD_PAGE_SIZE; if (codeName) url += '&search=' + encodeURIComponent(codeName); if (supplier) url += '&supplier=' + encodeURIComponent(supplier); if (prodActiveTab === '成品') { if (typeFilter) { url += '&product_type=' + encodeURIComponent(typeFilter); } else { // 成品基础表:排除包材和其他类 url += '&product_type=' + encodeURIComponent('非包材'); } } else { if (typeFilter) { url += '&product_type=' + encodeURIComponent(typeFilter); } else { // 非成品基础表:仅展示包材和其他 url += '&product_type=' + encodeURIComponent('非成品专用'); } } if (prodSortBy) { url += '&sort_by=' + encodeURIComponent(prodSortBy) + '&sort_order=' + (prodSortAsc ? 'asc' : 'desc'); } else if (prodActiveTab === '非成品') { // 已存在的包材同步后会刷新导入日期,默认排到包材列表前面。 url += '&sort_by=import_date&sort_order=desc'; } // 缓存当前页数据供快速编辑 if (!window._prodRowCache) window._prodRowCache = {}; fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('prodTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据'; } else { var startIdx = (page - 1) * PROD_PAGE_SIZE; // 缓存行数据 data.rows.forEach(function(r) { if (r.product_code) window._prodRowCache[r.product_code] = r; }); tbody.innerHTML = data.rows.map(function(r, i) { var html = '' + (startIdx + i + 1) + ''; PROD_COLUMNS.forEach(function(c) { if (!window.prodColVisible[c.field]) return; var raw = r[c.field]; var val = escHtml(raw || '-'); if (c.field === 'procurement_follower') { val = renderNameBadges(raw); } if (c.field === 'product_code') { val = '' + val + ''; } if (c.field === 'product_type') { var typeColors = {'成品':'tag-blue','包材':'tag-orange','赠品':'tag-green','小样':'tag-purple','物料':'tag-orange'}; var cls = typeColors[raw] || 'tag-grey'; val = '' + val + ''; } if (c.field === 'cost_price') { var p = parseFloat(raw); val = isNaN(p) ? '-' : '¥' + p.toFixed(2); } if (c.field === 'min_order_qty') { var q = parseInt(raw); val = isNaN(q) ? '-' : q.toLocaleString(); } if (c.field === 'created_at') { val = escHtml((raw || '').toString().slice(0, 10) || '-'); } html += '' + val + ''; }); var pcode = r.product_code || ''; html += ''; // 产品编码可能包含特殊字符(如 &, 空格等),必须按 JS 字符串转义,避免详情/编辑按钮传参异常。 html += ' '; html += ' '; if (prodActiveTab === '成品') html += ' '; if (prodActiveTab === '成品') { html += ''; } else { html += ''; } return '' + html + ''; }).join(''); } document.getElementById('prodCount').textContent = '共 ' + (data.total || 0) + ' 条'; var pel = document.getElementById('prodPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, PROD_PAGE_SIZE, 'loadProducts', 'PROD_PAGE_SIZE'); }); } window.searchProducts = function() { loadProducts(1); }; window.resetProductFilters = function() { document.getElementById('prodCodeName').value = ''; document.getElementById('prodSupplier').value = ''; var tf = document.getElementById('prodTypeFilter'); if (tf) tf.value = ''; loadProducts(1); }; function loadRecycleProducts(page) { var codeName = (document.getElementById('prodCodeName')?.value || '').trim(); var supplier = (document.getElementById('prodSupplier')?.value || '').trim(); var typeFilter = document.getElementById('prodTypeFilter')?.value || ''; var url = '/api/products/recycle_bin?page=' + page + '&size=' + PROD_PAGE_SIZE; if (codeName) url += '&search=' + encodeURIComponent(codeName); if (supplier) url += '&supplier=' + encodeURIComponent(supplier); if (typeFilter) url += '&product_type=' + encodeURIComponent(typeFilter); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert(res.msg || '回收站加载失败'); return; } var data = res.data || {}; var tbody = document.getElementById('prodTbody'); if (!tbody) return; var selectAll = document.getElementById('prodRecycleSelectAll'); if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; } if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '回收站暂无数据'; } else { if (!window._prodRowCache) window._prodRowCache = {}; data.rows.forEach(function(r) { if (r.product_code) window._prodRowCache[r.product_code] = r; }); tbody.innerHTML = data.rows.map(function(r, i) { var codeValue = escHtml(r.product_code || ''); var html = ''; html += '' + ((page - 1) * PROD_PAGE_SIZE + i + 1) + ''; PROD_COLUMNS.forEach(function(c) { var raw = r[c.field]; var val = escHtml(raw == null || raw === '' ? '-' : String(raw)); if (c.field === 'product_code') val = '' + val + ''; if (c.field === 'product_type') { var colors = {'成品':'tag-blue','包材':'tag-orange','赠品':'tag-green','小样':'tag-purple','物料':'tag-orange'}; val = '' + val + ''; } if (c.field === 'created_at' || c.field === 'import_date') val = escHtml(raw ? String(raw).slice(0, 10) : '-'); if (c.field === 'cost_price') { var price = parseFloat(raw); val = isNaN(price) ? '-' : '¥' + price.toFixed(2); } if (c.field === 'min_order_qty') { var qty = parseInt(raw); val = isNaN(qty) ? '-' : qty.toLocaleString(); } html += '' + val + ''; }); var pcode = escHtml(r.product_code || ''); html += '' + escHtml(r.deleted_at ? String(r.deleted_at).slice(0, 16) : '-') + ''; html += ''; html += ''; html += ''; return '' + html + ''; }).join(''); } document.getElementById('prodCount').textContent = '共 ' + (data.total || 0) + ' 条'; var pel = document.getElementById('prodPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, PROD_PAGE_SIZE, 'loadProducts', 'PROD_PAGE_SIZE'); }); } window.getSelectedRecycleCodes = function() { return Array.prototype.map.call( document.querySelectorAll('.prod-recycle-checkbox:checked'), function(cb) { return cb.value; } ); }; window.toggleRecycleSelectAll = function(checked) { document.querySelectorAll('.prod-recycle-checkbox').forEach(function(cb) { cb.checked = checked; }); syncRecycleSelectAll(); }; window.syncRecycleSelectAll = function() { var all = document.querySelectorAll('.prod-recycle-checkbox'); var checked = document.querySelectorAll('.prod-recycle-checkbox:checked'); var selectAll = document.getElementById('prodRecycleSelectAll'); if (!selectAll) return; selectAll.checked = all.length > 0 && checked.length === all.length; selectAll.indeterminate = checked.length > 0 && checked.length < all.length; }; window.batchRestoreRecycleProducts = function() { var codes = getSelectedRecycleCodes(); if (codes.length === 0) { alert('请先勾选需要恢复的产品'); return; } showCenteredConfirm('确认', '确认批量恢复已勾选的 ' + codes.length + ' 个产品吗?').then(function(ok) { if (!ok) return; fetch('/api/product/batch_restore', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: codes}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { codes.forEach(function(code) { if (window._prodRowCache) delete window._prodRowCache[code]; }); loadProducts(prodCurrentPage); } else { alert(res.msg || '批量恢复失败'); } }); }); }; window.batchDeleteRecycleProducts = function() { var codes = getSelectedRecycleCodes(); if (codes.length === 0) { alert('请先勾选需要删除的产品'); return; } var existing = document.getElementById('recycleBatchDeleteConfirmOverlay'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.id = 'recycleBatchDeleteConfirmOverlay'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); overlay.dataset.codes = JSON.stringify(codes); document.body.appendChild(overlay); }; window.confirmBatchDeleteRecycleProducts = function() { var overlay = document.getElementById('recycleBatchDeleteConfirmOverlay'); if (!overlay) return; var codes = JSON.parse(overlay.dataset.codes || '[]'); overlay.remove(); fetch('/api/product/batch_permanent_delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: codes}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { codes.forEach(function(code) { if (window._prodRowCache) delete window._prodRowCache[code]; }); loadProducts(prodCurrentPage); } else { alert(res.msg || '批量删除失败'); } }); }; window.restoreProductFromTab = function(productCode) { if (!productCode) return; showCenteredConfirm('确认', '确认将产品 ' + productCode + ' 恢复到成品基础信息表吗?').then(function(ok) { if (!ok) return; fetch('/api/product/restore', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { if (window._prodRowCache) delete window._prodRowCache[productCode]; loadProducts(prodCurrentPage); } else { alert(res.msg || '恢复失败'); } }); }); }; window.deleteRecycleProduct = function(productCode) { if (!productCode) return; var existing = document.getElementById('recycleDeleteConfirmOverlay'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.id = 'recycleDeleteConfirmOverlay'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); }; window.confirmDeleteRecycleProduct = function(productCode) { var overlay = document.getElementById('recycleDeleteConfirmOverlay'); if (overlay) overlay.remove(); fetch('/api/product/permanent_delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { if (window._prodRowCache) delete window._prodRowCache[productCode]; loadProducts(prodCurrentPage); } else { alert(res.msg || '删除失败'); } }); }; // ─── 新增产品 ─── // 在新标签页打开链接(隐藏a标签模拟点击,避免浏览器弹窗拦截提示) window.safeOpenUrl = function(url) { var a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; function openProductAddModal() { var modal = document.getElementById('prodAddModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'prodAddModal'; modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); updateAddTypeOptions(); updateAddFieldsVisibility(); window.applyPlannerLogic(); } else { modal.style.display = 'flex'; // 清空表单 modal.querySelectorAll('input, select').forEach(function(el) { el.value = ''; }); updateAddTypeOptions(); updateAddFieldsVisibility(); window.applyPlannerLogic(); } } function updateAddFieldsVisibility() { var hideIds = ['pa_brand','pa_lead','pa_logis','pa_follower','pa_planner','pa_platform','pa_operator','pa_developer','pa_created']; var isNonProd = prodActiveTab === '非成品'; hideIds.forEach(function(id) { var el = document.getElementById(id); // el的父级div就是grid的直接子元素 if (el && el.parentElement) { el.parentElement.style.display = isNonProd ? 'none' : ''; } }); } function updateAddTypeOptions() { var sel = document.getElementById('pa_type'); if (!sel) return; if (prodActiveTab === '非成品') { sel.innerHTML = ''; } else { sel.innerHTML = ''; } } function closeProductAddModal() { var modal = document.getElementById('prodAddModal'); if (modal) modal.style.display = 'none'; var dd = document.getElementById('pa_supDropdown'); if (dd) dd.style.display = 'none'; var fd = document.getElementById('pa_folDropdown'); if (fd) fd.style.display = 'none'; var bd = document.getElementById('pa_brandDropdown'); if (bd) bd.style.display = 'none'; } function submitProductAdd() { var data = { product_code: document.getElementById('pa_code').value.trim(), product_name: document.getElementById('pa_name').value.trim(), brand: document.getElementById('pa_brand').value.trim(), product_type: document.getElementById('pa_type').value, lead_time: document.getElementById('pa_lead').value, logistics_time: document.getElementById('pa_logis').value, supplier_code: document.getElementById('pa_supcode').value.trim(), supplier_name: document.getElementById('pa_supname').value.trim(), procurement_follower: document.getElementById('pa_follower').value.trim(), procurement_planner: document.getElementById('pa_planner').value.trim(), operator: document.getElementById('pa_operator').value.trim(), developer: document.getElementById('pa_developer').value.trim(), platform: document.getElementById('pa_platform').value.trim(), created_at: document.getElementById('pa_created')?.value || null, custom_field1: document.getElementById('pa_custom1')?.value.trim() || '', custom_field2: document.getElementById('pa_custom2')?.value.trim() || '', custom_field3: document.getElementById('pa_custom3')?.value.trim() || '', custom_field4: document.getElementById('pa_custom4')?.value.trim() || '', custom_field5: document.getElementById('pa_custom5')?.value.trim() || '' }; if (!data.product_code || !data.product_name) { alert('货品编号和名称不能为空'); return; } if (!data.brand) { alert('品牌不能为空'); return; } if (!data.product_type) { alert('货品类型不能为空'); return; } if (!data.supplier_name) { alert('供应商名称不能为空'); return; } if (!data.lead_time) { alert('货期不能为空'); return; } if (!data.logistics_time) { alert('物流时间不能为空'); return; } // 采购计划:海外品手动填写,其余按供应商自动带入 var overseas = window.applyPlannerLogic(); window.applySupplierFollower(data.supplier_code, data.supplier_name); data.procurement_follower = document.getElementById('pa_follower').value.trim(); data.procurement_planner = document.getElementById('pa_planner').value.trim(); if (overseas && !data.procurement_planner) { alert('海外品的采购计划需要手动填写'); return; } function afterResolve() { data.procurement_follower = document.getElementById('pa_follower').value.trim(); if (!overseas) data.procurement_planner = document.getElementById('pa_planner').value.trim(); if (!data.procurement_follower) { alert('未能获取跟单小组:该供应商在供应商管理中未维护跟单小组,也没有历史产品记录,请先在供应商管理中维护'); return; } if (!overseas && !data.procurement_planner) { alert('未能获取采购计划:该供应商在供应商管理中未维护采购计划,也没有历史产品记录,请先在供应商管理中维护'); return; } doSubmitProductAdd(data); } // 本地拿不到时向后端解析一次再提交 if (!data.procurement_follower || (!overseas && !data.procurement_planner)) { window.resolveSupplierBound(data.supplier_code, data.supplier_name).then(afterResolve); return; } doSubmitProductAdd(data); } function doSubmitProductAdd(data) { window.saveFollowerHistory(data.procurement_follower); if (!data.procurement_planner) { alert('采购计划不能为空'); return; } fetch('/api/product/create', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { var newCode = data.product_code; closeProductAddModal(); loadProducts(1); openBomBindModal(newCode); } else { alert('❌ ' + (res.msg || '创建失败')); } }) .catch(function() { alert('❌ 网络错误'); }); } // ─── 新建成品BOM绑定弹窗 ─── // ─── 生成单行BOM绑定行 ─── window._bomBindRowHtml = function(idx) { var html = ''; html += '' + idx + ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '✕'; html += ''; return html; }; window.openBomBindModal = function(productCode) { if (!productCode) return; var overlay = document.createElement('div'); overlay.id = 'bomBindModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); // 第一行自动绑定供应商搜索 window._bindBomSupEvents(document.querySelectorAll('#bomBindTbody .bom-sup-name')); }; window._bindBomSupEvents = function(els) { [].forEach.call(els, function(el) { el.removeEventListener('input', window._bomSupInputHandler); el.removeEventListener('focus', window._bomSupFocusHandler); el.addEventListener('input', window._bomSupInputHandler); el.addEventListener('focus', window._bomSupFocusHandler); }); }; window._bomSupInputHandler = function() { var dd = this.parentElement.querySelector('.bom-sup-dd'); if (!dd) return; var val = this.value.trim(); if (!val || !window._supplierList) { dd.style.display = 'none'; return; } var v = val.toLowerCase(); var matched = window._supplierList.filter(function(s) { return (s.supplier_name || '').toLowerCase().indexOf(v) >= 0; }); if (matched.length === 0 || matched.length > 50) { dd.style.display = 'none'; return; } dd.innerHTML = ''; // fixed定位避免被overflow裁剪 var rect = this.getBoundingClientRect(); dd.style.position = 'fixed'; dd.style.top = (rect.bottom + 2) + 'px'; dd.style.left = rect.left + 'px'; dd.style.width = rect.width + 'px'; dd.style.display = 'block'; dd.style.zIndex = 99999; matched.slice(0, 20).forEach(function(s) { var item = document.createElement('div'); item.style.cssText = 'padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0;line-height:1.6'; item.className = 'bom-sup-dd-item'; item.dataset.code = s.supplier_code || ''; item.dataset.name = s.supplier_name || ''; item.innerHTML = '' + escHtml(s.supplier_name) + '' + (s.supplier_code ? ' ' + escHtml(s.supplier_code) + '' : ''); dd.appendChild(item); }); // 滚动时隐藏 }; // 事件委托:点击供应商下拉选项 window.addEventListener('click', function(e) { // BOM绑定弹窗供应商选择 var item = e.target.closest('.bom-sup-dd-item'); if (item) { var row = item.closest('tr'); if (row) { var supCode = row.querySelector('.bom-sup-code'); var supName = row.querySelector('.bom-sup-name'); if (supCode) supCode.value = item.dataset.code; if (supName) supName.value = item.dataset.name; var dd = row.querySelector('.bom-sup-dd'); if (dd) dd.style.display = 'none'; } else { // 非表格行内(如新增弹窗成品供应商) var parent = item.closest('[style*="position:relative"]'); if (parent) { var inp = parent.querySelector('.bom-sup-name'); if (inp) inp.value = item.dataset.name; var dd = parent.querySelector('.bom-sup-dd'); if (dd) dd.style.display = 'none'; } } return; } // 编辑弹窗供应商选择 var pe = e.target.closest('.pe-sup-dd-item'); if (pe) { var nameInput = document.getElementById('pe_supplier_name_match'); var codeInput = document.getElementById('pe_supplier_code'); if (nameInput) nameInput.value = pe.dataset.name; if (codeInput) codeInput.value = pe.dataset.code; var dd = pe.closest('.pe-sup-dd'); if (dd) dd.style.display = 'none'; return; } }); window._bomSupFocusHandler = function() { // focus时不操作,由input触发 }; window.addBomBindRow = function() { var tbody = document.getElementById('bomBindTbody'); if (!tbody) return; var idx = tbody.querySelectorAll('.bom-bind-row').length + 1; tbody.insertAdjacentHTML('beforeend', window._bomBindRowHtml(idx)); window._bindBomSupEvents(tbody.querySelectorAll('.bom-bind-row:last-child .bom-sup-name')); }; window.removeBomBindRow = function(el) { var row = el.closest('tr'); if (row) row.remove(); var tbody = document.getElementById('bomBindTbody'); if (tbody) { [].forEach.call(tbody.querySelectorAll('.bom-bind-row'), function(r, i) { r.cells[0].textContent = i + 1; }); } }; window.submitBomBind = function(productCode) { var tbody = document.getElementById('bomBindTbody'); if (!tbody) return; var materials = []; [].forEach.call(tbody.querySelectorAll('.bom-bind-row'), function(row) { var code = (row.querySelector('.bom-mat-code')?.value || '').trim(); var name = (row.querySelector('.bom-mat-name')?.value || '').trim(); var supCode = (row.querySelector('.bom-sup-code')?.value || '').trim(); var supName = (row.querySelector('.bom-sup-name')?.value || '').trim(); var qty = parseFloat(row.querySelector('.bom-mat-qty')?.value) || 1; if (code) { materials.push({material_code: code, material_name: name, quantity: qty, supplier_code: supCode, supplier_name: supName}); } }); if (materials.length === 0) { alert('请填写至少一行物料信息'); return; } var btn = document.querySelector('.btn-primary[onclick*="submitBomBind"]'); if (btn) { btn.disabled = true; btn.textContent = '绑定中...'; } fetch('/api/bom/create_with_materials', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, materials: materials}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (btn) { btn.disabled = false; btn.textContent = '✅ 确认绑定'; } if (res.code === 0) { alert('✅ ' + res.msg); document.getElementById('bomBindModal').remove(); } else { alert('❌ ' + (res.msg || '绑定失败')); } }) .catch(function() { if (btn) { btn.disabled = false; btn.textContent = '✅ 确认绑定'; } alert('❌ 网络错误'); }); }; // ─── 查看成品BOM弹窗 ─── window.viewProductBom = function(productCode) { fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert(res.msg || '获取BOM失败'); return; } var materials = (res.data && res.data.bom_materials) || []; var old = document.getElementById('viewBomModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'viewBomModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var bodyHtml = ''; if (materials.length === 0) { bodyHtml = '暂无绑定包材'; } else { materials.forEach(function(m, i) { bodyHtml += '' + (i + 1) + '' + escHtml(m.material_code || '-') + '' + escHtml(m.material_name || '-') + '' + m.bom_qty + '' + escHtml(m.material_supplier || '-') + ''; }); } overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); }); }; // ─── 同步BOM数据到物料清单管理 ─── window.syncProductBom = function(productCode) { showCenteredConfirm('确认', '确认将 [' + productCode + '] 的包材数据同步到物料清单管理吗?').then(function(ok) { if (!ok) return; var btn = document.querySelector('.modal-footer .btn[onclick*="syncProductBom"]'); if (btn) { btn.disabled = true; btn.textContent = '同步中...'; } fetch('/api/product/sync_bom', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (btn) { btn.disabled = false; btn.textContent = '🔄 同步'; } if (res.code === 0) { alert('✅ ' + res.msg); // 刷新弹窗 document.getElementById('viewBomModal').remove(); window.viewProductBom(productCode); } else { alert('❌ ' + (res.msg || '同步失败')); } }) .catch(function() { if (btn) { btn.disabled = false; btn.textContent = '🔄 同步'; } alert('❌ 网络错误'); }); }); }; window.confirmCloseBomBind = function(productCode) { var rows = document.getElementById('bomBindTbody'); var hasData = false; if (rows) { [].forEach.call(rows.querySelectorAll('.bom-bind-row input'), function(inp) { if (inp.value.trim()) hasData = true; }); } if (hasData) { showCenteredConfirm('确认', '有未保存的物料信息,确定跳过吗?').then(function(ok) { if (ok) document.getElementById('bomBindModal').remove(); }); } else { document.getElementById('bomBindModal').remove(); } }; // ─── 产品导出 ─── // ─── 产品导出 ─── function exportProducts() { var type = prodActiveTab === '成品' ? '非包材' : '非成品专用'; // 关闭已有弹窗 var old = document.getElementById('prodExportModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'prodExportModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; // 导出可选字段 var exportFields = [ {field:'product_code', label:'货品编号'}, {field:'product_name', label:'货品名称'}, {field:'product_type', label:'货品类型'}, {field:'brand', label:'品牌'}, {field:'supplier_code', label:'供应商编码'}, {field:'supplier_name', label:'供应商名称'}, {field:'lead_time', label:'货期(天)'}, {field:'logistics_time', label:'物流时间(天)'}, {field:'procurement_follower', label:'采购跟单'}, {field:'procurement_planner', label:'采购计划'}, {field:'platform', label:'平台'}, {field:'operator', label:'运营'}, {field:'developer', label:'开发专员'}, {field:'created_at', label:'创建时间'}, {field:'import_date', label:'导入日期'}, ]; var checkboxesHtml = ''; exportFields.forEach(function(f) { var checked = window.prodColVisible && window.prodColVisible[f.field] !== false ? 'checked' : ''; checkboxesHtml += ''; }); overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); } window.toggleExportAll = function(checked) { document.querySelectorAll('.export-field-cb').forEach(function(cb) { cb.checked = checked; }); }; window.confirmExportProducts = function() { var type = prodActiveTab === '成品' ? '非包材' : '非成品专用'; var selected = []; document.querySelectorAll('.export-field-cb:checked').forEach(function(cb) { selected.push(cb.value); }); if (selected.length === 0) { alert('请至少选择一个字段'); return; } // 关闭弹窗 var overlay = document.getElementById('prodExportModal'); if (overlay) overlay.remove(); var url = '/api/products/export?product_type=' + encodeURIComponent(type) + '&fields=' + encodeURIComponent(selected.join(',')); safeOpenUrl(url); }; // ─── 产品导入 ─── function openProductImportModal() { // 每次打开重建弹窗,避免旧缓存导致元素缺失 var oldModal = document.getElementById('prodImportModal'); if (oldModal) oldModal.remove(); var modal = document.createElement('div'); modal.id = 'prodImportModal'; modal.className = 'modal-overlay'; modal.style.display = 'flex'; // 根据当前tab决定模板下载链接 var isPackaging = prodActiveTab === '非成品'; var templateUrl = isPackaging ? '/api/products/template?type=包材' : '/api/products/template'; var templateLabel = isPackaging ? '📥 下载包材导入模板' : '📥 下载导入模板'; modal.innerHTML = ` `; document.body.appendChild(modal); } function closeProductImportModal() { var modal = document.getElementById('prodImportModal'); if (modal) modal.style.display = 'none'; } function previewProductImportFile() { var fileInput = document.getElementById('prodImportFile'); var preview = document.getElementById('prodImportPreview'); var btn = document.getElementById('prodImportBtn'); if (fileInput.files && fileInput.files.length > 0) { preview.textContent = '已选择:' + fileInput.files[0].name + '(' + (fileInput.files[0].size / 1024).toFixed(1) + 'KB)'; btn.disabled = false; } else { preview.textContent = ''; btn.disabled = true; } } function submitProductImport() { var fileInput = document.getElementById('prodImportFile'); if (!fileInput.files || !fileInput.files[0]) return; var formData = new FormData(); formData.append('file', fileInput.files[0]); document.getElementById('prodImportBtn').disabled = true; document.getElementById('prodImportBtn').textContent = '导入中...'; fetch('/api/products/import', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { alert(res.msg || (res.code === 0 ? '✅ 导入成功' : '❌ 导入失败')); closeProductImportModal(); loadProducts(1); }) .catch(function() { alert('❌ 网络错误'); }) .finally(function() { document.getElementById('prodImportBtn').disabled = false; document.getElementById('prodImportBtn').textContent = '开始导入'; }); } // ─── 产品基础表 字段设置弹窗 ─── function renderProdColCheckboxes() { const grid = document.getElementById('prodColGrid'); if (!grid) return; grid.innerHTML = ''; const order = window._prodColDragOrder || PROD_COLUMNS.map(function(c) { return c.field; }); var tab = prodActiveTab; var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer']; order.forEach(function(field) { const col = PROD_COLUMNS.find(function(c) { return c.field === field; }); if (!col) return; // 包材tab隐藏不可用字段 if (tab === '非成品' && skipFields.indexOf(field) >= 0) return; const checked = window._prodColPendingVisible[field] !== false ? 'checked' : ''; const label = document.createElement('label'); label.draggable = true; label.dataset.field = field; label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px'; label.innerHTML = '⠿ ' + col.label; label.addEventListener('dragstart', function(e) { e.dataTransfer.setData('text/plain', field); this.style.opacity = '0.4'; }); label.addEventListener('dragend', function(e) { this.style.opacity = '1'; document.querySelectorAll('#prodColGrid label').forEach(function(el) { el.style.border = ''; }); }); label.addEventListener('dragover', function(e) { e.preventDefault(); document.querySelectorAll('#prodColGrid label').forEach(function(el) { el.style.border = ''; }); this.style.border = '2px dashed var(--primary)'; }); label.addEventListener('dragleave', function(e) { this.style.border = ''; }); label.addEventListener('drop', function(e) { e.preventDefault(); document.querySelectorAll('#prodColGrid label').forEach(function(el) { el.style.border = ''; }); const fromField = e.dataTransfer.getData('text/plain'); const toField = this.dataset.field; if (fromField === toField) return; const order = window._prodColDragOrder; const fromIdx = order.indexOf(fromField); const toIdx = order.indexOf(toField); if (fromIdx === -1 || toIdx === -1) return; order.splice(fromIdx, 1); order.splice(toIdx, 0, fromField); renderProdColCheckboxes(); }); grid.appendChild(label); }); // 同步全选状态 const allCb = document.getElementById('prod-cb-all'); if (allCb) { allCb.checked = PROD_COLUMNS.every(function(c) { return window._prodColPendingVisible[c.field] !== false; }); } } window.openProdColModal = function() { window._prodColDragOrder = PROD_COLUMNS.map(function(c) { return c.field; }); const grid = document.getElementById('prodColGrid'); if (grid) renderProdColCheckboxes(); PROD_COLUMNS.forEach(function(c) { window._prodColPendingVisible[c.field] = window.prodColVisible[c.field] !== false; const cb = document.getElementById('prod-cb-' + c.field); if (cb) cb.checked = window.prodColVisible[c.field] !== false; }); const allCb = document.getElementById('prod-cb-all'); if (allCb) { allCb.checked = PROD_COLUMNS.every(function(c) { return window.prodColVisible[c.field] !== false; }); } const searchInput = document.getElementById('prodColSearchInput'); if (searchInput) { searchInput.value = ''; } filterProdColCheckboxes(); document.getElementById('prodColModal').style.display = 'flex'; }; window.confirmProdCol = function() { // 应用拖拽后的新顺序 const newOrder = window._prodColDragOrder; if (newOrder) { const reordered = []; newOrder.forEach(function(field) { const col = PROD_COLUMNS.find(function(c) { return c.field === field; }); if (col) reordered.push(col); }); PROD_COLUMNS.forEach(function(c) { if (newOrder.indexOf(c.field) === -1) reordered.push(c); }); PROD_COLUMNS.length = 0; reordered.forEach(function(c) { PROD_COLUMNS.push(c); }); } PROD_COLUMNS.forEach(function(c) { window.prodColVisible[c.field] = window._prodColPendingVisible[c.field] !== false; }); // 重建表头 let theadHtml = '序号'; PROD_COLUMNS.forEach(function(c) { const vis = window.prodColVisible[c.field] ? '' : ' style="display:none"'; const sortAttr = c.sortable ? ' data-sort="' + c.field + '" class="sortable"' : ''; theadHtml += '' + c.label + ''; }); theadHtml += '操作'; theadHtml += ''; const thead = document.getElementById('prodThead'); if (thead) thead.innerHTML = theadHtml; // 刷新数据 loadProducts(1); document.getElementById('prodColModal').style.display = 'none'; }; window.closeProdColModal = function() { document.getElementById('prodColModal').style.display = 'none'; }; window.resetProdCol = function() { var defaultOrder = window._prodColDefaultOrder || PROD_COLUMNS.map(function(c) { return c.field; }); // 恢复拖拽顺序为默认 window._prodColDragOrder = defaultOrder.slice(); // 恢复拖拽UI renderProdColCheckboxes(); // 恢复字段显隐为默认 PROD_COLUMNS.forEach(function(c) { window._prodColPendingVisible[c.field] = c.defaultShow; var cb = document.getElementById('prod-cb-' + c.field); if (cb) cb.checked = c.defaultShow; }); // 全选checkbox同步 var allCb = document.getElementById('prod-cb-all'); if (allCb) { allCb.checked = PROD_COLUMNS.every(function(c) { return c.defaultShow; }); } // 清空搜索框 var searchEl = document.getElementById('prodColSearchInput'); if (searchEl) { searchEl.value = ''; window.filterProdColCheckboxes(); } }; window.toggleProdColPending = function(field) { window._prodColPendingVisible[field] = !window._prodColPendingVisible[field]; const allCb = document.getElementById('prod-cb-all'); if (allCb) { allCb.checked = PROD_COLUMNS.every(function(c) { return window._prodColPendingVisible[c.field] !== false; }); } }; window.toggleProdColPendingAll = function(checked) { PROD_COLUMNS.forEach(function(c) { window._prodColPendingVisible[c.field] = checked; const cb = document.getElementById('prod-cb-' + c.field); if (cb) cb.checked = checked; }); }; window.filterProdColCheckboxes = function() { const keyword = (document.getElementById('prodColSearchInput')?.value || '').trim(); const labels = document.querySelectorAll('#prodColCheckboxes label'); labels.forEach(function(label) { if (!keyword) { label.style.display = ''; } else { const text = label.textContent || ''; label.style.display = text.includes(keyword) ? '' : 'none'; } }); }; // ─── 仓库管理 ─── var WH_PAGE_SIZE = 15; var WH_PAGE_SIZE_OPTIONS = [15, 20, 50, 100]; var whCurrentPage = 1; function loadWarehousePage(container) { container.innerHTML = `

仓库列表

序号仓库编号仓库名称仓库分类仓库属性停用备注操作

➕ 添加/编辑仓库

`; fetchWarehouses(1); } function fetchWarehouses(page) { whCurrentPage = page || 1; var url = '/api/warehouse/list?page=' + whCurrentPage + '&size=' + WH_PAGE_SIZE; fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var data = res.data || {}; var rows = data.rows || []; var total = data.total || 0; var offset = (whCurrentPage - 1) * WH_PAGE_SIZE; var tbody = document.getElementById('whTbody'); tbody.innerHTML = rows.map(function(w, i) { var code = escHtml(w.warehouse_code || '-'); var wp = w.warehouse_property || ''; var wpValues = wp.split(',').map(function(p) { return p.trim(); }).filter(Boolean); var dp = w.disabled || '否'; // 仓库属性:选中项显示为标签 var wpLabels = wpValues.length ? wpValues.map(function(p) { return '' + escHtml(p) + ''; }).join('') : '-'; // 停用:选中状态 var dpLabel = dp === '是' ? '是' : '否'; return '' + (offset + i + 1) + '' + code + '' + escHtml(w.warehouse_name || '-') + '' + escHtml(w.category || '-') + '' + '' + '
' + '' + '' + '' + '
' + '' + '' + '' + escHtml(w.remark || '-') + '' + ''; ''; }).join('') || '暂无数据'; var pel = document.getElementById('whPagination'); if (pel) { var totalPages = Math.ceil(total / WH_PAGE_SIZE); if (totalPages <= 1) { pel.innerHTML = ''; } else { var ph = '
'; ph += ''; ph += ''; var start = Math.max(1, whCurrentPage - 2); var end = Math.min(totalPages, whCurrentPage + 2); for (var i = start; i <= end; i++) { ph += ''; } ph += ''; ph += ''; ph += '' + whCurrentPage + '/' + totalPages + '页 共' + total + '条'; ph += '
'; pel.innerHTML = ph; } } }); } window.saveWarehouse = function() { const code = document.getElementById('whCode').value.trim(); const name = document.getElementById('whName').value.trim(); const category = document.getElementById('whCategory').value; const remark = (document.getElementById('whRemark')?.value || '').trim(); // 仓库属性 var wpVals = []; if (document.getElementById('whPropYuncang') && document.getElementById('whPropYuncang').checked) wpVals.push('云仓'); if (document.getElementById('whPropWaicang') && document.getElementById('whPropWaicang').checked) wpVals.push('外仓'); if (document.getElementById('whPropHaiwaicang') && document.getElementById('whPropHaiwaicang').checked) wpVals.push('海外仓'); var warehouse_property = wpVals.join(','); var disabled = document.getElementById('whDisabled') ? document.getElementById('whDisabled').value : '否'; if (!code || !name) { document.getElementById('whSaveMsg').innerHTML = '⚠️ 编号和名称不能为空'; return; } const btn = document.querySelector('.btn-primary[onclick="saveWarehouse()"]'); btn.disabled = true; btn.textContent = '保存中...'; fetch('/api/warehouse/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({warehouse_code: code, warehouse_name: name, category: category, remark: remark, warehouse_property: warehouse_property, disabled: disabled}) }) .then(r => r.json()) .then(res => { btn.disabled = false; btn.textContent = '💾 保存仓库'; if (res.code === 0) { document.getElementById('whSaveMsg').innerHTML = '✅ ' + (res.msg || '保存成功') + ''; document.getElementById('whCode').value = ''; document.getElementById('whName').value = ''; fetchWarehouses(1); } else { document.getElementById('whSaveMsg').innerHTML = '⚠️ ' + (res.msg || '保存失败') + ''; } }).catch(() => { btn.disabled = false; btn.textContent = '💾 保存仓库'; document.getElementById('whSaveMsg').innerHTML = '⚠️ 网络错误'; }); } var _warehousePropertySaveQueue = {}; window.updateWhProperty = function(code) { // 将同一仓库的连续勾选操作串行提交,避免请求乱序覆盖最后一次选择。 var rowCbs = []; var rows = document.querySelectorAll('#whTbody tr'); for (var ri = 0; ri < rows.length; ri++) { var codeCell = rows[ri].querySelector('td:nth-child(2) code'); if (codeCell && codeCell.textContent.trim() === code) { rowCbs = rows[ri].querySelectorAll('input[type="checkbox"][data-wp]'); break; } } var vals = []; rowCbs.forEach(function(cb) { if (cb.checked) vals.push(cb.getAttribute('data-wp')); }); var propVal = vals.join(','); var previous = _warehousePropertySaveQueue[code] || Promise.resolve(); _warehousePropertySaveQueue[code] = previous.then(function() { return fetch('/api/warehouse/update_field', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({warehouse_code: code, field: 'warehouse_property', value: propVal}) }).then(function(r) { return r.json(); }); }).then(function(res) { if (res.code !== 0) { alert('保存失败: ' + (res.msg || '')); return; } // 仓库属性变更会影响计划预测的云仓/外仓库存,立即重算当前页面并清理缓存页。 if (res.cache_refreshed) refreshWarehousePropertyLinkedPages(); }).catch(function() {}); }; function refreshWarehousePropertyLinkedPages() { ['planning', 'products', 'purchase', 'purchase_ready'].forEach(function(page) { if (!window._pageCache || !window._pageCache[page]) return; var container = window._pageCache[page]; if (container.isConnected) { if (page === 'planning' && typeof loadPlanPage === 'function') loadPlanPage(1); else if (page === 'products' && typeof loadProducts === 'function') loadProducts(1); else if (page === 'purchase' && typeof loadPendingList === 'function') loadPendingList(); else if (page === 'purchase_ready' && typeof loadBackupList === 'function') loadBackupList(false); } else { delete window._pageCache[page]; if (window._pageDataCache) delete window._pageDataCache[page]; } }); } // 停用下拉变更 window.updateWhDisabled = function(code, val) { fetch('/api/warehouse/update_field', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({warehouse_code: code, field: 'disabled', value: val}) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) { alert('保存失败: ' + (res.msg || '')); } }).catch(function() {}); }; window.editWarehouse = function(btn) { // 从按钮所在行获取仓库数据 var row = btn.parentElement.parentElement; var cells = row.cells; if (cells.length < 7) return; var code = cells[1].textContent.trim(); var name = cells[2].textContent.trim(); var category = cells[3].textContent.trim(); // 读取仓库属性(从复选框获取) var wpCheckboxes = row.querySelectorAll('input[type="checkbox"][data-wp]'); var wpVals = []; wpCheckboxes.forEach(function(cb) { if (cb.checked) wpVals.push(cb.getAttribute('data-wp')); }); var warehouseProperty = wpVals.join(','); var warehousePropertyValues = warehouseProperty.split(',').filter(Boolean); // 读取停用 var dpSelect = row.querySelector('select'); var disabledVal = dpSelect ? dpSelect.value : '否'; var remark = cells[6] ? cells[6].textContent.trim() : ''; // 移除旧弹窗 var oldModal = document.getElementById('editWarehouseModal'); if (oldModal) oldModal.remove(); // 创建弹窗 var modal = document.createElement('div'); modal.id = 'editWarehouseModal'; modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ''; document.body.appendChild(modal); }; window.saveEditedWarehouse = function() { var code = document.getElementById('editWhCode').value.trim(); var name = document.getElementById('editWhName').value.trim(); var category = document.getElementById('editWhCategory').value; // 仓库属性 var wpVals = []; if (document.getElementById('editWhPropYuncang').checked) wpVals.push('云仓'); if (document.getElementById('editWhPropWaicang').checked) wpVals.push('外仓'); if (document.getElementById('editWhPropHaiwaicang').checked) wpVals.push('海外仓'); var warehouseProperty = wpVals.join(','); var disabled = document.getElementById('editWhDisabled').value; var remark = document.getElementById('editWhRemark').value.trim(); var msgEl = document.getElementById('editWhSaveMsg'); if (!code || !name) { msgEl.innerHTML = '⚠️ 编号和名称不能为空'; return; } fetch('/api/warehouse/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({warehouse_code: code, warehouse_name: name, category: category, warehouse_property: warehouseProperty, disabled: disabled, remark: remark}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { msgEl.innerHTML = '✅ ' + (res.msg || '保存成功') + ''; document.getElementById('editWarehouseModal').style.display = 'none'; fetchWarehouses(1); if (res.cache_refreshed) refreshWarehousePropertyLinkedPages(); } else { msgEl.innerHTML = '⚠️ ' + (res.msg || '保存失败') + ''; } }) .catch(function() { msgEl.innerHTML = '⚠️ 网络错误'; }); }; window.deleteWarehouse = function(code) { showCenteredConfirm('确认', '确认删除仓库 ' + code + ' 吗?').then(function(ok) { if (!ok) return; fetch('/api/warehouse/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({warehouse_code: code}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { fetchWarehouses(1); } else { alert(res.msg || '删除失败'); } }); }); }; // ─── 仓库导入 ─── window.openWarehouseImportModal = function() { var old = document.getElementById('whImportModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'whImportModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); }; window.previewWhImportFile = function() { var f = document.getElementById('whImportFile'); var p = document.getElementById('whImportPreview'); var b = document.getElementById('whImportBtn'); if (f.files && f.files.length > 0) { p.textContent = '已选择:' + f.files[0].name + '(' + (f.files[0].size / 1024).toFixed(1) + 'KB)'; b.disabled = false; } else { p.textContent = ''; b.disabled = true; } }; window.submitWhImport = function() { var f = document.getElementById('whImportFile'); if (!f.files || !f.files[0]) return; var btn = document.getElementById('whImportBtn'); btn.disabled = true; btn.textContent = '导入中...'; var formData = new FormData(); formData.append('file', f.files[0]); fetch('/api/warehouse/import', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { btn.disabled = false; btn.textContent = '开始导入'; if (res.code === 0) { alert(res.msg); document.getElementById('whImportModal').remove(); fetchWarehouses(1); } else { alert(res.msg || '导入失败'); } }) .catch(function(err) { btn.disabled = false; btn.textContent = '开始导入'; alert('请求失败:' + err.message); }); }; // ─── 库存信息(成品/包材) ─── let _invProductType = ''; function loadInventoryPage(container) { // 检测当前页面是成品库存还是包材库存 const path = window.location.pathname; let title, desc, type; if (path.indexOf('/finished') >= 0) { title = '成品库存'; desc = '成品库存明细'; type = '成品'; } else if (path.indexOf('/packaging') >= 0) { title = '包材库存'; desc = '包材库存明细(瓶子、泵头、彩盒、内料等)'; type = '包材'; } else { title = '库存信息'; desc = '各仓库库存明细'; type = ''; } _invProductType = type; container.innerHTML = `

${title}明细

${window._hasPagePerm(_invProductType === '成品' ? 'inventory_finished' : 'inventory_packaging', 'import') ? '' : ''}
${_invProductType === '成品' ? '' : ''}${_invProductType === '成品' ? '' : ''}${window._hasPagePerm(_invProductType === '成品' ? 'inventory_finished' : 'inventory_packaging', 'edit') ? '' : ''}
序号货品编号货品名称品牌仓库库存生产在途采购在途采购跟单采购计划供应商库存日期操作
`; // 并行加载:筛选选项 + 表格数据,互不等待 var filtersPromise = fetch('/api/inventory/filters').then(function(r){return r.json();}).then(function(res){ if(res.code!==0)return; var sel=document.getElementById('invDate'); (res.data.dates||[]).forEach(function(d){ var opt=document.createElement('option'); opt.value=d;opt.textContent=d; sel.appendChild(opt); }); if(sel.options.length>1) sel.selectedIndex=1; }).catch(function(){}); // 直接加载数据,不等待筛选选项 loadInventoryData(1); } // ─── 库存导入(成品/包材共用) ─── function _invImportPageId() { return _invProductType === '成品' ? 'inventory_finished' : 'inventory_packaging'; } function openInventoryImportModal() { if (!window._hasPagePerm(_invImportPageId(), 'import')) { showCenteredAlert('权限不足', '当前账号没有库存导入权限'); return; } var old = document.getElementById('invImportModal'); if (old) old.remove(); var label = _invProductType === '成品' ? '成品库存' : '包材库存'; var templateUrl = '/api/inventory/import_template?product_type=' + encodeURIComponent(_invProductType); var html = ''; document.body.insertAdjacentHTML('beforeend', html); } function closeInventoryImportModal() { var m = document.getElementById('invImportModal'); if (m) m.remove(); } function previewInventoryImportFile() { var f = document.getElementById('invImportFile'); var preview = document.getElementById('invImportPreview'); var btn = document.getElementById('invImportBtn'); if (f.files && f.files.length > 0) { preview.textContent = '已选择:' + f.files[0].name + '(' + (f.files[0].size / 1024).toFixed(1) + 'KB)'; btn.disabled = false; } else { preview.textContent = ''; btn.disabled = true; } } function submitInventoryImport() { var f = document.getElementById('invImportFile'); if (!f.files || !f.files[0]) return; var btn = document.getElementById('invImportBtn'); btn.disabled = true; btn.textContent = '导入中...'; var formData = new FormData(); formData.append('file', f.files[0]); formData.append('product_type', _invProductType); fetch('/api/inventory/import', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { btn.disabled = false; btn.textContent = '开始导入'; if (res.code === 0) { showCenteredAlert('导入完成', res.msg); closeInventoryImportModal(); loadInventoryData(1); } else { showCenteredAlert('导入失败', res.msg || '导入失败'); } }) .catch(function(err) { btn.disabled = false; btn.textContent = '开始导入'; showCenteredAlert('导入失败', '请求失败:' + err.message); }); } let invCurrentPage = 1; let INV_PAGE_SIZE = 50; function loadInventoryData(page) { invCurrentPage = page; const date = document.getElementById('invDate')?.value || ''; const code = document.getElementById('invProdCode')?.value || ''; const name = document.getElementById('invProdName')?.value || ''; const wh = document.getElementById('invWarehouse')?.value || ''; const codeName = (code || name).trim(); let url = '/api/inventory/list?page=' + page + '&size=' + INV_PAGE_SIZE; if (date) url += '&date=' + encodeURIComponent(date); if (codeName) url += '&search=' + encodeURIComponent(codeName); if (wh) url += '&warehouse=' + encodeURIComponent(wh); if (_invProductType) url += '&product_type=' + encodeURIComponent(_invProductType); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; const data = res.data; const tbody = document.getElementById('invTbody'); var tfoot = document.getElementById('invTfoot'); if (tfoot) { var summary = data.summary || {}; var summaryHasAction = window._hasPagePerm(_invProductType === '成品' ? 'inventory_finished' : 'inventory_packaging', 'edit'); var summaryLabelCells = '合计'; var summaryValueCells = '' + (summary.available_qty || 0) + '' + '' + (summary.production_in_transit || 0) + '' + '' + (summary.purchase_in_transit || 0) + ''; if (_invProductType === '成品') summaryValueCells += ''; summaryValueCells += ''; tfoot.innerHTML = '' + summaryLabelCells + summaryValueCells + ''; } if (!data.rows || data.rows.length === 0) { var cols = _invProductType === '成品' ? 14 : 11; tbody.innerHTML = '暂无数据'; } else { const startIdx = (page - 1) * INV_PAGE_SIZE; window._invRows = data.rows; tbody.innerHTML = data.rows.map((r, i) => { var code = r.product_code || ''; var brandCell = (_invProductType === '成品') ? '' + escHtml(r.brand || '-') + '' : ''; var personnelCells = (_invProductType === '成品') ? '' + renderNameBadges(r['采购跟单']) + '' + renderNameBadges(r['采购计划'], true) + '' : ''; return '' + (startIdx + i + 1) + '' + escHtml(code) + '' + escHtml(r.product_name || '-') + '' + brandCell + '' + escHtml(r.warehouse_name || '-') + '' + (r.available_qty || 0) + '' + (r.production_in_transit || 0) + '' + (r.purchase_in_transit || 0) + '' + personnelCells + '' + escHtml(r['供应商'] || '-') + '' + (r.data_date || '-') + '' + (window._hasPagePerm(_invProductType === '成品' ? 'inventory_finished' : 'inventory_packaging', 'edit') ? '' : '') + ''; }).join(''); invUpdateCount(); } document.getElementById('invCount').textContent = '共 ' + (data.total || 0) + ' 条'; const pel = document.getElementById('invPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, INV_PAGE_SIZE, 'loadInventoryData', 'INV_PAGE_SIZE'); }); } window.searchInventory = function() { loadInventoryData(1); }; window.resetInventoryFilters = function() { document.getElementById('invDate').selectedIndex = 0; document.getElementById('invProdCode').value = ''; document.getElementById('invProdName').value = ''; document.getElementById('invWarehouse').value = ''; loadInventoryData(1); }; // ─── 库存勾选/导出 ─── // 导出可选字段 var INV_EXPORT_FIELDS = [ {key:'product_code', label:'货品编号', def:true}, {key:'product_name', label:'货品名称', def:true}, {key:'warehouse_name', label:'仓库', def:true}, {key:'available_qty', label:'库存', def:true}, {key:'production_in_transit', label:'生产在途', def:true}, {key:'purchase_in_transit', label:'采购在途', def:true}, {key:'cost_price', label:'成本价', def:false}, {key:'brand', label:'品牌', def:false}, {key:'采购跟单', label:'采购跟单', def:false}, {key:'采购计划', label:'采购计划', def:false}, {key:'供应商', label:'供应商', def:false}, {key:'data_date', label:'库存日期', def:true} ]; if (!window._invExportSelectedFields) { window._invExportSelectedFields = INV_EXPORT_FIELDS.filter(function(f) { return f.def; }).map(function(f) { return f.key; }); } window.invOpenFieldSelector = function() { var old = document.getElementById('invFieldModal'); if (old) old.remove(); var html = '
'; html += '

选择导出字段

'; html += ''; html += '
'; INV_EXPORT_FIELDS.forEach(function(f) { var checked = window._invExportSelectedFields.indexOf(f.key) >= 0; html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; var m = document.createElement('div'); m.id = 'invFieldModal'; m.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:9999;display:flex;align-items:center;justify-content:center'; m.innerHTML = '
' + html + '
'; document.body.appendChild(m); }; window.invConfirmExportFields = function() { var checked = []; document.querySelectorAll('.inv-field-cb:checked').forEach(function(cb) { checked.push(cb.value); }); window._invExportSelectedFields = checked; document.getElementById('invFieldModal').remove(); }; window.invToggleAll = function(checked) { document.querySelectorAll('.inv-row-cb').forEach(function(cb) { cb.checked = checked; }); window._invExportAll = !!checked; var allCb = document.getElementById('invCbAll'); var allBtn = document.getElementById('invSelectAll'); if (allCb) allCb.checked = checked; if (allBtn) allBtn.checked = checked; invUpdateCount(); }; window.invUpdateCount = function() { var checked = document.querySelectorAll('.inv-row-cb:checked').length; var el = document.getElementById('invSelectedCount'); if (el) el.textContent = checked > 0 ? '已选 ' + checked + ' 条' : ''; }; window.invExportSelected = function() { // 导出不再依赖列表勾选,直接按当前筛选条件导出并在弹窗中选择日期范围。 var checked = []; var exportAll = true; var fmt = document.getElementById('invExportFormat')?.value || 'xlsx'; var type = window._invProductType || ''; // 先弹出字段选择 var curFields = window._invExportSelectedFields || INV_EXPORT_FIELDS.filter(function(f) { return f.def; }).map(function(f) { return f.key; }); var html = '
'; html += '

导出' + (type || '库存') + '数据

'; html += '

将按当前筛选条件导出数据,格式:' + fmt.toUpperCase() + '

'; html += '
至
'; html += '
'; html += ''; html += '
'; INV_EXPORT_FIELDS.forEach(function(f) { var checked2 = curFields.indexOf(f.key) >= 0; html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; var old = document.getElementById('invExportModal'); if (old) old.remove(); var m = document.createElement('div'); m.id = 'invExportModal'; m.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:9999;display:flex;align-items:center;justify-content:center'; m.innerHTML = '
' + html + '
'; document.body.appendChild(m); // 绑定确认导出 document.getElementById('invExportDoBtn').onclick = function() { var selFields = []; document.querySelectorAll('.inv-export-fc:checked').forEach(function(cb) { selFields.push(cb.value); }); var dateFrom = document.getElementById('invExportDateFrom')?.value || ''; var dateTo = document.getElementById('invExportDateTo')?.value || ''; if (selFields.length === 0) { alert('请至少选择一个字段'); return; } if (dateFrom && dateTo && dateFrom > dateTo) { alert('开始日期不能晚于结束日期'); return; } window._invExportSelectedFields = selFields; document.getElementById('invExportModal').remove(); var currentDate = document.getElementById('invDate')?.value || ''; // 未填写日期范围时沿用库存页面当前日期(默认最新库存日),避免全量扫描历史库存记录。 var exportDate = dateFrom || dateTo ? '' : currentDate; fetch('/api/inventory/export', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: checked, export_all: exportAll, prod_code: document.getElementById('invProdCode')?.value || '', prod_name: document.getElementById('invProdName')?.value || '', warehouse: document.getElementById('invWarehouse')?.value || '', format: fmt, date: exportDate, date_from: dateFrom, date_to: dateTo, product_type: type, fields: selFields}) }) .then(function(r) { if (!r.ok) return r.json().then(function(d) { throw new Error(d.msg || '导出失败'); }); var filename = 'inventory_export.' + fmt; return r.blob().then(function(blob) { var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); }); }) .catch(function(e) { alert('❌ ' + e.message); }); }; }; // ─── 供应商管理 ─── function loadSuppliersPage(container) { container.innerHTML = `

供应商列表

序号供应商编码供应商名称做货类型简称合同简称供应商分组跟单小组采购计划审核人审核日期数据状态禁用状态操作
`; // 清除筛选框搜索记忆 ['supCodeName'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); loadSuppliers(1); } let supCurrentPage = 1; let SUP_PAGE_SIZE = 50; function loadSuppliers(page) { supCurrentPage = page; const codeName = (document.getElementById('supCodeName')?.value || '').trim(); let url = '/api/suppliers/list?page=' + page + '&size=' + SUP_PAGE_SIZE; if (codeName) url += '&search=' + encodeURIComponent(codeName); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; const data = res.data; const tbody = document.getElementById('supTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据'; } else { const startIdx = (page - 1) * SUP_PAGE_SIZE; tbody.innerHTML = data.rows.map((r, i) => { var supCode = r.supplier_code || ''; var supName = r.supplier_name || ''; var html = '' + (startIdx + i + 1) + ''; html += '' + escHtml(supCode) + ''; html += '' + escHtml(supName) + ''; html += '' + escHtml(r.production_type || '-') + ''; html += '' + escHtml(r.short_name || '-') + ''; html += '' + escHtml(r.contract_short_name || '-') + ''; html += '' + escHtml(r.supplier_group || '-') + ''; html += '' + escHtml(r.procurement_follower || '-') + ''; html += '' + escHtml(r.procurement_planner || '-') + ''; html += '' + escHtml(r.reviewer || '-') + ''; html += '' + (r.review_date ? r.review_date : '-') + ''; html += '' + escHtml(r.data_status || '-') + ''; html += '' + escHtml(r.disabled_status || '-') + ''; html += ''; html += ''; html += ''; html += ''; return html; }).join(''); } document.getElementById('supCount').textContent = '共 ' + (data.total || 0) + ' 条'; const pel = document.getElementById('supPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, SUP_PAGE_SIZE, 'loadSuppliers', 'SUP_PAGE_SIZE'); }); } window.searchSuppliers = function() { loadSuppliers(1); }; window.resetSupplierFilters = function() { document.getElementById('supCodeName').value = ''; loadSuppliers(1); }; // ─── 供应商导出 ─── function exportSuppliers() { const codeName = (document.getElementById('supCodeName')?.value || '').trim(); let url = '/api/suppliers/export'; if (codeName) url += '?search=' + encodeURIComponent(codeName); safeOpenUrl(url); } // ─── 供应商导入 ─── function importSuppliers() { var overlayId = 'supImportOverlay-' + Date.now(); var overlay = document.createElement('div'); overlay.id = overlayId; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:30000;display:flex;align-items:center;justify-content:center;'; overlay.innerHTML = '
' + '

📥 导入供应商信息

' + '
' + '
' + '' + '📄 下载模板' + '
' + '

文件需包含供应商名称列,可更新简称、合同简称、跟单小组和采购计划。系统将根据供应商名称匹配更新。

' + '
' + '
📁
' + '
点击选择文件或拖拽文件到此处
' + '
仅支持 .xlsx 格式
' + '' + '
' + '
' + '
' + '
' + '' + '' + '
'; document.body.appendChild(overlay); } window._supImportFile = null; function handleSupImportSelect(e) { var file = e.target.files[0]; setSupImportFile(file); } function handleSupImportDrop(e) { e.preventDefault(); e.stopPropagation(); var dz = document.getElementById('supImportDropzone'); if (dz) { dz.style.borderColor = '#d9d9d9'; dz.style.background = ''; } var file = e.dataTransfer.files[0]; if (file && file.name.match(/\.xlsx?$/i)) { setSupImportFile(file); // 同步到隐藏的file input var fi = document.getElementById('supImportFile'); if (fi) { var dt = new DataTransfer(); dt.items.add(file); fi.files = dt.files; } } else { alert('请上传 .xlsx 格式的文件'); } } function handleSupImportDragOver(e) { e.preventDefault(); e.stopPropagation(); var dz = document.getElementById('supImportDropzone'); if (dz) { dz.style.borderColor = '#1890ff'; dz.style.background = '#f0f8ff'; } } function handleSupImportDragLeave(e) { e.preventDefault(); e.stopPropagation(); var dz = document.getElementById('supImportDropzone'); if (dz) { dz.style.borderColor = '#d9d9d9'; dz.style.background = ''; } } function setSupImportFile(file) { window._supImportFile = file; var btn = document.getElementById('supImportBtn'); var preview = document.getElementById('supImportPreview'); var dz = document.getElementById('supImportDropzone'); if (file) { btn.disabled = false; preview.textContent = '✅ 已选择:' + file.name; if (dz) { dz.style.borderColor = '#52c41a'; dz.style.background = '#f6ffed'; } } else { btn.disabled = true; preview.textContent = ''; if (dz) { dz.style.borderColor = '#d9d9d9'; dz.style.background = ''; } } } function closeImportOverlay() { var overlays = document.querySelectorAll('[id^="supImportOverlay-"]'); overlays.forEach(function(o) { o.remove(); }); } function doImportSuppliers() { if (!window._supImportFile) { alert('请先选择文件'); return; } var btn = document.getElementById('supImportBtn'); btn.disabled = true; btn.textContent = '⏳ 导入中...'; var formData = new FormData(); formData.append('file', window._supImportFile); fetch('/api/suppliers/import', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }) .then(function(res) { // 先移除导入弹窗,再显示结果 closeImportOverlay(); if (res.code !== 0) { alert('导入失败:' + (res.msg || '未知错误')); } else { var d = res.data; var msg = '✅ 导入完成\n\n' + '成功更新:' + d.updated + ' 条\n' + (d.not_found > 0 ? '未匹配:' + d.not_found + ' 条\n' : '') + (d.skipped > 0 ? '跳过无数据行:' + d.skipped + ' 条\n' : ''); if (d.not_found_list && d.not_found_list.length > 0) { msg += '\n未匹配的供应商名称(前20条):\n' + d.not_found_list.join('、'); } alert(msg); loadSuppliers(supCurrentPage); } }) .catch(function(err) { closeImportOverlay(); alert('导入异常:' + err.message); }); } // ─── 人员信息管理 ─── function loadPersonnelPage(container) { container.innerHTML = '
加载中...
'; fetch('/api/personnel/groups?_t=' + Date.now(), {credentials: 'same-origin'}) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) throw new Error(res.msg || '加载失败'); window._personnelGroups = res.data || []; rebuildPersonnelGroupMap(); renderPersonnelPage(container); }) .catch(function(e) { container.innerHTML = '
人员分组加载失败:' + escHtml(e.message) + '
'; }); } function rebuildPersonnelGroupMap() { window._nameToGroup = {}; (window._personnelGroups || []).forEach(function(g) { (g.members || []).forEach(function(m) { window._nameToGroup[m] = g; }); }); } function renderPersonnelPage(container) { var groups = window._personnelGroups || []; var followGroups = groups.filter(function(g) { return g.category !== '采购计划'; }); var planGroups = groups.filter(function(g) { return g.category === '采购计划'; }); var canEdit = window._currentRole === 'admin' || window._currentRole === 'super_admin'; var totalMembers = groups.reduce(function(total, g) { return total + (g.members || []).length; }, 0); var sectionHtml = function(title, icon, sectionGroups) { var total = sectionGroups.reduce(function(total, g) { return total + (g.members || []).length; }, 0); return '
' + '
' + '' + icon + '

' + title + '

' + '共 ' + total + ' 人' + '
' + renderPersonnelCards(sectionGroups, canEdit) + '
'; }; var cardsHtml = sectionHtml('采购跟单', '👥', followGroups) + sectionHtml('采购计划', '📋', planGroups); container.innerHTML = '
' + '' + '
当前共 ' + totalMembers + ' 人,可通过卡片编辑维护各组人员。
' + cardsHtml + '
'; } function renderPersonnelCards(groups, canEdit) { var html = ''; (groups || []).forEach(function(g) { html += '
' + '' + (g.icon || '👤') + '' + escHtml(g.name) + '' + '' + (g.members || []).length + '人' + (canEdit ? '' : '') + '
' + '
'; (g.members || []).forEach(function(m) { var isLeader = m === g.leader && g.leader !== ''; html += '' + (isLeader ? '⭐ ' : '') + escHtml(m) + (isLeader ? '组长' : '') + ''; }); html += '
'; }); return html; } function personnelModalShell(id, title, body, footer) { var old = document.getElementById(id); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = id; overlay.className = 'modal-overlay'; overlay.innerHTML = ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); return overlay; } function openPersonnelAddModal() { var body = '' + ''; var modal = personnelModalShell('personnelAddModal', '新增组别', body, ''); modal.querySelector('#personnelNewGroupName').focus(); } function openPersonnelEditModal(groupName) { var group = (window._personnelGroups || []).find(function(g) { return g.name === groupName; }); if (!group) return; var rows = (group.members || []).map(function(m) { return '
'; }).join(''); var body = '
编辑 ' + escHtml(group.name) + ' 的人员,可新增或删除成员。
' + rows + '
'; var modal = personnelModalShell('personnelEditModal', '编辑人员分组', body, ''); modal._groupName = groupName; refreshPersonnelLeaderOptions(); var leaderSelect = modal.querySelector('#personnelEditLeader'); if (leaderSelect) leaderSelect.value = group.leader || ''; } function addPersonnelEditRow() { var box = document.getElementById('personnelEditRows'); box.insertAdjacentHTML('beforeend', '
'); refreshPersonnelLeaderOptions(); } function refreshPersonnelLeaderOptions() { var select = document.getElementById('personnelEditLeader'); if (!select) return; var old = select.value, names = Array.from(document.querySelectorAll('.personnel-member-input')).map(function(i) { return i.value.trim(); }).filter(Boolean); select.innerHTML = '' + names.filter(function(n, i) { return names.indexOf(n) === i; }).map(function(n) { return ''; }).join(''); if (names.indexOf(old) >= 0) select.value = old; } function personnelGroupPayload(groupName) { var members = Array.from(document.querySelectorAll('.personnel-member-input')).map(function(i) { return i.value.trim(); }).filter(Boolean); return {name: groupName, members: members, leader: document.getElementById('personnelEditLeader').value}; } function confirmPersonnelGroupSave(groupName) { refreshPersonnelLeaderOptions(); showCenteredConfirm('确认', '确认保存“' + groupName + '”的人员调整吗?').then(function(ok) { if (ok) savePersonnelGroup(personnelGroupPayload(groupName)); }); } function saveNewPersonnelGroup() { var name = document.getElementById('personnelNewGroupName').value.trim(); var category = document.getElementById('personnelNewCategory').value; if (!name) { alert('请输入组别名称'); return; } showCenteredConfirm('确认', '确认新增“' + name + '”组别吗?').then(function(ok) { if (!ok) return; fetch('/api/personnel/groups/create', {method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:name, category:category})}) .then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) { alert(res.msg || '新增失败'); return; } document.getElementById('personnelAddModal')?.remove(); var page = document.getElementById('page-container-personnel'); if (page) loadPersonnelPage(page); else location.reload(); }).catch(function(e) { alert('新增失败:' + e.message); }); }); } function savePersonnelGroup(payload) { fetch('/api/personnel/groups/save', {method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}) .then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) { alert(res.msg || '保存失败'); return; } document.getElementById('personnelEditModal')?.remove(); document.getElementById('personnelAddModal')?.remove(); var page = document.getElementById('page-container-personnel'); if (page) loadPersonnelPage(page); else location.reload(); loadPersonnelGroupsForLinkedPages(); }).catch(function(e) { alert('保存失败:' + e.message); }); } // ─── 用户管理 ─── function downloadUserTemplate() { fetch('/api/users/template', {credentials: 'same-origin'}) .then(r => { if (!r.ok) throw new Error('下载失败'); return r.blob(); }) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '用户导入模板.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); }) .catch(e => alert(e.message)); } function handleImportUsers() { const input = document.getElementById('userImportFile'); const file = input.files[0]; if (!file) { alert('请先选择CSV文件'); return; } const btn = document.getElementById('importBtn'); if (btn) { btn.disabled = true; btn.textContent = '导入中...'; } const reader = new FileReader(); reader.readAsArrayBuffer(file); reader.onload = function(e) { const bytes = new Uint8Array(e.target.result); let binary = ''; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); const base64 = btoa(binary); fetch('/api/users/import', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({csv_base64: base64}) }) .then(r => { if (!r.ok) throw new Error('请求失败(' + r.status + ')'); return r.json(); }) .then(res => { if (res.code !== 0) { alert(res.msg); return; } let msg = res.msg; if (res.data.errors && res.data.errors.length > 0) { msg += '\n\n错误详情:\n' + res.data.errors.join('\n'); } alert(msg); loadUsers(); }) .catch(err => alert('导入失败:' + err.message)) .finally(() => { if (btn) { btn.disabled = false; btn.textContent = '导入用户'; } input.value = ''; }); }; reader.readAsText(file, 'UTF-8'); } function loadUsersPage(container) { container.innerHTML = `
🔐 权限角色: 超级管理员 全部权限 管理员 管理权限 查看者 只读权限

用户列表

序号用户名姓名角色 ↕级别岗位 手机邮箱状态最后登录操作
`; loadUserPositions().then(function() { loadUsers(); }).catch(function() { loadUsers(); }); } let userPageSize = 10; let userCurrentPage = 1; let userAllData = []; let _userFullSorted = []; // 缓存完整用户列表(未搜索时的排序),用于保留原始序号 let userRoleSortOrder = ''; // '' 'asc' 'desc' function userSearchKeydown(e) { if (e.key === 'Enter') { e.preventDefault(); userDoSearch(); } } function userDoSearch() { userCurrentPage = 1; loadUsers(); } function userFilterChange() { userCurrentPage = 1; loadUsers(); } function clearUserFilter() { document.getElementById('userSearchInput').value = ''; document.getElementById('userRoleFilter').value = ''; document.getElementById('userPositionFilter').value = ''; document.getElementById('userStatusFilter').value = ''; userCurrentPage = 1; loadUsers(); } function toggleRoleSort() { if (userRoleSortOrder === '' || userRoleSortOrder === 'desc') { userRoleSortOrder = 'asc'; } else { userRoleSortOrder = 'desc'; } document.getElementById('roleSortIcon').textContent = userRoleSortOrder === 'asc' ? '↑' : '↓'; userCurrentPage = 1; loadUsers(); } const ROLE_MAP = { 'super_admin':'超级管理员', 'admin':'管理员', 'viewer':'查看者' }; const ROLE_COLOR = { 'super_admin':'tag-red', 'admin':'tag-orange', 'viewer':'tag-green' }; const POSITION_MAP = { 'procurement_planner':'采购计划', 'procurement_follower':'采购跟单', 'product_developer':'产品开发', 'operator':'运营专员', 'bp':'BP', 'procurement_trainee':'采购管培生', 'finance_specialist':'财务专员' }; const LEVEL_MAP = {'employee':'员工','leader':'组长','director':'主管'}; let USER_POSITION_LIST = []; const LEVEL_STYLE = { 'employee':'background:#e6f7ff;padding:2px 8px;border-radius:4px;border:1px solid #91d5ff;font-size:12px', 'leader':'background:#fff7e6;padding:2px 8px;border-radius:4px;border:1px solid #ffd591;font-size:12px;font-weight:600', 'director':'background:#fff1f0;padding:2px 8px;border-radius:4px;border:1px solid #ffa39e;font-size:12px;font-weight:600' }; function updateUserPositionSelects(selectedCode) { var filter = document.getElementById('userPositionFilter'); var edit = document.getElementById('editPosition'); var filterValue = filter ? filter.value : ''; var editValue = selectedCode || (edit ? edit.value : ''); if (filter) { filter.innerHTML = ''; USER_POSITION_LIST.forEach(function(p) { var option = document.createElement('option'); option.value = p.code; option.textContent = p.name; filter.appendChild(option); }); filter.value = filterValue; } if (edit) { edit.innerHTML = ''; USER_POSITION_LIST.forEach(function(p) { var option = document.createElement('option'); option.value = p.code; option.textContent = p.name; edit.appendChild(option); }); edit.value = editValue; } } function loadUserPositions(selectedCode) { return fetch('/api/users/positions', {credentials: 'same-origin'}) .then(function(r) { if (!r.ok) throw new Error('岗位列表加载失败'); return r.json(); }) .then(function(res) { if (res.code !== 0) throw new Error(res.msg || '岗位列表加载失败'); USER_POSITION_LIST = res.data || []; USER_POSITION_LIST.forEach(function(p) { POSITION_MAP[p.code] = p.name; }); updateUserPositionSelects(selectedCode); return USER_POSITION_LIST; }); } function loadUsers(page) { page = page || 1; var search = document.getElementById('userSearchInput') ? document.getElementById('userSearchInput').value : ''; var role = document.getElementById('userRoleFilter') ? document.getElementById('userRoleFilter').value : ''; var positionFilter = document.getElementById('userPositionFilter') ? document.getElementById('userPositionFilter').value : ''; var status = document.getElementById('userStatusFilter') ? document.getElementById('userStatusFilter').value : ''; var sort = userRoleSortOrder ? 'role_' + userRoleSortOrder : ''; var url = '/api/users/list?search=' + encodeURIComponent(search) + '&role=' + encodeURIComponent(role) + '&position=' + encodeURIComponent(positionFilter) + '&status=' + encodeURIComponent(status) + '&sort=' + encodeURIComponent(sort); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; userAllData = res.data || []; var totalPages = Math.max(1, Math.ceil(userAllData.length / userPageSize)); if (page > totalPages) page = totalPages; // 缓存完整排序列表(不带任何筛选条件的全量数据),用于保留原始序号 var searchVal = (document.getElementById('userSearchInput') || {}).value || ''; var roleVal = (document.getElementById('userRoleFilter') || {}).value || ''; var positionVal = (document.getElementById('userPositionFilter') || {}).value || ''; var statusVal = (document.getElementById('userStatusFilter') || {}).value || ''; var isFull = !searchVal && !roleVal && !positionVal && !statusVal; if (isFull) { _userFullSorted = (res.data || []).slice(); } renderUserPage(page); }); } function renderUserPage(page) { userCurrentPage = page; const total = userAllData.length; const totalPages = Math.max(1, Math.ceil(total / userPageSize)); const start = (page - 1) * userPageSize; const end = Math.min(start + userPageSize, total); const pageData = userAllData.slice(start, end); const tbody = document.getElementById('userTbody'); tbody.innerHTML = ''; var canEditUser = window._currentRole === 'super_admin'; pageData.forEach((u, idx) => { var level = u.user_level || 'employee'; // 从完整列表中查找原始序号(保持搜索前后一致) var origIdx = -1; for (var oi = 0; oi < _userFullSorted.length; oi++) { if (_userFullSorted[oi].id === u.id) { origIdx = oi; break; } } var serialIdx = origIdx >= 0 ? origIdx : (start + idx); tbody.innerHTML += ` DY${String(serialIdx + 1).padStart(4,'0')} ${u.username} ${u.real_name || '-'} ${ROLE_MAP[u.role] || u.role} ${LEVEL_MAP[level] || level} ${POSITION_MAP[u.position] || u.position || '-'} ${u.phone || '-'} ${u.email || '-'} ${u.status==1 ? '启用' : '禁用'} ${u.last_login ? new Date(u.last_login).toLocaleDateString('zh-CN', {year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-') : '-'} ${u.is_protected ? '受保护' : ''} `; }); const pagination = document.getElementById('userPagination'); if (!pagination) return; if (totalPages <= 1) { pagination.innerHTML = ''; return; } let html = '
'; if (page > 1) html += ``; html += `第 ${page}/${totalPages} 页,共 ${total} 条`; if (page < totalPages) html += ``; html += '
'; pagination.innerHTML = html; } window.showUserModal = function() { document.getElementById('modalTitle').textContent = '新增用户'; document.getElementById('editUserId').value = ''; document.getElementById('editUsername').value = ''; document.getElementById('editPassword').value = ''; document.getElementById('editRealName').value = ''; document.getElementById('editRole').value = 'viewer'; document.getElementById('editPosition').value = ''; document.getElementById('editUserLevel').value = 'employee'; document.getElementById('editPhone').value = ''; document.getElementById('editEmail').value = ''; document.getElementById('editStatus').value = '1'; document.getElementById('pwHint').textContent = '(必填)'; document.getElementById('userModal').style.display = 'flex'; if (!USER_POSITION_LIST.length) loadUserPositions(); }; window.showAddPositionModal = function() { if (window._currentRole !== 'admin' && window._currentRole !== 'super_admin') { alert('权限不足,仅管理员可以新增岗位'); return; } document.getElementById('addPositionModal')?.remove(); var overlay = document.createElement('div'); overlay.id = 'addPositionModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;inset:0;z-index:100001;background:rgba(0,0,0,.5);align-items:center;justify-content:center'; overlay.setAttribute('onclick', 'if(event.target===this){}'); overlay.innerHTML = ''; document.body.appendChild(overlay); document.getElementById('newPositionName').focus(); }; window.saveNewPosition = function() { var input = document.getElementById('newPositionName'); var name = input ? input.value.trim() : ''; if (!name) { alert('请输入岗位名称'); if (input) input.focus(); return; } fetch('/api/users/positions/create', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name: name}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert(res.msg || '新增岗位失败'); return; } document.getElementById('addPositionModal')?.remove(); return loadUserPositions(res.data.code).then(function() { document.getElementById('editPosition').value = res.data.code; alert('岗位新增成功,已自动选中'); }); }) .catch(function(e) { alert('新增岗位失败:' + e.message); }); }; window.showUserPermissionDenied = function() { alert('权限不足,仅超级管理员可以编辑用户'); }; window.editUser = function(id) { if (window._currentRole !== 'super_admin') { showUserPermissionDenied(); return; } fetch('/api/users/list') .then(r => r.json()) .then(res => { const u = (res.data || []).find(x => x.id === id); if (!u) return; document.getElementById('modalTitle').textContent = '编辑用户'; document.getElementById('editUserId').value = u.id; document.getElementById('editUsername').value = u.username; document.getElementById('editPassword').value = ''; document.getElementById('editRealName').value = u.real_name || ''; document.getElementById('editRole').value = u.role || 'viewer'; document.getElementById('editUserLevel').value = u.user_level || 'employee'; document.getElementById('editPosition').value = u.position || ''; document.getElementById('editPhone').value = u.phone || ''; document.getElementById('editEmail').value = u.email || ''; document.getElementById('editStatus').value = u.status; document.getElementById('pwHint').textContent = '(留空不修改)'; document.getElementById('userModal').style.display = 'flex'; }); }; window.closeUserModal = function() { document.getElementById('userModal').style.display = 'none'; }; window.saveUser = function() { const id = document.getElementById('editUserId').value; const data = { username: document.getElementById('editUsername').value, password: document.getElementById('editPassword').value, real_name: document.getElementById('editRealName').value, position: document.getElementById('editPosition').value, role: document.getElementById('editRole').value, user_level: document.getElementById('editUserLevel').value, phone: document.getElementById('editPhone').value, email: document.getElementById('editEmail').value, status: parseInt(document.getElementById('editStatus').value) }; if (!id) data.id = 0; else data.id = parseInt(id); const url = id ? '/api/users/update' : '/api/users/create'; fetch(url, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)}) .then(r => r.json()) .then(res => { if (res.code === 0) { closeUserModal(); loadUsers(userCurrentPage); } else { alert(res.msg); } }); }; window.deleteUser = function(id, username) { showCenteredConfirm('确认', '确定删除用户 "' + username + '"?').then(function(ok) { if (!ok) return; fetch('/api/users/delete', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id})}) .then(r => r.json()) .then(res => { if (res.code === 0) loadUsers(userCurrentPage); else alert(res.msg); }); }); }; // ─── 角色权限配置 ─── var _permPages = []; var _permRoles = []; var _permData = {}; // {role_page: 1|0} function renderPermTable() { // 加载页面列表和角色列表 Promise.all([ fetch('/api/permissions/pages').then(function(r) { return r.json(); }), fetch('/api/permissions/roles').then(function(r) { return r.json(); }), fetch('/api/permissions/list').then(function(r) { return r.json(); }) ]).then(function(results) { if (results[0].code !== 0 || results[1].code !== 0) return; _permPages = results[0].data || []; _permRoles = results[1].data || []; var perms = results[2].data || []; // 构建权限索引 _permData = {}; perms.forEach(function(p) { _permData[p.role + '_' + p.page] = p.permission; }); // 渲染表头 var thead = document.getElementById('permThead'); if (!thead) return; var headHtml = '页面'; var roleColors = {'super_admin':'tag-red','admin':'tag-orange','viewer':'tag-green'}; var roleNames = {'super_admin':'超级管理员','admin':'管理员','viewer':'查看者'}; _permRoles.forEach(function(r) { var colorClass = roleColors[r.id] || 'tag-grey'; if (r.id === 'admin') { headHtml += '' + roleNames[r.id] + '
全部权限'; } else { headHtml += '' + roleNames[r.id] + ''; } }); headHtml += ''; thead.innerHTML = headHtml; // 按分组渲染行 var tbody = document.getElementById('permTbody'); if (!tbody) return; var groups = {}; _permPages.forEach(function(p) { if (!groups[p.group]) groups[p.group] = []; groups[p.group].push(p); }); var bodyHtml = ''; Object.keys(groups).forEach(function(group) { // 分组标题行 bodyHtml += '' + group + ''; groups[group].forEach(function(p) { bodyHtml += '' + p.name + ''; _permRoles.forEach(function(r) { var key = r.id + '_' + p.id; var checked = (_permData[key] == 1) ? 'checked' : ''; var disabled = (r.id === 'admin') ? 'disabled' : ''; bodyHtml += ''; }); bodyHtml += ''; }); }); tbody.innerHTML = bodyHtml; }); } window.savePermissions = function() { var items = []; document.querySelectorAll('.perm-cb').forEach(function(cb) { items.push({ role: cb.dataset.role, page: cb.dataset.page, permission: cb.checked ? 1 : 0 }); }); fetch('/api/permissions/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({items: items}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 权限配置已保存'); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败'); }); }; // ─── 用户字段权限管理 ─── window.switchPermTab = function(tab) { document.getElementById('permRolePane').style.display = tab === 'role' ? '' : 'none'; document.getElementById('permUserPane').style.display = tab === 'user' ? '' : 'none'; document.getElementById('permSaveBtn').style.display = tab === 'role' ? 'inline-block' : 'none'; document.getElementById('permFieldSaveBtn').style.display = tab === 'user' ? 'inline-block' : 'none'; var roleBtn = document.getElementById('permTabRole'); var userBtn = document.getElementById('permTabUser'); if (tab === 'role') { roleBtn.style.cssText = 'background:#1890ff;color:#fff;border:none'; userBtn.style.cssText = 'background:#f0f0f0;color:#333;border:1px solid #d9d9d9'; } else { roleBtn.style.cssText = 'background:#f0f0f0;color:#333;border:1px solid #d9d9d9'; userBtn.style.cssText = 'background:#1890ff;color:#fff;border:none'; loadFieldUserList(); } }; window.loadFieldUserList = function() { var sel = document.getElementById('permFieldUser'); if (sel.options.length > 1) return; fetch('/api/permissions/users') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; (res.data || []).forEach(function(u) { var opt = document.createElement('option'); opt.value = u.id; opt.textContent = u.real_name + ' (' + (u.role||'') + ')'; sel.appendChild(opt); }); }); }; window.loadUserFieldPerms = function() { try { var userId = document.getElementById('permFieldUser').value; var wrap = document.getElementById('permFieldWrap'); if (!userId) { if(wrap)wrap.innerHTML='
请先选择用户
'; return; } if (!wrap) { return; } wrap.innerHTML = '
加载中...
'; // 单API获取字段级权限+有效页面级权限(后端已合并角色+用户) fetch('/api/permissions/user/field-perms?user_id=' + userId + '&_t=' + Date.now()) .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function(fieldPermRes) { if (fieldPermRes.code !== 0) { wrap.innerHTML = '
API错误: ' + (fieldPermRes.msg || 'code=' + fieldPermRes.code) + '
'; return; } var fieldPermData = fieldPermRes.data || {}; var userFieldPerms = fieldPermData.field_perms || {}; // 字段级 var roleFieldPerms = fieldPermData.role_perms || {}; // 页面级有效权限 var html = ''; // DEBUG: 确认新API版本运行中 html += '
✅ 新API v2 - data_summary有效=' + JSON.stringify(roleFieldPerms.data_summary || {}) + '
'; _permMenuTree.forEach(function(group) { var items = group.items || []; if (items.length === 0) return; html += '
'; html += '
' + escHtml(group.name) + '
'; items.forEach(function(item) { var page = item.page; var feats = item.features || []; var fields = userFieldPerms[page] || {}; var fieldKeys = Object.keys(fields).sort(); // 页面级操作权限 html += '
'; html += '
'; html += '' + escHtml(item.name) + ''; feats.forEach(function(f) { // 有效权限(后端已合并角色+用户) var effPerms = roleFieldPerms[page] || {}; var effective = effPerms[f.id] === 1; // roleEnabled=角色原本有(用于title提示) var roleEnabled = effPerms[f.id] === 1; html += ''; }); html += '
'; // 字段级权限(仅当有字段时) if (fieldKeys.length > 0) { html += '
'; html += '
📋 字段权限 ▼
'; html += '
'; } html += '
'; }); html += '
'; }); wrap.innerHTML = html; window._fieldPermChanged = false; }).catch(function(e) { wrap.innerHTML = '
加载权限数据失败: ' + (e.message || '') + '
'; }); } catch(e) { var w = document.getElementById('permFieldWrap'); if(w) w.innerHTML = '
JS错误: ' + escHtml(e.message) + '
'; } }; window.markFieldPermChanged = function() { window._fieldPermChanged = true; }; window.saveUserFieldPerms = function() { var userId = document.getElementById('permFieldUser').value; if (!userId) return alert('请先选择用户'); // 先获取该用户当前的所有权限(包含page级权限),避免被覆盖 fetch('/api/permissions/users') .then(function(r) { return r.json(); }) .then(function(usersRes) { var users = usersRes.data || []; var curUser = users.find(function(u) { return u.id == userId; }); var pagePerms = (curUser && curUser.permissions) || {}; var items = []; // 页面级操作权限(从user-op-cb checkbox读取) var processedOps = {}; document.querySelectorAll('#permFieldWrap .user-op-cb').forEach(function(cb) { var page = cb.getAttribute('data-page'); var op = cb.getAttribute('data-op'); if (page && op) { // 勾选=用户增加权限,取消=移除用户级覆盖(由后端角色权限兜底) if (cb.checked) { items.push({page: page, operation: op, permission: 1}); } processedOps[page + '_' + op] = true; } }); // 保留已有用户级页面权限(本次没操作的) Object.keys(pagePerms).forEach(function(p) { Object.keys(pagePerms[p]).forEach(function(op) { if (!op.startsWith('field:') && !processedOps[p + '_' + op]) { items.push({page: p, operation: op, permission: pagePerms[p][op]}); } }); }); // 字段级权限(从checkbox读取) document.querySelectorAll('#permFieldWrap input[type="checkbox"]').forEach(function(cb) { var page = cb.getAttribute('data-page'); var field = cb.getAttribute('data-field'); var op = cb.getAttribute('data-op'); if (page && field && op) { items.push({ page: page, operation: 'field:' + field + ':' + op, permission: cb.checked ? 1 : 0 }); } }); return fetch('/api/permissions/user/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user_id: parseInt(userId), items: items}) }); }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 保存成功'); window._fieldPermChanged = false; // 刷新字段权限显示 loadUserFieldPerms(); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function() { alert('保存失败'); }); }; // ─── 字段级权限应用到表格(隐藏无查看权限的列) ─── window._applyTableFieldPerms = function(tableId, pageId, fieldOrder) { var table = document.getElementById(tableId); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; var ths = headerRow.querySelectorAll('th'); ths.forEach(function(th, idx) { // 优先用 data-field,否则用 th 文本内容匹配 var fieldName = th.getAttribute('data-field') || (th.textContent || '').trim().replace(/[^\u4e00-\u9fa5a-zA-Z0-9\-\/]/g, ''); if (fieldName && !window._canViewField(pageId, fieldName)) { th.style.display = 'none'; table.querySelectorAll('tbody tr').forEach(function(tr) { var td = tr.children[idx]; if (td) td.style.display = 'none'; }); } }); }; // ─── 操作日志 ─── function loadLogsPage(container) { container.innerHTML = `

日志列表

操作时间操作人操作页面操作字段操作内容
`; loadLogs(1); } let logCurrentPage = 1; let LOG_PAGE_SIZE = 30; function loadLogs(page) { logCurrentPage = page; const user = document.getElementById('logUser')?.value || ''; const action = document.getElementById('logAction')?.value || ''; let url = '/api/logs/list?page=' + page + '&size=' + LOG_PAGE_SIZE; if (user) url += '&user=' + encodeURIComponent(user); if (action) url += '&action=' + encodeURIComponent(action); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; const data = res.data; const tbody = document.getElementById('logTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无日志'; } else { tbody.innerHTML = data.rows.map(r => { // 兼容旧日志:page/field 为空时回退显示 action 到操作页面列 var pageVal = r.page || r.action || '-'; var fieldVal = r.field || '-'; return '' + (r.created_at || '-') + '' + escHtml(r.real_name || '-') + '' + escHtml(pageVal) + '' + escHtml(fieldVal) + '' + escHtml(r.detail || '-') + ''; }).join(''); } document.getElementById('logCount').textContent = '共 ' + (data.total || 0) + ' 条'; const pel = document.getElementById('logPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, LOG_PAGE_SIZE, 'loadLogs', 'LOG_PAGE_SIZE'); }); } window.resetLogFilters = function() { document.getElementById('logUser').value = ''; document.getElementById('logAction').value = ''; loadLogs(1); }; // ─── 访问记录(登录日志) ─── function loadLoginRecordsPage(container) { container.innerHTML = `

登录记录

时间用户名姓名角色IP地址
`; loadLoginRecords(1); } var loginLogCurrentPage = 1; var LOGIN_LOG_PAGE_SIZE = 30; function loadLoginRecords(page) { loginLogCurrentPage = page; var user = document.getElementById('loginLogUser')?.value || ''; var url = '/api/logs/login_list?page=' + page + '&size=' + LOGIN_LOG_PAGE_SIZE; if (user) url += '&user=' + encodeURIComponent(user); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('loginLogTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无登录记录'; } else { tbody.innerHTML = data.rows.map(function(r) { var roleLabel = r.role || '-'; return '' + escHtml(r.created_at || '-') + '' + escHtml(r.username || '-') + '' + escHtml(r.real_name || '-') + '' + escHtml(roleLabel) + '' + escHtml(r.ip_address || '-') + ''; }).join(''); } document.getElementById('loginLogCount').textContent = '共 ' + (data.total || 0) + ' 条'; var pel = document.getElementById('loginLogPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, LOGIN_LOG_PAGE_SIZE, 'loadLoginRecords', 'LOGIN_LOG_PAGE_SIZE'); }) .finally(function() { // 延迟2秒恢复按钮状态,让用户感知刷新完成 var notice = document.getElementById('loginLogRefreshNotice'); if (notice) { notice.innerHTML = '✅ 刷新成功 ' + new Date().toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit',second:'2-digit'}); notice.style.color = '#52c41a'; setTimeout(function() { notice.style.display = 'none'; notice.style.color = ''; }, 2000); } setTimeout(function() { var btn = document.getElementById('refreshLoginLogBtn'); if (btn) { btn.textContent = '🔄 刷新'; btn.disabled = false; } }, 2000); }); } window.refreshLoginLog = function() { var btn = document.getElementById('refreshLoginLogBtn'); if (btn) { btn.textContent = '⟳ 刷新中...'; btn.disabled = true; } // 显示刷新提示 var notice = document.getElementById('loginLogRefreshNotice'); if (notice) { notice.style.display = 'block'; notice.innerHTML = '⟳ 正在刷新数据...'; } loadLoginRecords(loginLogCurrentPage || 1); }; window.resetLoginLogFilters = function() { document.getElementById('loginLogUser').value = ''; loadLoginRecords(1); }; // ─── 参数配置 ─── // ─── 权限配置页面 ─── // ─── 权限配置 ─── function loadPermissionsPage(container) { container.innerHTML = `
⏳
加载中...
`; loadPermissionsUsers(); } // 全局缓存 var _permUsers = []; var _permPages = []; var _permOps = []; var _permRoles = []; var _roleOpPerms = []; var _permFields = {}; // 字段清单:{page: [{key,label,sensitive}]} function loadPermissionsUsers() { var el = document.getElementById('permissionsContent'); if (!el) return; Promise.all([ fetch('/api/permissions/users').then(function(r) { return r.json(); }), fetch('/api/permissions/pages').then(function(r) { return r.json(); }), fetch('/api/permissions/operations').then(function(r) { return r.json(); }), fetch('/api/permissions/roles').then(function(r) { return r.json(); }), fetch('/api/permissions/role-operations').then(function(r) { return r.json(); }), fetch('/api/permissions/menu-tree').then(function(r) { return r.json(); }), fetch('/api/permissions/fields').then(function(r) { return r.json(); }) ]) .then(function(results) { var usersRes = results[0]; var pagesRes = results[1]; var opsRes = results[2]; var rolesRes = results[3]; var roleOpRes = results[4]; var menuTreeRes = results[5]; var fieldsRes = results[6]; if (usersRes.code !== 0) { el.innerHTML = '
❌ 加载失败
'; return; } _permUsers = usersRes.data || []; _permPages = pagesRes.data || []; _permOps = opsRes.data || []; _permRoles = rolesRes.data || []; _roleOpPerms = roleOpRes.data || []; _permFields = (fieldsRes && fieldsRes.code === 0 && fieldsRes.data) ? fieldsRes.data : {}; // 权限配置菜单树由后端按侧边栏自动生成,新增子菜单无需改动前端 if (menuTreeRes && menuTreeRes.code === 0 && (menuTreeRes.data || []).length) { _permMenuTree = menuTreeRes.data; } // 收集去重的岗位和级别列表用于筛选下拉 var posSet = {}; var levelSet = {}; _permUsers.forEach(function(u) { if (u.position) posSet[u.position] = true; if (u.user_level) levelSet[u.user_level] = true; }); var posList = Object.keys(posSet).sort(); var levelList = Object.keys(levelSet).sort(); var positionNames = { 'bp': 'BP', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'procurement_trainee': '采购管培生', '采购开发': '采购开发' }; var levelNames = {'employee': '员工', 'leader': '组长', 'director': '主管'}; // 筛选栏 var filterHtml = '
'; filterHtml += '
'; filterHtml += ''; filterHtml += ''; filterHtml += ''; filterHtml += '
'; filterHtml += '
'; // 角色操作权限卡片 el.innerHTML = filterHtml; renderPermUserGrid(); }) .catch(function() { el.innerHTML = '
❌ 加载失败
'; }); } function resetPermFilter() { var input = document.getElementById('permFilterName'); if (input) input.value = ''; var posEl = document.getElementById('permFilterPos'); if (posEl) posEl.value = ''; var levelEl = document.getElementById('permFilterLevel'); if (levelEl) levelEl.value = ''; renderPermUserGrid(); } // 权限配置使用统一页面标识;运行页面继续兼容现有ID。 var PERM_PAGE_CANONICAL = { purchase:'purchase_pending', purchase_ready:'purchase_backup', orders_domestic:'order_domestic', orders_overseas:'order_overseas', orders_packaging:'order_packaging', orders_finished:'order_finished', 'login-records':'login_records' }; function normalizePermPage(page) { return PERM_PAGE_CANONICAL[page] || page; } // ─── 角色操作权限 ─── var _roleOpEditingRole = ''; var _roleOpData = {}; // {role_page_operation: 1|0} // 按功能ID给出偏安全的默认值(查看者以外的角色) var featureDefaults = { // 新增权限默认只开放查看,其余操作必须由管理员明确配置。 'view': 1 }; window.openRoleOpPermModal = function(roleId) { if (roleId === 'super_admin') { alert('超级管理员固定拥有全部权限,无需配置'); return; } if (window._currentRole === 'admin' && roleId === 'admin') { alert('管理员不能修改管理员角色权限'); return; } _roleOpEditingRole = roleId; // 构建权限索引,未保存的按角色默认值填充 _roleOpData = {}; _roleOpPerms.forEach(function(p) { _roleOpData[p.role + '_' + normalizePermPage(p.page) + '_' + p.operation] = p.permission; }); // 预填默认值 var isFullAdmin = (roleId === 'super_admin'); var isViewer = (roleId === 'viewer'); _permMenuTree.forEach(function(group) { group.items.forEach(function(item) { (item.features || []).forEach(function(feat) { var key = roleId + '_' + item.page + '_' + feat.id; if (!(key in _roleOpData)) { if (isFullAdmin) { _roleOpData[key] = 1; } else if (featureDefaults[feat.id] !== undefined) { _roleOpData[key] = featureDefaults[feat.id]; } else { _roleOpData[key] = (feat.id === 'view') ? 1 : 0; } } }); // 字段级权限默认值:敏感字段默认隐藏,非敏感字段默认可见;编辑一律默认拒绝 (_permFields[item.page] || []).forEach(function(f) { ['view', 'edit'].forEach(function(op) { var key = roleId + '_' + item.page + '_field:' + f.key + ':' + op; if (!(key in _roleOpData)) { if (isFullAdmin) { _roleOpData[key] = 1; } else if (op === 'view') { _roleOpData[key] = 1; } else { _roleOpData[key] = 0; } } }); }); }); }); // 渲染弹窗 var roleNames = { 'super_admin': '超级管理员', 'admin': '管理员', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'product_developer': '产品开发', 'operator': '运营专员', 'viewer': '查看者' }; var modalId = 'roleOpPermModal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; modal.innerHTML = '
' + '
' + '

⚙️ 角色操作权限配置 — ' + escHtml(roleNames[roleId] || roleId) + '

' + '' + '
' + '
' + '
' + '
' + '' + '' + '' + '
' + '
' + '' + '' + '
'; modal.onclick = function(e) { if (e.target === modal) {} }; document.body.appendChild(modal); renderRoleOpPermTable(roleId); }; window.togglePermissionGroup = function(header) { var body = header && header.nextElementSibling; if (!body) return; var collapsed = body.style.display === 'none'; body.style.display = collapsed ? '' : 'none'; header.firstChild.textContent = collapsed ? '▾ ' : '▸ '; }; window.filterPermissionCards = function(wrapId, keyword) { var wrap = document.getElementById(wrapId); if (!wrap) return; var kw = String(keyword || '').trim().toLowerCase(); wrap.querySelectorAll('.perm-config-group').forEach(function(group) { var visibleCount = 0; group.querySelectorAll('.perm-config-item').forEach(function(item) { var match = !kw || ((group.dataset.search || '') + ' ' + (item.dataset.search || '')).toLowerCase().includes(kw); item.style.display = match ? '' : 'none'; if (match) visibleCount++; }); group.style.display = visibleCount ? '' : 'none'; if (kw && visibleCount) { var body = group.querySelector('.perm-group-body'); if (body) body.style.display = ''; } }); }; // ─── 字段权限矩阵(角色/用户共用辅助) ─── window.toggleFieldPermSection = function(header) { var body = header && header.parentNode && header.parentNode.querySelector('.field-perm-body'); if (!body) return; var collapsed = body.style.display === 'none'; body.style.display = collapsed ? '' : 'none'; var arrow = header.querySelector('span'); if (arrow) arrow.textContent = collapsed ? '▼' : '▸'; }; // 生成角色字段权限勾选矩阵 HTML(查看/编辑两列) window._buildRoleFieldPermHtml = function(page, roleId) { var fields = _permFields[page] || []; if (!fields.length) return ''; var h = ''; h += '
'; h += '
📋 字段权限 ▼
'; h += '
'; return h; }; function renderRoleOpPermTable(roleId) { var tbody = document.getElementById('roleOpModalTbody'); if (!tbody) return; var bodyHtml = ''; _permMenuTree.forEach(function(group) { var items = group.items || []; if (items.length === 0) return; // 每组一个卡片 bodyHtml += '
'; bodyHtml += '
▾ ' + escHtml(group.name) + '
'; items.forEach(function(item) { var feats = item.features || []; bodyHtml += '
'; bodyHtml += '' + escHtml(item.name) + ''; feats.forEach(function(f) { var key = roleId + '_' + item.page + '_' + f.id; var checked = _roleOpData[key] == 1 ? 'checked' : ''; bodyHtml += ''; }); bodyHtml += window._buildRoleFieldPermHtml(item.page, roleId); bodyHtml += '
'; }); bodyHtml += '
'; }); tbody.innerHTML = ''; // 把卡片放到tbody的容器里(实际放在其父容器中) var wrap = document.getElementById('roleOpModalTable'); if (wrap) { var parent = wrap.parentNode; // 隐藏table,用卡片替代 wrap.style.display = 'none'; var cardWrap = document.getElementById('roleOpModalCardWrap'); if (!cardWrap) { cardWrap = document.createElement('div'); cardWrap.id = 'roleOpModalCardWrap'; parent.appendChild(cardWrap); } cardWrap.innerHTML = bodyHtml; } } function saveRoleOpPerms(modalId) { var items = []; var changes = []; document.querySelectorAll('.role-op-cb').forEach(function(cb) { var item = {role:cb.dataset.role,page:cb.dataset.page,operation:cb.dataset.op,permission:cb.checked?1:0}; items.push(item); var key = item.role + '_' + item.page + '_' + item.operation; var oldValue = _roleOpData[key] == 1 ? 1 : 0; if (oldValue !== item.permission) { var label = cb.parentNode ? cb.parentNode.textContent.trim() : item.operation; changes.push((item.permission ? '✅ 开启 ' : '⛔ 关闭 ') + escHtml(item.page + ' / ' + label)); } }); // 字段级权限勾选框 document.querySelectorAll('.role-field-cb').forEach(function(cb) { var item = {role:cb.dataset.role,page:cb.dataset.page,operation:'field:' + cb.dataset.field + ':' + cb.dataset.op,permission:cb.checked?1:0}; items.push(item); var key = item.role + '_' + item.page + '_' + item.operation; var oldValue = _roleOpData[key] == 1 ? 1 : 0; if (oldValue !== item.permission) { var opName = cb.dataset.op === 'view' ? '查看' : '编辑'; changes.push((item.permission ? '✅ 开启 ' : '⛔ 关闭 ') + escHtml(item.page + ' / ' + cb.dataset.field + '·' + opName)); } }); if (!changes.length) { alert('没有权限变更'); return; } var preview = '本次共变更 ' + changes.length + ' 项:
' + changes.slice(0,50).join('
') + (changes.length > 50 ? '
……其余 ' + (changes.length-50) + ' 项' : '') + '
'; showCenteredConfirm('权限变更预览', preview).then(function(confirmed) { if (!confirmed) return; fetch('/api/permissions/role-operations/save', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items:items})}) .then(function(r){return r.json();}).then(function(res){ if (res.code === 0) { _roleOpPerms.length = 0; items.forEach(function(item){_roleOpPerms.push(item);}); alert('✅ 角色操作权限已保存'); if (modalId) document.getElementById(modalId)?.remove(); } else alert(res.msg || '保存失败'); }).catch(function(e){alert('保存失败: ' + (e.message || '网络错误'));}); }); } function renderPermUserGrid() { var el = document.getElementById('permUserGrid'); if (!el) return; var kw = (document.getElementById('permFilterName')?.value || '').trim().toLowerCase(); var posFilter = document.getElementById('permFilterPos')?.value || ''; var levelFilter = document.getElementById('permFilterLevel')?.value || ''; var filtered = _permUsers.filter(function(u) { if (kw) { if (!(u.real_name || '').toLowerCase().includes(kw) && !(u.username || '').toLowerCase().includes(kw)) return false; } if (posFilter && u.position !== posFilter) return false; if (levelFilter && u.user_level !== levelFilter) return false; return true; }); var countEl = document.getElementById('permFilterCount'); if (countEl) countEl.textContent = filtered.length + ' / ' + _permUsers.length + ' 人'; var roleNames = { 'super_admin': '超级管理员', 'admin': '管理员', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'product_developer': '产品开发', 'operator': '运营专员', 'viewer': '查看者', '查看者': '查看者' }; var roleColors = { 'super_admin': '#f5222d', 'admin': '#1890ff', 'procurement_planner': '#722ed1', 'procurement_follower': '#13c2c2', 'product_developer': '#eb2f96', 'operator': '#fa8c16', 'viewer': '#8c8c8c', '查看者': '#8c8c8c' }; var roleBadgeColors = { 'super_admin': '#fff1f0', 'admin': '#e6f7ff', 'procurement_planner': '#f9f0ff', 'procurement_follower': '#e6fffb', 'product_developer': '#fff0f6', 'operator': '#fff7e6', 'viewer': '#f5f5f5', '查看者': '#f5f5f5' }; var roleTextColors = { 'super_admin': '#cf1322', 'admin': '#096dd9', 'procurement_planner': '#531dab', 'procurement_follower': '#08979c', 'product_developer': '#c41d7f', 'operator': '#d46b08', 'viewer': '#595959', '查看者': '#595959' }; // 按角色分组:超级管理员 → 管理员 → 查看者 → 其他 var groupSpecs = [ {role: 'super_admin', icon: '⭐', label: '超级管理员'}, {role: 'admin', icon: '🔐', label: '管理员'}, {role: 'viewer', icon: '👁️', label: '查看者'} ]; var roleGroups = {}; var otherUsers = []; filtered.forEach(function(u) { var found = false; groupSpecs.forEach(function(gs) { if (u.role === gs.role) { if (!roleGroups[gs.role]) roleGroups[gs.role] = []; roleGroups[gs.role].push(u); found = true; } }); if (!found) otherUsers.push(u); }); function renderUserSection(users, sectionInfo) { var h = ''; if (!users || users.length === 0) return ''; var icon = sectionInfo.icon || '👤'; var label = sectionInfo.label || '成员'; var roleId = sectionInfo.role || ''; h += '
'; h += '' + icon + ' ' + label + ''; h += '
'; h += '
'; users.forEach(function(u) { var initial = u.real_name ? u.real_name.charAt(0) : '?'; var roleName = roleNames[u.role] || u.role; var badgeBg = roleBadgeColors[u.role] || '#f5f5f5'; var badgeText = roleTextColors[u.role] || '#595959'; var avatarBg = roleColors[u.role] || '#8c8c8c'; var permCount = Object.keys(u.permissions || {}).length; var positionNames = { 'bp': 'BP', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'procurement_trainee': '采购管培生', '采购开发': '采购开发' }; var levelNames = {'employee': '员工', 'leader': '组长', 'director': '主管'}; var posName = positionNames[u.position] || u.position || ''; var levelName = levelNames[u.user_level] || u.user_level || ''; h += '
'; // 头像 h += '
' + escHtml(initial) + '
'; // 信息 h += '
'; h += '
'; h += escHtml(u.real_name || u.username); h += '' + escHtml(roleName) + ''; if (levelName) { h += '' + levelName + ''; } h += '
'; var infoParts = ['账号: ' + escHtml(u.username)]; if (posName) infoParts.push('岗位: ' + escHtml(posName)); if (levelName) infoParts.push('级别: ' + levelName); if (permCount > 0) infoParts.push('已配置 ' + permCount + ' 个页面'); h += '
' + infoParts.join(' · ') + '
'; h += '
'; h += '›'; h += '
'; }); h += '
'; return h; } var html = ''; var first = true; groupSpecs.forEach(function(gs) { var users = roleGroups[gs.role] || []; if (users.length > 0) { if (!first) html += '
'; html += renderUserSection(users, gs); first = false; } }); if (otherUsers.length > 0) { if (!first) html += '
'; html += renderUserSection(otherUsers, {icon:'👥', label:'其他成员', role:''}); } html += ''; if (filtered.length === 0) { html = '
无匹配用户
'; } el.innerHTML = html; } // 打开用户权限面板 - 三态配置:继承角色 / 单独允许 / 单独拒绝 window._buildUserFieldStateSelect = function(page, field, op, userVal, roleVal) { var state = userVal === 1 ? 'allow' : (userVal === 0 ? 'deny' : 'inherit'); var roleAllowed = roleVal === 1; var bg = state === 'allow' ? '#e6f7ff' : (state === 'deny' ? '#fff1f0' : '#fafafa'); var border = state === 'allow' ? '#91caff' : (state === 'deny' ? '#ffa39e' : '#d9d9d9'); var h = ''; return h; }; window.openUserPermPanel = function(userId) { var existing = document.querySelectorAll('.modal-overlay'); existing.forEach(function(el) { if (el.id !== 'permModal' && el.id !== 'userModal') el.remove(); }); var user = (_permUsers || []).find(function(u) { return u.id == userId; }); if (!user) { alert('未找到该用户'); return; } if (user.role === 'super_admin') { alert('超级管理员固定拥有全部权限,无需配置'); return; } if (window._currentRole === 'admin' && user.role === 'admin') { alert('管理员不能修改其他管理员的个人权限'); return; } fetch('/api/permissions/user/field-perms?user_id=' + userId + '&_t=' + Date.now()) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert('加载失败: ' + (res.msg || '')); return; } var data = res.data || {}; var roleDefaults = data.role_defaults || {}; var overrides = data.user_overrides || {}; var userFieldPerms = data.field_perms || {}; var roleFieldPerms = data.role_field_perms || {}; var html = ''; var div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div.firstElementChild); }).catch(function(e) { alert('请求失败: ' + e.message); }); }; window.saveUserPermPanel = function(userId) { var items = []; var fieldItems = []; var changes = []; document.querySelectorAll('.user-perm-state').forEach(function(sel) { var state = sel.value; var original = sel.options[sel.selectedIndex].defaultSelected ? state : (sel.dataset.original || 'inherit'); if (!sel.dataset.original) { for (var i=0;i 项:
' + changes.slice(0,50).join('
') + '
'; showCenteredConfirm('个人权限变更预览', preview).then(function(confirmed) { if (!confirmed) return; var saveBtn=document.getElementById('userPermSaveBtn'); if(saveBtn){saveBtn.disabled=true;saveBtn.textContent='保存中...';} var opPromise = fetch('/api/permissions/user/save',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({user_id:parseInt(userId),items:items})}).then(function(r){return r.json();}); var fieldPromise = fetch('/api/permissions/user/field-perms/save',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({user_id:parseInt(userId),items:fieldItems})}).then(function(r){return r.json();}); Promise.all([opPromise, fieldPromise]).then(function(results){ var opRes = results[0], fieldRes = results[1]; if (opRes.code===0 && fieldRes.code===0) { alert('✅ 保存成功'); document.querySelector('.modal-overlay')?.remove(); loadPermissionsUsers(); } else { var msg = (opRes.code!==0 ? opRes.msg : '') || (fieldRes.code!==0 ? fieldRes.msg : '') || '保存失败'; alert('❌ ' + msg); } }).catch(function(e){alert('保存失败: '+e.message);}).finally(function(){if(saveBtn){saveBtn.disabled=false;saveBtn.textContent='💾 保存';}}); }); }; var _editingUserId = null; var _rolePagePerms = {}; // {page: 1/0} - 角色默认的页面权限 // 菜单树结构(与侧边栏菜单一致) // 菜单树结构:每个菜单项列出实际的功能按钮(不是通用的增删改查) var _permMenuTree = []; // ─── 每个页面的字段/卡片定义 ─── window._permPageFields = { // 数据概览:按图表和卡片设置权限 'dashboard': ['销量概览卡片','库存概览卡片','采购待办卡片','出库趋势图','品类分布图','库存排行表'], // 出库数据 'data_summary': ['货品编号','货品名称','品牌','仓库','店铺','总数量','总成本','日期','数据来源'], 'data_detail': ['货品编号','货品名称','品牌','仓库','店铺','一级类目','二级类目','三级类目','货品数量','货品成本','日期','数据来源'], // 库存信息 'inventory_finished': ['货品编号','货品名称','品牌','仓库','库存','生产在途','采购在途','供应商','库存日期','采购跟单','采购计划'], 'inventory_packaging': ['货品编号','货品名称','品牌','仓库','库存','生产在途','采购在途','供应商','库存日期'], // 计划预测:与字段设置保持一致 'planning': [], // 将在渲染时从 PLAN_COLUMNS 动态生成 // 采购订单 'purchase': ['产品类型','货品编号','货品名称','确认下单','产品分级','供应商名称','建议下单数量','正式下单数量','下单备注','货期','物流','总支撑天数','平台','运营','审核价格','下单人员','下单时间','采购跟单','采购计划','加入时间'], 'purchase_ready': ['产品类型','货品编号','货品名称','下单时间','下单人','生产单号','建议下单数量','正式下单数量','产品分级','供应商名称','下单备注','货期','物流','总支撑天数','采购跟单','采购计划','平台','运营','是否审核价格'], 'orders_domestic': ['日期','计划下单时间','新旧品','货品编号','货品名称','货品类型','下单备注','单价','审核价格','数量','金额','供货商','仓库','金蝶采购单','采购员','下单人','进度','旺店通单号','采购开发','预计到货日期','金蝶入库'], 'orders_overseas': ['产品编码','货品名称','货品类型','日期','计划下单时间','单价','审核价格','数量','金额','供应商','仓库','新旧品','下单备注','采购员','下单人','进度','旺店通单号','采购开发','预计到货日期'], 'orders_packaging': ['日期','计划下单时间','产品编码','货品名称','货品类型','单价','审核价格','数量','金额','供应商','仓库','新旧品','采购员','下单人','进度','下单备注','采购开发','旺店通单号'], 'orders_finished': ['品类','货品编号','货品名称','货品类型','供应商','可用库存','昨日销量','3天均销','7天均销','15天均销','支撑天数','加工厂库存','采购总数量','生产在途','采购在途','产品进度','跟单小组','采购计划','备注'], // 基础信息 'products': ['货品编号','货品名称','货品类型','品牌','供应商编码','供应商名称','货期','物流时间','采购跟单','采购计划','平台','运营','开发专员','创建时间','导入日期'], 'brand_management': ['品牌','产品数量','分类','更新时间'], 'bom': ['成品编号','成品名称','物料编号','物料名称','用量','供应商','成本价','创建时间'], 'warehouse': ['仓库编号','仓库名称','仓库类型','地址','联系人','联系电话','创建时间'], 'suppliers': ['供应商编码','供应商名称','联系人','联系电话','地址','合作状态','创建时间'], // 知识库 'knowledge': ['标题','主题分类','创建人','创建时间','更新时间','状态'], // 系统设置 'users': ['用户名','姓名','角色','岗位','用户级别','手机号','邮箱','状态','最后登录'], 'permissions': ['用户名','姓名','角色','岗位'], 'login_records': ['用户名','登录时间','IP地址','操作'], 'logs': ['操作人','操作类型','操作详情','操作时间','IP地址'], 'config': ['配置项','配置值','说明','更新时间'], 'contract_templates': ['模板名称','适用类型','上传人','上传时间','文件大小'], 'feedback': ['编号','标题','反馈人','提交时间','状态','处理人','处理时间'] }; // 切换字段权限折叠 // ─── 参数配置 ─── function loadConfigPage(container) { container.innerHTML = `
`; fetch('/api/config/list') .then(r => r.json()) .then(res => { if (res.code !== 0) return; const d = res.data; const el = document.getElementById('configContent'); let trendHtml = renderConfigTable('趋势判断基础配置参数', ['趋势类型', '判断条件', '说明'], ['trend_type', 'condition_expr', 'notes'], d.trend_config); let salesHtml = renderSalesPredictConfigTable(d.sales_predict_config || []); let weightHtml = renderConfigTable('云仓外仓权重参数', ['参数名称', '参数计算逻辑'], ['param_name', 'param_logic'], d.param_list || []); let globalHtml = '

全局配置参数

'; globalHtml += ''; (d.global_config || []).forEach(function(g) { globalHtml += ''; globalHtml += ''; globalHtml += ''; globalHtml += ''; globalHtml += ''; globalHtml += ''; }); globalHtml += '
参数名称参数值说明操作
' + escHtml(g.config_key) + '' + escHtml(g.description || '') + '
'; window.saveGlobalConfig = function() { var items = []; (d.global_config || []).forEach(function(g) { var input = document.getElementById("cfg-" + g.config_key); if (input) { items.push({config_key: g.config_key, config_value: input.value}); } }); fetch('/api/config/save_global', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(items) }) .then(r => r.json()) .then(res => { alert(res.msg || '保存成功'); }); }; el.innerHTML = trendHtml + salesHtml + weightHtml + globalHtml; bindSalesPredictConfigEvents(); }); } function renderSalesPredictConfigTable(data) { var fields = [ {key: 'avg_3d', label: '3天平均销量'}, {key: 'avg_7d', label: '7天平均销量'}, {key: 'avg_14d', label: '14天平均销量'}, {key: 'adjust_factor', label: '调节系数'} ]; var html = '

日销量预测基础配置参数

修改后按回车保存
'; html += '
'; fields.forEach(function(f) { html += ''; }); html += ''; data.forEach(function(row) { html += ''; fields.forEach(function(f) { html += ''; }); html += ''; html += ''; }); html += '
' + f.label + '趋势类型状态
' + escHtml(row.trend_type || '') + '未修改
共 ' + data.length + ' 条记录;权重和调节系数支持按趋势分别调整。
'; return html; } function saveSalesPredictConfigRow(row) { if (!row || row.dataset.saving === '1') return; var inputs = row.querySelectorAll('.sales-config-input'); var item = {id: Number(row.getAttribute('data-sales-config-id'))}; var invalid = false; inputs.forEach(function(input) { var value = input.value.trim(); if (value === '' || !isFinite(Number(value)) || Number(value) < 0) { input.style.borderColor = '#ff4d4f'; invalid = true; } else { input.style.borderColor = ''; item[input.getAttribute('data-field')] = value; } }); if (invalid) { var status = row.querySelector('.sales-config-status'); if (status) { status.textContent = '请输入有效的非负数字'; status.style.color = '#ff4d4f'; } return; } item.trend_type = (row.querySelector('code') || {}).textContent || ''; row.dataset.saving = '1'; var status = row.querySelector('.sales-config-status'); if (status) { status.textContent = '保存中...'; status.style.color = '#1890ff'; } fetch('/api/config/save_sales', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}, body: JSON.stringify([item]) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) throw new Error(res.msg || '保存失败'); if (status) { status.textContent = '已保存'; status.style.color = '#52c41a'; } refreshConfigLinkedPages(); }).catch(function(err) { if (status) { status.textContent = err.message || '保存失败'; status.style.color = '#ff4d4f'; } }).finally(function() { row.dataset.saving = '0'; }); } function refreshConfigLinkedPages() { // 页面采用缓存容器,保存后当前页面立即重算;未打开页面清除旧容器,切回时重新加载最新结果。 ['planning', 'products', 'purchase', 'purchase_ready'].forEach(function(page) { if (!window._pageCache || !window._pageCache[page]) return; var container = window._pageCache[page]; var isCurrent = container.isConnected; if (isCurrent) { if (page === 'planning' && typeof loadPlanPage === 'function') loadPlanPage(1); else if (page === 'products' && typeof loadProducts === 'function') loadProducts(1); else if (page === 'purchase' && typeof loadPendingList === 'function') loadPendingList(); else if (page === 'purchase_ready' && typeof loadBackupList === 'function') loadBackupList(false); } else { delete window._pageCache[page]; if (window._pageDataCache) delete window._pageDataCache[page]; } }); } function bindSalesPredictConfigEvents() { document.querySelectorAll('#configContent tr[data-sales-config-id] .sales-config-input').forEach(function(input) { input.addEventListener('input', function() { var status = input.closest('tr').querySelector('.sales-config-status'); if (status) { status.textContent = '待保存'; status.style.color = '#fa8c16'; } }); input.addEventListener('keydown', function(event) { if (event.key === 'Enter') { event.preventDefault(); saveSalesPredictConfigRow(input.closest('tr')); } }); }); } function renderConfigTable(title, labels, fields, data) { let html = '
'; html += '

' + title + '

'; html += '
'; html += ''; labels.forEach(function(l) { html += ''; }); html += ''; data.forEach(function(r) { html += ''; fields.forEach(function(f) { var val = r[f]; if (val === null || val === undefined) val = '-'; html += ''; }); html += ''; }); html += '
' + l + '
' + escHtml(String(val)) + '
'; html += '
共 ' + data.length + ' 条记录
'; html += '
'; return html; } // 可排序的数值列(点击表头排序) var SORTABLE_FIELDS = { '昨日销量':1, '云仓昨日销量':1, '外仓昨日销量':1, '库存':1, '云仓库存':1, '外仓库存':1, '加工厂库存':1, '云仓可分配库存':1, '外仓可分配库存':1, '生产在途':1, '采购在途':1, '日销量预测':1, '云仓日销量预测':1, '外仓日销量预测':1, '库存健康指数':1, '成本价':1, 'cost_price':1, '3天平均销量':1, '云仓3天平均销量':1, '外仓3天平均销量':1, '7天平均销量':1, '云仓7天平均销量':1, '外仓7天平均销量':1, '14天平均销量':1, '云仓14天平均销量':1, '外仓14天平均销量':1, '30天平均销量':1, '60天平均销量':1, '3天最高销量':1, '7天最高销量':1, '14天最高销量':1, '30天最高销量':1, '14天最低销量':1, '14天标准差':1, '库存可支撑天数':1, '库存工厂可支撑天数':1, '加工厂库存支撑天数':1, '库存工厂待生产可支撑天数':1, '总支撑天数':1, '云仓支撑天数':1, '外仓支撑天数':1, '最低销量偏差率':1, '最高销量偏差率':1, '14天波动率':1, '建议下单数量':1, '实际下单数量':1, '建议回货数量':1, '生产货期':1, '物流时间':1, '最低起订量':1, '总库存':1, '总在途库存':1, '正式下单数量':1 }; // ─── 计划预测页面 ─── // 所有计划列定义(按照指定顺序和显隐) const PLAN_COLUMNS = [ {field: 'product_type', label: '产品类型', defaultShow: true}, {field: '产品种类', label: '产品种类', defaultShow: false}, {field: 'product_code', label: '货品编号', defaultShow: true}, {field: 'product_name', label: '货品名称', defaultShow: true}, {field: 'brand', label: '品牌', defaultShow: true}, {field: '批注', label: '批注', defaultShow: true}, {field: 'bom', label: '下单清单', defaultShow: true, filterable: false}, {field: '产品分级', label: '产品分级', defaultShow: true}, {field: '昨日销量', label: '昨日销量', defaultShow: true}, {field: '云仓昨日销量', label: '云仓昨日销量', defaultShow: false}, {field: '外仓昨日销量', label: '外仓昨日销量', defaultShow: false}, {field: '销售趋势', label: '销售趋势', defaultShow: true}, {field: '总库存', label: '总库存', defaultShow: true}, {field: '库存', label: '库存', defaultShow: true}, {field: '总在途库存', label: '总在途库存', defaultShow: true}, {field: '云仓库存', label: '云仓库存', defaultShow: false}, {field: '外仓库存', label: '外仓库存', defaultShow: false}, {field: '加工厂库存', label: '加工厂库存', defaultShow: true}, {field: '云仓可分配库存', label: '云仓可分配库存', defaultShow: true}, {field: '外仓可分配库存', label: '外仓可分配库存', defaultShow: true}, {field: '是否调拨', label: '是否调拨', defaultShow: true}, {field: '生产在途', label: '生产在途', defaultShow: true}, {field: '采购在途', label: '采购在途', defaultShow: true}, {field: '日销量预测', label: '日销量预测', defaultShow: true}, {field: '云仓日销量预测', label: '云仓日销量预测', defaultShow: false}, {field: '外仓日销量预测', label: '外仓日销量预测', defaultShow: false}, {field: '建议下单数量', label: '建议下单数量', defaultShow: true}, {field: '实际下单数量', label: '实际下单数量', defaultShow: true}, {field: '正式下单数量', label: '正式下单数量', defaultShow: true}, {field: '库存健康指数', label: '库存健康指数', defaultShow: true}, {field: '风险把控', label: '风险把控', defaultShow: true}, {field: '供应商名称', label: '供应商名称', defaultShow: true}, {field: '采购跟单', label: '跟单小组', defaultShow: true}, {field: '采购计划', label: '采购计划', defaultShow: true}, {field: '开发专员', label: '开发专员', defaultShow: true}, {field: '归属平台', label: '归属平台', defaultShow: true}, {field: '运营专员', label: '运营专员', defaultShow: true}, {field: 'cost_price', label: '成本价', defaultShow: false}, {field: '3天平均销量', label: '3天平均销量', defaultShow: false}, {field: '云仓3天平均销量', label: '云仓3天平均销量', defaultShow: false}, {field: '外仓3天平均销量', label: '外仓3天平均销量', defaultShow: false}, {field: '7天平均销量', label: '7天平均销量', defaultShow: false}, {field: '云仓7天平均销量', label: '云仓7天平均销量', defaultShow: false}, {field: '外仓7天平均销量', label: '外仓7天平均销量', defaultShow: false}, {field: '14天平均销量', label: '14天平均销量', defaultShow: false}, {field: '云仓14天平均销量', label: '云仓14天平均销量', defaultShow: false}, {field: '外仓14天平均销量', label: '外仓14天平均销量', defaultShow: false}, {field: '库存可支撑天数', label: '库存可支撑天数', defaultShow: false}, {field: '库存工厂可支撑天数', label: '现货采购在途', defaultShow: false}, {field: '加工厂库存支撑天数', label: '加工厂库存支撑', defaultShow: false}, {field: '库存工厂待生产可支撑天数', label: '定制品在途', defaultShow: false}, {field: '总支撑天数', label: '总支撑天数', defaultShow: false}, {field: '云仓支撑天数', label: '云仓支撑天数', defaultShow: true}, {field: '外仓支撑天数', label: '外仓支撑天数', defaultShow: true}, {field: '建议回货数量', label: '建议回货数量', defaultShow: false}, {field: 'specification', label: '产品规格', defaultShow: false}, {field: '30天平均销量', label: '30天平均销量', defaultShow: false}, {field: '60天平均销量', label: '60天平均销量', defaultShow: false}, {field: '3天最高销量', label: '3天最高销量', defaultShow: false}, {field: '7天最高销量', label: '7天最高销量', defaultShow: false}, {field: '14天最高销量', label: '14天最高销量', defaultShow: false}, {field: '30天最高销量', label: '30天最高销量', defaultShow: false}, {field: '14天最低销量', label: '14天最低销量', defaultShow: false}, {field: '14天标准差', label: '14天标准差', defaultShow: false}, {field: '最低销量偏差率', label: '最低销量偏差率', defaultShow: false}, {field: '最高销量偏差率', label: '最高销量偏差率', defaultShow: false}, {field: '极值判断', label: '极值判断', defaultShow: false}, {field: '14天波动率', label: '14天波动率', defaultShow: false}, {field: '波动风险', label: '波动风险', defaultShow: false}, {field: '生产货期', label: '生产货期', defaultShow: false}, {field: '物流时间', label: '物流时间', defaultShow: false}, {field: '最低起订量', label: '最低起订量', defaultShow: false}, {field: '创建时间', label: '创建时间', defaultShow: false}, {field: '是否新品', label: '是否新品', defaultShow: false}, {field: '一级类目', label: '一级类目', defaultShow: false}, {field: '二级类目', label: '二级类目', defaultShow: false}, {field: '三级类目', label: '三级类目', defaultShow: false}, {field: 'custom_field1', label: '自定义字段1', defaultShow: false}, {field: 'custom_field2', label: '自定义字段2', defaultShow: false}, {field: 'custom_field3', label: '自定义字段3', defaultShow: false}, {field: 'custom_field4', label: '自定义字段4', defaultShow: false}, {field: 'custom_field5', label: '自定义字段5', defaultShow: false}, ]; // 筛选字段预定义选项(下拉选择类型) const PLAN_FILTER_OPTIONS = { 'product_type': ['成品', '包材', '现货', '赠品', '清库存', '其他'], '产品分级': ['S级', 'A级', 'B级', 'C级', '不参与评级'], '销售趋势': ['开始上升', '开始下跌', '平稳趋势', '上升趋稳', '下跌趋稳', '持续上升', '持续下跌', '急增', '急跌', '近期无销售'], '是否新品': ['新品', '返单品'], '风险把控': ['正常', '新品', '呆滞品', '需启动清库存'], '波动风险': ['X', 'Y', 'Z'], '极值判断': ['正常', '极大', '极小', '极大/极小'], '产品种类': ['成品', '包材', '现货', '赠品', '清库存', '其他'], '是否调拨': ['云仓', '外仓', '云仓和外仓'], '库存健康指数': ['999'] }; // 动态筛选字段中排除的计算字段(这些值来自后台计算,不可直接查询) const PLAN_FILTER_EXCLUDE_FIELDS = { '产品分级':1, '昨日销量':1, '云仓昨日销量':1, '外仓昨日销量':1, '销售趋势':1, '库存':1, '云仓库存':1, '外仓库存':1, '加工厂库存':1, '云仓可分配库存':1, '外仓可分配库存':1, '是否调拨':1, '生产在途':1, '采购在途':1, '日销量预测':1, '云仓日销量预测':1, '外仓日销量预测':1, '库存健康指数':1, '风险把控':1, '库存可支撑天数':1, '库存工厂可支撑天数':1, '加工厂库存支撑天数':1, '库存工厂待生产可支撑天数':1, '总支撑天数':1, '云仓支撑天数':1, '外仓支撑天数':1, '建议下单数量':1, '实际下单数量':1, '建议回货数量':1, '3天平均销量':1, '7天平均销量':1, '14天平均销量':1, '30天平均销量':1, '60天平均销量':1, '3天最高销量':1, '7天最高销量':1, '14天最高销量':1, '30天最高销量':1, '14天最低销量':1, '14天标准差':1, '最低销量偏差率':1, '最高销量偏差率':1, '极值判断':1, '14天波动率':1, '波动风险':1, '是否新品':1 }; // 字段显隐状态(实际生效的) if (!window.planColVisible) { window.planColVisible = {}; PLAN_COLUMNS.forEach((c, i) => { window.planColVisible[c.field] = c.defaultShow; window['_planColIdx_' + c.field] = i; }); } // 三个Tab各自独立的字段显隐状态 if (!window._planViewVisibility) { window._planViewVisibility = {'default': {}, 'cloud': {}, 'external': {}}; PLAN_COLUMNS.forEach(function(c) { ['default', 'cloud', 'external'].forEach(function(view) { // 云仓库存/外仓库存/云仓昨日销量/外仓昨日销量仅国内品(cloud)视图默认显示 var cloudOnly = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量', '云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量', '云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测']; if (cloudOnly.indexOf(c.field) >= 0 && view !== 'cloud') { window._planViewVisibility[view][c.field] = false; } else if (cloudOnly.indexOf(c.field) >= 0 && view === 'cloud') { window._planViewVisibility[view][c.field] = true; } else { window._planViewVisibility[view][c.field] = c.defaultShow; } }); }); // 当前视图同步到planColVisible(默认default视图) PLAN_COLUMNS.forEach(function(c) { window.planColVisible[c.field] = window._planViewVisibility['default'][c.field]; }); } // 弹窗中的临时状态(确定后才生效) if (!window._planPendingVisible) { window._planPendingVisible = {}; PLAN_COLUMNS.forEach((c, i) => { window._planPendingVisible[c.field] = c.defaultShow; }); } // 弹窗中拖拽后的列顺序(存储字段名数组) if (!window._planColDragOrder) { window._planColDragOrder = PLAN_COLUMNS.map(c => c.field); } // 行内编辑功能 window.prodInlineEditClick = function(el) { inlineEditClick(el); }; window.inlineEditClick = function(el) { if (window._currentRole !== 'admin') return; var code = el.dataset.code; var field = el.dataset.field; var val = el.dataset.val; var input = document.createElement('input'); input.type = 'text'; input.className = 'form-input'; input.style.width = '80px'; input.style.height = '28px'; input.style.padding = '2px 6px'; input.value = val === '点击编辑' ? '' : val; input.dataset.code = code; input.dataset.field = field; el.parentNode.replaceChild(input, el); input.focus(); input.select(); function saveEdit() { var newVal = input.value.trim(); var fieldMap = {'生产货期':'lead_time','物流时间':'logistics_time','最低起订量':'min_order_qty'}; var apiField = fieldMap[field] || field; fetch('/api/product/update_fields', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, field: apiField, value: newVal || '0'}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert(res.msg); } loadPlanPage(planCurrentPage); }) .catch(function(e) { alert('保存失败'); }); } input.addEventListener('blur', saveEdit); input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { input.blur(); } if (e.key === 'Escape') { var span = document.createElement('span'); span.className = 'inline-edit'; span.dataset.code = code; span.dataset.field = field; span.dataset.val = val; span.onclick = function() { inlineEditClick(this); }; span.textContent = val || (window._currentRole === 'admin' ? '点击编辑' : '-'); input.parentNode.replaceChild(span, input); } }); } function applyPlanColVisible() { PLAN_COLUMNS.forEach((c, i) => { window['_planColIdx_' + c.field] = i; const show = window.planColVisible[c.field]; document.querySelectorAll('.plan-col-' + i).forEach(el => { el.style.display = show ? '' : 'none'; }); document.querySelectorAll('.plan-th-' + i).forEach(el => { el.style.display = show ? '' : 'none'; }); }); } function renderPlanColCheckboxes(grid) { if (!grid) grid = document.getElementById('planColGrid'); if (!grid) return; grid.innerHTML = ''; const order = window._planColDragOrder || PLAN_COLUMNS.map(c => c.field); order.forEach((field, i) => { const col = PLAN_COLUMNS.find(c => c.field === field); if (!col) return; // 云仓/外仓库存仅在国内品(cloud)视图的字段设置中显示 // 云仓相关字段仅在国内品(cloud)视图的字段设置中显示 var cloudOnlyFields = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量', '云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量', '云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测']; if (planActiveView !== 'cloud' && cloudOnlyFields.indexOf(field) >= 0) return; const checked = window._planPendingVisible[field] ? 'checked' : ''; const label = document.createElement('label'); label.draggable = true; label.dataset.field = field; label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px'; label.innerHTML = `⠿ ${col.label}`; label.addEventListener('dragstart', function(e) { e.dataTransfer.setData('text/plain', field); this.style.opacity = '0.4'; }); label.addEventListener('dragend', function(e) { this.style.opacity = '1'; document.querySelectorAll('#planColGrid label').forEach(el => el.style.border = ''); }); label.addEventListener('dragover', function(e) { e.preventDefault(); document.querySelectorAll('#planColGrid label').forEach(el => el.style.border = ''); this.style.border = '2px dashed var(--primary)'; }); label.addEventListener('dragleave', function(e) { this.style.border = ''; }); label.addEventListener('drop', function(e) { e.preventDefault(); document.querySelectorAll('#planColGrid label').forEach(el => el.style.border = ''); const fromField = e.dataTransfer.getData('text/plain'); const toField = this.dataset.field; if (fromField === toField) return; const order = window._planColDragOrder; const fromIdx = order.indexOf(fromField); const toIdx = order.indexOf(toField); if (fromIdx === -1 || toIdx === -1) return; order.splice(fromIdx, 1); order.splice(toIdx, 0, fromField); renderPlanColCheckboxes(); }); grid.appendChild(label); }); } function openPlanColModal() { // 初始化拖拽顺序 window._planColDragOrder = PLAN_COLUMNS.map(c => c.field); const grid = document.getElementById('planColGrid'); if (grid) renderPlanColCheckboxes(grid); // 把当前生效状态复制到临时状态 PLAN_COLUMNS.forEach((c, i) => { window._planPendingVisible[c.field] = window.planColVisible[c.field]; const cb = document.getElementById('plan-cb-' + c.field); if (cb) cb.checked = window.planColVisible[c.field]; }); // 同步全选checkbox状态 const allCb = document.getElementById('plan-cb-all'); if (allCb) { allCb.checked = PLAN_COLUMNS.every(c => window.planColVisible[c.field]); } // 重置搜索框 const searchInput = document.getElementById('planColSearchInput'); if (searchInput) { searchInput.value = ''; } filterPlanColCheckboxes(); document.getElementById('planColModal').style.display = 'flex'; // 刷新方案列表 refreshPlanSchemeList(); } function confirmPlanCol() { // 应用拖拽后的新顺序 var newOrder = window._planColDragOrder; if (newOrder) { var reordered = []; newOrder.forEach(function(field) { var col = PLAN_COLUMNS.find(function(c) { return c.field === field; }); if (col) reordered.push(col); }); PLAN_COLUMNS.forEach(function(c) { if (newOrder.indexOf(c.field) === -1) reordered.push(c); }); PLAN_COLUMNS.length = 0; reordered.forEach(function(c) { PLAN_COLUMNS.push(c); }); PLAN_COLUMNS.forEach(function(c, i) { window['_planColIdx_' + c.field] = i; }); } PLAN_COLUMNS.forEach(function(c, i) { // 非cloud视图不允许修改云仓/外仓相关字段显隐 var cloudOnlyFields = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量', '云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量', '云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测']; if (planActiveView !== 'cloud' && cloudOnlyFields.indexOf(c.field) >= 0) return; window.planColVisible[c.field] = window._planPendingVisible[c.field]; // 保存到当前视图的独立状态 if (window._planViewVisibility && window._planViewVisibility[planActiveView]) { window._planViewVisibility[planActiveView][c.field] = window._planPendingVisible[c.field]; } }); planCurrentPage = Math.max(1, planCurrentPage || 1); // 全量刷新页面(确保冻结列的 sticky 位置正确) if (window._pageCache) delete window._pageCache['planning']; loadPage('planning'); document.getElementById('planColModal').style.display = 'none'; } function closePlanColModal() { document.getElementById('planColModal').style.display = 'none'; } function togglePlanCol(field) { window._planPendingVisible[field] = !window._planPendingVisible[field]; const show = window._planPendingVisible[field]; const cb = document.getElementById('plan-cb-' + field); if (cb) cb.checked = show; } function filterPlanColCheckboxes() { const keyword = (document.getElementById('planColSearchInput')?.value || '').trim(); const labels = document.querySelectorAll('#planColCheckboxes label'); labels.forEach(label => { if (!keyword) { label.style.display = ''; } else { const text = label.textContent || ''; label.style.display = text.includes(keyword) ? '' : 'none'; } }); } function togglePlanColPendingAll(checked) { PLAN_COLUMNS.forEach((c, i) => { // 非cloud视图不操作云仓/外仓相关字段 var cloudOnlyFields = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量', '云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量', '云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测']; if (planActiveView !== 'cloud' && cloudOnlyFields.indexOf(c.field) >= 0) return; window._planPendingVisible[c.field] = checked; const cb = document.getElementById('plan-cb-' + c.field); if (cb) cb.checked = checked; }); } function loadPlanningPage(container) { // 冻结列:计算所有可见的冻结列及其 left 偏移 var frozenLefts = {}; if (planFrozen) { var leftAccum = 50; frozenLefts['_index'] = 0; planFrozenFields.forEach(function(f) { if (window.planColVisible[f]) { frozenLefts[f] = leftAccum; leftAccum += (FROZEN_WIDTHS[f] || 100); } }); } var frozenStyle = 'position:sticky;top:0;z-index:2'; if (planFrozen) frozenStyle = 'position:sticky;left:0;top:0;z-index:999;background:var(--bg-card);transform:translateZ(0)'; // 序号列始终同时冻结在左侧和表头位置,避免首次渲染或重建表头时样式短暂丢失。 let theadHtml = '
序号
'; PLAN_COLUMNS.forEach((c, i) => { if (!window.planColVisible[c.field] || !window._canViewField('planning', c.label)) return; const visibleClass = ''; var isSortable = (c.field === '产品分级') || SORTABLE_FIELDS[c.field]; var planTips = { '日销量预测': '日销量预测 = (3天平均×w3 + 7天平均×w7 + 14天平均×w14) × 调整系数', '库存可支撑天数': '库存可支撑天数 = 库存 / 日销量预测', '加工厂库存支撑天数': '加工厂库存支撑 = 加工厂库存 / 日销量预测', '库存工厂可支撑天数': '现货采购在途 = 采购在途 / 日销量预测', '库存工厂待生产可支撑天数': '定制品在途 = 生产在途 / 日销量预测', '总支撑天数': '总支撑天数 = (库存+加工厂库存+生产在途+采购在途) / 日销量预测', '库存健康指数': '日销量预测>0:库存可支撑天数÷(货期+物流);=0:库存可支撑天数÷(货期+物流)', '建议下单数量': '日销量预测=0且库存>0则0,否则(安全库存天数+货期+物流)×日销量预测', '实际下单数量': '建议下单数量向上取整到1000', '正式下单数量': '正式下单数量初始等于实际下单数量,可人工修改', '建议回货数量': 'ROUNDUP((日销量预测×回货天数-库存)×10)/10,上限在途总数', '是否新品': '创建时间5个月以内为新品,否则返单品', '成本价': '从产品信息匹配的成本价格', '销售趋势': '基于3/7/14天日均销量判断的9种趋势类型' }; var tip = planTips[c.label] || ''; var thPos = 'position:sticky;top:0;z-index:10'; if (planFrozen && frozenLefts[c.field] !== undefined) { thPos = 'position:sticky;top:0;left:' + frozenLefts[c.field] + 'px;z-index:999;background:var(--bg-card);transform:translateZ(0);min-width:' + (FROZEN_WIDTHS[c.field] || 100) + 'px'; } var canFilter = c.filterable !== false; var isActive = window._planFilters && window._planFilters[c.field]; var iconColor = isActive ? '#1890ff' : 'var(--text-light)'; var iconWeight = isActive ? 'bold' : 'normal'; var iconChar2 = isActive ? '▼' : '▾'; var filterIcon = canFilter ? '' + iconChar2 + '' : ''; var clickHandler = canFilter ? 'openPlanFilterMenu(\'' + escJs(c.field) + '\', event)' : ''; theadHtml += `${c.label}${filterIcon}`; }); theadHtml += renderCollapsibleActionHeader('planning', 'planTable', true); theadHtml += ''; let colToggleHtml = ''; colToggleHtml += ''; container.innerHTML = `
计划预测
计划预测-国内品
计划预测-海外品
创建时间

产品计划列表

${colToggleHtml}
${theadHtml}
`; // 冻结列按钮 - 事件绑定 var frozenBtn = document.getElementById('planFrozenBtn'); if (frozenBtn) frozenBtn.onclick = openPlanFrozenModal; // 填充字段设置弹窗(3列网格,可拖拽排序) const cbContainer = document.getElementById('planColCheckboxes'); if (cbContainer) { cbContainer.innerHTML = '
'; const grid = document.getElementById('planColGrid'); window._planColDragOrder = PLAN_COLUMNS.map(c => c.field); renderPlanColCheckboxes(grid); } // 禁用浏览器表单自动填充 var planSearchEl = document.getElementById('planSearch'); if (planSearchEl) planSearchEl.setAttribute('autocomplete', 'off'); // 初始化下拉清除按钮 setupFilterClearButtons(); // 恢复当前视图的tab高亮(避免从字段设置确认后tab跳回第一个) document.querySelectorAll('.plan-tab').forEach(function(el) { if (window._planOverseasOnly && el.dataset.view !== 'external') { el.style.display = 'none'; } else { el.style.display = ''; el.classList.toggle('active', el.dataset.view === planActiveView); } }); loadPlanPage(1); // 加载默认方案 loadDefaultPlanScheme(); // 后台预拉取计划页所有列的筛选值 prefetchPlanColumnValues(); } // 采购跟单/采购计划下拉框:选中值时悬浮显示×清除按钮 function setupFilterClearButtons() { var items = [ { el: document.getElementById('planFollowerFilter'), name: '跟单小组' }, { el: document.getElementById('planPlannerFilter'), name: '采购计划' } ]; items.forEach(function(item) { var sel = item.el; if (!sel) return; var wrap = sel.parentNode; if (!wrap.classList.contains('filter-select-wrap')) return; // 防重复 if (wrap.querySelector('.filter-clear-btn')) return; var clearBtn = document.createElement('span'); clearBtn.className = 'filter-clear-btn'; clearBtn.innerHTML = '✕'; clearBtn.style.cssText = 'display:none;position:absolute;right:4px;top:50%;transform:translateY(-50%);width:16px;height:16px;line-height:16px;text-align:center;font-size:12px;cursor:pointer;color:#999;background:#fff;border-radius:50%;z-index:5;user-select:none'; clearBtn.title = '清除筛选'; wrap.style.position = 'relative'; wrap.appendChild(clearBtn); // 更新清除按钮显隐 function updateClearBtn() { clearBtn.style.display = sel.value ? 'block' : 'none'; } updateClearBtn(); sel.addEventListener('change', function() { updateClearBtn(); // 选中值变化时自动查询 searchPlanning(); }); sel.addEventListener('mouseenter', function() { if (sel.value) clearBtn.style.display = 'block'; }); sel.addEventListener('mouseleave', function() { clearBtn.style.display = 'none'; }); clearBtn.addEventListener('mouseenter', function() { clearBtn.style.display = 'block'; }); clearBtn.addEventListener('mouseleave', function() { clearBtn.style.display = 'none'; }); clearBtn.addEventListener('click', function(e) { e.stopPropagation(); sel.value = ''; clearBtn.style.display = 'none'; searchPlanning(); }); }); } let planCurrentPage = 1; let planTotal = 0; let PLAN_PAGE_SIZE = 20; let planSortField = ''; let planSortAsc = false; const GRADE_ORDER = {'S级':0, 'A级':1, 'B级':2, 'C级':3, '不参与评级':4}; function planSortRows(rows) { if (!planSortField) return rows; return rows.sort(function(a, b) { var va = a[planSortField], vb = b[planSortField]; if (planSortField === '产品分级') { va = GRADE_ORDER[va] !== undefined ? GRADE_ORDER[va] : 999; vb = GRADE_ORDER[vb] !== undefined ? GRADE_ORDER[vb] : 999; } else { if (va === null || va === undefined || va === '') va = ''; if (vb === null || vb === undefined || vb === '') vb = ''; va = isNaN(Number(va)) ? String(va).toLowerCase() : Number(va); vb = isNaN(Number(vb)) ? String(vb).toLowerCase() : Number(vb); } if (va < vb) return planSortAsc ? -1 : 1; if (va > vb) return planSortAsc ? 1 : -1; return 0; }); } function loadPlanPage(page) { planCurrentPage = page; const search = document.getElementById('planSearch')?.value || ''; const dateCond = document.getElementById('planDateCond')?.value || ''; const dateVal = document.getElementById('planDateVal')?.value || ''; const supplierFilter = document.getElementById('planSupplierFilter')?.value || ''; const followerFilter = document.getElementById('planFollowerFilter')?.value || ''; const plannerFilter = document.getElementById('planPlannerFilter')?.value || ''; // 从全局筛选状态读取 var filterValue = ''; if (window._planFilters) { var activeCount = 0; var filters = []; Object.keys(window._planFilters).forEach(function(f) { var fp = window._planFilters[f]; if (fp) { fp.field = f; filters.push(fp); activeCount++; } }); if (activeCount > 0) { filterValue = '__multi__:' + JSON.stringify(filters); } } // 所有筛选(包括计算字段)统一由后端完成,再按当前页返回,避免一次渲染全量数据 let url = '/api/planning/list?page=' + page + '&size=' + PLAN_PAGE_SIZE; if (search) url += '&search=' + encodeURIComponent(search); if (filterValue) url += '&custom_field=__multi__&custom_value=' + encodeURIComponent(filterValue); if (dateCond && dateVal) url += '&date_cond=' + encodeURIComponent(dateCond) + '&date_val=' + encodeURIComponent(dateVal); if (supplierFilter) url += '&supplier=' + encodeURIComponent(supplierFilter); if (followerFilter) url += '&procurement_follower=' + encodeURIComponent(followerFilter); if (plannerFilter) url += '&procurement_planner=' + encodeURIComponent(plannerFilter); if (planSortField) url += '&sort_by=' + encodeURIComponent(planSortField) + '&sort_order=' + (planSortAsc ? 'asc' : 'desc'); if (planActiveView !== 'default') url += '&view=' + encodeURIComponent(planActiveView); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; const data = res.data; planTotal = data.total; // 缓存全量行数据(用于计算字段的值列表提取) if (!window._planAllRows) window._planAllRows = {}; if (data.rows) { data.rows.forEach(function(r) { window._planAllRows[r.product_code] = r; }); } window._planCurrentAllCodes = data.all_codes || []; renderPlanTable(planSortRows(data.rows)); document.getElementById('planCount').textContent = '共 ' + data.total + ' 条'; // 根据当前视图调整列显隐和名称 updatePlanViewColumns(); // 填充采购跟单/采购计划下拉选项(仅首次加载或当页面为第1页时更新) if (page === 1) { populatePlanPersonFilter(data.rows); setupFilterClearButtons(); } const pel = document.getElementById('planPagination'); if (pel) renderPaginationWithSize(pel, planCurrentPage, planTotal, PLAN_PAGE_SIZE, 'loadPlanPage', 'PLAN_PAGE_SIZE'); }); } function renderPlanTable(rows) { const tbody = document.getElementById('planTbody'); // 只渲染当前账号实际可见列,确保表头、表体和 colgroup 的列索引完全一致。 var permFilteredCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField('planning', c.label) && window.planColVisible[c.field]; }); if (!rows || rows.length === 0) { tbody.innerHTML = '暂无数据'; return; } var permFilterSet = {}; permFilteredCols.forEach(function(c) { permFilterSet[c.field] = true; }); const startIdx = (planCurrentPage - 1) * PLAN_PAGE_SIZE; const typeMap = {'成品':'blue','包材':'orange','赠品':'green','小样':'purple','物料':'orange','其他':'grey'}; tbody.innerHTML = rows.map((r, rowIdx) => { const vals = permFilteredCols.map(c => { const raw = r[c.field]; if (c.field === 'product_type') { const color = typeMap[raw] || 'grey'; return `${escHtml(raw || '-')}`; } if (c.field === '产品种类') { const color = typeMap[raw] || 'grey'; return `${escHtml(raw || '-')}`; } if (c.field === '产品种类') { const color = typeMap[raw] || 'grey'; return `${escHtml(raw || '-')}`; } if (c.field === 'product_code') { return `${escHtml(raw || '-')}`; } if (c.field === 'product_name') { return escHtml(raw || '-'); } if (c.field === 'bom') { var pcode = (r.product_code || ''); var pname = r.product_name || ''; var suggQty = r['建议下单数量'] || 0; var actQty = r['实际下单数量'] || 0; var remark = r['下单备注'] || ''; return ''; } if (c.field === '生产货期' || c.field === '物流时间' || c.field === '最低起订量') { var isAdmin = window._currentRole === 'admin'; var val = raw ?? ''; return '' + (val === '' || val === null || val === undefined ? (isAdmin ? '点击编辑' : '-') : val) + ''; } if (c.field === '创建时间') { return raw || '-'; } if (c.field === '昨日销量' || c.field === '云仓昨日销量' || c.field === '外仓昨日销量' || c.field === '3天平均销量' || c.field === '7天平均销量' || c.field === '14天平均销量' || c.field === '30天平均销量' || c.field === '60天平均销量' || c.field === '3天最高销量' || c.field === '7天最高销量' || c.field === '14天最高销量' || c.field === '30天最高销量' || c.field === '14天最低销量' || c.field === '14天标准差') { return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw); } if (c.field === '库存' || c.field === '云仓库存' || c.field === '外仓库存' || c.field === '加工厂库存' || c.field === '云仓可分配库存' || c.field === '外仓可分配库存' || c.field === '生产在途' || c.field === '采购在途') { return (raw === 0 || raw === null || raw === undefined || raw === '') ? '0' : String(raw); } if (c.field === '是否调拨') { return raw ? escHtml(String(raw)) : '-'; } if (c.field === 'cost_price') { var val = parseFloat(raw); return isNaN(val) ? '-' : '¥' + val.toFixed(2); } if (c.field === '最低销量偏差率' || c.field === '最高销量偏差率' || c.field === '14天波动率') { return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw); } if (c.field === '极值判断') { var cls = 'tag-grey'; if (raw === '极大/极小') cls = 'tag-red'; else if (raw === '极小') cls = 'tag-orange'; else if (raw === '极大') cls = 'tag-orange'; else if (raw === '正常') cls = 'tag-green'; return '' + (raw || '-') + ''; } if (c.field === '波动风险') { var cls = 'tag-blue'; if (raw === 'X') cls = 'tag-green'; else if (raw === 'Y') cls = 'tag-orange'; else if (raw === 'Z') cls = 'tag-red'; return '' + (raw || '-') + ''; } if (c.field === '产品分级') { var gradeColors = {'S级':'tag-red', 'A级':'tag-orange', 'B级':'tag-blue', 'C级':'tag-grey', '不参与评级':'tag-grey'}; var cls = gradeColors[raw] || 'tag-grey'; return '' + (raw || '-') + ''; } if (c.field === '销售趋势') { var trendColors = { '开始上升': 'tag-blue', '开始下跌': 'tag-purple', '平稳趋势': 'tag-green', '上升趋稳': 'tag-blue', '下跌趋稳': 'tag-purple', '持续上升': 'tag-blue', '急跌': 'tag-red', '持续下跌': 'tag-red', '急增': 'tag-orange', '近期无销售': 'tag-grey' }; var cls = trendColors[raw] || 'tag-grey'; return '' + (raw || '-') + ''; } if (c.field === '是否新品') { return (raw === '新品') ? '新品' : '返单品'; } if (c.field === '日销量预测') { var val = parseFloat(raw); if (isNaN(val) || val === 0) return '0'; return '' + Math.round(val) + ''; } if (c.field === '库存可支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '库存工厂可支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '加工厂库存支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '库存工厂待生产可支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '总支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '云仓支撑天数' || c.field === '外仓支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '库存健康指数') { var val = parseFloat(raw); if (isNaN(val) || val === 999) return '999'; if (val >= 1.5) return '' + val.toFixed(1) + ''; if (val >= 0.8) return '' + val.toFixed(1) + ''; return '' + val.toFixed(1) + ''; } if (c.field === '风险把控') { var riskColors = {'正常':'tag-green','新品':'tag-blue','呆滞品':'tag-orange','需启动清库存':'tag-red'}; var cls = riskColors[raw] || 'tag-grey'; return '' + (raw || '-') + ''; } if (c.field === '建议下单数量') { var val = parseInt(raw); if (isNaN(val) || val === 0) return '0'; return '' + val + ''; } if (c.field === '实际下单数量') { var val = parseInt(raw); if (isNaN(val) || val === 0) return '0'; return '' + val + ''; } if (c.field === '正式下单数量') { var currentVal = parseInt(r['实际下单数量']) || 0; var code = r.product_code || ''; var codeHtml = escHtml(code); var canEditFormal = window._canEditField && window._canEditField('planning', '正式下单数量'); return '' + '' + currentVal + '' + (canEditFormal ? '✏️' : '') + ''; } if (c.field === '建议回货数量') { var val = parseFloat(raw); if (isNaN(val) || val === 0) return '0'; return '' + val.toFixed(1) + ''; } // 人员列:彩色标签 if (c.field === '采购跟单') { return renderNameBadges(raw); } return escHtml(raw || '-'); }); // 冻结列:计算可见冻结列的 left 偏移 // 冻结列:计算可见冻结列的 left 偏移 var colLefts = {}; if (planFrozen) { var acc = 50; colLefts['_index'] = 0; planFrozenFields.forEach(function(f) { if (window.planColVisible[f]) { colLefts[f] = acc; acc += (FROZEN_WIDTHS[f] || 100); } }); } var idxSticky = planFrozen ? 'position:sticky;left:0;z-index:9;background:var(--bg-card);transform:translateZ(0);min-width:50px' : ''; var code2 = r.product_code || ''; let colsHtml = `${startIdx + rowIdx + 1}`; colsHtml += permFilteredCols.map((c, visibleIndex) => { var i = PLAN_COLUMNS.indexOf(c); var extraStyle = ''; if (planFrozen && colLefts[c.field] !== undefined) { extraStyle = 'position:sticky;left:' + colLefts[c.field] + 'px;z-index:9;background:var(--bg-card);transform:translateZ(0);min-width:' + (FROZEN_WIDTHS[c.field] || 100) + 'px'; } return `${vals[visibleIndex]}`; }).join(''); var code = r.product_code || ''; // 序列化下单所需数据 var formalQty = parseInt(r['实际下单数量']) || 0; var suggestedQty = parseInt(r['建议下单数量']) || 0; var orderData = JSON.stringify({ product_code: r.product_code || '', product_name: r.product_name || '', product_type: r.product_type || '', product_grade: r['产品分级'] || '', supplier_name: r['供应商名称'] || '', lead_time: r['生产货期'] || 0, logistics_time: r['物流时间'] || 0, total_support_days: r['总支撑天数'] || 0, procurement_follower: r['采购跟单'] || '', procurement_planner: r['采购计划'] || '', platform: r['归属平台'] || '', operator: r['运营专员'] || '', formal_order_qty: formalQty, suggested_qty: suggestedQty, remark: r['下单备注'] || '' }); colsHtml += ''; colsHtml += '
'; colsHtml += ''; if (window._hasPagePerm('planning', 'edit')) colsHtml += ''; if (!window._planOrderDataCache) window._planOrderDataCache = {}; window._planOrderDataCache[code] = orderData; if (window._hasPagePerm('planning', 'delete')) colsHtml += ''; colsHtml += '
'; return `${colsHtml}`; }).join(''); // 渲染完成后动态修正冻结列位置(实际列宽与 FROZEN_WIDTHS 有偏差时修正) // 更新表头排序箭头状态(在冻结列之前,因为 buildPlanTableHeader 会替换 thead DOM) var planTbl = document.getElementById('planTable'); if (planTbl) buildPlanTableHeader(planTbl); // 在表头重建后重新应用筛选图标 setTimeout(function() { if (typeof _restoreFilterIcons === 'function') _restoreFilterIcons(); }, 50); // 渲染完成后动态修正冻结列位置(实际列宽与 FROZEN_WIDTHS 有偏差时修正) restoreAdjustableTableColumnWidths('planTable', 'planning'); enableAdjustableTableColumnResize('planTable', 'planning'); applyCollapsibleActionState('planning'); updatePlanFrozenPositions(); // 更新筛选图标状态 if (typeof _restoreFilterIcons === 'function') _restoreFilterIcons(); } /** * 动态计算冻结列正确的 left 偏移(基于实际渲染宽度,而非固定 FROZEN_WIDTHS) */ function updatePlanFrozenPositions() { if (!planFrozen) return; var table = document.getElementById('planTable'); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; // 获取所有可见冻结列的实际宽度 var actualWidths = {}; // 序号列 var indexTh = headerRow.querySelector('.plan-col-index'); if (indexTh) actualWidths['_index'] = indexTh.offsetWidth; planFrozenFields.forEach(function(field) { var idx = window['_planColIdx_' + field]; if (idx === undefined) return; var th = headerRow.querySelector('.plan-col-' + idx + '.plan-th-' + idx); if (th && window.planColVisible[field]) { actualWidths[field] = th.offsetWidth; } }); // 用实际宽度重新计算 left 偏移,并应用到所有 th 和 td var acc = 0; // 序号列 if (indexTh && actualWidths['_index']) { indexTh.style.position = 'sticky'; indexTh.style.top = '0'; indexTh.style.left = acc + 'px'; indexTh.style.zIndex = '999'; indexTh.style.background = 'var(--bg-card)'; acc += actualWidths['_index']; } else { acc += 50; } planFrozenFields.forEach(function(field) { if (!window.planColVisible[field]) return; var idx = window['_planColIdx_' + field]; if (idx === undefined) return; var leftVal = acc + 'px'; // 更新表头 th(必须同时设 position/top/z-index,否则可能因 buildPlanTableHeader 而被抹掉) headerRow.querySelectorAll('.plan-col-' + idx + '.plan-th-' + idx).forEach(function(el) { el.style.position = 'sticky'; el.style.top = '0'; el.style.left = leftVal; el.style.zIndex = '999'; el.style.background = 'var(--bg-card)'; }); // 更新所有数据行中的 td table.querySelectorAll('tbody .plan-col-' + idx).forEach(function(el) { el.style.position = 'sticky'; el.style.left = leftVal; el.style.zIndex = '9'; el.style.background = 'var(--bg-card)'; }); acc += actualWidths[field] || FROZEN_WIDTHS[field] || 100; }); // 第二次修正:延迟确保渲染完成后重新计算(解决首次加载时可能计算不准) setTimeout(function() { // 重新测量实际宽度 var widths2 = {}; var idxTh2 = headerRow.querySelector('.plan-col-index'); if (idxTh2) widths2['_index'] = idxTh2.offsetWidth; planFrozenFields.forEach(function(field) { if (!window.planColVisible[field]) return; var idx = window['_planColIdx_' + field]; if (idx === undefined) return; var th = headerRow.querySelector('.plan-col-' + idx + '.plan-th-' + idx); if (th) widths2[field] = th.offsetWidth; }); var acc2 = 0; acc2 += widths2['_index'] || 50; if (idxTh2) { idxTh2.style.left = '0px'; table.querySelectorAll('tbody .plan-col-index').forEach(function(el) { el.style.left = '0px'; }); } planFrozenFields.forEach(function(field) { if (!window.planColVisible[field]) return; var idx = window['_planColIdx_' + field]; if (idx === undefined) return; var lv = acc2 + 'px'; headerRow.querySelectorAll('.plan-col-' + idx + '.plan-th-' + idx).forEach(function(el) { el.style.left = lv; }); table.querySelectorAll('tbody .plan-col-' + idx).forEach(function(el) { el.style.left = lv; }); acc2 += widths2[field] || FROZEN_WIDTHS[field] || 100; }); }, 50); } // ─── 冻结列弹窗 ─── function openPlanFrozenModal() { // 从当前视图加载冻结列设置 var viewFrozen = window._planViewFrozenFields[planActiveView] || planFrozenFields; window._planFrozenPendingFields = viewFrozen.slice(); var html = '
'; html += '
'; var candidates = [ {field:'_index', label:'序号'}, {field:'product_type', label:'产品类型'}, {field:'product_code', label:'货品编号'}, {field:'product_name', label:'货品名称'} ]; candidates.forEach(function(c) { var checked = window._planFrozenPendingFields.indexOf(c.field) >= 0; html += ''; }); html += '
'; document.getElementById('planFrozenModalBody').innerHTML = html; document.getElementById('planFrozenModal').style.display = 'flex'; } window.planFrozenCbChange = function() { var checked = []; document.querySelectorAll('.plan-frozen-cb:checked').forEach(function(cb) { checked.push(cb.value); }); window._planFrozenPendingFields = checked; var allCb = document.getElementById('planFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.plan-frozen-cb').length; }; window.planFrozenToggleAll = function(checked) { document.querySelectorAll('.plan-frozen-cb').forEach(function(cb) { cb.checked = checked; }); planFrozenCbChange(); }; function closePlanFrozenModal() { document.getElementById('planFrozenModal').style.display = 'none'; } function confirmPlanFrozen() { planFrozenFields = window._planFrozenPendingFields.slice(); planFrozen = planFrozenFields.length > 0; // 保存到当前视图的独立设置 window._planViewFrozenFields[planActiveView] = planFrozenFields.slice(); var page = 'planning'; if (window._pageCache && window._pageCache[page]) delete window._pageCache[page]; loadPage(page); } // ─── 表头筛选菜单 ─── var _planFilterMenuField = ''; // ─── Excel风格表头筛选 ─── var _planFilterMenuField = ''; var _planFilterSelected = {}; function openPlanFilterMenu(field, event) { event = event || window.event; event.stopPropagation(); var existing = document.getElementById('planFilterMenu'); if (existing) existing.remove(); _planFilterMenuField = field; var th = event.currentTarget; var rect = th.getBoundingClientRect(); // 跟随表头滚动:监听滚动事件更新菜单位置 document.addEventListener('scroll', planFilterScrollHandler, true); var label = ''; PLAN_COLUMNS.forEach(function(c) { if (c.field === field) label = c.label; }); // 采购计划直接取成品基础信息全量原值,避免分页数据和旧缓存造成选项缺失。 if (field === '采购计划' || field === 'procurement_planner') { var plannerMenu = _createFilterMenuShell(field, label, rect); document.body.appendChild(plannerMenu); var plannerViewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : ''; var plannerFilterParam = ''; if (window._planFilters && Object.keys(window._planFilters).length > 0) { var linkedPlannerFilters = Object.assign({}, window._planFilters); delete linkedPlannerFilters[field]; if (Object.keys(linkedPlannerFilters).length > 0) { plannerFilterParam = '&plan_filter=' + encodeURIComponent(JSON.stringify(linkedPlannerFilters)); } } fetch('/api/planning/column-values?field=' + encodeURIComponent(field) + plannerViewParam + plannerFilterParam) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { _populateFilterMenuList(field, res.data, false); } }); setupFilterMenuClose(plannerMenu, th); return; } // 判断是否为数字列 // 产品分级虽然可排序,但只有 S/A/B/C 四种值,用列表筛选 var productGradeFields = ['产品分级']; var isNumericCol = SORTABLE_FIELDS[field] && !productGradeFields.includes(field); var menu = null; if (isNumericCol) { menu = document.createElement('div'); menu.id = 'planFilterMenu'; var isSorted = planSortField === field; var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : ''; _buildNumericFilterMenu(field, label, rect, isSorted, currentOrder, menu); } else { // 获取该列所有唯一值 var values = []; var predefined = PLAN_FILTER_OPTIONS[field]; var hasPlanFilter = window._planFilters && Object.keys(window._planFilters).length > 0; var isExcludedField = PLAN_FILTER_EXCLUDE_FIELDS[field] ? true : false; // 联动筛选:有筛选时从表格提取唯一值(基于已筛选行);但当前字段已有筛选时用预定义列表保持全量选项 if (predefined && hasPlanFilter && window._planFilters[field]) { // 当前字段本身已有筛选 → 用完整预定义列表,勾选状态由 _populateFilterMenuList 回显 values = predefined.slice(); } else if (predefined && hasPlanFilter) { // 其他字段有筛选 → 从表格提取当前已筛选行的唯一值(联动筛选) values = []; var tbody = document.getElementById('planTbody'); if (tbody) { var permCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField && window._canViewField('planning', c.label); }); var colIdx = permCols.findIndex(function(c) { return c.field === field; }); if (colIdx >= 0) { colIdx = colIdx + 1; // +1 for 序号列 var seen = {}; tbody.querySelectorAll('tr').forEach(function(tr) { var td = tr.cells[colIdx]; if (td) { var txt = td.textContent.trim(); if (txt && !seen[txt]) { seen[txt] = 1; values.push(txt); } } }); } } if (values.length === 0) values = predefined.slice(); } else if (predefined && !hasPlanFilter) { values = predefined.slice(); } else if (field === '产品分级' && hasPlanFilter && isExcludedField) { // 从表格提取 values = []; var tbody2 = document.getElementById('planTbody'); if (tbody2) { var permCols2 = PLAN_COLUMNS.filter(function(c) { return window._canViewField && window._canViewField('planning', c.label); }); var colIdx2 = permCols2.findIndex(function(c) { return c.field === field; }); if (colIdx2 >= 0) { colIdx2 = colIdx2 + 1; // +1 for 序号列 var seen2 = {}; tbody2.querySelectorAll('tr').forEach(function(tr) { var td = tr.cells[colIdx2]; if (td) { var txt = td.textContent.trim(); if (txt && !seen2[txt]) { seen2[txt] = 1; values.push(txt); } } }); } } if (values.length === 0) values = ['S', 'A', 'B', 'C']; } else if (field === '产品分级' && (!hasPlanFilter || !isExcludedField)) { values = ['S', 'A', 'B', 'C']; } else if (field === 'brand') { // 品牌列:优先使用缓存 var planFilterParam = ''; var hasPF = window._planFilters && Object.keys(window._planFilters).length > 0; if (hasPF) { planFilterParam = (planActiveView !== 'default' ? '&' : '?') + 'plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters)); } var cacheKey = 'plan:brand:' + (planActiveView || 'default') + (hasPF ? ':filtered' : ''); var cached = !hasPF && window._planColumnCache && window._planColumnCache[cacheKey]; if (cached) { // 命中缓存:直接构建完整菜单 if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; var isSorted = planSortField === field; var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : ''; var menuBrand = document.createElement('div'); menuBrand.id = 'planFilterMenu'; _setupPlanFilterMenuHTML(menuBrand, field, label, rect, currentOrder, isSorted); document.body.appendChild(menuBrand); _populateFilterMenuList(field, cached, false); setupFilterMenuClose(menuBrand, th); } else { var menu = _createFilterMenuShell(field, label, rect); document.body.appendChild(menu); var viewParam = planActiveView !== 'default' ? '?view=' + encodeURIComponent(planActiveView) : ''; fetch('/api/planning/brands' + viewParam + planFilterParam) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { if (!hasPF) { window._planColumnCache = window._planColumnCache || {}; window._planColumnCache[cacheKey] = res.data; } _populateFilterMenuList(field, res.data, false); } }); setupFilterMenuClose(menu, th); } return; } else { // 非预定义非品牌的文本列 var isExcl = PLAN_FILTER_EXCLUDE_FIELDS[field] ? true : false; var hasPF = window._planFilters && Object.keys(window._planFilters).length > 0; // 计算字段+联动筛选:从表格提取 if (hasPF && isExcl) { values = []; if (window._planAllRows) { var seen3 = {}; Object.keys(window._planAllRows).forEach(function(pc) { var r = window._planAllRows[pc]; var rawVal = r[field]; if (rawVal !== undefined && rawVal !== null && rawVal !== '') { var txt = String(rawVal).trim(); if (txt && txt !== '-' && !seen3[txt]) { seen3[txt] = 1; values.push(txt); } } }); values.sort(); } if (values.length === 0) values = ['暂无']; } else { // 非预定义文本列:优先使用缓存 var planFilterParam2 = ''; if (hasPF) { var linkedPlanFilters = Object.assign({}, window._planFilters); delete linkedPlanFilters[field]; if (Object.keys(linkedPlanFilters).length > 0) { planFilterParam2 = '&plan_filter=' + encodeURIComponent(JSON.stringify(linkedPlanFilters)); } } var cacheKey2 = 'plan:field:' + field + ':' + (planActiveView || 'default') + (hasPF ? ':filtered' : ''); var cached2 = !hasPF && window._planColumnCache && window._planColumnCache[cacheKey2]; if (cached2) { // 命中缓存:直接构建完整菜单 if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; var isSorted2 = planSortField === field; var currentOrder2 = isSorted2 ? (planSortAsc ? 'asc' : 'desc') : ''; var menuField = document.createElement('div'); menuField.id = 'planFilterMenu'; _setupPlanFilterMenuHTML(menuField, field, label, rect, currentOrder2, isSorted2); document.body.appendChild(menuField); // 采购跟单/采购计划:人员姓名转组别名 var valsCached = cached2; if (field === '采购跟单' || field === 'procurement_follower') { valsCached = normalizePlanPersonnelGroupValues(field, valsCached); } _populateFilterMenuList(field, valsCached, false); setupFilterMenuClose(menuField, th); } else { // 未命中缓存:从后端API获取全量唯一值 var menu = _createFilterMenuShell(field, label, rect); document.body.appendChild(menu); var viewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : ''; fetch('/api/planning/column-values?field=' + encodeURIComponent(field) + viewParam + planFilterParam2) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { var vals = res.data; // 采购跟单/采购计划:人员姓名转组别名(需拆逗号分隔值) if (field === '采购跟单' || field === 'procurement_follower') { vals = normalizePlanPersonnelGroupValues(field, vals); } if (!hasPF) { window._planColumnCache = window._planColumnCache || {}; window._planColumnCache[cacheKey2] = vals.slice(); } _populateFilterMenuList(field, vals, false); } }); setupFilterMenuClose(menu, th); } return; } } if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; var isSorted = planSortField === field; var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : ''; menu = document.createElement('div'); menu.id = 'planFilterMenu'; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; var savedPf = window._planFilters && window._planFilters[field]; var isListPf = savedPf && savedPf.type === 'list' && savedPf.values; var isSearchPf = savedPf && savedPf.type === 'search' && savedPf.value; var allChecked = (isListPf || isSearchPf) ? 0 : 1; html += '
'; html += ''; html += '清除筛选
'; html += '
'; if (isSearchPf && savedPf.value) { var pfSearchInput = document.getElementById('planFilterSearch'); if (pfSearchInput) { pfSearchInput.value = savedPf.value; setTimeout(function() { if (typeof planFilterSearchList === 'function') planFilterSearchList(savedPf.value); }, 50); } } values.forEach(function(v) { var key = escHtml(v).replace(/'/g, "\\'"); var checked = 'checked'; if (isListPf) { checked = (savedPf.values.indexOf(v) >= 0) ? 'checked' : ''; } else if (isSearchPf) { checked = (v.indexOf(savedPf.value) >= 0) ? 'checked' : ''; } html += ''; }); html += '
'; html += '
'; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); } _setupFilterMenuCommon(field, menu, th); } // 构建计划页完整筛选菜单HTML(缓存命中时用,含排序按钮+全选) function _setupPlanFilterMenuHTML(menu, field, label, rect, currentOrder, isSorted) { menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:450px;font-size:13px;display:flex;flex-direction:column'; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; } // 创建筛选菜单外壳(用于异步加载的字段) function _createFilterMenuShell(field, label, rect) { var existing = document.getElementById('planFilterMenu'); if (existing) existing.remove(); _planFilterMenuField = field; if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; var menu = document.createElement('div'); menu.id = 'planFilterMenu'; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; return menu; } // 填充筛选列表 function _populateFilterMenuList(field, values, hasSortBtns) { var listEl = document.getElementById('planFilterList'); if (!listEl) return; if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; // 读取已保存的筛选状态 var savedFilter = window._planFilters && window._planFilters[field]; var isListFilter = savedFilter && savedFilter.type === 'list' && savedFilter.values; var isSearchFilter = savedFilter && savedFilter.type === 'search' && savedFilter.value; // 预填搜索框(搜索模式回显) if (isSearchFilter) { var si = document.getElementById('planFilterSearch'); if (si) { si.value = savedFilter.value; setTimeout(function() { if (typeof planFilterSearchList === 'function') { planFilterSearchList(si.value); } }, 50); } } var html = ''; var allChecked = (isListFilter || isSearchFilter) ? 0 : 1; html += '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = 'checked'; if (isListFilter) { checked = (savedFilter.values.indexOf(v) >= 0) ? 'checked' : ''; } else if (isSearchFilter) { checked = (v.indexOf(savedFilter.value) >= 0) ? 'checked' : ''; } var labelText = (v === '__EMPTY__') ? '空' : escHtml(v); var valAttr = escHtml(v); html += ''; }); listEl.innerHTML = html; // 搜索自动聚焦 setTimeout(function() { var inp = document.getElementById('planFilterSearch'); if (inp) inp.focus(); }, 150); } function setupFilterMenuClose(menu, th) { document.addEventListener('scroll', planFilterScrollHandler, true); var handler = function(e) { if (!menu.contains(e.target) && e.target !== th && !th.contains(e.target)) { menu.remove(); document.removeEventListener('click', handler); } }; setTimeout(function() { document.addEventListener('click', handler); }, 10); } function _setupFilterMenuCommon(field, menu, th) { // 跟随表头滚动 document.addEventListener('scroll', planFilterScrollHandler, true); // 搜索自动聚焦 setTimeout(function() { var inp = document.getElementById('planFilterSearch'); if (inp) inp.focus(); }, 150); // 点击外部关闭 var handler = function(e) { if (!menu.contains(e.target) && e.target !== th && !th.contains(e.target)) { menu.remove(); document.removeEventListener('click', handler); } }; setTimeout(function() { document.addEventListener('click', handler); }, 10); } // ─── 数字列条件筛选 ─── function _buildNumericFilterMenu(field, label, rect, isSorted, currentOrder, menu) { menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:260px;max-width:320px;font-size:13px;display:flex;flex-direction:column;max-height:450px'; var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '内容筛选'; html += '条件筛选
'; // 排序按钮 html += '
'; html += '' + ''; html += '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += ''; html += '
加载中...
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); // 从后端API获取唯一值:无筛选全量,有筛选联动 var viewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : ''; var planFilterParam = ''; if (window._planFilters && Object.keys(window._planFilters).length > 0) { planFilterParam = '&plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters)); } var savedNumFilter = window._planFilters && window._planFilters[field]; fetch('/api/planning/column-values?field=' + encodeURIComponent(field) + viewParam + planFilterParam) .then(function(r) { return r.json(); }) .then(function(res) { var numValues = []; if (res.code === 0 && res.data) { numValues = res.data.map(function(v) { return String(v).trim(); }).filter(function(v) { return v !== '' && v !== '-'; }); } numValues.sort(function(a,b) { return parseFloat(a) - parseFloat(b); }); renderNumList(numValues); }); function renderNumList(vals) { var list = document.getElementById('planNumList'); if (!list) return; if (vals.length === 0) { list.innerHTML = '
暂无数据
'; return; } var html = ''; vals.forEach(function(v) { html += ''; }); list.innerHTML = html; if (savedNumFilter && savedNumFilter.type === 'list' && savedNumFilter.values) { list.querySelectorAll('.plan-num-cb').forEach(function(cb) { cb.checked = savedNumFilter.values.indexOf(cb.value) >= 0; }); planNumUpdateAll(); } } if (savedNumFilter && savedNumFilter.type === 'num') { var condSel = document.getElementById('planNumCond'); var valInput = document.getElementById('planNumVal'); if (condSel) condSel.value = savedNumFilter.cond || ''; if (valInput) valInput.value = savedNumFilter.val || ''; planNumSwitchTab('cond'); } setTimeout(function() { var inp = document.getElementById('planNumVal'); if (inp) inp.focus(); }, 100); } function planNumFilterApply() { var field = _planFilterMenuField; var condPanel = document.getElementById('planNumCondPanel'); var isCondTab = condPanel && condPanel.style.display !== 'none'; if (isCondTab) { // 条件筛选 var cond = document.getElementById('planNumCond')?.value || ''; var val = document.getElementById('planNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } if (!window._planFilters) window._planFilters = {}; window._planFilters[field] = {type:'num', cond: cond, val: val}; } else { // 内容筛选:复选框值创建列表筛选 var cbs = document.querySelectorAll('.plan-num-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (!window._planFilters) window._planFilters = {}; if (checked.length === 0 || checked.length === document.querySelectorAll('.plan-num-cb').length) { delete window._planFilters[field]; } else { window._planFilters[field] = {type:'list', values: checked}; } } _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); } function planFilterApply() { var field = _planFilterMenuField; // 先检查搜索框是否有内容 var searchInput = document.getElementById('planFilterSearch'); var searchText = searchInput ? searchInput.value.trim() : ''; if (searchText) { // 搜索框有内容 → 文本模糊匹配 if (!window._planFilters) window._planFilters = {}; window._planFilters[field] = {type: 'search', value: searchText}; } else { // 搜索框为空 → 用复选框选择 var cbs = document.querySelectorAll('.plan-filter-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (!window._planFilters) window._planFilters = {}; if (checked.length === 0 || checked.length === document.querySelectorAll('.plan-filter-cb').length) { // 全选或没选 → 清除该列筛选 delete window._planFilters[field]; } else { window._planFilters[field] = {type:'list', values: checked}; } } _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); } function _syncFiltersToUI() { // 仅用于更新表头筛选图标状态 var activeFilters = []; Object.keys(window._planFilters || {}).forEach(function(f) { var filter = window._planFilters[f]; if (filter) { filter.field = f; activeFilters.push(filter); } }); _updateFilterIcons(activeFilters); } function _updateFilterIcons(activeFilters) { var activeFields = {}; activeFilters.forEach(function(f) { activeFields[f.field] = true; }); PLAN_COLUMNS.forEach(function(c, i) { var th = document.querySelector('.plan-th-' + i); if (th) { var isActive = activeFields[c.field]; var icon = th.querySelector('span[title="筛选"]'); if (icon) { icon.style.color = isActive ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = isActive ? 'bold' : 'normal'; icon.textContent = isActive ? '▼' : '▾'; } } }); } function _restoreFilterIcons() { var activeFilters = []; Object.keys(window._planFilters || {}).forEach(function(f) { var fp = window._planFilters[f]; if (fp) { fp.field = f; activeFilters.push(fp); } }); _updateFilterIcons(activeFilters); } function planFilterSearchList(q) { var items = document.querySelectorAll('#planFilterList label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function planFilterToggleAll(checked) { document.querySelectorAll('.plan-filter-cb').forEach(function(cb) { cb.checked = checked; }); } function planFilterCbChange() { var all = document.querySelectorAll('.plan-filter-cb'); var checked = document.querySelectorAll('.plan-filter-cb:checked'); var cb = document.getElementById('planFilterSelectAll'); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } function planFilterSort(field, order) { if (order === 'asc') { planSortField = field; planSortAsc = true; } else if (order === 'desc') { planSortField = field; planSortAsc = false; } else { planSortField = ''; } closePlanFilterMenu(); loadPlanPage(1); } function planFilterClear() { var field = _planFilterMenuField; if (window._planFilters) delete window._planFilters[field]; _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); } // 重置所有筛选条件 window.planFilterResetAll = function() { window._planFilters = {}; window._planFilterSelected = {}; planSortField = ''; planSortAsc = true; // 清空主搜索框和下拉筛选 ['planSearch','planSupplierFilter','planFollowerFilter','planPlannerFilter','planDateCond','planDateVal'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); }; // ─── 数字列双模式筛选辅助函数 ─── function planNumSwitchTab(tab) { var contentPanel = document.getElementById('planNumContentPanel'); var condPanel = document.getElementById('planNumCondPanel'); var tabContent = document.getElementById('planTabContent'); var tabCond = document.getElementById('planTabCond'); var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; if (tab === 'content') { if (contentPanel) contentPanel.style.display = ''; if (condPanel) condPanel.style.display = 'none'; if (tabContent) tabContent.style.cssText = tabAct; if (tabCond) tabCond.style.cssText = tabDef; } else { if (contentPanel) contentPanel.style.display = 'none'; if (condPanel) condPanel.style.display = ''; if (tabContent) tabContent.style.cssText = tabDef; if (tabCond) tabCond.style.cssText = tabAct; } } function planNumSearchList(q) { var items = document.querySelectorAll('#planNumList label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function planNumToggleAll(checked) { document.querySelectorAll('.plan-num-cb').forEach(function(cb) { cb.checked = checked; }); } function planNumUpdateAll() { var all = document.querySelectorAll('.plan-num-cb'); var checked = document.querySelectorAll('.plan-num-cb:checked'); var cb = document.getElementById('planNumSelAll'); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } function closePlanFilterMenu() { var m = document.getElementById('planFilterMenu'); if (m) m.remove(); document.removeEventListener('scroll', planFilterScrollHandler, true); } var planFilterScrollHandler = function() { var menu = document.getElementById('planFilterMenu'); if (!menu) return; var colIdx = window['_planColIdx_' + _planFilterMenuField]; if (colIdx === undefined) return; var th = document.querySelector('.plan-th-' + colIdx); if (th) { var r = th.getBoundingClientRect(); menu.style.top = (r.bottom + 4) + 'px'; menu.style.left = Math.max(r.left, 10) + 'px'; } }; function togglePlanSort(field) { if (planSortField === field) { planSortAsc = !planSortAsc; } else { planSortField = field; planSortAsc = true; } loadPlanPage(1); } function searchPlanning() { loadPlanPage(1); } // 导出计划预测全部数据(Excel) function exportPlanningData() { var view = window.planActiveView || 'default'; // 导出列:全部 PLAN_COLUMNS(除 bom 操作列) var columns = PLAN_COLUMNS.filter(function(c) { return c.field !== 'bom'; }) .map(function(c) { return {field: c.field, label: c.label}; }); var payload = { view: view, sort_by: window.planSortField || '', sort_order: window.planSortAsc === false ? 'desc' : 'asc', columns: columns }; if (view === 'cloud') { // 云仓视图:若隐藏了云仓/外仓列则仍导出全部列,保持与列表一致即可 } fetch('/api/planning/export', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }).then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.blob(); }).then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = '计划预测_' + new Date().toISOString().slice(0, 10) + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }).catch(function(e) { showCenteredAlert('导出失败', '导出失败: ' + e.message); }); } // 根据视图调整列显隐和名称(仅cloud视图显示云仓/外仓库存) function updatePlanViewColumns() { var isCloud = planActiveView === 'cloud'; // 同步所有列的显隐状态(planColVisible → DOM) PLAN_COLUMNS.forEach(function(c, i) { var show = window.planColVisible[c.field]; var els = document.querySelectorAll('.plan-col-' + i + ', .plan-th-' + i); els.forEach(function(el) { el.style.display = show ? '' : 'none'; }); }); } var planActiveView = (window._planOverseasOnly ? 'external' : 'default'); function switchPlanView(view) { if (window._planOverseasOnly) view = 'external'; // 保存当前视图的字段显隐状态 if (window._planViewVisibility) { PLAN_COLUMNS.forEach(function(c) { window._planViewVisibility[planActiveView][c.field] = window.planColVisible[c.field]; }); // 切换到新视图的显隐状态 PLAN_COLUMNS.forEach(function(c) { window.planColVisible[c.field] = window._planViewVisibility[view][c.field]; }); } // 保存/切换冻结列设置 if (window._planViewFrozenFields) { window._planViewFrozenFields[planActiveView] = planFrozenFields.slice(); planFrozenFields = (window._planViewFrozenFields[view] || ['product_type', 'product_code', 'product_name']).slice(); } planActiveView = view; document.querySelectorAll('.plan-tab').forEach(function(el) { el.classList.toggle('active', el.dataset.view === view); }); // 切换视图时重置筛选条件并加载 resetPlanningFiltersNoSearch(); loadPlanPage(1); } function resetPlanningFiltersNoSearch() { document.getElementById('planSearch').value = ''; document.getElementById('planDateCond').value = ''; document.getElementById('planDateVal').value = ''; document.getElementById('planSupplierFilter').value = ''; document.getElementById('planFollowerFilter').value = ''; document.getElementById('planPlannerFilter').value = ''; window._planFilters = {}; setupFilterClearButtons(); } // 从数据行中提取采购跟单/采购计划去重值,填充下拉选项 function populatePlanPersonFilter(rows) { function getDistinct(field) { var set = new Set(); (rows || []).forEach(function(r) { var v = r[field]; if (v && v !== '-' && v.trim()) { v.split(',').forEach(function(name) { name = name.trim(); if (name) set.add(name); }); } }); return Array.from(set).sort(); } ['planFollowerFilter', 'planPlannerFilter'].forEach(function(elId) { var sel = document.getElementById(elId); if (!sel) return; var currentVal = sel.value; var field = elId === 'planFollowerFilter' ? '采购跟单' : '采购计划'; var label = elId === 'planFollowerFilter' ? '采购跟单' : '采购计划'; var names = field === '采购跟单' ? _personFilterValuesFromItems(rows, field, function(item) { return item[field]; }) : getDistinct(field); if (field === '采购跟单') PLAN_FILTER_OPTIONS['采购跟单'] = names.slice(); function applyNames(optionNames) { sel.innerHTML = '' + optionNames.map(function(n) { return ''; }).join(''); sel.value = currentVal; } if (field === '采购计划') { fetch('/api/planning/column-values?field=采购计划' + (planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : '')) .then(function(r) { return r.json(); }) .then(function(res) { applyNames(res.code === 0 && res.data ? res.data : names); }) .catch(function() { applyNames(names); }); } else { applyNames(names); } }); } function resetPlanningFilters() { document.getElementById('planSearch').value = ''; document.getElementById('planDateCond').value = ''; document.getElementById('planDateVal').value = ''; document.getElementById('planSupplierFilter').value = ''; document.getElementById('planFollowerFilter').value = ''; document.getElementById('planPlannerFilter').value = ''; // 清除所有列筛选状态 window._planFilters = {}; // 清除排序状态 planSortField = ''; planSortAsc = true; // 更新清除按钮状态 setupFilterClearButtons(); loadPlanPage(1); } // ─── 列方案管理 ─── // 构建当前列配置JSON function buildPlanColumnsJSON() { var order = window._planColDragOrder || PLAN_COLUMNS.map(function(c) { return c.field; }); var cols = []; order.forEach(function(field) { var colDef = null; PLAN_COLUMNS.forEach(function(c) { if (c.field === field) colDef = c; }); if (colDef) { cols.push({ field: colDef.field, label: colDef.label, visible: window._planPendingVisible[field] !== undefined ? window._planPendingVisible[field] : true }); } }); PLAN_COLUMNS.forEach(function(c) { var found = false; cols.forEach(function(col) { if (col.field === c.field) found = true; }); if (!found) { cols.push({ field: c.field, label: c.label, visible: window._planPendingVisible[c.field] !== undefined ? window._planPendingVisible[c.field] : c.defaultShow }); } }); return JSON.stringify(cols); } // 保存方案 window.savePlanScheme = function() { var name = document.getElementById('planSchemeNameInput'); if (!name) return; var schemeName = name.value.trim(); if (!schemeName) { alert('请输入方案名称'); name.focus(); return; } var columnsJson = buildPlanColumnsJSON(); fetch('/api/planning/scheme/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ scheme_name: schemeName, columns_json: columnsJson, page: 'planning', is_default: 0 }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { name.value = ''; refreshPlanSchemeList(); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败,请检查网络'); }); }; // 加载方案并预览(在弹窗中勾选对应字段,不关闭弹窗) window.loadPlanScheme = function(schemeId) { if (schemeId === undefined || schemeId === null) return; fetch('/api/planning/schemes') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var target = null; for (var i = 0; i < schemes.length; i++) { if (schemes[i].id == schemeId) { target = schemes[i]; break; } } if (!target) { alert('方案不存在或已被删除'); refreshPlanSchemeList(); return; } var cols; try { cols = JSON.parse(target.columns_json); } catch(e) { alert('方案数据异常'); return; } if (!cols || !Array.isArray(cols) || cols.length === 0) { alert('方案数据为空'); return; } // 只更新临时状态(弹窗内的勾选),不关闭弹窗 var newOrder = []; var pendingVisible = {}; cols.forEach(function(c) { if (c && c.field) { newOrder.push(c.field); pendingVisible[c.field] = c.visible !== false; } }); PLAN_COLUMNS.forEach(function(c) { if (pendingVisible[c.field] === undefined) { newOrder.push(c.field); pendingVisible[c.field] = false; } }); // 更新临时勾选状态和拖拽顺序 window._planColDragOrder = newOrder.slice(); Object.keys(pendingVisible).forEach(function(field) { window._planPendingVisible[field] = pendingVisible[field]; }); window._planPendingVisible = pendingVisible; // 刷新弹窗内的 checkbox 状态 var grid = document.getElementById('planColGrid'); if (grid) renderPlanColCheckboxes(grid); // 同步全选状态 var allCb = document.getElementById('plan-cb-all'); if (allCb) { var allChecked = true; PLAN_COLUMNS.forEach(function(c) { if (!window._planPendingVisible[c.field]) allChecked = false; }); allCb.checked = allChecked; } }) .catch(function(err) { console.error('loadPlanScheme error:', err); }); }; // 构建表头 function buildPlanTableHeader(planTable) { // 只生成当前可见表头,避免 display:none 单元格与 colgroup 轨道发生偏移。 var permFilteredCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField('planning', c.label) && window.planColVisible[c.field]; }); var newThead = '
序号
'; permFilteredCols.forEach(function(c, i) { var planTips = {'日销量预测':'日销量预测 = (3天平均×w3 + 7天平均×w7 + 14天平均×w14) × 调整系数','库存可支撑天数':'库存可支撑天数 = 库存 / 日销量预测,为0时显示999','加工厂库存支撑天数':'加工厂库存支撑 = 加工厂库存 / 日销量预测,为0时显示999','库存工厂可支撑天数':'现货采购在途 = 采购在途 / 日销量预测','库存工厂待生产可支撑天数':'定制品在途 = 生产在途 / 日销量预测','总支撑天数':'总支撑天数 = (库存 + 加工厂库存 + 生产在途 + 采购在途) / 日销量预测','库存健康指数':'日销量预测>0:库存可支撑天数÷(货期+物流);=0:库存可支撑天数÷(货期+物流)','建议下单数量':'日销量预测=0且库存>0则0,否则(安全库存天数+货期+物流)×日销量预测','实际下单数量':'建议下单数量向上取整到1000','正式下单数量':'正式下单数量初始等于实际下单数量,可人工修改','建议回货数量':'ROUNDUP((日销量预测×回货天数-库存)×10)/10,上限在途总数','是否新品':'创建时间5个月以内为新品,否则返单品','成本价':'从产品信息匹配的成本价格','销售趋势':'基于3/7/14天日均销量判断的9种趋势类型'}; var tip = planTips[c.label] || ''; var canFilter = c.filterable !== false; var isActive = window._planFilters && window._planFilters[c.field]; var iconColor = isActive ? '#1890ff' : 'var(--text-light)'; var iconWeight = isActive ? 'bold' : 'normal'; var iconChar = isActive ? '▼' : '▾'; var filterIcon = canFilter ? ' ' + iconChar + '' : ''; var clickHandler = canFilter ? 'openPlanFilterMenu(\'' + escJs(c.field) + '\', event)' : ''; var planIndex = PLAN_COLUMNS.indexOf(c); newThead += '' + c.label + filterIcon + ''; }); newThead += renderCollapsibleActionHeader('planning', 'planTable', true); newThead += ''; planTable.querySelector('thead').innerHTML = newThead; } // 删除方案 window.deletePlanScheme = function(schemeId) { showCenteredConfirm('确认', '确定要删除此方案吗?').then(function(ok) { if (!ok) return; fetch('/api/planning/scheme/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { refreshPlanSchemeList(); } else { alert(res.msg || '删除失败'); } }); }); }; // 设置默认方案 window.setDefaultPlanScheme = function(schemeId) { fetch('/api/planning/scheme/set_default', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId, page: 'planning'}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { // 设置默认同时加载该方案的字段配置到弹窗 loadPlanScheme(schemeId); refreshPlanSchemeList(); } else { alert(res.msg || '设置失败'); } }) .catch(function(err) { console.error('setDefaultPlanScheme error:', err); }); }; // 刷新方案列表UI function refreshPlanSchemeList() { var container = document.getElementById('planSchemeList'); if (!container) return; fetch('/api/planning/schemes') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; if (schemes.length === 0) { container.innerHTML = '暂无保存方案'; return; } var html = ''; schemes.forEach(function(s) { var isDefault = s.is_default == 1; html += ''; html += '' + escHtml(s.scheme_name) + ''; if (isDefault) { html += '★默认'; } html += '📌'; html += '✕'; html += ''; }); container.innerHTML = html; }); } // 加载默认方案(页面初始化时调用) function loadDefaultPlanScheme() { fetch('/api/planning/schemes') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var defaultScheme = null; schemes.forEach(function(s) { if (s.is_default == 1) defaultScheme = s; }); if (defaultScheme) { window.loadPlanScheme(defaultScheme.id); } }); } // ─── 采购订单-待下单管理 ─── var _pendingItems = []; var pendingOrderFilter = {}; // 列筛选状态 var pendingFilterMenu = null; // 当前打开的筛选弹窗 var pendingFilterField = ''; // 当前筛选字段 var pendingSortField = ''; // 待下单排序字段 var pendingSortAsc = false; // 待下单排序方向 var pendingSearch = ''; // 编码/名称搜索 var pendingSupplier = ''; // 供应商筛选 var pendingPlanner = ''; // 采购计划筛选 var pendingOrderer = ''; // 下单人员筛选 var PENDING_PAGE_SIZE = 50; var pendingCurrentPage = 1; // 待下单列定义(含类型,用于筛选菜单) // 待下单列中文名→数据库字段名映射 var _pendingFieldMap = { '产品类型': 'product_type', '货品编号': 'product_code', '货品名称': 'product_name', '产品分级': 'product_grade', '供应商名称': 'supplier_name', '建议下单数量': 'suggested_qty', '正式下单数量': 'formal_order_qty', '下单备注': 'remark', '货期': 'lead_time', '物流': 'logistics_time', '总支撑天数': 'total_support_days', '平台': 'platform', '运营': 'operator', '审核价格': 'audit_price', '下单人员': 'created_by', '下单时间': 'created_at', '采购跟单': 'procurement_follower', '采购计划': 'procurement_planner', '加入时间': 'created_at', '库存支撑天数': 'inventory_support_days', '备货支撑天数': 'stocking_support_days', '3天平均销量': 'avg_3d_sales', '7天平均销量': 'avg_7d_sales', '14天平均销量': 'avg_14d_sales', '销售趋势': 'sales_trend', }; var _pendingDateFilterFields = {'下单时间': true, '加入时间': true}; var _pendingEmptyFilterFields = {'下单备注': true, '平台': true, '运营': true}; var PENDING_FILTER_COLS = [ {field:'产品类型', label:'产品类型', type:'text'}, {field:'货品编号', label:'货品编号', type:'text'}, {field:'货品名称', label:'货品名称', type:'text'}, {field:'产品分级', label:'产品分级', type:'text'}, {field:'供应商名称', label:'供应商名称', type:'text'}, {field:'建议下单数量', label:'建议下单数量', type:'number'}, {field:'正式下单数量', label:'正式下单数量', type:'number'}, {field:'下单备注', label:'下单备注', type:'text'}, {field:'货期', label:'货期', type:'number'}, {field:'物流', label:'物流', type:'number'}, {field:'总支撑天数', label:'总支撑天数', type:'number'}, {field:'平台', label:'平台', type:'text'}, {field:'运营', label:'运营', type:'text'}, {field:'审核价格', label:'审核价格', type:'number'}, {field:'下单人员', label:'下单人员', type:'text'}, {field:'下单时间', label:'下单时间', type:'date'}, {field:'采购跟单', label:'采购跟单', type:'text'}, {field:'采购计划', label:'采购计划', type:'text'}, {field:'加入时间', label:'加入时间', type:'date'}, {field:'库存支撑天数', label:'库存支撑天数', type:'number'}, {field:'备货支撑天数', label:'备货支撑天数', type:'number'}, {field:'3天平均销量', label:'3天平均销量', type:'number'}, {field:'7天平均销量', label:'7天平均销量', type:'number'}, {field:'14天平均销量', label:'14天平均销量', type:'number'}, {field:'销售趋势', label:'销售趋势', type:'text'}, ]; function loadPurchasePage(container) { container.innerHTML = `

📋 待下单商品

序号 产品类型 ▾ 货品编号 ▾ 货品名称 ▾ 库存支撑天数 ▾ 备货支撑天数 ▾ 3天平均销量 ▾ 7天平均销量 ▾ 14天平均销量 ▾ 销售趋势 ▾ 确认下单 产品分级 ▾ 供应商名称 ▾ 建议下单数量 ▾ 正式下单数量 ▾ 下单备注 ▾ 货期 ▾ 物流 ▾ 总支撑天数 ▾ 平台 ▾ 运营 ▾ 审核价格 ▾ 下单人员 ▾ 下单时间 ▾ 采购跟单 ▾ 采购计划 ▾ 加入时间 ▾ 操作
`; loadPendingList(); restoreAdjustableTableColumnWidths('pendingTable', 'purchase'); enableAdjustableTableColumnResize('pendingTable', 'purchase'); // 加载默认字段方案 loadDefaultPendingScheme(); // 设置缓存重载器:从缓存恢复时重新拉取最新数据 if (!window._pageReloaders) window._pageReloaders = {}; if (!window._pageDataCache) window._pageDataCache = {}; window._pageReloaders['purchase'] = function() { loadPendingList(); }; window._pageDataCache['purchase'] = { _ts: Date.now() }; } function loadPendingList() { var tbody = document.getElementById('pendingTbody'); if (!tbody) return; tbody.innerHTML = '
'; fetch('/api/purchase/pending_list') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; _pendingItems = res.data || []; // 填充供应商/采购计划下拉选项 ['pendingPlanner','pendingOrderer'].forEach(function(selId) { var sel = document.getElementById(selId); if (!sel) return; var currentVal = sel.value; var fieldMap = { pendingPlanner: 'procurement_planner', pendingOrderer: 'created_by' }; var field = fieldMap[selId]; var values = {}; _pendingItems.forEach(function(it) { var v = (it[field] || '').trim(); if (v) values[v] = true; }); var keys = Object.keys(values).sort(); var placeholderMap = { pendingPlanner: '📋 全部采购计划', pendingOrderer: '👤 全部下单人员' }; var placeholder = placeholderMap[selId]; sel.innerHTML = ''; keys.forEach(function(k) { var selected = (k === currentVal) ? ' selected' : ''; sel.innerHTML += ''; }); }); // 清筛选缓存(数据已刷新) invalidateOrderColumnCache('pending'); _syncPendingFilterIcons(); _renderFilteredPending(); }); } function _pendingFormatNumber(value) { var n = parseFloat(value); return isNaN(n) ? '0' : String(n); } function _pendingFormatSupportDays(value) { var n = parseFloat(value); if (isNaN(n)) return '-'; return n === 999 ? '999' : n.toFixed(1); } function _pendingFormatTrend(value) { var colors = {'开始上升':'tag-blue','开始下跌':'tag-purple','平稳趋势':'tag-green','上升趋稳':'tag-blue','下跌趋稳':'tag-purple','持续上升':'tag-blue','持续下跌':'tag-red','急增':'tag-orange','急跌':'tag-red','近期无销售':'tag-grey'}; return '' + escHtml(value || '-') + ''; } // 提取待下单行渲染为独立函数(供 loadPendingList 和 _renderFilteredPending 共用) function _pendingRowsHtml(items) { return items.map(function(item, idx) { var code = escHtml(item.product_code); return '' + '' + '' + (idx + 1) + '' + '' + escHtml(item.product_type || '-') + '' + '' + code + '' + '' + escHtml(item.product_name || '-') + '' + '' + _pendingFormatSupportDays(item.inventory_support_days) + '' + '' + _pendingFormatSupportDays(item.stocking_support_days) + '' + '' + _pendingFormatNumber(item.avg_3d_sales) + '' + '' + _pendingFormatNumber(item.avg_7d_sales) + '' + '' + _pendingFormatNumber(item.avg_14d_sales) + '' + '' + _pendingFormatTrend(item.sales_trend) + '' + '' + '' + escHtml(item.product_grade || '-') + '' + '' + escHtml(item.supplier_name || '-') + '' + '' + (item.suggested_qty || 0) + '' + '' + '' + '' + (item.lead_time || 0) + '' + '' + (item.logistics_time || 0) + '' + '' + (item.total_support_days || 0) + '' + '' + escHtml(item.platform || '-') + '' + '' + renderNameBadges(item.operator) + '' + '' + '' + escHtml(item.created_by || '-') + '' + '' + ((item.created_at || '').toString().slice(0, 19) || '-') + '' + '' + renderNameBadges(item.procurement_follower) + '' + '' + renderNameBadges(item.procurement_planner, true) + '' + '' + ((item.created_at || '').toString().slice(0, 19) || '-') + '' + '' + ' ' + '' + '' + ''; }).join(''); } // ─── 待下单表头筛选 ─── function _pendingGetColType(field) { for (var i = 0; i < PENDING_FILTER_COLS.length; i++) { if (PENDING_FILTER_COLS[i].field === field) return PENDING_FILTER_COLS[i].type; } return 'text'; } function _pendingGetFilterValue(item, field) { var raw = item[_pendingFieldMap[field] || field]; if (raw === null || raw === undefined || String(raw).trim() === '') { return _pendingEmptyFilterFields[field] ? '__EMPTY__' : ''; } var value = String(raw).trim(); return _pendingDateFilterFields[field] ? value.slice(0, 10) : value; } function _pendingFilterValueLabel(value) { return value === '__EMPTY__' ? '空' : value; } function openPendingFilterMenu(field, event) { event = event || window.event; event.stopPropagation(); if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } pendingFilterField = field; var th = event.currentTarget; while (th && th.tagName !== 'TH') th = th.parentNode; var rect = th.getBoundingClientRect(); var label = field; var colType = _pendingGetColType(field); var linkedPendingFilters = Object.assign({}, pendingOrderFilter); delete linkedPendingFilters[field]; var savedPendingFilters = pendingOrderFilter; pendingOrderFilter = linkedPendingFilters; var linkedItems = _applyPendingFilters(_applyPendingSearch(_pendingItems)); pendingOrderFilter = savedPendingFilters; var hasLinkedFilter = Object.keys(linkedPendingFilters).length > 0 || !!(pendingSearch || pendingSupplier || pendingPlanner || pendingOrderer); if (colType === 'number') { _buildPendingNumericFilter(field, label, rect, linkedItems); } else if (hasLinkedFilter) { _buildPendingFilterShell(field, label, rect); var vals2 = _personFilterValuesFromItems(linkedItems, field, _pendingGetFilterValue); if (vals2.length === 0) { var seen2 = {}; linkedItems.forEach(function(item) { var v = _pendingGetFilterValue(item, field); if (v && !seen2[v]) { seen2[v] = 1; vals2.push(v); } }); vals2.sort(); } _populatePendingFilterList(field, vals2); } else { // 从已加载数据提取唯一值(已缓存则直接用) var cacheKey = 'pending:' + field; var cached = window._orderColumnCache && window._orderColumnCache[cacheKey]; if (cached) { _buildPendingListFilter(field, label, rect, cached); } else { // 显示外壳 + 异步提取 _buildPendingFilterShell(field, label, rect); var vals = []; var seen = {}; _pendingItems.forEach(function(item) { var v = _pendingGetFilterValue(item, field); if (v && !seen[v]) { seen[v] = 1; vals.push(v); } }); vals.sort(); window._orderColumnCache = window._orderColumnCache || {}; window._orderColumnCache[cacheKey] = vals; _populatePendingFilterList(field, vals); } } if (pendingFilterMenu) { var handler = function(e) { if (pendingFilterMenu && !pendingFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) { pendingFilterMenu.remove(); pendingFilterMenu = null; document.removeEventListener('click', handler); } }; setTimeout(function() { document.addEventListener('click', handler); }, 10); setTimeout(function() { var inp = document.getElementById('pendingFilterSearch'); if (inp) inp.focus(); }, 150); } } function _buildPendingFilterShell(field, label, rect) { var menu = document.createElement('div'); menu.id = 'pendingFilterMenu'; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
加载中...
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); pendingFilterMenu = menu; } function _buildPendingListFilter(field, label, rect, values) { values = _normalizePersonnelFilterValues(field, values); var menu = document.createElement('div'); menu.id = 'pendingFilterMenu'; var isSorted = pendingSortField === field; var currentOrder = isSorted ? (pendingSortAsc ? 'asc' : 'desc') : ''; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var sel = pendingOrderFilter[field] || {}; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var checked = sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : 'checked'; var displayValue = _pendingFilterValueLabel(v); html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); pendingFilterMenu = menu; } function _populatePendingFilterList(field, values) { values = _normalizePersonnelFilterValues(field, values); var listEl = document.getElementById('pendingFilterList'); if (!listEl) return; var sel = pendingOrderFilter[field] || {}; var html = '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : 'checked'; var displayValue = _pendingFilterValueLabel(v); html += ''; }); listEl.innerHTML = html; } function _buildPendingNumericFilter(field, label, rect, sourceItems) { var isSorted = pendingSortField === field; var currentOrder = isSorted ? (pendingSortAsc ? 'asc' : 'desc') : ''; var menu = document.createElement('div'); menu.id = 'pendingFilterMenu'; var current = pendingOrderFilter[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; var html = '
' + escHtml(label) + '
'; html += '
'; html += '内容筛选'; html += '条件筛选
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += ''; html += '
加载中...
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; menu.innerHTML = html; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:260px;max-width:320px;font-size:13px;display:flex;flex-direction:column;max-height:450px'; document.body.appendChild(menu); pendingFilterMenu = menu; // 基于其他筛选条件后的数据提取唯一数值,排除当前列自身筛选 var numValues = []; var seen = {}; (sourceItems || _pendingItems).forEach(function(item) { var v = _pendingGetFilterValue(item, field); if (v && v !== '-' && !seen[v]) { seen[v] = 1; numValues.push(v); } }); numValues.sort(function(a,b) { return parseFloat(a) - parseFloat(b); }); _pendingRenderNumList(numValues); // 回显已保存的数字筛选状态 if (current && current.type === 'num') { var condSel = document.getElementById('pendingNumCond'); var valInput = document.getElementById('pendingNumVal'); if (condSel) condSel.value = current.cond || ''; if (valInput) valInput.value = current.val || ''; pendingNumSwitchTab('cond'); } else if (current && current.type === 'list' && current.values) { var cbs = document.querySelectorAll('#pendingNumList .pending-num-cb'); cbs.forEach(function(cb) { cb.checked = current.values.indexOf(cb.value) >= 0; }); } setTimeout(function() { var inp = document.getElementById('pendingNumVal'); if (inp) inp.focus(); }, 100); } function pendingFilterSearch(q) { var items = document.querySelectorAll('#pendingFilterList label'); q = (q || '').toLowerCase(); items.forEach(function(item) { item.style.display = item.textContent.toLowerCase().indexOf(q) >= 0 ? 'flex' : 'none'; }); } function pendingFilterToggleAll(checked) { document.querySelectorAll('#pendingFilterList .pending-filter-cb').forEach(function(cb) { cb.checked = checked; }); } function pendingFilterCbChange() { var all = document.querySelectorAll('#pendingFilterList .pending-filter-cb'); var checked = document.querySelectorAll('#pendingFilterList .pending-filter-cb:checked'); var sa = document.getElementById('pendingFilterSelectAll'); if (sa) sa.checked = checked.length === all.length; } function applyPendingFilter() { var field = pendingFilterField; var searchInput = document.getElementById('pendingFilterSearch'); var searchText = searchInput ? searchInput.value.trim() : ''; if (searchText) { pendingOrderFilter[field] = {type: 'search', value: searchText}; } else { var cbs = document.querySelectorAll('#pendingFilterList .pending-filter-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); var allCbs = document.querySelectorAll('#pendingFilterList .pending-filter-cb'); if (checked.length === 0 || checked.length === allCbs.length) { delete pendingOrderFilter[field]; } else { pendingOrderFilter[field] = {type: 'list', values: checked}; } } pendingCurrentPage = 1; if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } _syncPendingFilterIcons(); _renderFilteredPending(); } function applyPendingNumFilter() { var field = pendingFilterField; var condPanel = document.getElementById('pendingNumCondPanel'); var isCondTab = condPanel && condPanel.style.display !== 'none'; if (isCondTab) { var cond = document.getElementById('pendingNumCond')?.value || ''; var val = document.getElementById('pendingNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } pendingOrderFilter[field] = {type: 'num', cond: cond, val: val}; } else { var cbs = document.querySelectorAll('#pendingNumList .pending-num-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (checked.length === 0 || checked.length === document.querySelectorAll('#pendingNumList .pending-num-cb').length) { delete pendingOrderFilter[field]; } else { pendingOrderFilter[field] = {type: 'list', values: checked}; } } pendingCurrentPage = 1; if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } _syncPendingFilterIcons(); _renderFilteredPending(); } // ─── 待下单数字列双模式筛选辅助函数 ─── function pendingNumSwitchTab(tab) { var contentPanel = document.getElementById('pendingNumContentPanel'); var condPanel = document.getElementById('pendingNumCondPanel'); var tabContent = document.getElementById('pendingTabContent'); var tabCond = document.getElementById('pendingTabCond'); var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; if (tab === 'content') { if (contentPanel) contentPanel.style.display = ''; if (condPanel) condPanel.style.display = 'none'; if (tabContent) tabContent.style.cssText = tabAct; if (tabCond) tabCond.style.cssText = tabDef; } else { if (contentPanel) contentPanel.style.display = 'none'; if (condPanel) condPanel.style.display = ''; if (tabContent) tabContent.style.cssText = tabDef; if (tabCond) tabCond.style.cssText = tabAct; } } function pendingNumSearchList(q) { var items = document.querySelectorAll('#pendingNumList label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function pendingNumToggleAll(checked) { document.querySelectorAll('#pendingNumList .pending-num-cb').forEach(function(cb) { cb.checked = checked; }); } function pendingNumUpdateAll() { var all = document.querySelectorAll('#pendingNumList .pending-num-cb'); var checked = document.querySelectorAll('#pendingNumList .pending-num-cb:checked'); var cb = document.getElementById('pendingNumSelAll'); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } function _pendingRenderNumList(vals) { var list = document.getElementById('pendingNumList'); if (!list) return; if (vals.length === 0) { list.innerHTML = '
暂无数据
'; return; } var html = ''; vals.forEach(function(v) { html += ''; }); list.innerHTML = html; } // 旧版条件筛选入口(保持兼容) function pendingNumFilterApply() { applyPendingNumFilter(); } function clearPendingFilter() { delete pendingOrderFilter[pendingFilterField]; pendingCurrentPage = 1; if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } _syncPendingFilterIcons(); _renderFilteredPending(); } function _applyPendingFilters(items) { var keys = Object.keys(pendingOrderFilter); if (keys.length === 0) return items; return items.filter(function(item) { return keys.every(function(f) { var fp = pendingOrderFilter[f]; if (!fp) return true; var v = _pendingGetFilterValue(item, f); if (fp.type === 'search') { return v.toLowerCase().indexOf((fp.value || '').toLowerCase()) >= 0; } if (fp.type === 'list') { if (['采购跟单','采购计划','运营'].indexOf(f) >= 0) return _matchesPersonnelFilter(v, fp.values); return fp.values.indexOf(v) >= 0; } if (fp.type === 'num') { var n = parseFloat(v); var tv = parseFloat(fp.val); if (isNaN(n) || isNaN(tv)) return false; switch (fp.cond) { case 'eq': return n === tv; case 'neq': return n !== tv; case 'gt': return n > tv; case 'gte': return n >= tv; case 'lt': return n < tv; case 'lte': return n <= tv; } } return true; }); }); } function pendingDoSearch() { pendingSearch = document.getElementById('pendingSearch')?.value?.trim() || ''; pendingSupplier = document.getElementById('pendingSupplier')?.value || ''; pendingPlanner = document.getElementById('pendingPlanner')?.value || ''; pendingOrderer = document.getElementById('pendingOrderer')?.value || ''; pendingCurrentPage = 1; _renderFilteredPending(); } function pendingResetSearch() { var searchEl = document.getElementById('pendingSearch'); var supplierEl = document.getElementById('pendingSupplier'); var plannerEl = document.getElementById('pendingPlanner'); var ordererEl = document.getElementById('pendingOrderer'); if (searchEl) searchEl.value = ''; if (supplierEl) supplierEl.value = ''; if (plannerEl) plannerEl.value = ''; if (ordererEl) ordererEl.value = ''; pendingSearch = ''; pendingSupplier = ''; pendingPlanner = ''; pendingOrderer = ''; pendingOrderFilter = {}; pendingCurrentPage = 1; _syncPendingFilterIcons(); _renderFilteredPending(); } function _applyPendingSearch(items) { if (!pendingSearch && !pendingSupplier && !pendingPlanner && !pendingOrderer) return items; return items.filter(function(it) { if (pendingSearch) { var code = (it.product_code || '').toLowerCase(); var name = (it.product_name || '').toLowerCase(); var kw = pendingSearch.toLowerCase(); if (!code.includes(kw) && !name.includes(kw)) return false; } if (pendingSupplier && !(it.supplier_name || '').toLowerCase().includes(pendingSupplier.toLowerCase())) return false; if (pendingPlanner && (it.procurement_planner || '') !== pendingPlanner) return false; if (pendingOrderer && (it.created_by || '') !== pendingOrderer) return false; return true; }); } function _renderFilteredPending() { var filtered = _applyPendingSearch(_pendingItems); filtered = _applyPendingFilters(filtered); var countEl = document.getElementById('pendingCount'); if (countEl) countEl.textContent = '共 ' + filtered.length + ' / ' + _pendingItems.length + ' 条'; var tbody = document.getElementById('pendingTbody'); if (!tbody) return; var pel = document.getElementById('pendingPagination'); if (filtered.length === 0) { tbody.innerHTML = '暂无匹配数据'; if (pel) pel.innerHTML = ''; return; } var totalPages = Math.ceil(filtered.length / PENDING_PAGE_SIZE); if (pendingCurrentPage > totalPages) pendingCurrentPage = totalPages; if (pendingCurrentPage < 1) pendingCurrentPage = 1; var startIdx = (pendingCurrentPage - 1) * PENDING_PAGE_SIZE; tbody.innerHTML = _pendingRowsHtml(filtered.slice(startIdx, startIdx + PENDING_PAGE_SIZE)); if (pel && typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, pendingCurrentPage, filtered.length, PENDING_PAGE_SIZE, 'loadPendingListPage', 'PENDING_PAGE_SIZE'); } pendingUpdateCount(); } function loadPendingListPage(page) { pendingCurrentPage = page; _renderFilteredPending(); } function _syncPendingFilterIcons() { var icons = document.querySelectorAll('#pendingTable .order-filt-icon'); icons.forEach(function(icon) { var field = icon.parentNode.textContent.trim().replace(/▾/,'').trim(); var hasFilter = pendingOrderFilter[field]; icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = hasFilter ? 'bold' : 'normal'; }); } // 待下单排序 function pendingFilterSort(field, order) { if (order === 'asc') { pendingSortField = field; pendingSortAsc = true; } else if (order === 'desc') { pendingSortField = field; pendingSortAsc = false; } else { pendingSortField = ''; } if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } _syncPendingFilterIcons(); _renderFilteredPending(); } function pendingToggleAll(checked) { document.querySelectorAll('.pending-row-cb').forEach(function(cb) { cb.checked = checked; }); pendingUpdateCount(); } function pendingUpdateCount() { var total = document.querySelectorAll('.pending-row-cb').length; var checked = document.querySelectorAll('.pending-row-cb:checked').length; var el = document.getElementById('pendingSelectedCount'); if (el) el.textContent = checked > 0 ? '已选 ' + checked + '/' + total : ''; document.getElementById('pendingSelectAllCb').checked = total > 0 && checked === total; } function showPendingConfirmModal(item, onConfirm) { // 先显示 loading 弹窗,同时加载BOM数据 var modal = document.createElement('div'); modal.id = 'pendingConfirmModal'; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; modal.innerHTML = '
' + '
加载中...
'; document.body.appendChild(modal); // 异步加载BOM详情(获取包材供应商和单价) fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(item.product_code)) .then(function(r) { return r.json(); }) .then(function(res) { var bomMaterials = (res.code === 0 && res.data && res.data.bom_materials) ? res.data.bom_materials : []; renderPendingConfirmModal(modal, item, bomMaterials, onConfirm); }) .catch(function() { renderPendingConfirmModal(modal, item, [], onConfirm); }); } function renderPendingConfirmModal(modal, item, bomMaterials, onConfirm) { var productCode = item.product_code || ''; var supplierName = item.supplier_name || '-'; var createdBy = item.created_by || '-'; var suggestedQty = item.suggested_qty || 0; var actualQty = item.formal_order_qty || 0; var auditPrice = item.audit_price || 0; var hasBom = bomMaterials.length > 0; // 成品信息字段:2列布局 var prodPairs = [ [['货品编号', productCode], ['货品名称', item.product_name || '-']], [['供应商名称', supplierName], ['下单人员', createdBy]], [['建议下单数量', suggestedQty], ['实际下单数量', actualQty]] ]; // 包材信息字段:2列布局 // 每行展示一个物料的所有4个字段:包材编号 | 包材名称 | 供应商名称 | 单价 var formalQtyConfirm = item.formal_order_qty || 0; // 获取已保存的包材实际下单数量 var savedActualMap = {}; if (item.bom_items) { try { var savedBom = typeof item.bom_items === 'string' ? JSON.parse(item.bom_items) : item.bom_items; savedBom.forEach(function(s) { if (s.material_code) savedActualMap[s.material_code] = s.actual_quantity || 0; }); } catch(e) {} } var bomRows = []; if (hasBom) { // 过滤掉内料(名称含内料或编码含-NL) bomMaterials = bomMaterials.filter(function(b) { return !((b.material_name && b.material_name.indexOf('内料') !== -1) || (b.material_code && b.material_code.indexOf('-NL') !== -1)); }); bomMaterials.forEach(function(b) { var matSupplier = b.material_supplier || '-'; var matPrice = b.material_cost !== undefined ? '¥' + parseFloat(b.material_cost || 0).toFixed(4) : '-'; var bomQty = b.bom_qty || 0; var suggestedQty = Math.ceil(formalQtyConfirm * bomQty); var actualQty = savedActualMap[b.material_code] || 0; bomRows.push({ code: b.material_code || '', name: b.material_name || '', suggested_qty: suggestedQty, actual_qty: actualQty, supplier: matSupplier, price: matPrice }); }); } var cellBorder = 'border:1px solid #e8e8e8'; var thBaseLight = 'padding:7px 10px;' + cellBorder + ';text-align:center;background:#e8e8e8;color:#555;font-weight:500;white-space:nowrap'; var thBaseDark = 'padding:7px 10px;' + cellBorder + ';text-align:center;background:#555;color:#fff;font-weight:500;white-space:nowrap'; var tdBase = 'padding:7px 10px;' + cellBorder + ';text-align:center'; var html = '
'; html += '

请核对商品信息

'; // ─── 成品信息 ─── html += '
'; html += '
🏭 成品信息
'; html += '
'; html += ''; prodPairs.forEach(function(pair) { var left = pair[0], right = pair[1]; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
' + left[0] + '' + escHtml(String(left[1] || '-')) + '' + right[0] + '' + escHtml(String(right[1] || '-')) + '
'; // ─── 包材信息 ─── if (hasBom && bomRows.length > 0) { html += '
'; html += '
📦 包材信息
'; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; if (window._canViewField('planning', '成本价')) html += ''; html += ''; bomRows.forEach(function(b, i) { var bg = i % 2 === 0 ? '#fff' : '#f5f5f5'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; if (window._canViewField('planning', '成本价')) html += ''; html += ''; }); html += '
包材编号包材名称建议下单数量实际下单数量供应商名称单价
' + escHtml(b.code) + '' + escHtml(b.name) + '' + (b.suggested_qty || 0).toLocaleString() + '' + (b.actual_qty || 0).toLocaleString() + '' + escHtml(b.supplier) + '' + escHtml(b.price) + '
'; } else { html += '
'; html += '
📦 包材信息
'; html += '
暂无包材数据
'; } html += '
'; html += '
'; html += ''; html += '
'; modal.innerHTML = '
' + html + '
'; document.getElementById('pendingConfirmCancelBtn').onclick = function() { modal.remove(); }; document.getElementById('pendingConfirmOkBtn').onclick = function() { modal.remove(); if (onConfirm) onConfirm(); }; } function _refreshLinkedOrderLists() { // 只重新请求当前已挂载的订单表,沿用各自现有的筛选、表头筛选和页码状态。 if (document.getElementById('domesticOrdersTbody') && typeof domesticLoadOrders === 'function') domesticLoadOrders(domesticOrderPage || 1); if (document.getElementById('overseasOrdersTbody') && typeof overseasLoadOrders === 'function') overseasLoadOrders(overseasOrderPage || 1); if (document.getElementById('packagingOrdersTbody') && typeof packagingLoadOrders === 'function') packagingLoadOrders(packagingOrderPage || 1); } function showConfirmRedirect(hasProduct, hasPackaging) { var links = []; if (hasProduct) { links.push({ label: '📦 国内/海外订单管理', page: '/orders/domestic' }); } if (hasPackaging) { links.push({ label: '📦 包材订单管理', page: '/orders/packaging' }); } // 下单备份始终存在(已标记为ordered的记录) links.push({ label: '📋 下单备份管理', page: '/purchase' }); if (links.length === 0) return; var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:100000;display:flex;align-items:center;justify-content:center;'; var html = '
' + '
✅
' + '

下单完成

' + '

数据已生成,请前往对应页面查看

'; links.forEach(function(l) { html += ''; }); html += '
'; overlay.innerHTML = html; overlay.id = 'confirmRedirectModal'; document.body.appendChild(overlay); } // 防重复提交锁:确认下单请求进行中时阻止再次发起(防止双击/并发重复生成订单) var _pendingConfirmLock = false; function pendingBatchConfirm() { if (!window._hasPagePerm('purchase', 'batch_confirm')) { showCenteredAlert('权限不足', '当前账号没有确认下单权限'); return; } if (_pendingConfirmLock) { showCenteredAlert('提示', '正在提交中,请稍候…'); return; } var checkedCodes = []; document.querySelectorAll('.pending-row-cb:checked').forEach(function(cb) { checkedCodes.push(cb.value); }); if (checkedCodes.length === 0) { showCenteredAlert('提示', '请先勾选要确认下单的商品'); return; } showCenteredConfirm('批量下单确认', '确认对选中的 ' + checkedCodes.length + ' 个商品进行下单操作?').then(function(confirmed) { if (!confirmed) return; if (_pendingConfirmLock) { showCenteredAlert('提示', '正在提交中,请稍候…'); return; } _pendingConfirmLock = true; // 收集这些商品的BOM前端修改数据(如有) var bomMap = {}; if (window._pendingBomData) { checkedCodes.forEach(function(code) { if (window._pendingBomData[code]) { bomMap[code] = window._pendingBomData[code]; } }); } fetch('/api/purchase/confirm_order', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: checkedCodes, bom_data_map: bomMap}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { _refreshLinkedOrderLists(); showCenteredAlert('操作成功', '✅ 批量下单成功,共 ' + checkedCodes.length + ' 个', function() { showConfirmRedirect(res.has_product_order, res.has_packaging_order); }); loadPendingList(); } else { showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败')); } }) .catch(function(err) { showCenteredAlert('网络错误', '❌ 网络错误: ' + (err.message || '请求失败')); }) .finally(function() { _pendingConfirmLock = false; }); }); } function _capturePendingDetailBom(productCode) { var modal = document.getElementById('pendingDetailModal'); if (!modal) return false; var productInput = modal.querySelector('.pend-product-actual-qty'); var productActualQty = productInput ? (parseInt(productInput.value) || 0) : 0; var materials = [{ material_code: '成品_' + productCode, actual_quantity: productActualQty, remark: '' }]; var seen = {}; modal.querySelectorAll('.pend-detail-editable').forEach(function(inp) { var matCode = inp.getAttribute('data-material'); var col = inp.getAttribute('data-col'); if (!matCode) return; if (!seen[matCode]) { var row = inp.closest('tr'); var matName = ''; var bomQty = 0; if (row) { var nameCell = row.querySelector('td:nth-child(2)'); var qtyCell = row.querySelector('td:nth-child(3)'); if (nameCell) matName = nameCell.textContent.trim(); if (qtyCell) bomQty = parseFloat(qtyCell.textContent.trim()) || 0; } seen[matCode] = { material_code: matCode, material_name: matName, quantity: bomQty, actual_quantity: 0 }; materials.push(seen[matCode]); } if (col === 'actual_qty') seen[matCode].actual_quantity = parseInt(inp.value) || 0; else if (col === 'pre_pack') seen[matCode].pre_pack_qty = parseFloat(inp.value) || 0; else if (col === 'remark') seen[matCode].remark = inp.value.trim(); else if (col === 'manuscript') seen[matCode].change_manuscript = inp.value; }); if (!window._pendingBomData) window._pendingBomData = {}; window._pendingBomData[productCode] = materials; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) it.formal_order_qty = productActualQty; }); return true; } function pendingConfirmOrder(productCode) { if (!window._hasPagePerm('purchase', 'batch_confirm')) { showCenteredAlert('权限不足', '当前账号没有确认下单权限'); return; } if (_pendingConfirmLock) { showCenteredAlert('提示', '正在提交中,请稍候…'); return; } // 如果从待下单详情弹窗直接确认,先读取当前未点击“保存”的成品/包材输入值。 // 否则这些临时值不会进入 bom_data_map,后端会回退到基础BOM。 _capturePendingDetailBom(productCode); // 查找该商品在列表中的数据 var item = null; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) item = it; }); if (!item) { showCenteredAlert('提示', '未找到该商品数据'); return; } showCenteredConfirm('下单确认', '确认对 [' + escHtml(productCode) + '] 进行下单操作?').then(function(confirmed) { if (!confirmed) return; if (_pendingConfirmLock) { showCenteredAlert('提示', '正在提交中,请稍候…'); return; } _pendingConfirmLock = true; // 携带前端已保存的BOM修改数据(如有) var bomMap = {}; if (window._pendingBomData && window._pendingBomData[productCode]) { bomMap[productCode] = window._pendingBomData[productCode]; } fetch('/api/purchase/confirm_order', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: [productCode], bom_data_map: bomMap}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { _refreshLinkedOrderLists(); showCenteredAlert('操作成功', '✅ 下单成功', { zIndex: 100000, onConfirm: function() { showConfirmRedirect(res.has_product_order, res.has_packaging_order); } }); loadPendingList(); } else { showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败')); } }) .catch(function(err) { showCenteredAlert('网络错误', '❌ 网络错误: ' + (err.message || '请求失败')); }) .finally(function() { _pendingConfirmLock = false; }); }); } // ─── 下单备注保存 ─── window.remarkEnterSave = function(e) { if (e.key === 'Enter') { e.preventDefault(); e.target.blur(); } }; window.savePendingRemark = function(input) { if (!window._hasPagePerm('purchase', 'edit')) { showCenteredAlert('权限不足', '当前账号没有编辑下单信息权限'); return; } var remark = input.value.trim(); var code = input.getAttribute('data-pcode'); if (!code) return; // 同步更新本地缓存 (_pendingItems || []).forEach(function(it) { if (it.product_code === code) it.remark = remark; }); fetch('/api/purchase/update_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, remark: remark}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) console.warn('保存备注失败:', res.msg); }) .catch(function(e){ console.warn('保存备注出错:', e); }); }; // 待下单冻结列功能 window._pendingFrozen = false; // 待下单管理 - 列字段设置 window._pendingColVisible = {}; window._pendingColOrder = [ 'checkbox','index','product_type','product_code','product_name', 'inventory_support_days','stocking_support_days','avg_3d_sales','avg_7d_sales','avg_14d_sales','sales_trend', 'confirm_order','product_grade','supplier_name','remark', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator', 'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action' ]; var PENDING_COL_LABELS = { checkbox:'✔','index':'序号','product_type':'产品类型','product_code':'货品编号','product_name':'货品名称', confirm_order:'确认下单','product_grade':'产品分级','supplier_name':'供应商名称','remark':'下单备注', lead_time:'货期','logistics_time':'物流','total_support_days':'总支撑天数', procurement_follower:'采购跟单','procurement_planner':'采购计划','platform':'归属平台','operator':'运营专员', audit_price:'审核价格',created_by:'下单人员',created_time:'下单时间', inventory_support_days:'库存支撑天数',stocking_support_days:'备货支撑天数',avg_3d_sales:'3天平均销量',avg_7d_sales:'7天平均销量',avg_14d_sales:'14天平均销量',sales_trend:'销售趋势', 'suggested_qty':'建议下单数量','formal_qty':'正式下单数量', created_at:'加入时间','action':'操作' }; window.openPendingColModal = function() { // 确保字段顺序完整无重复 var defaultOrder = ['checkbox','index','product_type','product_code','product_name', 'confirm_order','product_grade','supplier_name','remark', 'inventory_support_days','stocking_support_days','avg_3d_sales','avg_7d_sales','avg_14d_sales','sales_trend', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator', 'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action']; if (!window._pendingColOrder || window._pendingColOrder.length < 10) { window._pendingColOrder = defaultOrder.slice(); } // 去重 var seen = {}; window._pendingColOrder = window._pendingColOrder.filter(function(f) { if (seen[f]) return false; seen[f] = true; return true; }); // 补齐缺失字段 defaultOrder.forEach(function(f) { if (window._pendingColOrder.indexOf(f) === -1) { window._pendingColOrder.push(f); } }); var order = window._pendingColOrder.slice(); var modalId = 'pendingColModal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px'; var bodyHtml = '
' + '
' + '

⚙ 字段设置

' + '' + '
' + '
' + '
' + '
' + '📁 方案管理' + '' + '' + '
' + '
' + '暂无保存方案' + '
' + '
' + '
' + '' + '
' + '
'; order.forEach(function(field, idx) { if (field === 'checkbox' || field === 'action') return; var label = PENDING_COL_LABELS[field] || field; var checked = window._pendingColVisible[field] !== false ? ' checked' : ''; bodyHtml += '
' + '⠿' + ' ' + label + '
'; }); bodyHtml += '
' + '
' + '' + '' + '
'; modal.innerHTML = bodyHtml; document.body.appendChild(modal); // 加载已保存方案列表 refreshPendingSchemeList(modalId); }; window.savePendingColSettings = function(modalId) { // 读取顺序 var rows = document.querySelectorAll('#' + modalId + '-body .pending-col-row'); var newOrder = ['checkbox', 'index']; rows.forEach(function(row) { newOrder.push(row.dataset.field); }); newOrder.push('action'); window._pendingColOrder = newOrder; // 读取可见性 document.querySelectorAll('#' + modalId + '-body .pending-col-cb').forEach(function(cb) { window._pendingColVisible[cb.dataset.field] = cb.checked; }); document.getElementById(modalId).remove(); applyPendingColSettings(); }; // ─── 待下单字段方案管理(与计划预测保持一致) ─── // 构建当前字段配置JSON(顺序+可见性) function buildPendingColumnsJSON() { var order = window._pendingColOrder || []; var cols = []; order.forEach(function(field) { if (field === 'checkbox' || field === 'action') return; var label = PENDING_COL_LABELS[field] || field; var visible = window._pendingColVisible[field] !== false; cols.push({field: field, label: label, visible: visible}); }); return JSON.stringify(cols); } // 保存方案 window.savePendingScheme = function(modalId) { var nameInput = document.getElementById(modalId + '-scheme-name'); if (!nameInput) return; var schemeName = nameInput.value.trim(); if (!schemeName) { alert('请输入方案名称'); nameInput.focus(); return; } // 先把当前弹窗内的顺序和勾选同步到全局状态 var rows = document.querySelectorAll('#' + modalId + '-body .pending-col-row'); var newOrder = ['checkbox', 'index']; rows.forEach(function(row) { newOrder.push(row.dataset.field); }); newOrder.push('action'); window._pendingColOrder = newOrder; document.querySelectorAll('#' + modalId + '-body .pending-col-cb').forEach(function(cb) { window._pendingColVisible[cb.dataset.field] = cb.checked; }); var columnsJson = buildPendingColumnsJSON(); fetch('/api/planning/scheme/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ scheme_name: schemeName, columns_json: columnsJson, page: 'pending', is_default: 0 }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { nameInput.value = ''; refreshPendingSchemeList(modalId); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败,请检查网络'); }); }; // 加载方案(预览到弹窗,不关闭弹窗) window.loadPendingScheme = function(modalId, schemeId) { if (schemeId === undefined || schemeId === null) return; fetch('/api/planning/schemes?page=pending') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var target = null; for (var i = 0; i < schemes.length; i++) { if (schemes[i].id == schemeId) { target = schemes[i]; break; } } if (!target) { alert('方案不存在或已被删除'); refreshPendingSchemeList(modalId); return; } var cols; try { cols = JSON.parse(target.columns_json); } catch(e) { alert('方案数据异常'); return; } if (!cols || !Array.isArray(cols) || cols.length === 0) { alert('方案数据为空'); return; } var defaultOrder = ['checkbox','index','product_type','product_code','product_name', 'confirm_order','product_grade','supplier_name','remark', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator', 'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action']; var newOrder = ['checkbox', 'index']; var pendingVisible = {}; cols.forEach(function(c) { if (c && c.field && c.field !== 'checkbox' && c.field !== 'action') { newOrder.push(c.field); pendingVisible[c.field] = c.visible !== false; } }); // 补齐缺失字段 defaultOrder.forEach(function(f) { if (f === 'checkbox' || f === 'action') return; if (newOrder.indexOf(f) === -1) { newOrder.push(f); pendingVisible[f] = false; } }); newOrder.push('action'); // 更新全局状态 window._pendingColOrder = newOrder; Object.keys(pendingVisible).forEach(function(field) { window._pendingColVisible[field] = pendingVisible[field]; }); // 重新渲染弹窗内的checkbox(重建body区域) var body = document.getElementById(modalId + '-body'); if (body) { var html = ''; newOrder.forEach(function(field) { if (field === 'checkbox' || field === 'action') return; var label = PENDING_COL_LABELS[field] || field; var checked = window._pendingColVisible[field] !== false ? ' checked' : ''; html += '
' + '⠿' + ' ' + label + '
'; }); body.innerHTML = html; } // 同步全选状态 var allCb = document.getElementById(modalId + '-all'); if (allCb) { var allChecked = true; Object.keys(window._pendingColVisible).forEach(function(f) { if (!window._pendingColVisible[f]) allChecked = false; }); allCb.checked = allChecked; } }) .catch(function(err) { console.error('loadPendingScheme error:', err); }); }; // 删除方案 window.deletePendingScheme = function(modalId, schemeId) { showCenteredConfirm('确认', '确定要删除此方案吗?').then(function(ok) { if (!ok) return; fetch('/api/planning/scheme/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { refreshPendingSchemeList(modalId); } else { alert(res.msg || '删除失败'); } }); }); }; // 设置默认方案 window.setDefaultPendingScheme = function(modalId, schemeId) { fetch('/api/planning/scheme/set_default', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId, page: 'pending'}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadPendingScheme(modalId, schemeId); refreshPendingSchemeList(modalId); } else { alert(res.msg || '设置失败'); } }) .catch(function(err) { console.error('setDefaultPendingScheme error:', err); }); }; // 刷新方案列表UI function refreshPendingSchemeList(modalId) { var container = document.getElementById(modalId + '-scheme-list'); if (!container) return; fetch('/api/planning/schemes?page=pending') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; if (schemes.length === 0) { container.innerHTML = '暂无保存方案'; return; } var html = ''; schemes.forEach(function(s) { var isDefault = s.is_default == 1; html += ''; html += '' + escHtml(s.scheme_name) + ''; if (isDefault) { html += '★默认'; } html += '📌'; html += '✕'; html += ''; }); container.innerHTML = html; }); } // 加载默认方案(页面初始化时调用) function loadDefaultPendingScheme() { fetch('/api/planning/schemes?page=pending') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var defaultScheme = null; schemes.forEach(function(s) { if (s.is_default == 1) defaultScheme = s; }); if (defaultScheme) { var cols; try { cols = JSON.parse(defaultScheme.columns_json); } catch(e) { return; } if (!cols || !Array.isArray(cols)) return; var newOrder = ['checkbox', 'index']; var pendingVisible = {}; cols.forEach(function(c) { if (c && c.field && c.field !== 'checkbox' && c.field !== 'action') { newOrder.push(c.field); pendingVisible[c.field] = c.visible !== false; } }); var defaultOrder = ['checkbox','index','product_type','product_code','product_name', 'inventory_support_days','stocking_support_days','avg_3d_sales','avg_7d_sales','avg_14d_sales','sales_trend', 'confirm_order','product_grade','supplier_name','remark', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator', 'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action']; defaultOrder.forEach(function(f) { if (f === 'checkbox' || f === 'action') return; if (newOrder.indexOf(f) === -1) { newOrder.push(f); pendingVisible[f] = false; } }); newOrder.push('action'); window._pendingColOrder = newOrder; Object.keys(pendingVisible).forEach(function(field) { window._pendingColVisible[field] = pendingVisible[field]; }); applyPendingColSettings(); } }); } function applyPendingColSettings() { var order = window._pendingColOrder || []; var tbl = document.getElementById('pendingTable'); if (!tbl) return; var thead = tbl.querySelector('thead tr'); var tbody = document.getElementById('pendingTbody'); if (!thead) return; // 获取字段到TH/TD索引的映射(按当前DOM顺序) var fieldToThIdx = {}; var ths = thead.querySelectorAll('th'); var fieldMap = ['checkbox','index','product_type','product_code','product_name', 'inventory_support_days','stocking_support_days','avg_3d_sales','avg_7d_sales','avg_14d_sales','sales_trend', 'confirm_order','product_grade','supplier_name','remark', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator', 'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action']; ths.forEach(function(th, i) { if (i < fieldMap.length) fieldToThIdx[fieldMap[i]] = i; }); // 重新排列表头 order.forEach(function(field, newIdx) { var oldIdx = fieldToThIdx[field]; if (oldIdx === undefined || oldIdx >= ths.length) return; var target = thead.children[newIdx]; var el = ths[oldIdx]; if (el !== target) { thead.insertBefore(el, target || null); } }); // 重新排列数据行 if (tbody) { var fieldToTdIdx = {}; fieldMap.forEach(function(f, i) { fieldToTdIdx[f] = i; }); tbody.querySelectorAll('tr').forEach(function(tr) { var tds = tr.querySelectorAll('td'); order.forEach(function(field, newIdx) { var oldIdx = fieldToTdIdx[field]; if (oldIdx === undefined || oldIdx >= tds.length) return; var target = tr.children[newIdx]; var el = tds[oldIdx]; if (el !== target) { tr.insertBefore(el, target || null); } }); }); } // 设置可见性 order.forEach(function(field) { var vis = window._pendingColVisible[field]; var hide = vis === false || (field !== 'checkbox' && field !== 'index' && field !== 'action' && vis === false); // 简化:隐藏字段通过样式 }); // 刷新冻结 if (window._pendingFrozenActive) { applyPendingFrozen(window._pendingFrozenFields); } } // ─── 下单备份管理 - 字段设置 ─── if (!window._backupColVisible) { window._backupColVisible = {}; window._backupColOrder = null; } if (!window._backupPendingVisible) window._backupPendingVisible = {}; if (!window._backupPendingOrder) window._backupPendingOrder = null; if (!window._backupSchemePendingLoaded) window._backupSchemePendingLoaded = false; var BACKUP_COL_ORDER = ['index','product_type','product_code','product_name', 'ordered_at','unit_price','created_by','production_order_no','suggested_qty','formal_qty', 'product_grade','supplier_name','remark', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator', 'audit_price','created_at','action']; var BACKUP_COL_LABELS = { index:'序号',product_type:'产品类型',product_code:'货品编号',product_name:'货品名称', ordered_at:'下单时间',product_grade:'产品分级',supplier_name:'供应商名称',remark:'下单备注', lead_time:'货期',logistics_time:'物流',total_support_days:'总支撑天数', procurement_follower:'采购跟单',procurement_planner:'采购计划',platform:'归属平台',operator:'运营专员',created_by:'下单人员', audit_price:'是否审核价格',production_order_no:'生产单号',suggested_qty:'建议下单数量',formal_qty:'正式下单数量', created_at:'加入时间',action:'操作',unit_price:'单价' }; function _backupVisibleOrder() { var order = window._backupColOrder || BACKUP_COL_ORDER; return order.filter(function(field) { return field === 'index' || field === 'action' || window._backupColVisible[field] !== false; }); } function _backupHeaderHtml(isOrder) { var filters = { product_type:'产品类型',product_code:'货品编号',product_name:'货品名称',created_by:'下单人', production_order_no:'生产单号',suggested_qty:'建议下单数量',formal_qty:'正式下单数量',product_grade:'产品分级', supplier_name:'供应商名称',remark:'下单备注',lead_time:'货期',logistics_time:'物流',total_support_days:'总支撑天数', procurement_follower:'采购跟单',procurement_planner:'采购计划',platform:'平台',operator:'运营',audit_price:'是否审核价格' }; return _backupVisibleOrder().map(function(field) { if (field === 'index') return '序号'; if (field === 'action') return isOrder ? renderCollapsibleActionHeader('purchase_ready', 'orderTable', false) : renderCollapsibleBackupStatusHeader('backupTable'); var label = BACKUP_COL_LABELS[field] || field; var filter = filters[field]; return '' + label + (filter ? ' ▾' : '') + ''; }).join(''); } function _backupSchemeColumnsJson() { var order = window._backupPendingOrder || window._backupColOrder || BACKUP_COL_ORDER; var visible = window._backupPendingVisible; return JSON.stringify(order.map(function(field) { return {field: field, label: BACKUP_COL_LABELS[field] || field, visible: field === 'action' || field === 'index' || visible[field] !== false}; })); } function _backupRefreshSchemes() { var box = document.getElementById('backupSchemeList'); if (!box) return; fetch('/api/planning/schemes?page=purchase_ready').then(function(r){return r.json();}).then(function(res) { var list = (res.data || []).filter(function(s){return s.page === 'purchase_ready';}); if (!list.length) { box.innerHTML = '暂无保存方案'; return; } box.innerHTML = list.map(function(s) { return '' + escHtml(s.scheme_name) + '' + (s.is_default == 1 ? '★默认' : '') + '✕'; }).join(''); }); } window.saveBackupScheme = function() { var input = document.getElementById('backupSchemeNameInput'); var name = input && input.value.trim(); if (!name) { alert('请输入方案名称'); if (input) input.focus(); return; } var modal = input && input.closest('[id^="backupColModal-"]'); var modalBody = modal && modal.querySelector('[id$="-body"]'); if (modalBody) { window._backupPendingOrder = Array.prototype.map.call(modalBody.querySelectorAll('label[data-field]'), function(label) { return label.dataset.field; }); modalBody.querySelectorAll('.backup-col-cb').forEach(function(cb) { window._backupPendingVisible[cb.dataset.field] = cb.checked; }); } fetch('/api/planning/scheme/save', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({scheme_name:name,columns_json:_backupSchemeColumnsJson(),page:'purchase_ready',is_default:0})}).then(function(r){return r.json();}).then(function(res){ if(res.code===0){input.value='';_backupRefreshSchemes();alert('方案已保存');}else alert(res.msg||'保存失败'); }).catch(function(){alert('保存失败,请检查网络');}); }; window.loadBackupScheme = function(id) { fetch('/api/planning/schemes?page=purchase_ready').then(function(r){return r.json();}).then(function(res){ var target=(res.data||[]).find(function(s){return s.id==id;}); if(!target)return; var parsed; try{parsed=JSON.parse(target.columns_json);}catch(e){alert('方案数据异常');return;} var known={}; window._backupPendingVisible = {}; parsed.forEach(function(c){if(c&&c.field){known[c.field]=true;window._backupPendingVisible[c.field]=c.visible!==false;}}); window._backupPendingOrder=parsed.map(function(c){return c.field;}).filter(function(f){return BACKUP_COL_ORDER.indexOf(f)>=0;}).concat(BACKUP_COL_ORDER.filter(function(f){return !known[f];})); BACKUP_COL_ORDER.forEach(function(f){if(window._backupPendingVisible[f]===undefined)window._backupPendingVisible[f]=false;}); var modal=document.querySelector('[id^="backupColModal-"]'); if(modal) { modal.remove(); window._backupSchemePendingLoaded = true; window.openBackupColModal(); } }); }; window.deleteBackupScheme = function(id) { showCenteredConfirm('确认','确定要删除这个方案吗?').then(function(ok){if(!ok)return;fetch('/api/planning/scheme/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id})}).then(function(r){return r.json();}).then(function(){_backupRefreshSchemes();});}); }; window.openBackupColModal = function() { if (!window._backupColOrder) { window._backupColOrder = BACKUP_COL_ORDER.slice(); } if (!window._backupSchemePendingLoaded) { window._backupPendingOrder = window._backupColOrder.slice(); window._backupPendingVisible = Object.assign({}, window._backupColVisible); } window._backupSchemePendingLoaded = false; var order = (window._backupPendingOrder || window._backupColOrder || BACKUP_COL_ORDER).slice(); var modalId = 'backupColModal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px'; var bodyHtml = '
' + '
' + '

⚙ 字段设置(下单备份)

' + '' + '
' + '
' + '
' + '' + '拖动字段可调整顺序' + '
' + '
📁 方案管理
' + '
' + '
'; order.forEach(function(field) { if (field === 'action' || field === 'index') return; var label = BACKUP_COL_LABELS[field] || field; var checked = window._backupPendingVisible[field] !== false ? ' checked' : ''; bodyHtml += ''; }); bodyHtml += '
' + '
' + '' + '' + '
'; modal.innerHTML = bodyHtml; document.body.appendChild(modal); _backupRefreshSchemes(); var grid = document.getElementById(modalId + '-body'); var dragged = null; grid.querySelectorAll('label[data-field]').forEach(function(label) { label.addEventListener('dragstart', function(e) { dragged = this; this.style.opacity = '0.45'; e.dataTransfer.effectAllowed = 'move'; }); label.addEventListener('dragend', function() { dragged = null; this.style.opacity = '1'; grid.querySelectorAll('label').forEach(function(el) { el.style.borderColor = 'transparent'; }); }); label.addEventListener('dragover', function(e) { e.preventDefault(); if (dragged && dragged !== this) this.style.borderColor = '#1677ff'; }); label.addEventListener('dragleave', function() { this.style.borderColor = 'transparent'; }); label.addEventListener('drop', function(e) { e.preventDefault(); if (!dragged || dragged === this) return; this.style.borderColor = 'transparent'; var labels = Array.prototype.slice.call(grid.querySelectorAll('label[data-field]')); var from = labels.indexOf(dragged), to = labels.indexOf(this); if (from < to) grid.insertBefore(dragged, this.nextSibling); else grid.insertBefore(dragged, this); }); }); }; window.saveBackupColSettings = function(modalId) { var body = document.getElementById(modalId + '-body'); if (!body) return; var order = Array.prototype.map.call(body.querySelectorAll('label[data-field]'), function(label) { return label.dataset.field; }); // 序号列固定在最前、操作列固定在最后,且始终可见,不允许在字段设置里取消 window._backupPendingOrder = ['index'].concat(order).concat(['action']); body.querySelectorAll('.backup-col-cb').forEach(function(cb) { window._backupPendingVisible[cb.dataset.field] = cb.checked; }); window._backupPendingVisible['index'] = true; window._backupColOrder = window._backupPendingOrder.slice(); window._backupColVisible = Object.assign({}, window._backupPendingVisible); document.getElementById(modalId).remove(); _renderFilteredBackup(); alert('字段设置已保存'); }; // 待下单管理 - 冻结列标签(显示在每列底部) var _pendingFrozenFields = []; var _pendingFrozenActive = false; window.togglePendingFrozen = function() { var labels = ['序号','产品类型','货品编号','货品名称']; var modalId = 'pendingFrozenModal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px'; var html = '
' + '
' + '

🔒 选择冻结列

' + '' + '
' + '
'; labels.forEach(function(label, i) { var checked = _pendingFrozenFields.indexOf(i + 1) !== -1 ? ' checked' : ''; html += ''; }); html += '
' + '
' + '' + '' + '
'; modal.innerHTML = html; modal.onclick = function(e) { if (e.target === modal) modal.remove(); }; document.body.appendChild(modal); }; function applyPendingFrozen(frozenIdx) { _pendingFrozenActive = frozenIdx.length > 0; var btn = document.getElementById('pendingFrozenBtn'); if (btn) { btn.style.background = _pendingFrozenActive ? '#bae7ff' : ''; btn.style.border = _pendingFrozenActive ? '1px solid #1890ff' : ''; btn.textContent = _pendingFrozenActive ? '🔓 取消冻结' : '🔒 冻结列'; } var tbl = document.getElementById('pendingTable'); if (!tbl) return; // 清除所有冻结 tbl.querySelectorAll('[style*="sticky"]').forEach(function(el) { el.style.position = ''; el.style.left = ''; el.style.zIndex = ''; el.style.background = ''; }); tbl.querySelectorAll('th, td').forEach(function(el) { el.style.position = ''; el.style.left = ''; el.style.zIndex = ''; el.style.background = ''; }); var ths = tbl.querySelectorAll('thead tr th'); var rows = tbl.querySelectorAll('tbody tr'); // 按从左到右顺序冻结选中列 frozenIdx.sort(function(a,b){return a-b;}); // 先冻结th frozenIdx.forEach(function(idx) { if (!ths[idx]) return; var left = 0; for (var i = 0; i < idx; i++) { if (frozenIdx.indexOf(i) !== -1) { left += (ths[i] ? (ths[i].offsetWidth || ths[i].getBoundingClientRect().width || 50) : 50); } } ths[idx].style.cssText = 'position:sticky;left:' + left + 'px;z-index:11;background:var(--bg,#f5f5f5)'; }); // 同步冻结数据行 frozenIdx.forEach(function(idx) { rows.forEach(function(tr) { var tds = tr.querySelectorAll('td'); if (!tds[idx]) return; var left = 0; for (var i = 0; i < idx; i++) { if (frozenIdx.indexOf(i) !== -1) { left += (tds[i] ? (tds[i].offsetWidth || tds[i].getBoundingClientRect().width || 50) : 50); } } tds[idx].style.cssText = 'position:sticky;left:' + left + 'px;z-index:2;background:var(--card-bg,#fff)'; }); }); } // 采购跟单/计划按名称生成颜色标签 function colorForName(name) { if (!name || name === '-') return null; var colors = ['#4d9aff','#e8579e','#7ecf4f','#fba84a','#9d5ee0','#4dd4d4','#f062ae','#fbc44d','#5f7af0','#b8e54a','#f75a62','#52abff','#63be3f','#e56540','#7d4ac4']; var hash = 0; for (var i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } function textColorForBG(bg) { if (!bg) return '#333'; var c = bg.replace('#',''); var r = parseInt(c.substr(0,2),16), g = parseInt(c.substr(2,2),16), b = parseInt(c.substr(4,2),16); return (r*0.299 + g*0.587 + b*0.114) > 140 ? '#333' : '#fff'; } // ─── 人员分组映射(唯一来源:人员信息管理接口) ─── window._personnelGroups = []; window._nameToGroup = {}; window._personnelGroupsLoaded = false; function rebuildPersonnelGroupMap() { window._nameToGroup = {}; (window._personnelGroups || []).forEach(function(g) { (g.members || []).forEach(function(m) { window._nameToGroup[m] = g; }); }); } function refreshPersonnelLinkedPages() { var productsPage = document.getElementById('page-container-products'); if (productsPage && typeof loadProducts === 'function') loadProducts(prodCurrentPage || 1); var planningPage = document.getElementById('page-container-planning'); if (planningPage && typeof loadPlanPage === 'function') loadPlanPage(planCurrentPage || 1); var purchasePage = document.getElementById('page-container-purchase'); if (purchasePage && typeof loadPendingList === 'function') loadPendingList(); } function loadPersonnelGroupsForLinkedPages() { return fetch('/api/personnel/groups?_t=' + Date.now(), {credentials: 'same-origin'}) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) throw new Error(res.msg || '人员分组加载失败'); window._personnelGroups = res.data || []; rebuildPersonnelGroupMap(); window._personnelGroupsLoaded = true; // Discard values collected before personnel groups finished loading. window._planColumnCache = {}; PLAN_FILTER_OPTIONS['采购跟单'] = []; PLAN_FILTER_OPTIONS['采购计划'] = []; refreshPersonnelLinkedPages(); return window._personnelGroups; }) .catch(function() { // 分组接口失败时保留空映射,回退显示产品基础信息中的人员姓名。 window._personnelGroups = []; window._nameToGroup = {}; window._personnelGroupsLoaded = true; return []; }); } function normalizePlanPersonnelGroupValues(field, values) { var isFollower = field === '采购跟单' || field === 'procurement_follower'; if (!isFollower) return values || []; var category = '采购跟单'; var groupNames = {}; // Group filters use Personnel Management as their authoritative option source. (window._personnelGroups || []).forEach(function(g) { if (g && g.name && (!g.category || g.category === category)) groupNames[g.name] = 1; }); (values || []).forEach(function(v) { String(v).split(',').forEach(function(n) { n = n.trim(); if (!n) return; var g = window._nameToGroup && window._nameToGroup[n]; if (g && g.name && (!g.category || g.category === category)) groupNames[g.name] = 1; else if (!g && !(window._personnelGroups || []).length) groupNames[n] = 1; }); }); return Object.keys(groupNames).sort(); } function _personFilterValuesFromItems(items, field, valueGetter) { var groupFields = {'采购跟单':1,'采购计划':1,'采购员':1,'采购开发':1,'包材开发':1,'采购下单人':1,'运营':1}; var seen = {}; (items || []).forEach(function(item) { var raw = valueGetter(item, field); if (!raw || raw === '__EMPTY__') return; if (!groupFields[field]) { seen[raw] = 1; return; } var names = String(raw).split(',').map(function(n){ return n.trim(); }).filter(Boolean); var group = null; var sameGroup = names.length > 0; names.forEach(function(name) { var g = window._nameToGroup && window._nameToGroup[name]; if (!g) sameGroup = false; else if (!group) group = g; else if (group.name !== g.name) sameGroup = false; }); if (sameGroup && group) seen[group.name] = 1; else names.forEach(function(name) { seen[name] = 1; }); }); return Object.keys(seen).sort(); } function _normalizePersonnelFilterValues(field, values) { var groupFields = {'采购跟单':1,'采购计划':1,'采购员':1,'采购开发':1,'包材开发':1,'采购下单人':1,'运营':1}; if (!groupFields[field]) return values || []; var items = (values || []).map(function(value){ return {value: value}; }); return _personFilterValuesFromItems(items, field, function(item){ return item.value; }); } function _normalizeOrderPersonnelFilterValues(field, values) { var orderGroupFields = {'采购员':1, '采购下单人':1}; if (!orderGroupFields[field]) return values || []; var groupNames = {}; var unmatchedNames = {}; var hasPersonnelGroups = (window._personnelGroups || []).length > 0; (window._personnelGroups || []).forEach(function(group) { if (group && group.name && (!group.category || group.category === '采购跟单')) { groupNames[group.name] = 1; } }); (values || []).forEach(function(value) { if (value === '__EMPTY__') { unmatchedNames[value] = 1; return; } String(value || '').replace(/,/g, ',').split(',').forEach(function(name) { name = name.trim(); if (!name) return; var directGroup = (window._personnelGroups || []).find(function(group) { return group && group.name === name && (!group.category || group.category === '采购跟单'); }); var memberGroup = window._nameToGroup && window._nameToGroup[name]; if (directGroup) groupNames[directGroup.name] = 1; else if (memberGroup && memberGroup.name && (!memberGroup.category || memberGroup.category === '采购跟单')) groupNames[memberGroup.name] = 1; else if (!hasPersonnelGroups) unmatchedNames[name] = 1; else unmatchedNames[name] = 1; }); }); return Object.keys(groupNames).sort().concat(Object.keys(unmatchedNames).sort()); } function _matchesPersonnelFilter(rawValue, selectedValues) { var raw = String(rawValue || ''); var names = raw.split(',').map(function(n){ return n.trim(); }).filter(Boolean); return (selectedValues || []).some(function(selected) { var group = (window._personnelGroups || []).find(function(g){ return g.name === selected; }); if (group) return names.some(function(name){ return group.members.indexOf(name) >= 0; }); return names.indexOf(selected) >= 0 || raw === selected; }); } function renderNameBadges(names, preserveNames) { if (!names || names === '-' || names === '') return '-'; var nameList = names.split(',').map(function(n) { return n.trim(); }).filter(function(n) { return n; }); if (nameList.length === 0) return '-'; if (preserveNames) { return nameList.map(function(name) { var bg = colorForName(name) || '#e6f7ff'; var fc = textColorForBG(bg); return '' + escHtml(name) + ''; }).join(''); } // 人员字段已经保存为组名时,直接显示组名并保留悬停成员提示。 var directGroup = (window._personnelGroups || []).find(function(g) { return nameList.length === 1 && g.name === nameList[0]; }); if (directGroup) { var directMembers = (directGroup.members || []).join('、'); var directColor = directGroup.tagColor || '#1677ff'; return '📦 ' + escHtml(directGroup.name) + ''; } // 人员全部属于同一人员信息管理分组时,显示分组并悬停查看成员。 var group = null; var allInSameGroup = nameList.length > 0; nameList.forEach(function(name) { var g = _nameToGroup[name]; if (!g) allInSameGroup = false; else if (!group) group = g; else if (g.name !== group.name) allInSameGroup = false; }); // 如果同属一个人员信息管理分组,显示分组名 if (allInSameGroup && group) { var namesStr = group.members.join('、'); return '📦 ' + escHtml(group.name) + ''; } // 否则按原来的方式渲染单个标签 var html = ''; nameList.forEach(function(name) { var bg = colorForName(name) || '#e6f7ff'; var fc = textColorForBG(bg); html += '' + escHtml(name) + ''; }); return html; } // 将人名渲染为独立彩色标签(跳过分组合并) function renderPersonBadges(names) { if (!names || names === '-' || names === '') return '-'; var nameList = names.split(',').map(function(n) { return n.trim(); }).filter(function(n) { return n; }); if (nameList.length === 0) return '-'; var html = ''; nameList.forEach(function(name) { var bg = colorForName(name) || '#e6f7ff'; var fc = textColorForBG(bg); html += '' + escHtml(name) + ''; }); return html; } // 渲染下单人下拉选择:按订单左侧跟单小组限制人员范围。 function renderOrdererSelect(row, groupField) { var current = (row['下单人'] || '').trim(); var allFollowers = domesticFollowers.slice(); var groupValue = String(row[groupField || '采购员'] || '').trim(); var selectedGroup = (window._personnelGroups || []).find(function(group) { return group && group.name === groupValue && (!group.category || group.category === '采购跟单'); }); var followerOptions = selectedGroup ? (selectedGroup.members || []).slice() : allFollowers; if (current && followerOptions.indexOf(current) < 0) followerOptions.unshift(current); followerOptions = followerOptions.filter(function(name, index, list) { return name && list.indexOf(name) === index; }); if (followerOptions.length === 0) { return '-'; } var initBg = '#fff', initFc = '#333'; var html = ''; return html; } function toggleBom(btn, productCode) { var tr = btn.closest('tr'); var codeSafe = productCode.replace(/[^a-zA-Z0-9]/g, '_'); var bomRowId = 'bom-row-' + codeSafe; var existing = document.getElementById(bomRowId); if (existing) { existing.remove(); btn.textContent = '📋 BOM'; return; } // 查找BOM数据 var item = null; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) item = it; }); var bom = item ? item.bom_items : null; var bomList = []; if (bom) { try { bomList = typeof bom === 'string' ? JSON.parse(bom) : bom; } catch(e) {} } if (!bomList || bomList.length === 0) { alert('该商品暂无物料清单'); return; } var formalQty = item ? (item.formal_order_qty || 0) : 0; // BOM内容 var bomContent = '
'; bomContent += ''; bomList.forEach(function(b) { var matCode = escHtml(b.material_code || ''); var qty = b.quantity || 0; var orderQty = Math.ceil(formalQty * qty); // 使用已保存的实际下单数量(如有) var actualQty = (b.actual_quantity !== undefined && b.actual_quantity !== null) ? b.actual_quantity : orderQty; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; }); bomContent += '
物料编号物料名称用量建议下单数量实际下单数量下单前包材下单后包材下单备注更换稿件操作
' + matCode + '' + escHtml(b.material_name || '') + '' + qty + '
'; // 弹窗展示 var modalId = 'bom-modal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; modal.innerHTML = '
' + '
' + '

📋 物料清单 [' + escHtml(productCode) + ']

' + '' + '
' + '
' + bomContent + '
' + '
' + '' + '' + '
'; modal.onclick = function(e) { if (e.target === modal) modal.remove(); }; document.body.appendChild(modal); } window.saveBomData = function(productCode) { var inputs = document.querySelectorAll('.bom-actual-qty[data-product="' + productCode + '"]'); var materials = []; // 读取成品下单信息 var productActualInput = document.querySelector('.bom-product-actual-qty[data-product="' + productCode + '"]'); var productRemarkInput = document.querySelector('.bom-product-remark[data-product="' + productCode + '"]'); var productActualQty = productActualInput ? (parseInt(productActualInput.value) || 0) : 0; var productRemark = productRemarkInput ? productRemarkInput.value.trim() : ''; // 保存成品数据(单独存,允许为0) materials.push({ material_code: '成品_' + productCode, actual_quantity: productActualQty, remark: productRemark }); inputs.forEach(function(inp) { var qty = parseInt(inp.value) || 0; { var matCode = inp.getAttribute('data-material'); // 读取物料名称(从同行第二个td获取) var matName = ''; var row = inp.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) matName = nameCell.textContent.trim(); } // 读取同行备注 var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); var remarkVal = remarkInput ? remarkInput.value.trim() : ''; // 读取包材备注 var pkgRemarkInput = document.querySelector('.bom-pkg-remark[data-material="' + matCode + '"]'); var pkgRemarkVal = pkgRemarkInput ? pkgRemarkInput.value.trim() : ''; // 读取是否修改稿件 var manuscriptSelect = document.querySelector('.bom-manuscript[data-material="' + matCode + '"]'); var manuscriptVal = manuscriptSelect ? manuscriptSelect.value : ''; // 读取下单前包材(下单后包材由确认下单逻辑计算,不接受前端修改) var prePackInput = document.querySelector('.bom-pre-pack-qty[data-material="' + matCode + '"]'); var prePackQty = prePackInput ? (parseInt(prePackInput.value) || 0) : 0; var postPackQty = 0; materials.push({ material_code: matCode, material_name: matName, quantity: qty, actual_quantity: qty, remark: remarkVal, pkg_remark: pkgRemarkVal, change_manuscript: manuscriptVal, pre_pack_qty: prePackQty, post_pack_qty: postPackQty }); } }); // 成品下单信息可单独保存;未绑定包材时 materials 仅包含成品记录。 var nonProductMaterials = materials.filter(function(m) { return m.material_code.indexOf('成品_') !== 0; }); if (materials.length === 0) { alert('没有需要保存的下单信息'); return; } // 存到前端变量,下单时一起提交 if (!window._pendingBomData) window._pendingBomData = {}; window._pendingBomData[productCode] = materials; // 关闭下单信息弹窗;无包材时通过成品输入框定位弹窗。 var modalSourceInput = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"]') || productActualInput; if (modalSourceInput) { var modal = modalSourceInput.closest('[style*="z-index: 10000"]') || modalSourceInput.closest('[id^="bom-modal-"]'); if (modal) modal.remove(); } // 持久化到后端:BOM数据、成品下单备注同步到 pending_orders var updatePayload = {product_code: productCode, bom_items: materials}; updatePayload.remark = productRemark; fetch('/api/purchase/update_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(updatePayload) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) console.warn('同步BOM数据失败:', res.msg); }).catch(function(e) { console.warn('同步BOM数据出错:', e); }); // 同步下单前/后包材数据到 bom_packaging 表 var pkgPayload = {product_code: productCode, materials: nonProductMaterials}; fetch('/api/bom_packaging/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(pkgPayload) }).catch(function(e) { console.warn('保存包材数据出错:', e); }); var saveMessage = nonProductMaterials.length > 0 ? '✅ 成品及包材下单信息已保存并同步到数据库' : '✅ 成品下单信息已保存,无包材商品可直接下单成品'; showCenteredAlert('保存成功', saveMessage); }; // ─── 计划预测 BOM 物料清单(完整编辑功能) ─── function planToggleBom(productCode, productName, suggestedQty, actualQty, orderRemark) { // 从已保存的BOM数据中恢复成品实际下单数量和备注 if (window._pendingBomData && window._pendingBomData[productCode]) { window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === '成品_' + productCode) { // 修复:允许恢复为0(用户手动设0表示不下单) if (m.actual_quantity >= 0) actualQty = m.actual_quantity; if (m.remark) orderRemark = m.remark; } }); } // 后端按品牌自动决定是否含内料:海外品牌返回内料,其余品牌不含内料 fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode) + '&include_inner=auto') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data) { alert('❌ 获取物料清单失败'); return; } // 没有绑定包材时仍显示成品下单信息,包材区域由下方统一展示暂无数据。 var bom = res.data.bom_materials || []; // 获取正式下单数量(从计划预测页面元素获取) var formalQtySpan = document.getElementById('fqv-' + productCode); var formalQty = formalQtySpan ? (parseInt(formalQtySpan.dataset.val) || 0) : 0; var modalId = 'bom-modal-' + Date.now(); // 批注同步外面列表的批注内容(products.remark,与计划预测批注列同源) var productRemark = (res.data.product && res.data.product['批注']) || ''; var productSummaryHtml = '
' + '
📦 成品下单信息
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
货品编号货品名称建议下单数量实际下单数量
' + escHtml(productCode) + '' + escHtml(productName) + '' + suggestedQty + '
' + '
'; var bomContent = productSummaryHtml + '
' + '
📝 下单备注
' + '' + '
' + '
' + '
📝 批注信息
' + '' + '
' + '
' + '
📦 包材下单信息
'; if (bom.length === 0) { bomContent += '
暂无数据
'; } else { bomContent += '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; } // 读取之前保存的包材数据 var savedBom = window._pendingBomData && window._pendingBomData[productCode] ? window._pendingBomData[productCode] : []; var savedMap = {}; var savedRemarkMap = {}; var savedManuscriptMap = {}; var savedPrePackMap = {}; var savedPostPackMap = {}; var savedPkgRemarkMap = {}; savedBom.forEach(function(s) { savedMap[s.material_code] = s.actual_quantity; savedRemarkMap[s.material_code] = s.remark || ''; savedManuscriptMap[s.material_code] = s.change_manuscript || ''; savedPkgRemarkMap[s.material_code] = s.pkg_remark || ''; // 下单前包材以 bom_packaging 数据库当前值为准;前端待下单缓存中的旧值不参与回填。 // 这样表格导入的3000不会被历史缓存中的0覆盖。 }); bom.forEach(function(b) { // 内料是否展示由接口按品牌分类判定(include_inner=auto),此处不再重复过滤 var matCode = escHtml(b.material_code || ''); var qty = b.bom_qty || 0; var orderQty = Math.ceil(formalQty * qty); // 修复:物料在 savedMap 中时(即使实际数量为0),使用保存值;否则用默认的建议值 var hasSaved = b.material_code in savedMap; var savedActual = savedMap[b.material_code]; var actualQty = hasSaved ? (savedActual === undefined ? orderQty : savedActual) : orderQty; // 下单前/后包材均以 API 返回的 bom_packaging 当前值为准,不使用前端缓存覆盖。 var prePackVal = (b.pre_pack_qty || 0); var postPackVal = (b.post_pack_qty || 0); var remarkVal = savedRemarkMap[b.material_code] !== undefined ? savedRemarkMap[b.material_code] : (b.pending_remark || ''); var pkgRemarkVal = savedPkgRemarkMap[b.material_code] !== undefined ? savedPkgRemarkMap[b.material_code] : (b.pkg_remark || ''); bomContent += '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }); if (bom.length > 0) bomContent += '
物料编号物料名称用量实际下单数量下单前包材下单后包材包材备注更换稿件下单备注
' + matCode + '' + escHtml(b.material_name || '') + '' + qty + '' + postPackVal + '
'; var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; modal.innerHTML = '
' + '
' + '

📋 下单信息 [' + escHtml(productCode) + ']

' + '' + '
' + '
' + bomContent + '
' + '
' + '' + '' + '' + '
'; modal.onclick = function(e) { if (e.target === modal) {} }; document.body.appendChild(modal); // 下单后包材按当前弹窗数据即时展示:下单前包材 + 包材实际下单数量 - 成品实际下单数量×包材用量。 // 确认下单时后端会用同一公式最终写入数据库。 function refreshPostPackValues() { var productInput = modal.querySelector('.bom-product-actual-qty[data-product="' + productCode + '"]'); var productQty = productInput ? (parseInt(productInput.value) || 0) : 0; modal.querySelectorAll('.bom-actual-qty[data-product="' + productCode + '"]').forEach(function(input) { var row = input.closest('tr'); if (!row) return; var preInput = row.querySelector('.bom-pre-pack-qty'); var postSpan = row.querySelector('.bom-post-pack-qty'); if (!preInput || !postSpan) return; var preQty = parseInt(preInput.value) || 0; var packQty = parseInt(input.value) || 0; var bomQty = parseFloat(row.querySelector('td:nth-child(3)').textContent) || 0; postSpan.textContent = preQty + packQty - productQty * bomQty; }); } modal.querySelectorAll('.bom-product-actual-qty, .bom-actual-qty, .bom-pre-pack-qty').forEach(function(input) { input.addEventListener('input', refreshPostPackValues); input.addEventListener('change', refreshPostPackValues); }); refreshPostPackValues(); // 根据内容行数自动设置 textarea 高度 modal.querySelectorAll('textarea').forEach(function(ta) { var lines = (ta.value || '').split('\n').length; ta.setAttribute('rows', Math.max(1, lines)); }); // 恢复已保存的更换稿件状态 modal.querySelectorAll('.bom-manuscript').forEach(function(sel) { var matCode = sel.getAttribute('data-material'); if (savedManuscriptMap[matCode]) { sel.value = savedManuscriptMap[matCode]; } sel.style.color = sel.value === '是' ? '#f5222d' : '#333'; }); // 实际下单数量输入框:点击自动全选 + 回车跳到下一行 var actualInputs = modal.querySelectorAll('.bom-actual-qty'); actualInputs.forEach(function(inp, idx) { // 点击自动全选 inp.addEventListener('focus', function() { this.select(); }); inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); var matCode = inp.getAttribute('data-material'); var qty = parseInt(inp.value) || 0; if (!window._pendingBomData) window._pendingBomData = {}; if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = []; // 更新或添加当前物料 var found = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === matCode) { m.actual_quantity = qty; // 同时更新其他字段 var orderInput = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); if (orderInput) { m.quantity = parseInt(orderInput.value) || 0; var row = orderInput.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) m.material_name = nameCell.textContent.trim(); } } var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); if (remarkInput) m.remark = remarkInput.value.trim(); found = true; } }); if (!found) { // 0 也是明确的“不下单”,必须写入缓存,不能回退为成品数量。 // 读取同行建议下单数量、物料名称和备注 var orderInput = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); var bomQty = orderInput ? (parseInt(orderInput.value) || 0) : 0; var matName = ''; var remarkVal = ''; if (orderInput) { var row = orderInput.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) matName = nameCell.textContent.trim(); } } // 读取备注 var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); if (remarkInput) remarkVal = remarkInput.value.trim(); window._pendingBomData[productCode].push({ material_code: matCode, material_name: matName, quantity: bomQty, actual_quantity: qty, remark: remarkVal }); } inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 800); // 跳到下一行 var nextInput = actualInputs[idx + 1]; if (nextInput) { nextInput.focus(); nextInput.select(); } // 没有下一行:不做处理,保存完成 } }); }); // 成品下单信息 - 实际下单数量:点击全选 + 回车保存 var productActualInputs = modal.querySelectorAll('.bom-product-actual-qty'); productActualInputs.forEach(function(inp) { inp.addEventListener('focus', function() { this.select(); }); inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); var newQty = parseInt(inp.value) || 0; showCenteredConfirm('确认保存', '确认保存成品实际下单数量为 ' + newQty + ' 吗?保存后将同步更新下方包材的实际下单数量。').then(function(confirmed) { if (!confirmed) return; // 保存成品数量 if (!window._pendingBomData) window._pendingBomData = {}; if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = []; var found = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === '成品_' + productCode) { m.actual_quantity = newQty; found = true; } }); if (!found) { window._pendingBomData[productCode].push({ material_code: '成品_' + productCode, material_name: '', quantity: 0, actual_quantity: newQty, remark: '' }); } // 退出编辑状态 inp.blur(); inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 800); // 同步更新下方包材实际下单数量 if (bom && bom.length > 0) { var materialActualInputs = modal.querySelectorAll('.bom-actual-qty'); materialActualInputs.forEach(function(mInp) { var matCode = mInp.getAttribute('data-material'); // 查找该物料的BOM用量 var bomQty = 0; bom.forEach(function(b) { if (b.material_code === matCode) bomQty = b.bom_qty || 0; }); if (bomQty > 0) { var newMatQty = Math.ceil(newQty * bomQty); mInp.value = newMatQty; // 更新存储 var fMat = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === matCode) { m.actual_quantity = newMatQty; var orderInput = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); if (orderInput) { m.quantity = parseInt(orderInput.value) || 0; var row = orderInput.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) m.material_name = nameCell.textContent.trim(); } } var rInp = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); if (rInp) m.remark = rInp.value.trim(); fMat = true; } }); if (!fMat) { var oInp = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); var bQty = oInp ? (parseInt(oInp.value) || 0) : 0; var mName = ''; if (oInp) { var row = oInp.closest('tr'); if (row) { var nCell = row.querySelector('td:nth-child(2)'); if (nCell) mName = nCell.textContent.trim(); } } var rVal = ''; var rInp2 = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); if (rInp2) rVal = rInp2.value.trim(); window._pendingBomData[productCode].push({ material_code: matCode, material_name: mName, quantity: bQty, actual_quantity: newMatQty, remark: rVal }); } } }); // 程序化赋值不会触发 input/change 事件,需显式重新计算下单后包材 refreshPostPackValues(); showCenteredAlert('保存成功', '✅ 成品实际下单数量已保存,下方包材数量已同步更新'); } }); } }); }); // 成品下单信息 - 下单备注:回车保存 var productRemarkInputs = modal.querySelectorAll('.bom-product-remark'); productRemarkInputs.forEach(function(inp) { inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); if (!window._pendingBomData) window._pendingBomData = {}; if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = []; var found = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === '成品_' + productCode) { m.remark = inp.value; found = true; } }); if (!found) { window._pendingBomData[productCode].push({ material_code: '成品_' + productCode, material_name: '', quantity: 0, actual_quantity: 0, remark: inp.value }); } inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 800); } }); }); // 包材下单信息 - 下单备注:回车保存 var remarkInputs = modal.querySelectorAll('.bom-remark'); remarkInputs.forEach(function(inp) { inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); var matCode = inp.getAttribute('data-material'); var val = inp.value; if (!window._pendingBomData) window._pendingBomData = {}; if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = []; var found = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === matCode) { m.remark = val; found = true; } }); if (!found) { window._pendingBomData[productCode].push({ material_code: matCode, remark: val }); } inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 800); } }); }); }) .catch(function() { alert('❌ 网络错误'); }); } // 计划预测下单信息弹窗中的下单按钮:读取当前弹窗值后复用外部下单流程。 window.planOrderFromBomModal = function(productCode, modalId) { var modal = document.getElementById(modalId); var cached = window._planOrderDataCache && window._planOrderDataCache[productCode]; var data = null; if (cached) { try { data = JSON.parse(cached); } catch (e) {} } if (!modal || !data) { planOrder(productCode); return; } var productInput = modal.querySelector('.bom-product-actual-qty[data-product="' + productCode + '"]'); var productRemarkInput = modal.querySelector('.bom-product-remark[data-product="' + productCode + '"]'); var items = [{ material_code: '成品_' + productCode, actual_quantity: productInput ? (parseInt(productInput.value) || 0) : (parseInt(data.formal_order_qty) || 0), remark: productRemarkInput ? productRemarkInput.value.trim() : (data.remark || '') }]; modal.querySelectorAll('.bom-actual-qty[data-product="' + productCode + '"]').forEach(function(input) { var row = input.closest('tr'); var code = input.getAttribute('data-material') || ''; var name = row && row.querySelector('td:nth-child(2)'); var bomQtyCell = row && row.querySelector('td:nth-child(3)'); var bomQty = bomQtyCell ? (parseFloat(bomQtyCell.textContent) || 0) : 0; var remark = row && row.querySelector('.bom-remark'); var pkgRemark = row && row.querySelector('.bom-pkg-remark'); var manuscript = row && row.querySelector('.bom-manuscript'); var prePack = row && row.querySelector('.bom-pre-pack-qty'); items.push({ material_code: code, material_name: name ? name.textContent.trim() : '', quantity: bomQty, actual_quantity: parseInt(input.value) || 0, remark: remark ? remark.value.trim() : '', pkg_remark: pkgRemark ? pkgRemark.value.trim() : '', change_manuscript: manuscript ? manuscript.value : '', pre_pack_qty: prePack ? (parseInt(prePack.value) || 0) : 0, post_pack_qty: 0 }); }); if (!window._pendingBomData) window._pendingBomData = {}; window._pendingBomData[productCode] = items; data.formal_order_qty = items[0].actual_quantity; data.remark = items[0].remark; modal.remove(); planOrder(data); }; // 计划预测 BOM - 生成物料订单(不依赖 _pendingItems) function planGenerateMatOrder(productCode, materialCode, materialName) { var input = document.querySelector('.bom-actual-qty[data-material="' + materialCode + '"][data-product="' + productCode + '"]'); var qty = input ? parseInt(input.value) || 0 : 0; if (qty <= 0) { alert('请先填写有效的下单数量'); return; } showCenteredConfirm('确认', '确认生成物料 [' + materialCode + '] 的采购订单,数量:' + qty + '?').then(function(ok) { if (!ok) return; fetch('/api/purchase/create_material_order', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ product_code: productCode, material_code: materialCode, material_name: materialName, order_qty: qty }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 物料采购单已生成: ' + res.order_no); } else { alert('❌ ' + (res.msg || '生成失败')); } }); }); } function generateMaterialOrder(productCode, materialCode) { var input = document.querySelector('.bom-actual-qty[data-material="' + materialCode + '"][data-product="' + productCode + '"]'); var qty = input ? parseInt(input.value) || 0 : 0; if (qty <= 0) { alert('请先填写有效的下单数量'); return; } // 查找物料名称 var item = null; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) item = it; }); var bomList = []; if (item && item.bom_items) { try { bomList = typeof item.bom_items === 'string' ? JSON.parse(item.bom_items) : item.bom_items; } catch(e) {} } var matName = ''; bomList.forEach(function(b) { if (b.material_code === materialCode) matName = b.material_name; }); showCenteredConfirm('确认', '确认生成物料 [' + materialCode + '] 的采购订单,数量:' + qty + '?').then(function(ok) { if (!ok) return; // 调用后端生成物料采购订单 fetch('/api/purchase/create_material_order', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ product_code: productCode, material_code: materialCode, material_name: matName, order_qty: qty }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 物料采购单已生成: ' + res.order_no); } else { alert('❌ ' + (res.msg || '生成失败')); } }); }); } function updateFormalQty(productCode, val) { if (!window._hasPagePerm('purchase', 'edit')) { showCenteredAlert('权限不足', '当前账号没有编辑下单信息权限'); return; } var qty = parseInt(val) || 0; fetch('/api/purchase/add_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, formal_order_qty: qty}) }); } function pendingDetail(productCode) { var item = null; _pendingItems.forEach(function(i) { if (i.product_code === productCode) item = i; }); if (!item) { showProductDetail(productCode); return; } var old = document.getElementById('pendingDetailModal'); if (old) old.remove(); var modal = document.createElement('div'); modal.id = 'pendingDetailModal'; modal.className = 'modal-overlay'; modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:flex-start;justify-content:center;padding:40px 0;overflow-y:auto'; modal.onclick = function(e) { if (e.target === modal) {} }; var html = ''; modal.innerHTML = html; document.body.appendChild(modal); // 并行加载规划数据 + BOM Promise.all([ fetch('/api/product/planning_detail?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }), fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }) ]).then(function(results) { var planRes = results[0]; var bomRes = results[1]; // 成品信息字段 var p = planRes.code === 0 ? planRes.data : {}; var prodFieldsHtml = ''; var fields = [ {label:'产品类型', val: item.product_type || '-'}, {label:'货品编号', val: item.product_code}, {label:'货品名称', val: item.product_name || '-'}, {label:'昨日销量', val: p['昨日销量'] !== undefined ? p['昨日销量'] : '-'}, {label:'库存', val: p['库存'] !== undefined ? p['库存'] : '-'}, {label:'加工厂库存', val: p['加工厂库存'] !== undefined ? p['加工厂库存'] : '-'}, {label:'生产在途', val: p['生产在途'] !== undefined ? p['生产在途'] : '-'}, {label:'采购在途', val: p['采购在途'] !== undefined ? p['采购在途'] : '-'}, {label:'总支撑天数', val: item.total_support_days !== undefined ? item.total_support_days : '-'}, {label:'建议下单数量', val: item.suggested_qty || 0, readonly: true}, {label:'实际下单数量', val: item.formal_order_qty || 0, editable: true}, {label:'下单备注', val: item.remark || '-', editable: true} ]; fields.forEach(function(f) { var valHtml = escHtml(String(f.val)); if (f.editable) { var _extraCls = (f.label === '实际下单数量') ? ' pend-product-actual-qty' : ''; valHtml = ''; } else if (f.readonly) { valHtml = '' + valHtml + ''; } else { valHtml = '' + valHtml + ''; } prodFieldsHtml += '
' + f.label + '' + valHtml + '
'; }); var pf = document.getElementById('pendingProdFields'); if (pf) pf.innerHTML = prodFieldsHtml; // BOM var container = document.getElementById('pendingBomContent'); if (!container) return; var bom = (bomRes.code === 0 && bomRes.data && bomRes.data.bom_materials) ? bomRes.data.bom_materials : []; // 过滤掉内料,只显示非内料包材(名称含内料或编码含-NL) bom = bom.filter(function(b) { return !((b.material_name && b.material_name.indexOf('内料') !== -1) || (b.material_code && b.material_code.indexOf('-NL') !== -1)); }); if (bom.length === 0) { container.innerHTML = '
暂无包材物料清单
'; return; } var formalQty = item.formal_order_qty || 0; // 读取已保存的包材实际下单数量 var savedBomActuals = {}; var savedBomRemarks = {}; var savedBomManuscript = {}; if (item.bom_items) { try { var savedBomList = typeof item.bom_items === 'string' ? JSON.parse(item.bom_items) : item.bom_items; savedBomList.forEach(function(s) { if (s.material_code) { if (s.actual_quantity !== undefined) savedBomActuals[s.material_code] = s.actual_quantity; if (s.remark) savedBomRemarks[s.material_code] = s.remark; if (s.change_manuscript) savedBomManuscript[s.material_code] = s.change_manuscript; } }); } catch(e) {} } var cellBorder = 'border:1px solid #d9d9d9'; var thBase = 'padding:7px 8px;' + cellBorder + ';text-align:left;white-space:nowrap'; var tdBase = 'padding:7px 8px;' + cellBorder + ';text-align:left;white-space:nowrap'; var tableHtml = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; bom.forEach(function(b) { var matCode = escHtml(b.material_code || ''); var qty = b.bom_qty || 0; var orderQty = Math.ceil(formalQty * qty); // 使用已保存的实际下单数量(包括0),没有保存则用建议值 var actualQty = b.material_code in savedBomActuals ? (savedBomActuals[b.material_code] || 0) : orderQty; tableHtml += '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }); tableHtml += '
物料编号物料名称用量建议下单数量实际下单数量下单前包材下单后包材下单备注更换稿件
' + matCode + '' + escHtml(b.material_name || '') + '' + qty + '' + orderQty + '' + (b.post_pack_qty || 0) + '
'; container.innerHTML = tableHtml; // 下单后包材即时计算:下单前包材 + 包材实际下单数量 - 成品实际下单数量×该包材用量(与计划预测下单信息一致) function refreshPendingPostPack() { var prodInput = modal.querySelector('.pend-product-actual-qty'); var productQty = prodInput ? (parseInt(prodInput.value) || 0) : 0; container.querySelectorAll('.pend-detail-editable[data-col="actual_qty"]').forEach(function(input) { var row = input.closest('tr'); if (!row) return; var preInput = row.querySelector('.pend-detail-editable[data-col="pre_pack"]'); var postSpan = row.querySelector('.bom-post-pack-qty'); if (!preInput || !postSpan) return; var preQty = parseInt(preInput.value) || 0; var packQty = parseInt(input.value) || 0; var bomQtyCell = row.querySelector('td:nth-child(3)'); var bomQty = bomQtyCell ? (parseFloat(bomQtyCell.textContent.trim()) || 0) : 0; postSpan.textContent = preQty + packQty - productQty * bomQty; }); } var _postPackInputs = [modal.querySelector('.pend-product-actual-qty')]; container.querySelectorAll('.pend-detail-editable[data-col="actual_qty"], .pend-detail-editable[data-col="pre_pack"]').forEach(function(inp) { _postPackInputs.push(inp); }); _postPackInputs.forEach(function(inp) { if (!inp) return; inp.addEventListener('input', refreshPendingPostPack); inp.addEventListener('change', refreshPendingPostPack); }); refreshPendingPostPack(); // 恢复已保存的更换稿件状态 container.querySelectorAll('.bom-manuscript').forEach(function(sel) { var mc = sel.getAttribute('data-material'); if (savedBomManuscript[mc]) { sel.value = savedBomManuscript[mc]; } sel.style.color = sel.value === '是' ? '#f5222d' : '#333'; }); // 可编辑输入框:回车保存当前项并跳转到下一项 var editableInputs = container.querySelectorAll('.pend-detail-editable'); // 按 data-col 分组,同列内从上往下跳转 var colGroups = {}; editableInputs.forEach(function(inp) { var col = inp.getAttribute('data-col'); if (!colGroups[col]) colGroups[col] = []; colGroups[col].push(inp); }); editableInputs.forEach(function(inp) { inp.addEventListener('focus', function() { this.select(); }); inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 600); var col = inp.getAttribute('data-col'); var group = colGroups[col] || []; var idx = group.indexOf(inp); var next = group[idx + 1]; if (next) { next.focus(); next.select(); } } }); }); }); } // 详情弹窗:保存BOM编辑数据 function saveDetailBom(productCode) { var modal = document.getElementById('pendingDetailModal'); if (!modal) return; var inputs = modal.querySelectorAll('.pend-detail-editable'); var materials = []; var seen = {}; inputs.forEach(function(inp) { var matCode = inp.getAttribute('data-material'); var col = inp.getAttribute('data-col'); var val = inp.value.trim(); if (!matCode) return; if (!seen[matCode]) { var row = inp.closest('tr'); var matName = ''; var bomQty = 0; if (row) { // 物料名称在第2个td var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) matName = nameCell.textContent.trim(); // 用量在第3个td var qtyCell = row.querySelector('td:nth-child(3)'); if (qtyCell) bomQty = parseFloat(qtyCell.textContent.trim()) || 0; } seen[matCode] = {material_code: matCode, material_name: matName, bom_qty: bomQty}; materials.push(seen[matCode]); } if (col === 'actual_qty') seen[matCode].actual_quantity = parseInt(val) || 0; else if (col === 'pre_pack') seen[matCode].pre_pack_qty = parseFloat(val) || 0; else if (col === 'post_pack') seen[matCode].post_pack_qty = parseFloat(val) || 0; else if (col === 'price') seen[matCode].unit_price = parseFloat(val) || 0; else if (col === 'remark') seen[matCode].remark = val; else if (col === 'manuscript') seen[matCode].change_manuscript = val; }); // 只保存实际下单数量>0的物料 var toSave = materials.filter(function(m) { return m.actual_quantity >= 0; }); if (toSave.length === 0) { alert('没有实际下单数量>=0的物料'); return; } // 从 _pendingItems 获取当前数据,避免保存时其他字段被清空 // 优先从DOM读取最新值(用户可能在表行或详情弹窗中修改过) var curItem = null; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) curItem = it; }); // 从待下单详情弹窗的成品信息卡片读取最新备注,不能读取列表中的旧输入框值。 var detailRemarkInput = null; modal.querySelectorAll('#pendingProdFields .pd-row').forEach(function(row) { var label = row.querySelector('.pd-label'); if (label && label.textContent.trim() === '下单备注') detailRemarkInput = row.querySelector('input'); }); var latestRemark = detailRemarkInput ? detailRemarkInput.value : (curItem ? (curItem.remark || '') : ''); var saveData = { product_code: productCode, remark: latestRemark, bom_items: toSave }; fetch('/api/purchase/update_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(saveData) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { if (curItem) { curItem.remark = latestRemark; curItem.bom_items = toSave; } if (modal) modal.remove(); loadPendingList(); showCenteredAlert('保存成功', '✅ 包材数据已保存'); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function() { alert('❌ 网络错误'); }); } // 成品信息字段更新(附带所有字段,防止ON DUPLICATE KEY UPDATE清零其他字段) window.updatePendingProdField = function(productCode, field, val) { var curItem = null; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) curItem = it; }); var saveData = { product_code: productCode, product_name: curItem ? curItem.product_name : '', product_type: curItem ? curItem.product_type : '', product_grade: curItem ? curItem.product_grade : '', supplier_name: curItem ? curItem.supplier_name : '', lead_time: curItem ? curItem.lead_time : 0, logistics_time: curItem ? curItem.logistics_time : 0, total_support_days: curItem ? curItem.total_support_days : 0, procurement_follower: curItem ? curItem.procurement_follower : '', procurement_planner: curItem ? curItem.procurement_planner : '', platform: curItem ? curItem.platform : '', operator: curItem ? curItem.operator : '', formal_order_qty: curItem ? curItem.formal_order_qty : 0, suggested_qty: curItem ? curItem.suggested_qty : 0, actual_order_qty: curItem ? curItem.actual_order_qty : 0, audit_price: curItem ? curItem.audit_price : 0, remark: curItem ? (curItem.remark || '') : '' }; // 覆盖当前修改的字段 if (field === '实际下单数量') { saveData.formal_order_qty = parseFloat(val) || 0; } else if (field === '下单备注') { saveData.remark = val; } fetch('/api/purchase/add_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(saveData) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { if (typeof loadPendingList === 'function') loadPendingList(); } else { console.warn('更新失败:', res.msg); } }).catch(function(e){ console.warn('更新出错:', e); }); }; function removePending(productCode) { if (!window._hasPagePerm('purchase', 'delete')) { showCenteredAlert('权限不足', '当前账号没有移出待下单权限'); return; } showCenteredConfirm('回退确认', '确认从待下单列表移除 [' + escHtml(productCode) + ']?').then(function(confirmed) { if (!confirmed) return; fetch('/api/purchase/remove_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: [productCode]}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadPendingList(); } else { showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败')); } }); }); } function updateAuditPrice(productCode, val) { if (!window._hasPagePerm('purchase', 'edit')) { showCenteredAlert('权限不足', '当前账号没有编辑下单信息权限'); return; } var price = parseFloat(val) || 0; fetch('/api/purchase/add_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, audit_price: price}) }); } // ─── 采购订单-下单备份管理(双 Tab:订单管理 / 订单备份管理)─── var _backupItems = []; var _backupAllItems = []; // 原始全量数据(未受搜索过滤) var backupOrderFilter = {}; // 表头筛选状态 var backupFilterMenu = null; // 当前筛选弹窗(单例) var backupFilterField = ''; // 当前筛选字段 var backupSortField = ''; // 下单备份排序字段 var backupSortAsc = false; // 下单备份排序方向 // 双 Tab 状态:order=订单管理,backup=订单备份管理 var _backupTabState = { order: { items: [], allItems: [], filter: {}, sortField: '', sortAsc: false, page: 1 }, backup: { items: [], allItems: [], filter: {}, sortField: '', sortAsc: false, page: 1 } }; var backupActiveTab = 'order'; // 当前激活 Tab function _bkS() { return _backupTabState[backupActiveTab]; } function _bkEl(id) { return document.getElementById(backupActiveTab + id); } function _bkItems() { return _bkS().items; } function _bkAllItems() { return _bkS().allItems; } // 备份列中文名→数据库字段名映射 var _backupFieldMap = { '产品类型': 'product_type', '货品编号': 'product_code', '货品名称': 'product_name', '下单人': 'created_by', '生产单号': 'production_order_no', '建议下单数量': 'suggested_qty', '正式下单数量': 'formal_order_qty', '产品分级': 'product_grade', '供应商名称': 'supplier_name', '下单备注': 'remark', '货期': 'lead_time', '物流': 'logistics_time', '总支撑天数': 'total_support_days', '采购跟单': 'procurement_follower', '采购计划': 'procurement_planner', '平台': 'platform', '运营': 'operator', '是否审核价格': 'audit_price', }; var BACKUP_FILTER_COLS = [ {field:'产品类型', label:'产品类型', type:'text'}, {field:'货品编号', label:'货品编号', type:'text'}, {field:'货品名称', label:'货品名称', type:'text'}, {field:'下单人', label:'下单人', type:'text'}, {field:'生产单号', label:'生产单号', type:'text'}, {field:'建议下单数量', label:'建议下单数量', type:'number'}, {field:'正式下单数量', label:'正式下单数量', type:'number'}, {field:'产品分级', label:'产品分级', type:'text'}, {field:'供应商名称', label:'供应商名称', type:'text'}, {field:'下单备注', label:'下单备注', type:'text'}, {field:'货期', label:'货期', type:'number'}, {field:'物流', label:'物流', type:'number'}, {field:'总支撑天数', label:'总支撑天数', type:'number'}, {field:'采购跟单', label:'采购跟单', type:'text'}, {field:'采购计划', label:'采购计划', type:'text'}, {field:'平台', label:'平台', type:'text'}, {field:'运营', label:'运营', type:'text'}, {field:'是否审核价格', label:'是否审核价格', type:'number'}, ]; function _backupGetFilterValue(item, field) { var raw = item[_backupFieldMap[field] || field]; return raw === null || raw === undefined || String(raw).trim() === '' ? '__EMPTY__' : String(raw).trim(); } var BACKUP_PAGE_SIZE = 50; var backupCurrentPage = 1; // ─── 双 Tab 切换 ─── window.switchBackupTab = function(tab) { if (tab !== 'order' && tab !== 'backup') return; backupActiveTab = tab; document.querySelectorAll('.bk-tab-btn').forEach(function(btn) { var isActive = btn.getAttribute('data-tab') === tab; btn.classList.toggle('active', isActive); btn.style.cssText = isActive ? 'padding:8px 22px;border:none;background:#1677ff;color:#fff;border-radius:6px 6px 0 0;font-size:14px;font-weight:600;cursor:pointer;margin-right:4px' : 'padding:8px 22px;border:none;background:#eef1f5;color:#555;border-radius:6px 6px 0 0;font-size:14px;font-weight:500;cursor:pointer;margin-right:4px'; }); ['order','backup'].forEach(function(t) { var panel = document.getElementById('bkPanel-' + t); if (panel) panel.style.display = (t === tab) ? '' : 'none'; }); // 切换到可见后再重新计算列宽:隐藏面板初次加载时 offsetWidth=0,所有列会被锁成最小宽度 setTimeout(function() { restoreAdjustableTableColumnWidths(tab + 'Table', 'purchase_ready'); enableAdjustableTableColumnResize(tab + 'Table', 'purchase_ready'); }, 0); _renderFilteredBackup(); }; // 渲染单个 tab 面板(prefix: order / backup) function _renderBackupPanelHtml(prefix) { var isOrder = prefix === 'order'; var cap = prefix.charAt(0).toUpperCase() + prefix.slice(1); return `

${isOrder ? '📦 订单管理' : '📋 订单备份管理'}

${!isOrder ? '' : ''}
${_backupHeaderHtml(isOrder)}
`; } function loadPurchaseReadyPage(container) { container.innerHTML = `
${_renderBackupPanelHtml('order')}
`; loadBackupList(); // 只初始化当前可见的 order 表列宽;backup 表隐藏时 offsetWidth=0,会在首次切换到 backup 时重新计算列宽 restoreAdjustableTableColumnWidths('orderTable', 'purchase_ready'); enableAdjustableTableColumnResize('orderTable', 'purchase_ready'); applyCollapsibleActionState('purchase_ready'); if (!window._pageReloaders) window._pageReloaders = {}; if (!window._pageDataCache) window._pageDataCache = {}; window._pageReloaders['purchase_ready'] = function() { loadBackupList(); }; window._pageDataCache['purchase_ready'] = { _ts: Date.now() }; } window.openBackupExportModal = function() { if (!window._hasPagePerm('purchase_ready', 'export')) { showCenteredAlert('权限不足', '当前账号没有下单备份导出权限'); return; } var modalId = 'backupExportModal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px'; var fieldsHtml = BACKUP_COL_ORDER.filter(function(field) { return field !== 'index' && field !== 'action'; }).map(function(field) { return ''; }).join(''); modal.innerHTML = '
' + '

📤 导出下单备份

' + '
' + '
选择下单时间范围
' + '
至
' + '
不选择时间时导出全部下单备份;按下单时间(含首尾日期)筛选
' + '
选择导出字段
' + '
' + fieldsHtml + '
' + '
' + '
' + '
' + '
'; modal.onclick = function(e) { if (e.target === modal) {} }; document.body.appendChild(modal); }; window.downloadBackupExport = function(modalId) { var modal = document.getElementById(modalId); if (!modal) return; var msg = document.getElementById(modalId + '-msg'); var fields = Array.prototype.map.call(modal.querySelectorAll('.backup-export-field:checked'), function(el) { return el.value; }); if (!fields.length) { if (msg) msg.textContent = '请至少选择一个导出字段'; return; } var dateFrom = (document.getElementById(modalId + '-from') || {}).value || ''; var dateTo = (document.getElementById(modalId + '-to') || {}).value || ''; if (dateFrom && dateTo && dateFrom > dateTo) { if (msg) msg.textContent = '开始日期不能晚于结束日期'; return; } var url = '/api/purchase/pending_export?fields=' + encodeURIComponent(fields.join(',')) + '&date_from=' + encodeURIComponent(dateFrom) + '&date_to=' + encodeURIComponent(dateTo); fetch(url, {credentials:'same-origin'}).then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.msg || '导出失败'); }); return r.blob(); }).then(function(blob) { var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = '下单备份.xlsx'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); modal.remove(); }).catch(function(e) { if (msg) msg.textContent = e.message || '导出失败'; }); }; function clearBackupFilter() { [backupActiveTab + 'Search', backupActiveTab + 'OrderDate', backupActiveTab + 'Orderer', backupActiveTab + 'Planner'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); _bkS().filter = {}; _syncBackupFilterIcons(); _renderFilteredBackup(); } function loadBackupList(resetPage) { if (resetPage !== false) _bkS().page = 1; var tbody = _bkEl('Tbody'); if (!tbody) return; tbody.innerHTML = '
'; fetch('/api/purchase/pending_list?status=backup') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; _backupAllItems = res.data || []; // 包含当前已下单和已回退的完整历史 var activeOrderItems = _backupAllItems.filter(function(it) { return it.status === 'ordered'; }); // 订单管理只展示当前有效订单;订单备份管理展示全部下单历史(含已回退)。 _backupTabState.order.allItems = activeOrderItems; _backupTabState.backup.allItems = _backupAllItems; _backupTabState.order.items = activeOrderItems.slice(); _backupTabState.backup.items = _backupAllItems.slice(); _bkS().items = _bkS().allItems.slice(); invalidateOrderColumnCache('backup'); // 填充两个 Tab 的下拉选项(两个 Tab 共用同一批数据) [['order','created_by','👤 全部下单人'],['order','procurement_planner','📋 全部采购计划'],['backup','created_by','👤 全部下单人'],['backup','procurement_planner','📋 全部采购计划']].forEach(function(pair) { var p = pair[0], field = pair[1], placeholder = pair[2]; var selId = p + (field === 'created_by' ? 'Orderer' : 'Planner'); var sel = document.getElementById(selId); if (!sel) return; var currentVal = sel.value; var values = {}; var sourceItems = p === 'order' ? activeOrderItems : _backupAllItems; sourceItems.forEach(function(it) { var v = (it[field] || '').trim(); if (v) values[v] = true; }); var keys = Object.keys(values).sort(); sel.innerHTML = ''; keys.forEach(function(k) { var selected = (k === currentVal) ? ' selected' : ''; sel.innerHTML += ''; }); }); _syncBackupFilterIcons(); _renderFilteredBackup(); }); } // 提取下单备份行渲染为独立函数 function _backupRowsHtml(items, startIdx, showActions) { var visible = window._backupColVisible || {}; var order = window._backupColOrder || BACKUP_COL_ORDER; return items.map(function(item, idx) { var code = escHtml(item.product_code); var bid = item.id; var orderedAt = (item.ordered_at || '').toString().slice(0, 19); var name = escHtml(item.product_name || '-'); var cells = { index: '' + (startIdx + idx + 1) + '', product_type: '' + escHtml(item.product_type || '-') + '', product_code: '' + code + '', product_name: '' + name + '', ordered_at: '' + (orderedAt || '-') + '', unit_price: '' + ((item['单价'] === null || item['单价'] === undefined || item['单价'] === '' || Number(item['单价']) === 0) ? '-' : Number(item['单价']).toFixed(2)) + '', created_by: '' + escHtml(item.created_by || '-') + '', production_order_no: '' + escHtml(item.production_order_no || '-') + '', suggested_qty: '' + (item.suggested_qty || 0) + '', formal_qty: '' + (item.formal_order_qty || 0) + '', product_grade: '' + escHtml(item.product_grade || '-') + '', supplier_name: '' + escHtml(item.supplier_name || '-') + '', remark: '' + escHtml(item.remark || '-') + '', lead_time: '' + (item.lead_time || 0) + '', logistics_time: '' + (item.logistics_time || 0) + '', total_support_days: '' + (item.total_support_days || 0) + '', procurement_follower: '' + renderNameBadges(item.procurement_follower) + '', procurement_planner: '' + renderNameBadges(item.procurement_planner, true) + '', platform: '' + escHtml(item.platform || '-') + '', operator: '' + renderNameBadges(item.operator) + '', audit_price: '' + (item.audit_price || 0) + '', created_at: '' + ((item.created_at || '').toString().slice(0, 19) || '-') + '' }; var html = ''; order.forEach(function(field) { if (field === 'action') return; if (field !== 'index' && visible[field] === false) return; if (cells[field]) html += cells[field]; }); html += !showActions ? '' + (item.status === 'rolled_back' ? '已回退' : '正常') + '' : '
'; return html + ''; }).join(''); } function _applyBackupSearch(items) { var kw = (_bkEl('Search')?.value || '').trim().toLowerCase(); var orderDate = (_bkEl('OrderDate')?.value || '').trim(); var orderer = (_bkEl('Orderer')?.value || '').trim().toLowerCase(); var planner = (_bkEl('Planner')?.value || '').trim().toLowerCase(); if (!kw && !orderDate && !orderer && !planner) return items; return items.filter(function(it) { if (kw) { var name = (it.product_name || '').toLowerCase(); var code = (it.product_code || '').toLowerCase(); if (!name.includes(kw) && !code.includes(kw)) return false; } if (orderDate && String(it.ordered_at || '').slice(0, 10) !== orderDate) return false; if (orderer && !(it.created_by || '').toLowerCase().includes(orderer)) return false; if (planner && !(it.procurement_planner || '').toLowerCase().includes(planner)) return false; return true; }); } function _applyBackupHeaderFilter(items) { var keys = Object.keys(_bkS().filter); if (keys.length === 0) return items; return items.filter(function(item) { return keys.every(function(f) { var fp = _bkS().filter[f]; if (!fp) return true; var v = _backupGetFilterValue(item, f); if (fp.type === 'search') { return v.toLowerCase().indexOf((fp.value || '').toLowerCase()) >= 0; } if (fp.type === 'list') { if (['采购跟单','采购计划','运营'].indexOf(f) >= 0) return _matchesPersonnelFilter(v, fp.values); return fp.values.indexOf(v) >= 0; } if (fp.type === 'num') { var n = parseFloat(v); var tv = parseFloat(fp.val); if (isNaN(n) || isNaN(tv)) return false; switch (fp.cond) { case 'eq': return n === tv; case 'neq': return n !== tv; case 'gt': return n > tv; case 'gte': return n >= tv; case 'lt': return n < tv; case 'lte': return n <= tv; } } return true; }); }); } function _renderFilteredBackup() { var st = _bkS(); var activeTable = document.getElementById(backupActiveTab + 'Table'); if (activeTable) { var thead = activeTable.querySelector('thead'); if (thead) thead.innerHTML = '' + _backupHeaderHtml(backupActiveTab === 'order') + ''; } var items = st.allItems.slice(); // 从原始全量数据开始 items = _applyBackupSearch(items); // 先应用搜索栏筛选 items = _applyBackupHeaderFilter(items); // 再应用表头筛选 st.items = items; var countEl = _bkEl('Count'); if (countEl) countEl.textContent = '共 ' + items.length + ' / ' + st.allItems.length + ' 条'; var tbody = _bkEl('Tbody'); if (!tbody) return; if (items.length === 0) { tbody.innerHTML = '暂无匹配数据'; restoreAdjustableTableColumnWidths(backupActiveTab + 'Table', 'purchase_ready'); applyCollapsibleActionState('purchase_ready'); applyCollapsibleBackupStatusState(); return; } var total = items.length; var totalPages = Math.ceil(total / BACKUP_PAGE_SIZE); if (st.page > totalPages) st.page = totalPages; if (st.page < 1) st.page = 1; var startIdx = (st.page - 1) * BACKUP_PAGE_SIZE; var pageItems = items.slice(startIdx, startIdx + BACKUP_PAGE_SIZE); var showActions = backupActiveTab === 'order'; // 订单管理保留操作列,订单备份管理不显示 tbody.innerHTML = _backupRowsHtml(pageItems, startIdx, showActions); // 字段显隐/顺序变化后重建 colgroup,清除旧字段的宽度轨道,避免右侧残留空白。 restoreAdjustableTableColumnWidths(backupActiveTab + 'Table', 'purchase_ready'); syncAdjustableTableWidth(document.getElementById(backupActiveTab + 'Table')); applyCollapsibleActionState('purchase_ready'); applyCollapsibleBackupStatusState(); var pel = _bkEl('Pagination'); if (pel && typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, st.page, total, BACKUP_PAGE_SIZE, 'loadBackupListPage', 'BACKUP_PAGE_SIZE'); } } // ─── 下单备份表头筛选 ─── function _backupGetColType(field) { for (var i = 0; i < BACKUP_FILTER_COLS.length; i++) { if (BACKUP_FILTER_COLS[i].field === field) return BACKUP_FILTER_COLS[i].type; } return 'text'; } function openBackupFilterMenu(field, event) { event = event || window.event; event.stopPropagation(); if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } backupFilterField = field; var th = event.currentTarget; while (th && th.tagName !== 'TH') th = th.parentNode; var rect = th.getBoundingClientRect(); var label = field; var colType = _backupGetColType(field); var hasSearch = !!(_bkEl('Search')?.value.trim() || _bkEl('OrderDate')?.value || _bkEl('Orderer')?.value || _bkEl('Planner')?.value); var linkedBackupFilters = Object.assign({}, _bkS().filter); delete linkedBackupFilters[field]; var hasHeaderFilter = Object.keys(linkedBackupFilters).length > 0; var savedBackupFilters = _bkS().filter; _bkS().filter = linkedBackupFilters; var filtered = _applyBackupHeaderFilter(_applyBackupSearch(_bkAllItems())); _bkS().filter = savedBackupFilters; if (colType === 'number') { _buildBackupNumericFilter(field, label, rect, filtered); } else if (hasSearch || hasHeaderFilter) { // 有筛选条件:从搜索过滤后的数据提取唯一值(联动筛选) _buildBackupFilterShell(field, label, rect); var vals = _personFilterValuesFromItems(filtered, field, _backupGetFilterValue); if (vals.length === 0) { var seen = {}; filtered.forEach(function(item) { var v = _backupGetFilterValue(item, field); if (v && !seen[v]) { seen[v] = 1; vals.push(v); } }); vals.sort(); } _populateBackupFilterList(field, vals); } else { var cacheKey = 'backup:' + field; var cached = window._orderColumnCache && window._orderColumnCache[cacheKey]; if (cached) { _buildBackupListFilter(field, label, rect, cached); } else { _buildBackupFilterShell(field, label, rect); var vals = []; var seen = {}; // 从搜索过滤后的数据提取唯一值(联动搜索条件) var searchFiltered = _applyBackupSearch(_bkAllItems()); searchFiltered.forEach(function(item) { var v = _backupGetFilterValue(item, field); if (v && !seen[v]) { seen[v] = 1; vals.push(v); } }); vals.sort(); window._orderColumnCache = window._orderColumnCache || {}; window._orderColumnCache[cacheKey] = vals; _populateBackupFilterList(field, vals); } } if (backupFilterMenu) { var handler = function(e) { if (backupFilterMenu && !backupFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) { backupFilterMenu.remove(); backupFilterMenu = null; document.removeEventListener('click', handler); } }; setTimeout(function() { document.addEventListener('click', handler); }, 10); setTimeout(function() { var inp = document.getElementById('backupFilterSearch'); if (inp) inp.focus(); }, 150); } } function _buildBackupFilterShell(field, label, rect) { var menu = document.createElement('div'); menu.id = 'backupFilterMenu'; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
加载中...
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); backupFilterMenu = menu; } function _buildBackupListFilter(field, label, rect, values) { values = _normalizePersonnelFilterValues(field, values); var menu = document.createElement('div'); menu.id = 'backupFilterMenu'; var isSorted = _bkS().sortField === field; var currentOrder = isSorted ? (_bkS().sortAsc ? 'asc' : 'desc') : ''; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var sel = _bkS().filter[field] || {}; allChecked = true; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var checked = sel.type === 'list' && sel.values ? (sel.values.indexOf(v) >= 0 ? 'checked' : '') : 'checked'; var displayValue = v === '__EMPTY__' ? '空' : v; html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); backupFilterMenu = menu; } function _buildBackupNumericFilter(field, label, rect, sourceItems) { var isSorted = _bkS().sortField === field; var currentOrder = isSorted ? (_bkS().sortAsc ? 'asc' : 'desc') : ''; var menu = document.createElement('div'); menu.id = 'backupFilterMenu'; var current = _bkS().filter[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; var html = '
' + escHtml(label) + '
'; html += '
'; html += '内容筛选'; html += '条件筛选
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += ''; html += '
加载中...
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; menu.innerHTML = html; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:260px;max-width:320px;font-size:13px;display:flex;flex-direction:column;max-height:450px'; document.body.appendChild(menu); backupFilterMenu = menu; // 基于其他筛选条件后的数据提取唯一数值,排除当前列自身筛选 var numValues = []; var seen = {}; (sourceItems || _applyBackupSearch(_bkAllItems())).forEach(function(item) { var v = _backupGetFilterValue(item, field); if (v !== '' && v !== '-' && v !== '__EMPTY__' && !seen[v]) { seen[v] = 1; numValues.push(v); } }); numValues.sort(function(a,b) { return parseFloat(a) - parseFloat(b); }); _backupRenderNumList(numValues); // 回显已保存的数字筛选状态 if (current && current.type === 'num') { var condSel = document.getElementById('backupNumCond'); var valInput = document.getElementById('backupNumVal'); if (condSel) condSel.value = current.cond || ''; if (valInput) valInput.value = current.val || ''; backupNumSwitchTab('cond'); } else if (current && current.type === 'list' && current.values) { var cbs = document.querySelectorAll('#backupNumList .backup-num-cb'); cbs.forEach(function(cb) { cb.checked = current.values.indexOf(cb.value) >= 0; }); } setTimeout(function() { var inp = document.getElementById('backupNumVal'); if (inp) inp.focus(); }, 100); } function _populateBackupFilterList(field, values) { values = _normalizePersonnelFilterValues(field, values); var listEl = document.getElementById('backupFilterList'); if (!listEl) return; var sel = _bkS().filter[field] || {}; var html = '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = sel.type === 'list' && sel.values ? (sel.values.indexOf(v) >= 0 ? 'checked' : '') : 'checked'; var displayValue = v === '__EMPTY__' ? '空' : v; html += ''; }); listEl.innerHTML = html; } function backupFilterSearch(q) { var items = document.querySelectorAll('#backupFilterList label'); q = (q || '').toLowerCase(); items.forEach(function(item) { item.style.display = item.textContent.toLowerCase().indexOf(q) >= 0 ? 'flex' : 'none'; }); } function backupFilterToggleAll(checked) { document.querySelectorAll('#backupFilterList .backup-filter-cb').forEach(function(cb) { cb.checked = checked; }); } function backupFilterCbChange() { var all = document.querySelectorAll('#backupFilterList .backup-filter-cb'); var checked = document.querySelectorAll('#backupFilterList .backup-filter-cb:checked'); var sa = document.getElementById('backupFilterSelectAll'); if (sa) sa.checked = checked.length === all.length; } function applyBackupFilter() { var field = backupFilterField; var searchInput = document.getElementById('backupFilterSearch'); var searchText = searchInput ? searchInput.value.trim() : ''; if (searchText) { _bkS().filter[field] = {type: 'search', value: searchText}; } else { var cbs = document.querySelectorAll('#backupFilterList .backup-filter-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); var allCbs = document.querySelectorAll('#backupFilterList .backup-filter-cb'); if (checked.length === 0 || checked.length === allCbs.length) { delete _bkS().filter[field]; } else { _bkS().filter[field] = {type: 'list', values: checked}; } } if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } function applyBackupNumFilter() { var field = backupFilterField; var condPanel = document.getElementById('backupNumCondPanel'); var isCondTab = condPanel && condPanel.style.display !== 'none'; if (isCondTab) { var cond = document.getElementById('backupNumCond')?.value || ''; var val = document.getElementById('backupNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } _bkS().filter[field] = {type: 'num', cond: cond, val: val}; } else { var cbs = document.querySelectorAll('#backupNumList .backup-num-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (checked.length === 0 || checked.length === document.querySelectorAll('#backupNumList .backup-num-cb').length) { delete _bkS().filter[field]; } else { _bkS().filter[field] = {type: 'list', values: checked}; } } if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } // ─── 下单备份数字列双模式筛选辅助函数 ─── function backupNumSwitchTab(tab) { var contentPanel = document.getElementById('backupNumContentPanel'); var condPanel = document.getElementById('backupNumCondPanel'); var tabContent = document.getElementById('backupTabContent'); var tabCond = document.getElementById('backupTabCond'); var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; if (tab === 'content') { if (contentPanel) contentPanel.style.display = ''; if (condPanel) condPanel.style.display = 'none'; if (tabContent) tabContent.style.cssText = tabAct; if (tabCond) tabCond.style.cssText = tabDef; } else { if (contentPanel) contentPanel.style.display = 'none'; if (condPanel) condPanel.style.display = ''; if (tabContent) tabContent.style.cssText = tabDef; if (tabCond) tabCond.style.cssText = tabAct; } } function backupNumSearchList(q) { var items = document.querySelectorAll('#backupNumList label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function backupNumToggleAll(checked) { document.querySelectorAll('#backupNumList .backup-num-cb').forEach(function(cb) { cb.checked = checked; }); } function backupNumUpdateAll() { var all = document.querySelectorAll('#backupNumList .backup-num-cb'); var checked = document.querySelectorAll('#backupNumList .backup-num-cb:checked'); var cb = document.getElementById('backupNumSelAll'); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } function _backupRenderNumList(vals) { var list = document.getElementById('backupNumList'); if (!list) return; if (vals.length === 0) { list.innerHTML = '
暂无数据
'; return; } var html = ''; vals.forEach(function(v) { html += ''; }); list.innerHTML = html; var savedListFilter = _bkS().filter[backupFilterField]; if (savedListFilter && savedListFilter.type === 'list' && savedListFilter.values) { list.querySelectorAll('.backup-num-cb').forEach(function(cb) { cb.checked = savedListFilter.values.indexOf(cb.value) >= 0; }); backupNumUpdateAll(); } } function backupNumFilterApply() { applyBackupNumFilter(); } function clearBackupHeaderFilter() { delete _bkS().filter[backupFilterField]; if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } function _syncBackupFilterIcons() { var icons = document.querySelectorAll('#' + backupActiveTab + 'Table .bk-filt-icon'); icons.forEach(function(icon) { var field = icon.parentNode.textContent.trim().replace(/▾/, '').trim(); var hasFilter = _bkS().filter[field]; icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = hasFilter ? 'bold' : 'normal'; }); } // 下单备份排序 function backupFilterSort(field, order) { if (order === 'asc') { _bkS().sortField = field; _bkS().sortAsc = true; } else if (order === 'desc') { _bkS().sortField = field; _bkS().sortAsc = false; } else { _bkS().sortField = ''; } if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } function loadBackupListPage(page) { _bkS().page = page; _renderFilteredBackup(); } // ─── 下单备份管理:详情+删除 ─── window._backupItems = []; window.viewBackupDetail = function(bid, detailItem) { if (!detailItem) { fetch('/api/purchase/backup/detail?id=' + encodeURIComponent(bid)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data) { alert(res.msg || '未找到数据'); return; } viewBackupDetail(bid, res.data); }) .catch(function() { alert('详情加载失败'); }); return; } var item = null; _bkItems().forEach(function(it) { if (it.id === bid || String(it.id) === String(bid)) item = it; }); item = detailItem || item; if (!item) { alert('未找到数据'); return; } var code = item.product_code; // 解析包材数据 var bomList = []; try { var raw = item.bom_items; if (raw) { bomList = typeof raw === 'string' ? JSON.parse(raw) : (Array.isArray(raw) ? raw : []); } } catch(e) { bomList = []; } bomList = bomList.filter(function(b) { return b && b.material_code && String(b.material_code).indexOf('成品_') !== 0; }); var html = '
'; html += '

📋 下单详情 - ' + escHtml(code) + '

'; // ─── 板块1:成品信息 ─── html += '

📦 成品信息

'; // 第一列字段 var col1Fields = [ ['产品类型', item.product_type], ['货品编号', item.product_code], ['货品名称', item.product_name], ['供应商名称', item.supplier_name], ['采购计划', item.procurement_planner], ['采购跟单', item.procurement_follower], ['成品下单人', item.product_orderer] ]; // 第二列字段 var col2Fields = [ ['建议下单数量', item.suggested_qty], ['实际下单数量', item.formal_order_qty || item.actual_order_qty || 0], ['单价', (item['单价'] === null || item['单价'] === undefined || item['单价'] === '' || Number(item['单价']) === 0) ? '-' : Number(item['单价']).toFixed(2)], ['下单人', item.created_by], ['下单时间', (item.ordered_at || '').toString().slice(0, 19)], ['下单备注', item.remark], ['生产单号', item.production_order_no || ''] ]; html += '
'; html += ''; var detailRows = Math.max(col1Fields.length, col2Fields.length); for (var g = 0; g < detailRows; g++) { var f1 = col1Fields[g]; var f2 = col2Fields[g]; html += ''; // 第一列 html += ''; html += ''; // 第二列 var isProdNo = f2 && f2[0] === '生产单号'; html += ''; if (isProdNo) { html += ''; } else { html += ''; } html += ''; } html += '
' + (f1 ? f1[0] : '') + '' + (f1 ? escHtml(String(f1[1] || '-')) : '') + '' + (f2 ? f2[0] : '') + '' + (f2 ? escHtml(String(f2[1] || '-')) : '') + '
'; // ─── 板块2:包材信息 ─── html += '

📦 包材信息

'; if (bomList.length > 0) { html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; bomList.forEach(function(b) { var qty = parseInt(b.actual_quantity) || 0; var unitPrice = parseFloat(b.unit_price) || 0; var supplierShortName = b.supplier_short_name || '-'; var amount = qty * unitPrice; var auditState = b.price_audit_status || '未审核'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
物料编号物料名称下单数量包材下单人供应商单价金额备注更换稿件采购单号审核价格
' + escHtml(b.material_code || '-') + '' + escHtml(b.material_name || '-') + '' + qty + '' + escHtml(b.packaging_orderer || '-') + '' + escHtml(supplierShortName) + '' + unitPrice.toFixed(2) + '' + amount.toFixed(2) + '' + escHtml(b.remark || '-') + '
'; } else { html += '
暂无包材数据
'; } html += '
'; html += '
'; html += '
修改后点击保存按钮可同时保存生产单号和采购单号
'; html += '
'; html += ''; html += '
'; var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = '
' + html + '
'; document.body.appendChild(overlay); overlay.querySelectorAll('.backup-manuscript').forEach(function(sel) { var found = bomList.find(function(b) { return b.material_code === sel.getAttribute('data-matcode'); }); sel.value = found && found.change_manuscript === '是' ? '是' : ''; }); }; window.auditBackupMaterialPrice = function(button, materialCode, price, productCode, backupId) { if (button.textContent.trim() === '已审核') return; showCenteredConfirm('价格审核', '价格 ' + escHtml(price) + ' 是否正确?').then(function(confirmed) { if (!confirmed) return; fetch('/api/purchase/backup/audit_material_price', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, material_code: materialCode, backup_id: backupId}) }).then(function(r){ return r.json(); }).then(function(res) { if (res.code === 0) { button.textContent = '已审核'; button.classList.remove('is-pending'); button.classList.add('is-audited'); // 包材订单管理价格已同步,失效缓存使切回时重新加载 if (window._pageCache) delete window._pageCache['orders_packaging']; } else { showCenteredAlert('审核失败', res.msg || '审核失败'); } }); }); }; // 包材生产单号回车保存 window.backupProdNoKeydown = function(e, input, materialCode, backupId) { if (e.key === 'Enter') { e.preventDefault(); var val = input.value.trim(); // infer product_code from the modal/overlay var overlay = input.closest('.modal-overlay'); if (!overlay) return; var codeEl = overlay.querySelector('h3'); if (!codeEl) return; var match = codeEl.textContent.match(/\[(.+?)\]/); var productCode = match ? match[1] : ''; if (!productCode) return; fetch('/api/purchase/backup/update_mat_prod_no', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, material_code: materialCode, production_order_no: val, backup_id: backupId}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) showCenteredAlert('保存失败', res.msg || '保存失败'); }); } }; // 保存成品生产单号 window.saveBackupProdNo = function(bid, code) { var inp = document.getElementById('backupProdNo_' + code); if (!inp) return; var val = inp.value.trim(); fetch('/api/purchase/backup/update_prod_no', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, production_order_no: val, backup_id: bid}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { inp.blur(); // 更新缓存数据 _bkItems().forEach(function(it) { if (it.product_code === code) it.production_order_no = val; }); _refreshLinkedOrderLists(); // 同步更新表格中的生产单号列 var tbody = _bkEl('Tbody'); if (tbody) { tbody.querySelectorAll('tr').forEach(function(tr) { var codeEl = tr.querySelector('code'); if (codeEl && codeEl.textContent.trim() === code) { var tds = tr.querySelectorAll('td'); // 生产单号是第7列(序号0,产品类型1,货品编号2,货品名称3,下单时间4,下单人5,生产单号6) if (tds.length > 6) { tds[6].textContent = val || '-'; } } }); } showCenteredAlert('保存成功', '✅ 生产单号已保存'); } else { inp.style.borderColor = '#f5222d'; setTimeout(function(){ inp.style.borderColor = ''; }, 1500); } }); }; // 保存包材采购单号 window.saveBackupMatPurchaseNo = function(input) { if (!input) return; var productCode = input.getAttribute('data-pcode'); var materialCode = input.getAttribute('data-matcode'); var backupId = input.getAttribute('data-bid'); if (!productCode || !materialCode) return; var val = input.value.trim(); fetch('/api/purchase/backup/update_mat_purchase_no', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, material_code: materialCode, purchase_order_no: val, backup_id: backupId}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { input.blur(); // 同步更新前端缓存 _bkItems().forEach(function(it) { if (it.product_code === productCode && it.bom_items) { try { var bom = typeof it.bom_items === 'string' ? JSON.parse(it.bom_items) : it.bom_items; if (Array.isArray(bom)) { bom.forEach(function(b) { if (b.material_code === materialCode) { b.purchase_order_no = val; } }); it.bom_items = JSON.stringify(bom); } } catch(e) {} } }); // 后台刷新列表数据 if (typeof loadBackupList === 'function') { loadBackupList(); } // 包材订单管理数据已同步(旺店通单号等),失效缓存使切回时重新加载 if (window._pageCache) delete window._pageCache['orders_packaging']; showCenteredAlert('保存成功', '✅ 采购单号已保存'); } else { input.style.borderColor = '#f5222d'; setTimeout(function(){ input.style.borderColor = ''; }, 1500); } }); }; // 保存生产单号+采购单号(统一保存按钮) window.saveBackupAll = function(bid, code, saveBtn) { if (!code) return; if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '⏳ 保存中...'; } // 1. 收集生产单号 var prodNoInp = document.getElementById('backupProdNo_' + code); // 2. 收集所有包材采购单号(含空值,用于清空已有数据) var orderInputs = []; var overlay = saveBtn ? saveBtn.closest('.modal-overlay') : null; if (overlay) { overlay.querySelectorAll('.backup-purchase-no').forEach(function(inp) { var pc = inp.getAttribute('data-pcode'); var mc = inp.getAttribute('data-matcode'); if (pc && mc) { var manuscript = overlay.querySelector('.backup-manuscript[data-matcode=\"' + mc + '\"]'); orderInputs.push({input: inp, productCode: pc, materialCode: mc, val: inp.value.trim(), manuscript: manuscript ? manuscript.value : ''}); } }); } // 3. 依次保存(串行执行,避免并发读改写互相覆盖) var seq = Promise.resolve(); var results = []; // 保存生产单号 if (prodNoInp) { var prodVal = prodNoInp.value.trim(); seq = seq.then(function() { return fetch('/api/purchase/backup/update_prod_no', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, production_order_no: prodVal, backup_id: bid}) }).then(function(r){return r.json()}).then(function(res){ results.push(res); return res; }); }); } // 保存采购单号 orderInputs.forEach(function(item) { seq = seq.then(function() { return fetch('/api/purchase/backup/update_mat_purchase_no', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: item.productCode, material_code: item.materialCode, purchase_order_no: item.val, change_manuscript: item.manuscript, backup_id: bid}) }).then(function(r){return r.json()}).then(function(res){ results.push(res); return res; }); }); }); // 4. 全部完成后刷新缓存并提示 seq.then(function() { if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; } var hasError = results.some(function(r) { return r.code !== 0; }); if (hasError) { showCenteredAlert('保存失败', '❌ 部分数据保存失败,请重试'); } else { // ---- 同步更新前端缓存,确保关闭后再打开看到新数据 ---- var newProdNo = prodNoInp ? prodNoInp.value.trim() : ''; _bkItems().forEach(function(it) { if (it.product_code === code) { // 更新生产单号 it.production_order_no = newProdNo; // 更新包材采购单号(bom_items JSON内) if (it.bom_items) { try { var bom = typeof it.bom_items === 'string' ? JSON.parse(it.bom_items) : it.bom_items; if (Array.isArray(bom)) { orderInputs.forEach(function(oi) { bom.forEach(function(b) { if (b.material_code === oi.materialCode) { b.purchase_order_no = oi.val; } }); }); it.bom_items = JSON.stringify(bom); } } catch(e) {} } } }); // 同步更新表格中的生产单号列 var tbody = _bkEl('Tbody'); if (tbody) { tbody.querySelectorAll('tr').forEach(function(tr) { var codeEl = tr.querySelector('code'); if (codeEl && codeEl.textContent.trim() === code) { var tds = tr.querySelectorAll('td'); if (tds.length > 6) { tds[6].textContent = newProdNo || '-'; } } }); } // 后台刷新列表数据(用户下次查看时拉取最新) if (typeof loadBackupList === 'function') { loadBackupList(); } _refreshLinkedOrderLists(); // 包材订单管理数据已同步(旺店通单号等),保留当前筛选并重新拉取数据 var detailOverlay = saveBtn && saveBtn.closest('.modal-overlay'); showCenteredAlert('保存成功', '✅ 生产单号、采购单号已保存'); if (detailOverlay) detailOverlay.remove(); } }).catch(function() { if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; } showCenteredAlert('保存失败', '❌ 网络错误,请重试'); }); }; // 下单回退:确认弹窗 + 调用后端(按下单时间定位,仅回退当前批次) window.showBackupRollbackConfirm = function(bid, orderedAt) { if (!window._hasPagePerm('purchase_ready', 'rollback')) { showCenteredAlert('权限不足', '当前账号没有回退订单权限'); return; } // 根据备份行 id 找到商品编码 var code = ''; _bkItems().forEach(function(it) { if (it.id === bid || String(it.id) === String(bid)) code = it.product_code; }); if (!code) { showCenteredAlert('提示', '未找到该备份记录'); return; } showCenteredPrompt('回退原因', '请输入回退原因(至少2个字):').then(function(reason) { if (reason === null) return; reason = (reason || '').trim(); if (reason.length < 2) { showCenteredAlert('提示', '请填写至少2个字的回退原因'); return; } showCenteredConfirm('回退确认', '确认将 [' + escHtml(code) + '] 回退到待下单列表?
' + (orderedAt ? '下单时间:' + escHtml(orderedAt) + '
' : '') + '回退原因:' + escHtml(reason) + '
' + '系统将按该条下单时间定位订单,仅回退此批次,其他历史批次不受影响。' ).then(function(confirmed) { if (!confirmed) return; fetch('/api/purchase/rollback_order', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, rollback_reason: reason, backup_id: bid, ordered_at: orderedAt}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { loadBackupList(); // 回退接口会新建一条 pending 记录;如果待下单页面已挂载,立即同步刷新, // 避免用户切换页面前看到旧列表。 if (document.getElementById('pendingTbody') && typeof loadPendingList === 'function') { loadPendingList(); } // 待下单页面未挂载时,清掉旧缓存;下次切入待下单管理会重新请求接口。 if (window._pageDataCache) window._pageDataCache['purchase'] = { _ts: Date.now() }; } else { alert('回退失败: ' + (res.msg || '')); } }); }); }); }; // ─── 采购订单-订单管理(国内/海外/包材)─── // ──────────── 国内订单管理 ──────────── var DOMESTIC_PAGE_SIZE = 100; var domesticOrderPage = 1; var domesticSearch = ''; var domesticFilterStatus = ''; var domesticFilterSupplier = ''; var domesticFilterOrderer = ''; var domesticOrderFilter = {}; // 列筛选状态 {field:{type:'list'|'num', ...}} var domesticOrderFilterActive = {}; // 高亮标记 var domesticOrderFilterMenu = null; // 当前打开的筛选弹窗 var domesticOrderFilterField = ''; // 当前筛选字段 // ─── 列筛选值缓存(避免每次打开筛选都查数据库)─── window._orderColumnCache = window._orderColumnCache || {}; window._planColumnCache = window._planColumnCache || {}; // ─── 后台预拉取订单表所有列的筛选值 ─── function prefetchOrderColumnValues(type) { var cols = _getOrderColumns(type); var apiMap = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'}; cols.forEach(function(col) { var cacheKey = type + ':' + col.field; if (window._orderColumnCache[cacheKey]) return; // 货品类型使用专门的低基数查询;点击列筛选时按需加载,避免页面初始化时重复扫描订单表。 if (col.field === '货品类型') return; var colType = _getOrderColType(type, col.field); if (colType === 'number') return; fetch(apiMap[type] + '?field=' + encodeURIComponent(col.field)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data && res.data.length > 0) { window._orderColumnCache[cacheKey] = _normalizeOrderPersonnelFilterValues(col.field, res.data); } }) .catch(function() { /* 静默失败 */ }); }); } // ─── 后台预拉取计划页所有列的筛选值 ─── function prefetchPlanColumnValues() { var view = planActiveView || 'default'; // 品牌列 var brandKey = 'plan:brand:' + view; if (!window._planColumnCache[brandKey]) { var brandUrl = '/api/planning/brands' + (view !== 'default' ? '?view=' + encodeURIComponent(view) : ''); fetch(brandUrl) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { window._planColumnCache[brandKey] = res.data; } }) .catch(function() { /* 静默失败 */ }); } // 非预定义非品牌非数字的非排除列 var productGradeFields = ['产品分级']; PLAN_COLUMNS.forEach(function(c) { if (PLAN_FILTER_OPTIONS[c.field]) return; // 预定义,skip if (c.field === 'brand') return; // 品牌已处理 if (SORTABLE_FIELDS[c.field] && !productGradeFields.includes(c.field)) return; // 数字列 if (PLAN_FILTER_EXCLUDE_FIELDS[c.field]) return; // 排除字段 var key = 'plan:field:' + c.field + ':' + view; if (window._planColumnCache[key]) return; var url = '/api/planning/column-values?field=' + encodeURIComponent(c.field) + (view !== 'default' ? '&view=' + encodeURIComponent(view) : ''); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { window._planColumnCache[key] = res.data; } }) .catch(function() { /* 静默失败 */ }); }); } // ─── 清除筛选值缓存(数据变更后调用)─── function invalidateOrderColumnCache(type) { if (!type) { window._orderColumnCache = {}; window._planColumnCache = {}; return; } var keys = Object.keys(window._orderColumnCache); keys.forEach(function(k) { if (k.indexOf(type + ':') === 0) delete window._orderColumnCache[k]; }); } var domesticFrozen = localStorage.getItem('domesticFrozen') !== 'false'; var domesticFollowers = []; // 采购跟单人员列表 var _orderFollowersPromise = null; function loadOrderFollowers() { if (_orderFollowersPromise) return _orderFollowersPromise; _orderFollowersPromise = fetch('/api/users/followers').then(function(r){return r.json()}).then(function(res){ if (res.code === 0) domesticFollowers = res.data || []; return domesticFollowers; }); return _orderFollowersPromise; } // 冻结列索引列表(默认冻结前6列),存JSON数组 // 冻结列默认值 var domesticFrozenFields = [0,1,2]; var domesticFrozen = true; // ─── SKU详情弹窗(点击编码/SKU) ─── window.showSkuInfoModal = function(el, title) { var code = el.textContent.trim(); if (!code) return; // 显示加载中的弹窗 var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = ''; document.body.appendChild(overlay); // 通过API获取商品详情 fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(code)) .then(function(r) { return r.json(); }) .then(function(res) { if (!res.data) { overlay.innerHTML = ''; return; } var d = res.data && res.data.product || {}; var fields = []; if (d.product_code) fields.push(['货品编号', d.product_code]); if (d.product_name) fields.push(['货品名称', d.product_name]); if (d.brand) fields.push(['品牌', d.brand]); if (d.product_type) fields.push(['产品类型', d.product_type]); if (d.product_grade) fields.push(['产品分级', d.product_grade]); if (d.cost_price) fields.push(['成本价', d.cost_price]); if (d.supplier_name) fields.push(['供应商', d.supplier_name]); if (d.procurement_follower) fields.push(['采购跟单', d.procurement_follower]); if (d.procurement_planner) fields.push(['采购计划', d.procurement_planner]); if (d.developer) fields.push(['开发专员', d.developer]); if (d.operator) fields.push(['运营专员', d.operator]); if (d.platform) fields.push(['平台', d.platform]); if (d.lead_time) fields.push(['生产货期', d.lead_time + '天']); if (d.logistics_time) fields.push(['物流时间', d.logistics_time + '天']); if (d.min_order_qty) fields.push(['最低起订量', d.min_order_qty]); if (d.created_at) fields.push(['创建时间', d.created_at]); var bomCount = (res.data.bom_materials || []).length; if (bomCount > 0) fields.push(['关联包材', bomCount + ' 种']); // 调试:记录API返回 if (typeof console !== 'undefined') { var _debugTransit = res.data.orders_in_transit || []; console.log('[SKU] ' + code + ' 在途订单: ' + _debugTransit.length + ' 条'); } // 构建弹窗HTML var html = ''; overlay.innerHTML = html; }) .catch(function() { overlay.innerHTML = ''; }); } // 详情弹窗下拉框按选项着色:是否更换稿件为“是”时红色,为“否”时黑色。 window.DETAIL_SELECT_OPTION_COLORS = window.DETAIL_SELECT_OPTION_COLORS || { '有无新稿件': {'是': '#f5222d', '否': '#333333'} }; window.applyDetailSelectOptionColor = function(sel) { if (!sel) return; var map = (window.DETAIL_SELECT_OPTION_COLORS || {})[sel.getAttribute('data-field')] || null; if (!map) { sel.style.color = ''; return; } var color = map[sel.value]; sel.style.color = color ? color : ''; }; // ─── 订单详情弹窗(通用) ─── function showOrderDetail(encodedStr, title) { // 兼容两种调用方式:URI编码的行数据 或 直接行对象 var row; if (typeof encodedStr === 'string') { try { row = JSON.parse(decodeURIComponent(encodedStr)); } catch(e) { row = JSON.parse(encodedStr); } } else { row = encodedStr; } var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; // 字段配置:label, dbField, editable var col1, col2, apiUrl, refreshFn, orderType; var isFinishedDetail = (title && title.indexOf('成品提货') !== -1) ? true : false; if (title && title.indexOf('海外') !== -1) { orderType = 'overseas'; apiUrl = '/api/overseas/orders/update'; refreshFn = 'overseasLoadOrders'; col1 = [ {label:'产品类型', field:'product_type', editable:false}, {label:'货品编号', field:'产品编码', editable:false}, {label:'供应商名称', field:'供应商', editable:true, type:'supplier'}, {label:'仓库', field:'仓库', editable:true, type:'warehouse'}, {label:'计划下单日期', field:'计划下单时间', editable:false}, {label:'单价', field:'单价', editable:true}, {label:'跟单小组', field:'采购员', editable:true}, {label:'下单人', field:'下单人', editable:true, type:'ordererSelect', groupField:'采购员'}, {label:'金蝶是否开单', field:'是否已做采购单', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, {label:'旺店通是否已录入', field:'是否已录入到旺店通', editable:true, type:'select', options:['是','否'], defaultValue:'否'} ]; col2 = [ {label:'预计到货日期', field:'预计到货日期', editable:true, type:'date'}, {label:'货品名称', field:'货品名称', editable:false}, {label:'跟单下单日期', field:'日期', editable:true, type:'date'}, {label:'实际下单数量', field:'数量', editable:true}, {label:'采购计划', field:'procurement_planner', editable:false}, {label:'金蝶是否入库', field:'是否做入库', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, {label:'旺店通单号', field:'旺店通生产单号', editable:true}, {label:'订单所属平台', field:'platform', editable:true}, {label:'状态', field:'状态', editable:true, type:'select', options:['未完结','已完结','分批送货','已取消'], defaultValue:'未完结'} ]; } else if (title && title.indexOf('包材') !== -1) { orderType = 'packaging'; apiUrl = '/api/packaging/orders/update'; refreshFn = 'packagingLoadOrders'; col1 = [ {label:'货品编号', field:'编码', editable:false}, {label:'新旧品', field:'新旧品', editable:true, type:'select', options:['新品','旧品'], includeEmpty:true, emptyLabel:'无'}, {label:'供应商名称', field:'供应商', editable:true, type:'supplier'}, {label:'跟单小组', field:'采购下单人', editable:true}, {label:'下单人', field:'下单人', editable:true, type:'ordererSelect', groupField:'采购下单人'}, {label:'采购计划', field:'采购计划', editable:false}, {label:'单价', field:'单价', editable:true}, {label:'下单备注', field:'下单备注', editable:true}, {label:'金蝶是否开单', field:'是否已做采购单', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, {label:'旺店通是否已入库', field:'是否已录入旺店通', editable:true, type:'select', options:['是','否'], defaultValue:'否'} ]; col2 = [ {label:'货品名称', field:'名称', editable:false}, {label:'到货仓库', field:'仓库到货', editable:true, type:'supplier'}, {label:'实际下单数量', field:'数量', editable:true}, {label:'计划下单日期', field:'计划下单时间', editable:false}, {label:'跟单下单日期', field:'日期', editable:true, type:'date'}, {label:'是否更换稿件', field:'有无新稿件', editable:true, type:'select', options:['是','否']}, {label:'旺店通单号', field:'旺店通编号', editable:false}, {label:'金蝶是否入库', field:'是否已做入库', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, {label:'状态', field:'状态', editable:true, type:'select', options:['未完结','已完结','分批送货','已取消'], defaultValue:'未完结'} ]; } else if (title && title.indexOf('成品提货') !== -1) { orderType = 'finished'; apiUrl = '/api/finished/orders/update'; refreshFn = 'finishedLoadOrders'; col1 = [ {label:'货品编号', field:'货品编号', editable:false}, {label:'供应商名称', field:'供应商', editable:false}, {label:'跟单小组', field:'采购跟单', editable:false}, {label:'可用库存', field:'可用库存', editable:false} ]; col2 = [ {label:'货品名称', field:'货品名称', editable:false}, {label:'采购计划', field:'采购计划', editable:false}, {label:'昨日销量', field:'昨日销量', editable:false}, {label:'支撑天数', field:'支撑天数', editable:false} ]; } else { orderType = 'domestic'; apiUrl = '/api/domestic/orders/update'; refreshFn = 'domesticLoadOrders'; col1 = [ {label:'产品类型', field:'product_type', editable:false}, {label:'货品编号', field:'货品编号', editable:false}, {label:'供应商名称', field:'供货商', editable:true, type:'supplier'}, {label:'单价', field:'单价', editable:true}, {label:'仓库', field:'仓库', editable:true, type:'warehouse'}, {label:'跟单小组', field:'采购员', editable:true}, {label:'下单人', field:'下单人', editable:true, type:'ordererSelect', groupField:'采购员'}, {label:'金蝶是否开单', field:'金蝶采购单', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, {label:'旺店通是否已录入', field:'是否已录入旺店通', editable:true, type:'select', options:['是','否'], defaultValue:'否'} ]; col2 = [ {label:'预计到货日期', field:'预计到货日期', editable:true, type:'date'}, {label:'货品名称', field:'货品名称', editable:false}, {label:'新旧品', field:'新旧品', editable:true, type:'select', options:['新品','旧品'], includeEmpty:true}, {label:'跟单下单日期', field:'日期', editable:true, type:'date'}, {label:'实际下单数量', field:"数量", editable:true}, {label:'采购计划', field:'procurement_planner', editable:false}, {label:'金蝶是否入库', field:'金蝶入库', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, {label:'旺店通单号', field:'旺店通生产单号', editable:true}, {label:'状态', field:'状态', editable:true, type:'select', options:['未完结','已完结','分批送货','已取消'], defaultValue:'未完结'} ]; } var renderValue = function(cfg, val) { if (val === null || val === undefined) val = ''; var sv = String(val); if (!cfg.editable) { return '' + escHtml(sv || '-') + ''; } var origAttr = ' data-orig="' + escHtml(sv) + '"'; // 下单人:按订单跟单小组过滤人员范围(与表格下单人列一致) if (cfg.type === 'ordererSelect') { var selHtml = renderOrdererSelect(row, cfg.groupField || '采购员'); // 兼容详情弹窗保存逻辑:补充 data-field 和 class selHtml = selHtml.replace('class="orderer-select"', 'class="orderer-select detail-input detail-select"'); if (selHtml.indexOf('data-field=') < 0) { selHtml = selHtml.replace('data-id="' + row.id + '"', 'data-id="' + row.id + '" data-field="' + escHtml(cfg.field) + '"'); } selHtml = selHtml.replace('>', origAttr + '>'); return selHtml; } if (cfg.type === 'select' && cfg.options) { var selectOptions = cfg.options.slice(); if (sv && selectOptions.indexOf(sv) < 0) selectOptions.unshift(sv); if (cfg.defaultValue && (!sv || selectOptions.indexOf(sv) === -1)) { sv = cfg.defaultValue; } var opts = ''; if (cfg.includeEmpty) { opts += ''; } opts += selectOptions.map(function(o) { var sel = (o === sv) ? ' selected' : ''; return ''; }).join(''); var colorMap = (window.DETAIL_SELECT_OPTION_COLORS || {})[cfg.field] || null; var colorStyle = ''; var colorOnChange = ''; if (colorMap) { var curColor = colorMap[sv]; if (curColor) colorStyle = ' style="color:' + curColor + '"'; colorOnChange = ' onchange="applyDetailSelectOptionColor(this)"'; } return ''; } if (cfg.type === 'date') { // 去掉时间部分,只取日期 var dateVal = window._padDateStr(sv.split(' ')[0]); return ''; } if (cfg.type === 'warehouse') { return ''; } if (cfg.type === 'supplier') { return ''; } if (cfg.type === 'pendingQty') { return ''; } return ''; }; var html = '
'; html += '
'; col1.forEach(function(cfg) { var val = row[cfg.field]; html += '
' + escHtml(cfg.label) + '
' + renderValue(cfg, val) + '
'; }); html += '
'; html += '
'; col2.forEach(function(cfg) { var val = row[cfg.field]; html += '
' + escHtml(cfg.label) + '
' + renderValue(cfg, val) + '
'; }); html += '
'; html += '
'; // 成品提货的进度字段整行显示,数据来自国内订单中最早的未完结订单,只读。 if (isFinishedDetail) { var finishedProgress = row['产品进度'] || row['进度'] || ''; html += '
进度
' + escHtml(String(finishedProgress) || '-') + '
'; } else { var progressVal = row['进度']; if (progressVal === null || progressVal === undefined) progressVal = ''; html += '
进度
'; } // 到货批次及数量列表(动态加载)——成品提货详情改名未完结订单列表(只读,自动抓取国内/海外未完结订单) html += '
'; html += '

' + (isFinishedDetail ? '📋 未完结订单列表' : '📋 到货批次及数量') + '

'; if (isFinishedDetail) { // 未完结订单:只读展示,数据由后端按货品编号从国内/海外订单抓取 html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
序号下单日期下单数量到货数量未到货数进度
加载中...
'; } else { html += '
'; html += '
'; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
批次编号到货日期到货数量订单未完结数量备注
合计00
'; } html += '
'; // 包材信息(动态加载)——包材订单详情不展示,成品提货详情不展示,其他订单类型保留 if (orderType !== 'packaging' && !isFinishedDetail) { html += '
'; html += '

📦 包材信息

'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
下单时间货品编号货品名称下单数量到货数量到货日期进度
加载中...
'; html += '
'; } overlay.innerHTML = '
' + '
📋 ' + escHtml(title || '产品详情明细') + '
' + '
' + html + '
' + '
'; document.body.appendChild(overlay); // 实际下单数量变更时实时重新计算未完结数量 overlay.addEventListener('input', function(e) { if (e.target.matches('.detail-input[data-field="数量"]') || e.target.matches('.batch-qty')) { recalcArrivalUnfinished(); } }); // 动态加载到货批次 + 包材信息(成品提货详情改为加载未完结订单列表) (function() { var orderId = row.id; var productCode = row['货品编号'] || row['产品编码'] || row['编码'] || ''; if (isFinishedDetail) { // 成品提货:自动抓取国内/海外未完结订单 fetch('/api/finished/unfinished-orders?product_code=' + encodeURIComponent(productCode)) .then(function(r) { return r.json(); }) .then(function(res) { var body = overlay.querySelector('#unfinishedOrderBody'); var latestProgress = (res.data && res.data.product_progress) || ''; var progressEl = overlay.querySelector('#finishedDetailProgress'); if (progressEl) progressEl.textContent = latestProgress || '-'; if (productCode && typeof finishedLoadOrders === 'function') { finishedLoadOrders(finishedPage || 1); } if (!body) return; if (res.code !== 0 || !res.data || !res.data.rows || res.data.rows.length === 0) { body.innerHTML = '暂无未完结订单'; return; } var html = ''; res.data.rows.forEach(function(o, idx) { html += ''; html += '' + (idx + 1) + ''; html += '' + escHtml(o['下单日期'] || '-') + ''; html += '' + (parseInt(o['下单数量']) || 0).toLocaleString() + ''; html += '' + (parseInt(o['到货数量']) || 0).toLocaleString() + ''; html += '' + (parseInt(o['未到货数']) || 0).toLocaleString() + ''; html += '' + escHtml(o['订单进度'] || '') + ''; html += ''; }); body.innerHTML = html; }) .catch(function() { var body = overlay.querySelector('#unfinishedOrderBody'); if (body) body.innerHTML = '加载失败'; }); return; } fetch('/api/domestic/orders/detail_extras?order_id=' + encodeURIComponent(orderId) + '&product_code=' + encodeURIComponent(productCode) + '&order_type=' + encodeURIComponent(orderType)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { var abBody = overlay.querySelector('#arrivalBatchBody'); if (abBody) abBody.innerHTML = '暂无数据'; var pmBody = overlay.querySelector('#packagingMaterialBody'); if (pmBody) pmBody.innerHTML = '暂无数据'; return; } var data = res.data; // 到货批次 var abBody = overlay.querySelector('#arrivalBatchBody'); var totalQtyEl = overlay.querySelector('#arrivalTotalQty'); if (abBody) { var abHtml = ''; var totalQty = 0; var batchCount = data.arrival_batches ? data.arrival_batches.length : 0; // 数据行 data.arrival_batches.forEach(function(b, idx) { var qty = parseInt(b.arrival_qty) || 0; totalQty += qty; var dateVal = (b.arrival_date || '').substring(0, 10); var batchId = b.id || ''; abHtml += ''; abHtml += '' + (idx + 1) + ''; abHtml += '' + '' + '0' + '
' + '' + '' + '
'; abHtml += ''; }); // 补齐最少3行空白占位(可编辑) var placeholderRows = Math.max(0, 3 - batchCount); for (var i = 0; i < placeholderRows; i++) { abHtml += ''; abHtml += '' + (batchCount + i + 1) + ''; abHtml += ''; abHtml += ''; abHtml += '-'; abHtml += '
'; abHtml += ''; } abBody.innerHTML = abHtml; if (totalQtyEl) totalQtyEl.textContent = totalQty; recalcArrivalUnfinished(); } // 包材信息 var pmBody = overlay.querySelector('#packagingMaterialBody'); if (pmBody && data.packaging_materials) { if (data.packaging_materials.length === 0) { pmBody.innerHTML = '无关联包材'; } else { var pmHtml = ''; data.packaging_materials.forEach(function(m) { pmHtml += ''; pmHtml += '' + escHtml(m.order_time || '-') + ''; pmHtml += '' + escHtml(m.material_code || '-') + ''; pmHtml += '' + escHtml(m.material_name || '-') + ''; pmHtml += '' + m.order_qty + ''; pmHtml += '' + m.arrival_qty + ''; pmHtml += '' + escHtml(m.arrival_date || '-') + ''; pmHtml += '📊 ' + escHtml(m.progress || '-') + ''; pmHtml += ''; }); pmBody.innerHTML = pmHtml; } } }) .catch(function(err) { console.error('加载详情额外数据失败:', err); var abBody = overlay.querySelector('#arrivalBatchBody'); if (abBody) abBody.innerHTML = '加载失败'; var pmBody = overlay.querySelector('#packagingMaterialBody'); if (pmBody) pmBody.innerHTML = '加载失败'; }); })(); } function addArrivalBatchRow(btn) { btn.disabled = true; btn.textContent = '⋯'; var overlay = btn.closest('.modal-overlay'); if (!overlay) { var tbl = btn.closest('table'); if (tbl) overlay = tbl.closest('.modal-overlay'); } var orderId = btn.getAttribute('data-order-id'); var orderType = btn.getAttribute('data-order-type') || 'domestic'; var tbody = overlay.querySelector('#arrivalBatchBody'); if (!tbody) { btn.disabled = false; btn.textContent = '➕ 添加一行'; return; } if (tbody.querySelector('.batch-editing')) { btn.disabled = false; btn.textContent = '➕ 添加一行'; return; } // 计算下一个批次编号 var lastNum = 0; var rows = tbody.querySelectorAll('tr:not(.batch-placeholder)'); rows.forEach(function(r) { var firstTd = r.querySelector('td'); if (firstTd) { var num = parseInt(firstTd.textContent); if (!isNaN(num) && num > lastNum) lastNum = num; } }); var nextNum = Math.max(lastNum + 1, 4); // 插入编辑行(在最后一个数据行之后,占位行之前) var tr = document.createElement('tr'); tr.className = 'batch-editing'; tr.innerHTML = '' + '' + nextNum + '' + '' + '' + '' + '
' + '' + '' + '' + '
'; // 追加到表格末尾(合计行上方) tbody.appendChild(tr); recalcArrivalUnfinished(); var inp = tr.querySelector('input[type="date"]'); if (inp) inp.focus(); } /** * 取消新增行(删除未保存的行) */ function cancelArrivalBatchRow(btn) { showCenteredConfirm('确认', '确认取消本行?未保存的内容将丢失。').then(function(ok) { if (!ok) return; var tr = btn.closest('tr'); if (!tr) return; // 恢复添加按钮状态 var addBtn = document.getElementById('addBatchBtn'); if (addBtn) { addBtn.disabled = false; addBtn.textContent = '➕ 添加一行'; } tr.remove(); recalcArrivalUnfinished(); }); } function refreshOrderListFromArrivalModal(btn) { var overlay = btn && btn.closest ? btn.closest('.modal-overlay') : null; var footerBtn = overlay && overlay.querySelector('.pd-footer .btn-primary'); var refreshFn = footerBtn && footerBtn.getAttribute('data-refresh'); if (refreshFn === 'packagingLoadOrders' && typeof packagingLoadOrders === 'function') { packagingLoadOrders(packagingOrderPage || 1); } else if (refreshFn === 'overseasLoadOrders' && typeof overseasLoadOrders === 'function') { overseasLoadOrders(overseasOrderPage || 1); } else if (refreshFn === 'domesticLoadOrders' && typeof domesticLoadOrders === 'function') { domesticLoadOrders(domesticOrderPage || 1); } } function saveNewArrivalBatch(btn, orderId, orderType) { var tr = btn.closest('tr'); if (!tr) return; var date = tr.querySelector('.batch-date')?.value || ''; var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0; var remark = tr.querySelector('.batch-remark')?.value || ''; if (!date && !qty) { alert('请输入到货日期或数量'); return; } btn.disabled = true; btn.textContent = '⋯'; fetch('/api/domestic/orders/add_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_id: orderId, order_type: orderType, arrival_date: date, arrival_qty: qty, remark: remark}) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { var batchNum = ''; var firstTd = tr.querySelector('td'); if (firstTd) batchNum = firstTd.textContent || ''; var batchId = res.data && res.data.id || ''; tr.setAttribute('data-batch-id', batchId); tr.className = ''; tr.innerHTML = '' + '' + batchNum + '' + '' + '' + '0' + '
'; // 更新合计 var totalEl = tr.closest('.modal-overlay').querySelector('#arrivalTotalQty'); if (totalEl) { var cur = parseInt(totalEl.textContent) || 0; totalEl.textContent = cur + qty; } recalcArrivalUnfinished(); refreshOrderListFromArrivalModal(btn); var addBtn = tr.closest('.modal-overlay').querySelector('#addBatchBtn'); if (addBtn) { addBtn.disabled = false; addBtn.textContent = '➕ 添加一行'; } } else { alert('保存失败: ' + (res.msg || '未知错误')); btn.disabled = false; btn.textContent = '✅'; } }).catch(function(){ alert('网络错误'); btn.disabled = false; btn.textContent = '✅'; }); } function saveArrivalBatch(btn, batchId, orderId, orderType) { var tr = btn.closest('tr'); if (!tr) return; var date = tr.querySelector('.batch-date')?.value || ''; var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0; var remark = tr.querySelector('.batch-remark')?.value || ''; btn.disabled = true; btn.textContent = '⋯'; fetch('/api/domestic/orders/update_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: batchId, order_type: orderType, arrival_date: date, arrival_qty: qty, remark: remark}) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { btn.textContent = '✅'; // 同步 data-orig,避免右下角保存时重复提交同一修改 var dd = tr.querySelector('.batch-date'); var dq = tr.querySelector('.batch-qty'); var dr = tr.querySelector('.batch-remark'); if (dd) dd.setAttribute('data-orig', dd.value || ''); if (dq) dq.setAttribute('data-orig', dq.value || ''); if (dr) dr.setAttribute('data-orig', dr.value || ''); recalcArrivalUnfinished(); refreshOrderListFromArrivalModal(btn); setTimeout(function() { btn.textContent = '✅'; }, 500); } else { alert('保存失败: ' + (res.msg || '未知错误')); } btn.disabled = false; }).catch(function(){ alert('网络错误'); btn.disabled = false; btn.textContent = '✅'; }); } function deleteArrivalBatch(btn, batchId, orderType) { showCenteredConfirm('确认', '确认删除本行数据?删除后不可恢复。').then(function(ok) { if (!ok) return; var tr = btn.closest('tr'); if (!tr) return; var overlay = tr.closest('.modal-overlay'); var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0; btn.disabled = true; btn.textContent = '⋯'; fetch('/api/domestic/orders/delete_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: batchId, order_type: orderType}) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { tr.remove(); recalcArrivalUnfinished(); refreshOrderListFromArrivalModal(btn); if (overlay) { var totalEl = overlay.querySelector('#arrivalTotalQty'); if (totalEl) { var sumQty = 0; overlay.querySelectorAll('#arrivalBatchBody .batch-qty').forEach(function(inp) { sumQty += parseInt(inp.value) || 0; }); totalEl.textContent = sumQty; } } } else { alert('删除失败: ' + (res.msg || '未知错误')); btn.disabled = false; btn.textContent = '✕'; } }).catch(function(){ alert('网络错误'); btn.disabled = false; }); }); } /** * 到货数量输入框回车保存 */ function batchQtyKeydown(event) { if (event.key === 'Enter') { event.preventDefault(); var tr = event.target.closest('tr'); if (!tr) return; var saveBtn = tr.querySelector('button[onclick*="saveArrivalBatch"], button[onclick*="saveNewArrivalBatch"]'); if (saveBtn) { saveBtn.click(); setTimeout(function() { event.target.blur(); }, 100); } } } /** * 滚动计算订单未完结数量 * 实际下单数量 - 累计到货数量 = 当前行未完结数量 */ function recalcArrivalUnfinished() { var table = document.getElementById('arrivalBatchTable'); if (!table) return; // 从详情表单中的实际下单数量输入框读取值 var overlay = table.closest('.modal-overlay'); var orderInput = overlay ? overlay.querySelector('.detail-input[data-field="数量"]') : null; var orderQty = orderInput ? (parseInt(orderInput.value) || 0) : (parseInt(table.getAttribute('data-order-qty')) || 0); var body = table.querySelector('#arrivalBatchBody'); if (!body) return; var rows = body.querySelectorAll('tr'); var cumulative = 0; var lastUnfinished = 0; var hasUnfinishedValue = false; rows.forEach(function(tr) { var qtyInput = tr.querySelector('.batch-qty'); var unFinishedTd = tr.querySelector('.batch-unfinished'); if (qtyInput && unFinishedTd) { var arrivalQty = parseInt(qtyInput.value) || 0; cumulative += arrivalQty; var remaining = orderQty - cumulative; if (qtyInput.getAttribute('placeholder') === '数量' && !qtyInput.value) { unFinishedTd.textContent = '-'; unFinishedTd.style.color = '#ccc'; } else { unFinishedTd.textContent = remaining; unFinishedTd.style.color = remaining < 0 ? '#f5222d' : ''; lastUnfinished = remaining; hasUnfinishedValue = true; } } }); // 合计行显示最后一个有数据行的未完结数量(总剩余) var footEl = table.querySelector('#arrivalUnfinishedTotal'); if (footEl) { // 没有填写任何到货批次时,未完结数量 = 实际下单数量(即全部未到货) var totalVal = hasUnfinishedValue ? lastUnfinished : orderQty; footEl.textContent = totalVal; footEl.style.color = totalVal < 0 ? '#f5222d' : ''; } // 更新到货数量合计(遍历所有行的 batch-qty 累加) var totalQtyEl = table.querySelector('#arrivalTotalQty'); if (totalQtyEl) { var sumQty = 0; body.querySelectorAll('tr .batch-qty').forEach(function(inp) { sumQty += parseInt(inp.value) || 0; }); totalQtyEl.textContent = sumQty; } } function saveDetailChanges(btn) { var overlay = btn.closest('.modal-overlay'); var orderId = btn.getAttribute('data-order-id'); var productCode = btn.getAttribute('data-product-code') || ''; var apiUrl = btn.getAttribute('data-api') || '/api/domestic/orders/update'; var refreshFn = btn.getAttribute('data-refresh') || 'domesticLoadOrders'; var orderType = refreshFn === 'packagingLoadOrders' ? 'packaging' : (refreshFn === 'overseasLoadOrders' ? 'overseas' : 'domestic'); var inputs = overlay.querySelectorAll('.detail-input, .detail-textarea'); var hasError = false; // 只收集实际发生变化的字段(对比 data-orig),一次请求批量提交 var changedFields = {}; inputs.forEach(function(inp) { var field = inp.getAttribute('data-field'); if (!field) return; var value = inp.value; var orig = inp.getAttribute('data-orig'); if (orig === null || orig === undefined) orig = ''; // 空字符串视为与空相同,避免无意义提交 if (String(value) !== String(orig)) { changedFields[field] = value; } }); // 收集到货批次改动:新增行(有内容未保存)+ 修改行(值变化) var batchAdds = []; // {arrival_date, arrival_qty, remark} var batchUpdates = []; // {id, arrival_date, arrival_qty, remark} overlay.querySelectorAll('#arrivalBatchBody tr').forEach(function(tr) { if (tr.classList.contains('batch-placeholder') || tr.classList.contains('batch-editing')) { // 新增行:有日期或数量才保存 var d = tr.querySelector('.batch-date')?.value || ''; var q = parseInt(tr.querySelector('.batch-qty')?.value) || 0; var r = tr.querySelector('.batch-remark')?.value || ''; if (d || q) { batchAdds.push({arrival_date: d, arrival_qty: q, remark: r}); } } else { var bid = tr.getAttribute('data-batch-id'); if (!bid) return; var d = tr.querySelector('.batch-date')?.value || ''; var q = tr.querySelector('.batch-qty')?.value || ''; var r = tr.querySelector('.batch-remark')?.value || ''; var od = tr.querySelector('.batch-date')?.getAttribute('data-orig') || ''; var oq = tr.querySelector('.batch-qty')?.getAttribute('data-orig') || ''; var or_ = tr.querySelector('.batch-remark')?.getAttribute('data-orig') || ''; if (String(d) !== String(od) || String(q) !== String(oq) || String(r) !== String(or_)) { batchUpdates.push({id: bid, arrival_date: d, arrival_qty: parseInt(q) || 0, remark: r}); } } }); var doClose = function() { overlay.remove(); if (refreshFn === 'packagingLoadOrders' && typeof packagingLoadOrders === 'function') { packagingLoadOrders(packagingOrderPage || 1); } else if (refreshFn === 'overseasLoadOrders' && typeof overseasLoadOrders === 'function') { overseasLoadOrders(overseasOrderPage || 1); } else if (refreshFn === 'domesticLoadOrders' && typeof domesticLoadOrders === 'function') { domesticLoadOrders(domesticOrderPage || 1); } else if (refreshFn === 'finishedLoadOrders' && typeof finishedLoadOrders === 'function') { finishedLoadOrders(finishedPage || 1); } }; // 没有任何改动:直接关闭,不产生日志 if (Object.keys(changedFields).length === 0 && batchAdds.length === 0 && batchUpdates.length === 0) { doClose(); return; } btn.disabled = true; btn.textContent = '保存中...'; var promises = []; // 只有一个字段变更时,沿用列表内联编辑使用的单字段接口,避免详情弹窗单价保存被批量接口/附加日志链路阻塞。 // 多个字段仍使用批量接口,保持原有一次提交行为。 if (Object.keys(changedFields).length > 0) { var changedKeys = Object.keys(changedFields); var updatePromise; if (changedKeys.length === 1) { updatePromise = fetch(apiUrl, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: orderId, field: changedKeys[0], value: changedFields[changedKeys[0]]}) }); } else { var batchUrl = apiUrl.replace(/\/update$/, '/update_fields'); updatePromise = fetch(batchUrl, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: orderId, product_code: productCode, fields: changedFields}) }); } promises.push( updatePromise.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function(res){ if (res.code !== 0) hasError = true; }).catch(function(){ hasError = true; }) ); } // 新增批次 batchAdds.forEach(function(b) { promises.push( fetch('/api/domestic/orders/add_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_id: orderId, order_type: orderType, arrival_date: b.arrival_date, arrival_qty: b.arrival_qty, remark: b.remark}) }).then(function(r){return r.json()}).then(function(res){ if (res.code !== 0) hasError = true; }).catch(function(){ hasError = true; }) ); }); // 修改批次 batchUpdates.forEach(function(b) { promises.push( fetch('/api/domestic/orders/update_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: b.id, order_type: orderType, arrival_date: b.arrival_date, arrival_qty: b.arrival_qty, remark: b.remark}) }).then(function(r){return r.json()}).then(function(res){ if (res.code !== 0) hasError = true; }).catch(function(){ hasError = true; }) ); }); Promise.all(promises).then(function() { btn.disabled = false; btn.textContent = '💾 保存'; if (hasError) { alert('部分字段保存失败,请重试'); } else { doClose(); if (refreshFn === 'finishedLoadOrders') { showCenteredAlert('保存成功', '待提数量已保存'); } } }); } window._padDateStr = window._padDateStr || function(str) { if (!str) return str; str = str.replace(/\//g, '-'); var parts = str.split(' '); if (parts.length >= 1) { var dp = parts[0].split('-'); if (dp.length >= 2) dp[1] = ('0' + dp[1]).slice(-2); if (dp.length >= 3) dp[2] = ('0' + dp[2]).slice(-2); parts[0] = dp.join('-'); } if (parts.length >= 2) { var tp = parts[1].split(':'); if (tp.length >= 1) tp[0] = ('0' + tp[0]).slice(-2); parts[1] = tp.join(':'); } return parts.join(' '); }; function loadDomesticOrdersPage(container) { container.innerHTML = `

📦 国内采购下单明细

`; // 动态生成表头列 (function() { var cols = DOMESTIC_COLUMNS; var order = window._domesticDragOrder || cols.map(function(c) { return c.field; }); order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; if (!window.domesticColVisible[field]) return; var label = col.label; var fieldAttr = col.field; var thStyle = ''; if (fieldAttr === '货品编号') thStyle = 'min-width:120px;position:sticky;left:90px;top:0;z-index:11;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '货品名称') thStyle = 'min-width:200px;position:sticky;left:210px;top:0;z-index:11;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '货品类型') thStyle = 'min-width:100px;position:sticky;left:410px;top:0;z-index:11;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '日期') thStyle = 'position:sticky;left:510px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '计划下单时间' || fieldAttr === '新旧品') thStyle = 'white-space:nowrap'; else if (fieldAttr === '下单备注') thStyle = 'min-width:150px;white-space:normal'; else if (fieldAttr === '单价' || fieldAttr === '是否审核价格' || fieldAttr === '数量' || fieldAttr === '金额' || fieldAttr === '供货商') thStyle = ''; else if (fieldAttr === '仓库') thStyle = 'min-width:80px'; else if (fieldAttr === '金蝶采购单') thStyle = 'min-width:100px'; else if (fieldAttr === '采购员') thStyle = 'min-width:150px'; else if (fieldAttr === '下单人') thStyle = 'min-width:70px'; else if (fieldAttr === '进度') thStyle = 'min-width:80px'; else if (fieldAttr === '旺店通生产单号') thStyle = 'min-width:120px'; else if (fieldAttr === '采购开发') thStyle = 'min-width:100px'; else if (fieldAttr === '实际到货日期' || fieldAttr === '预计到货日期') thStyle = 'min-width:100px'; else { thStyle = ''; } var filterControl = ' ▾'; container.querySelector('#domesticOrdersHeader').innerHTML += ''; }); })(); container.querySelector('#domesticOrdersHeader').innerHTML += ''; container.querySelector('#domesticOrdersHeader').innerHTML += renderOrderActionHeader('domestic'); container.querySelector('#domesticOrdersTable').innerHTML += ''; applyOrderActionState('domestic'); // 添加供应商 datalist(模糊搜索用) container.querySelector('#domesticContent').innerHTML += ''; // 冻结列弹窗 + 字段设置弹窗 container.innerHTML += ` `; // 初始化字段设置checkbox var dcGrid = document.getElementById('domesticColGrid'); if (dcGrid) { window._domesticDragOrder = DOMESTIC_COLUMNS.map(function(c) { return c.field; }); renderOrderColCheckboxes('domestic'); } domesticLoadFilters(); Promise.all([loadOrderFollowers(), loadPersonnelGroupsForLinkedPages()]).then(function() { domesticLoadOrders(1); }); loadDefaultOrderScheme('domestic'); // 后台预拉取所有列的筛选值 prefetchOrderColumnValues('domestic'); } function domesticLoadFilters() { fetch('/api/domestic/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var so = document.getElementById('domesticOrdererFilter'); (res.data.orderers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; so.appendChild(opt); }); // 填充供应商 datalist(模糊搜索用) }); } function domesticDoSearch() { domesticApplyFilters(); } function domesticApplyFilters() { domesticSearch = document.getElementById('domesticSearchInput').value.trim(); domesticFilterSupplier = document.getElementById('domesticSupplierFilter').value.trim(); domesticFilterStatus = document.getElementById('domesticStatusFilter').value; domesticFilterOrderer = document.getElementById('domesticOrdererFilter').value; domesticLoadOrders(1); } function domesticResetFilters() { document.getElementById('domesticSearchInput').value = ''; document.getElementById('domesticSupplierFilter').value = ''; document.getElementById('domesticStatusFilter').value = ''; document.getElementById('domesticOrdererFilter').value = ''; domesticSearch = ''; domesticFilterSupplier = ''; domesticFilterStatus = ''; domesticFilterOrderer = ''; domesticOrderFilter = {}; domesticOrderFilterActive = {}; _syncOrderFilterIcons('domestic'); domesticLoadOrders(1); } function domesticLoadOrders(page) { domesticOrderPage = page; var params = '?page=' + page + '&size=' + DOMESTIC_PAGE_SIZE; if (domesticSearch) params += '&search=' + encodeURIComponent(domesticSearch); if (domesticFilterStatus) params += '&status=' + encodeURIComponent(domesticFilterStatus); if (domesticFilterSupplier) params += '&supplier=' + encodeURIComponent(domesticFilterSupplier); if (domesticFilterOrderer) params += '&orderer=' + encodeURIComponent(domesticFilterOrderer); fetch('/api/domestic/orders/list' + params, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: domesticOrderFilter}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('domesticOrdersTbody'); var totalInfo = document.getElementById('domesticTotalInfo'); if (totalInfo) totalInfo.textContent = '共 ' + data.total + ' 条'; if (!data.rows || data.rows.length === 0) { tbody.innerHTML = ''; } else { var startIdx = (page - 1) * DOMESTIC_PAGE_SIZE; var html = ''; // 国内订单单元格渲染器映射 var domesticCellRenderers = { '货品编号': function(r) { return ''; }, '货品名称': function(r) { return ''; }, '货品类型': function(r) { return ''; }, '日期': function(r) { return ''; }, '计划下单时间': function(r) { var t = window._padDateStr(r['计划下单时间'] || ''); if (t.length > 16) t = t.slice(0, 16); return ''; }, '新旧品': function(r) { return ''; }, '下单备注': function(r) { return ''; }, '单价': function(r) { return ''; }, '是否审核价格': function(r) { return ''; }, '数量': function(r) { return ''; }, '金额': function(r) { var price = parseFloat(r['单价']) || 0; var qty = parseInt(r['数量']) || 0; var amt = price * qty; return ''; }, '供货商': function(r) { return ''; }, '仓库': function(r) { return ''; }, '金蝶采购单': function(r) { return ''; }, '采购员': function(r) { return ''; }, '下单人': function(r) { return ''; }, '进度': function(r) { return ''; }, '旺店通生产单号': function(r) { return ''; }, '是否已录入旺店通': function(r) { return ''; }, '采购开发': function(r) { return ''; }, '实际到货日期': function(r) { return ''; }, '未到货数': function(r) { return ''; }, '预计到货日期': function(r) { return ''; }, '金蝶入库': function(r) { return ''; }, '状态': function(r) { return ''; }, '实际到货数量': function(r) { return ''; } }; data.rows.forEach(function(r, i) { var rowNum = startIdx + i + 1; html += ''; html += ''; html += ''; // 严格按当前页面已生成的表头顺序渲染单元格,避免账号字段方案异步加载后错列。 var order = Array.from(document.querySelectorAll('#domesticOrdersHeader th[data-field]')).map(function(th) { return th.getAttribute('data-field'); }); order.forEach(function(field) { var renderer = domesticCellRenderers[field]; if (renderer) { html += renderer(r); } else { // 未定义的字段默认渲染 html += ''; } }); // 做货周期 = 实际到货日期 - 跟单下单日期 var manuDays = ''; if (r['实际到货日期'] && r['日期']) { var d1 = new Date(r['实际到货日期'].replace(/-/g, '/')); var d2 = new Date(r['日期'].replace(/-/g, '/')); if (!isNaN(d1) && !isNaN(d2)) { var diff = Math.round((d1 - d2) / 86400000); if (diff >= 0) manuDays = diff + '天'; } } html += ''; html += ''; html += ''; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('domesticOrdersTable', 'orders_domestic'); // 先恢复保存的列宽,再计算冻结列位置(基于实际宽度) restoreColumnWidths('domesticOrdersTable', 'domesticColWidths'); applyDomesticFrozenPositions(); enableColumnResize('domesticOrdersTable', 'domesticColWidths'); var pel = document.getElementById('domesticOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, DOMESTIC_PAGE_SIZE, 'domesticLoadOrders', 'DOMESTIC_PAGE_SIZE'); } else { // 简单分页 var totalPages = Math.ceil((data.total || 0) / DOMESTIC_PAGE_SIZE); pel.innerHTML = '
'; var phtml = ''; if (page > 1) phtml += ''; phtml += '第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) phtml += ''; pel.innerHTML += phtml + '
'; } } }); } // 订单状态列统一下拉框 function renderOrderStatusSelect(row, type) { var status = ['未完结','已完结','分批送货','已取消'].indexOf(row['状态']) >= 0 ? row['状态'] : '未完结'; return ''; } window.saveOrderStatus = function(sel) { var type = sel.getAttribute('data-type'); var config = { domestic: {api: '/api/domestic/orders/update', load: domesticLoadOrders, page: function(){ return domesticOrderPage; }}, overseas: {api: '/api/overseas/orders/update', load: overseasLoadOrders, page: function(){ return overseasOrderPage; }} }[type]; if (!config) return; var oldVal = sel.getAttribute('data-current') || '未完结'; var val = sel.value; sel.disabled = true; fetch(config.api, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: sel.getAttribute('data-id'), field: '状态', value: val}) }).then(function(r){return r.json()}).then(function(res){ sel.disabled = false; if (res.code === 0) { sel.setAttribute('data-current', val); sel.style.borderColor = '#52c41a'; setTimeout(function(){ sel.style.borderColor = ''; config.load(config.page() || 1); }, 300); } else { sel.value = oldVal; alert(res.msg || '保存失败'); } }).catch(function(){ sel.disabled = false; sel.value = oldVal; alert('网络错误'); }); }; // ─── 状态筛选框同步:根据列表内容更新状态选项 ─── function syncStatusFilter(type, rows) { if (!rows || rows.length === 0) return; var selectId = type + 'StatusFilter'; var sel = document.getElementById(selectId); if (!sel) return; var currentVal = sel.value; // 收集已有选项 var existing = {}; [].forEach.call(sel.options, function(opt) { if (opt.value) existing[opt.value] = true; }); // 补充数据中有的状态(不替换已有选项) (rows || []).forEach(function(r) { var s = r['状态'] || ''; if (s && !existing[s]) { existing[s] = true; var opt = document.createElement('option'); opt.value = s; opt.textContent = s; sel.appendChild(opt); } }); if (currentVal) sel.value = currentVal; } function renderCollapsibleBackupStatusHeader(tableId) { var collapsed = localStorage.getItem('purchase_readyBackupStatusCollapsed') === 'true'; var title = collapsed ? '展开状态列' : '折叠状态列'; return ''; } function applyCollapsibleBackupStatusState() { var table = document.getElementById('backupTable'); if (!table) return; var collapsed = localStorage.getItem('purchase_readyBackupStatusCollapsed') === 'true'; table.classList.toggle('backup-status-collapsed', collapsed); var actionWidth = collapsed ? 34 : 128; var cols = table.querySelectorAll('colgroup.adjustable-width-cols col'); if (cols.length) { var statusCol = cols[cols.length - 1]; statusCol.style.width = actionWidth + 'px'; statusCol.style.minWidth = actionWidth + 'px'; statusCol.style.maxWidth = actionWidth + 'px'; } var head = table.querySelector('thead tr:last-child th:last-child'); if (head) { head.style.width = actionWidth + 'px'; head.style.minWidth = actionWidth + 'px'; head.style.maxWidth = actionWidth + 'px'; } table.querySelectorAll('tbody tr td.backup-status-cell').forEach(function(cell) { cell.style.setProperty('width', actionWidth + 'px', 'important'); cell.style.setProperty('min-width', actionWidth + 'px', 'important'); cell.style.setProperty('max-width', actionWidth + 'px', 'important'); var status = cell.querySelector('span'); if (status) status.style.setProperty('display', collapsed ? 'none' : 'inline', 'important'); }); var toggle = table.querySelector('.order-action-toggle'); if (toggle) { toggle.textContent = collapsed ? '«' : '»'; toggle.title = collapsed ? '展开状态列' : '折叠状态列'; } } window.toggleBackupStatusColumn = function() { localStorage.setItem('purchase_readyBackupStatusCollapsed', String(localStorage.getItem('purchase_readyBackupStatusCollapsed') !== 'true')); applyCollapsibleBackupStatusState(); }; // ─── 统一操作列折叠/展开(计划预测、下单备份、成品提货沿用订单管理布局) ─── function getCollapsibleActionKey(type) { return type + 'OrderActionsCollapsed'; } function isCollapsibleActionCollapsed(type) { return localStorage.getItem(getCollapsibleActionKey(type)) === 'true'; } function renderCollapsibleActionHeader(type, tableId, wide) { var collapsed = isCollapsibleActionCollapsed(type); var title = collapsed ? '展开操作列' : '折叠操作列'; var widthClass = wide ? ' order-action-wide' : ''; return ''; } function applyCollapsibleActionState(type) { var tableIds = {planning:['planTable'], purchase_ready:['orderTable'], finished:['finishedOrdersTable']}[type] || []; tableIds.forEach(function(tableId) { var table = document.getElementById(tableId); if (!table) return; var collapsed = isCollapsibleActionCollapsed(type); table.classList.toggle('order-actions-collapsed', collapsed); var actionWidth = collapsed ? 34 : (type === 'planning' ? 200 : 128); var widthCols = table.querySelectorAll('colgroup col'); if (widthCols.length) { var actionCol = widthCols[widthCols.length - 1]; actionCol.style.width = actionWidth + 'px'; actionCol.style.minWidth = actionWidth + 'px'; actionCol.style.maxWidth = actionWidth + 'px'; } var head = table.querySelector('thead tr:last-child th:last-child'); if (head) { head.classList.add('order-action-head'); head.style.width = actionWidth + 'px'; head.style.minWidth = head.style.width; } table.querySelectorAll('tbody tr td:last-child').forEach(function(cell) { cell.classList.add('order-action-cell'); cell.style.setProperty('width', actionWidth + 'px', 'important'); cell.style.setProperty('min-width', actionWidth + 'px', 'important'); cell.style.setProperty('max-width', actionWidth + 'px', 'important'); var buttons = cell.querySelector('.order-action-buttons'); if (buttons) buttons.style.setProperty('display', collapsed ? 'none' : 'flex', 'important'); }); var toggle = table.querySelector('.order-action-toggle'); if (toggle) { toggle.textContent = collapsed ? '«' : '»'; toggle.title = collapsed ? '展开操作列' : '折叠操作列'; toggle.setAttribute('aria-label', toggle.title); } }); } window.toggleCollapsibleActionColumn = function(type) { localStorage.setItem(getCollapsibleActionKey(type), String(!isCollapsibleActionCollapsed(type))); applyCollapsibleActionState(type); }; // ─── 三类订单操作列折叠/展开 ─── function getOrderActionTableId(type) { return {domestic: 'domesticOrdersTable', overseas: 'overseasOrdersTable', packaging: 'packagingOrdersTable'}[type] || ''; } function isOrderActionCollapsed(type) { return localStorage.getItem(type + 'OrderActionsCollapsed') === 'true'; } function renderOrderActionHeader(type) { var collapsed = isOrderActionCollapsed(type); var icon = collapsed ? '«' : '»'; var title = collapsed ? '展开操作列' : '折叠操作列'; return ''; } function applyOrderActionState(type) { var table = document.getElementById(getOrderActionTableId(type)); if (!table) return; var collapsed = isOrderActionCollapsed(type); table.classList.toggle('order-actions-collapsed', collapsed); var toggle = table.querySelector('.order-action-toggle'); if (toggle) { var title = collapsed ? '展开操作列' : '折叠操作列'; toggle.textContent = collapsed ? '«' : '»'; toggle.title = title; toggle.setAttribute('aria-label', title); } // 操作列折叠时只调整操作列自身和表格总宽度,其他列保持原宽度。 var cols = table.querySelectorAll('colgroup.order-width-cols col'); if (cols.length) { setOrderTableColumnWidth(table, cols.length - 1, collapsed ? 34 : 128); syncOrderTableWidth(table); } } window.toggleOrderActionColumn = function(type) { localStorage.setItem(type + 'OrderActionsCollapsed', String(!isOrderActionCollapsed(type))); applyOrderActionState(type); }; // ─── 三类订单表格固定列轨道:调整某列时不重新分配其他列 ─── function setOrderTableColumnWidth(table, index, width) { var ths = table.querySelectorAll('thead tr:first-child th'); var cols = table.querySelectorAll('colgroup.order-width-cols col'); var minWidth = index === ths.length - 1 ? 34 : 60; var w = Math.max(minWidth, Math.round(width)); if (cols[index]) { cols[index].style.width = w + 'px'; cols[index].style.minWidth = w + 'px'; cols[index].style.maxWidth = w + 'px'; } if (ths[index] && index !== ths.length - 1) { ths[index].style.width = w + 'px'; ths[index].style.minWidth = w + 'px'; ths[index].style.maxWidth = w + 'px'; } } function syncOrderTableWidth(table) { var cols = table.querySelectorAll('colgroup.order-width-cols col'); if (!cols.length) return; var total = 0; for (var i = 0; i < cols.length; i++) total += parseFloat(cols[i].style.width) || 0; table.style.width = total + 'px'; table.style.minWidth = total + 'px'; table.style.maxWidth = total + 'px'; } function getOrderSafeColumnWidth(th, index) { var computedMin = parseFloat(window.getComputedStyle(th).minWidth) || 0; var contentWidth = th.scrollWidth || 0; var fallback = index < 2 ? 60 : 80; // 页面切换或异步方案加载期间 offsetWidth 可能暂时为 0,不能把它锁成低于60px。 return Math.max(th.offsetWidth || 0, computedMin, contentWidth, fallback); } function lockOrderTableColumnWidths(table) { var ths = table.querySelectorAll('thead tr:first-child th'); if (!ths.length) return; var measured = []; for (var i = 0; i < ths.length; i++) measured.push(getOrderSafeColumnWidth(ths[i], i)); var oldGroup = table.querySelector('colgroup.order-width-cols'); if (oldGroup) oldGroup.remove(); var group = document.createElement('colgroup'); group.className = 'order-width-cols'; for (var i = 0; i < ths.length; i++) group.appendChild(document.createElement('col')); table.insertBefore(group, table.firstChild); table.style.tableLayout = 'fixed'; var collapsed = table.classList.contains('order-actions-collapsed'); for (var i = 0; i < ths.length; i++) { var width = i === ths.length - 1 ? (collapsed ? 34 : 128) : measured[i]; setOrderTableColumnWidth(table, i, width); } syncOrderTableWidth(table); } // ─── 按账号、页面和字段名恢复列宽 ─── var _orderColumnWidthCache = {}; var _orderColumnWidthRequests = {}; function getOrderWidthPage(tableId) { if (tableId === 'domesticOrdersTable') return 'orders_domestic'; if (tableId === 'overseasOrdersTable') return 'orders_overseas'; if (tableId === 'packagingOrdersTable') return 'orders_packaging'; return ''; } function getOrderColumnKey(table, th, index, total) { var field = th.getAttribute('data-field'); if (field) return field; if (index === 0) return '__select__'; if (index === 1) return '__index__'; if (index === total - 1) return '__action__'; if ((table.id === 'domesticOrdersTable' || table.id === 'overseasOrdersTable') && index === total - 2) { return '__manufacturing_cycle__'; } return ''; } function readLegacyOrderColumnWidths(table, storageKey) { var result = {}; try { var saved = JSON.parse(localStorage.getItem(storageKey) || 'null'); if (!saved) return result; var ths = table.querySelectorAll('thead tr:first-child th'); for (var i = 0; i < ths.length - 1; i++) { var key = getOrderColumnKey(table, ths[i], i, ths.length); var rawWidth = saved[key] !== undefined ? saved[key] : saved[i]; var width = parseFloat(rawWidth); if (key && width >= 60) result[key] = Math.round(width); } } catch(e) {} return result; } function applyOrderColumnWidthMap(table, widths) { if (!table || !widths) return; var ths = table.querySelectorAll('thead tr:first-child th'); for (var i = 0; i < ths.length - 1; i++) { var key = getOrderColumnKey(table, ths[i], i, ths.length); if (key && widths[key] !== undefined) { setOrderTableColumnWidth(table, i, widths[key]); } } syncOrderTableWidth(table); } function saveOrderColumnWidthsToServer(page, widths) { if (!page || !widths || Object.keys(widths).length === 0) return Promise.resolve(); _orderColumnWidthCache[page] = widths; return fetch('/api/planning/column-widths/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({page: page, widths: widths}) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) throw new Error(res.msg || '列宽保存失败'); }).catch(function(err) { console.error('账号列宽保存失败:', err); }); } function refreshOrderFrozenPositions(tableId) { if (tableId === 'domesticOrdersTable') applyDomesticFrozenPositions(); else if (tableId === 'overseasOrdersTable') applyOverseasFrozenPositions(); else if (tableId === 'packagingOrdersTable') applyPackagingFrozenPositions(); } function restoreColumnWidths(tableId, storageKey) { var table = document.getElementById(tableId); if (!table || !table.querySelector('thead')) return; var page = getOrderWidthPage(tableId); var legacyWidths = readLegacyOrderColumnWidths(table, storageKey); // 先锁定当前默认轨道,避免接口返回前表格重新分配列宽。 lockOrderTableColumnWidths(table); if (_orderColumnWidthCache[page]) { applyOrderColumnWidthMap(table, _orderColumnWidthCache[page]); return; } if (!_orderColumnWidthRequests[page]) { _orderColumnWidthRequests[page] = fetch('/api/planning/column-widths?page=' + encodeURIComponent(page)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) throw new Error(res.msg || '列宽加载失败'); var serverWidths = res.data || {}; // 服务器尚无配置时,迁移当前浏览器已有列宽,避免升级后丢失用户刚调好的设置。 if (Object.keys(serverWidths).length === 0 && Object.keys(legacyWidths).length > 0) { serverWidths = legacyWidths; saveOrderColumnWidthsToServer(page, serverWidths); } _orderColumnWidthCache[page] = serverWidths; return serverWidths; }) .catch(function(err) { console.error('账号列宽加载失败:', err); _orderColumnWidthCache[page] = legacyWidths; return legacyWidths; }) .then(function(widths) { var currentTable = document.getElementById(tableId); if (currentTable) { applyOrderColumnWidthMap(currentTable, widths); refreshOrderFrozenPositions(tableId); } return widths; }); } } // ─── 列宽拖拽调整 ─── var _resizeData = null; var _resizeFrame = 0; function updateOrderColumnResize(clientX) { if (!_resizeData) return; var newW = Math.max(60, _resizeData.startW + clientX - _resizeData.startX); // 拖拽期间由 colgroup 统一控制整列,避免每个鼠标事件都逐行改写单元格。 if (_resizeData.kind === 'adjustable') { setAdjustableTableColumnWidth(_resizeData.table, _resizeData.key, newW); syncAdjustableTableWidth(_resizeData.table); if (_resizeData.tableId === 'planTable') updatePlanFrozenPositions(); } else { setOrderTableColumnWidth(_resizeData.table, _resizeData.idx, newW); syncOrderTableWidth(_resizeData.table); } } function enableColumnResize(tableId, storageKey) { var table = document.getElementById(tableId); if (!table) return; var thead = table.querySelector('thead'); if (!thead) return; var ths = thead.querySelectorAll('tr:first-child th'); var lastTh = ths.length - 1; for (var i = 0; i < ths.length; i++) { var th = ths[i]; if (i === lastTh) continue; if (th.querySelector('.col-resize-handle')) continue; var handle = document.createElement('div'); handle.className = 'col-resize-handle'; handle.style.cssText = 'position:absolute;right:-1px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:20'; handle.addEventListener('mousedown', function(idx) { return function(ev) { ev.preventDefault(); ev.stopPropagation(); var cols = table.querySelectorAll('colgroup.order-width-cols col'); var startW = cols[idx] ? parseFloat(cols[idx].style.width) : ths[idx].offsetWidth; _resizeData = { tableId: tableId, storageKey: storageKey, table: table, idx: idx, startX: ev.clientX, startW: startW }; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; }(i)); if (th.style.position !== 'sticky') th.style.position = 'relative'; th.appendChild(handle); } } // 通用表格列宽规则:列宽按当前账号和页面保存,拖拽时只改变当前列。 var _adjustableColumnWidthCache = {}; var _adjustableColumnWidthRequests = {}; function getAdjustableTableColumnKey(th, index, total) { var field = th.getAttribute('data-field'); if (field) return field; if (index === total - 1) return '__action__'; var label = (th.textContent || '').replace(/[▾▼▲↕]/g, '').trim(); return label || ('__column_' + index + '__'); } function getAdjustableTableColumnIndex(table, key) { var ths = table.querySelectorAll('thead tr:first-child th'); for (var i = 0; i < ths.length; i++) { if (getAdjustableTableColumnKey(ths[i], i, ths.length) === key) return i; } return -1; } function setAdjustableTableColumnWidth(table, indexOrKey, width) { var ths = table.querySelectorAll('thead tr:first-child th'); var index = typeof indexOrKey === 'number' ? indexOrKey : getAdjustableTableColumnIndex(table, indexOrKey); if (index < 0) return; var cols = table.querySelectorAll('colgroup.adjustable-width-cols col'); var w = Math.max(60, Math.round(parseFloat(width) || 60)); if (cols[index]) cols[index].style.width = w + 'px'; if (ths[index]) { ths[index].style.width = w + 'px'; ths[index].style.minWidth = w + 'px'; ths[index].style.maxWidth = w + 'px'; } } function syncAdjustableTableWidth(table) { if (!table) return; var cols = table.querySelectorAll('colgroup.adjustable-width-cols col'); var total = 0; for (var i = 0; i < cols.length; i++) total += parseFloat(cols[i].style.width) || 0; var viewport = table.parentElement ? table.parentElement.clientWidth : 0; // 仅在内容不足视口时铺满,避免操作列左侧残留空白;超出视口时保留真实内容宽度供横向滚动。 var width = Math.max(total, viewport); if (width) { table.style.width = width + 'px'; table.style.minWidth = '0'; table.style.maxWidth = 'none'; } } function lockAdjustableTableColumnWidths(table, page) { if (!table) return; var ths = table.querySelectorAll('thead tr:first-child th'); if (!ths.length) return; var oldGroup = table.querySelector('colgroup.adjustable-width-cols'); if (oldGroup) oldGroup.remove(); var group = document.createElement('colgroup'); group.className = 'adjustable-width-cols'; for (var i = 0; i < ths.length; i++) { var col = document.createElement('col'); col.setAttribute('data-field', getAdjustableTableColumnKey(ths[i], i, ths.length)); group.appendChild(col); } table.insertBefore(group, table.firstChild); table.style.tableLayout = 'fixed'; table.style.width = 'max-content'; table.style.minWidth = '0'; for (var i = 0; i < ths.length; i++) { var key = getAdjustableTableColumnKey(ths[i], i, ths.length); var hidden = window.getComputedStyle(ths[i]).display === 'none'; var width = hidden ? 0 : Math.max(ths[i].offsetWidth || 0, ths[i].scrollWidth || 0, 60); var saved = _adjustableColumnWidthCache[page] && _adjustableColumnWidthCache[page][key]; if (hidden) group.children[i].style.width = '0px'; else setAdjustableTableColumnWidth(table, key, saved || width); } syncAdjustableTableWidth(table); } function restoreAdjustableTableColumnWidths(tableId, page) { var table = document.getElementById(tableId); if (!table) return; lockAdjustableTableColumnWidths(table, page); if (tableId === 'planTable') updatePlanFrozenPositions(); if (_adjustableColumnWidthCache[page]) return; if (!_adjustableColumnWidthRequests[page]) { _adjustableColumnWidthRequests[page] = fetch('/api/planning/column-widths?page=' + encodeURIComponent(page)) .then(function(r) { return r.json(); }) .then(function(res) { return res.code === 0 ? (res.data || {}) : {}; }) .catch(function() { return {}; }) .then(function(widths) { _adjustableColumnWidthCache[page] = widths; var current = document.getElementById(tableId); if (current) { lockAdjustableTableColumnWidths(current, page); if (tableId === 'planTable') updatePlanFrozenPositions(); } }); } } function enableAdjustableTableColumnResize(tableId, page) { var table = document.getElementById(tableId); if (!table) return; var ths = table.querySelectorAll('thead tr:first-child th'); for (var i = 0; i < ths.length - 1; i++) { var th = ths[i]; if (th.querySelector('.col-resize-handle')) continue; var handle = document.createElement('div'); handle.className = 'col-resize-handle'; handle.style.cssText = 'position:absolute;right:-1px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:20'; handle.addEventListener('mousedown', function(idx) { return function(ev) { ev.preventDefault(); ev.stopPropagation(); var cols = table.querySelectorAll('colgroup.adjustable-width-cols col'); _resizeData = {kind:'adjustable', tableId:tableId, page:page, table:table, idx:idx, key:getAdjustableTableColumnKey(ths[idx], idx, ths.length), startX:ev.clientX, startW:parseFloat(cols[idx]?.style.width) || ths[idx].offsetWidth}; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; }(i)); if (th.style.position !== 'sticky') th.style.position = 'relative'; th.appendChild(handle); } } if (!window._colResizeInit) { window._colResizeInit = true; document.addEventListener('mousemove', function(e) { if (!_resizeData) return; _resizeData.pendingX = e.clientX; if (_resizeFrame) return; _resizeFrame = requestAnimationFrame(function() { _resizeFrame = 0; if (_resizeData) updateOrderColumnResize(_resizeData.pendingX); }); }); document.addEventListener('mouseup', function(e) { if (!_resizeData) return; if (_resizeFrame) { cancelAnimationFrame(_resizeFrame); _resizeFrame = 0; } // 松开时应用最终位置;列宽始终只由 colgroup 控制。 updateOrderColumnResize(e.clientX); var table = _resizeData.table; if (_resizeData.kind === 'adjustable') { try { var adjustableThs = table.querySelectorAll('thead tr:first-child th'); var adjustableCols = table.querySelectorAll('colgroup.adjustable-width-cols col'); var adjustableWidths = {}; for (var ai = 0; ai < adjustableThs.length - 1; ai++) { var adjustableKey = getAdjustableTableColumnKey(adjustableThs[ai], ai, adjustableThs.length); var adjustableWidth = adjustableCols[ai] ? parseFloat(adjustableCols[ai].style.width) : adjustableThs[ai].offsetWidth; if (window.getComputedStyle(adjustableThs[ai]).display === 'none') continue; if (adjustableKey && adjustableWidth >= 60) adjustableWidths[adjustableKey] = Math.round(adjustableWidth); } _adjustableColumnWidthCache[_resizeData.page] = adjustableWidths; saveOrderColumnWidthsToServer(_resizeData.page, adjustableWidths); } catch(e) {} document.body.style.cursor = ''; document.body.style.userSelect = ''; _resizeData = null; return; } try { var ths = table.querySelectorAll('thead tr:first-child th'); var cols = table.querySelectorAll('colgroup.order-width-cols col'); var allW = {}; for (var i = 0; i < ths.length - 1; i++) { var key = getOrderColumnKey(table, ths[i], i, ths.length); var width = cols[i] ? parseFloat(cols[i].style.width) : ths[i].offsetWidth; if (key && width >= 60) allW[key] = Math.round(width); } // 本地副本只作为断网兜底;正式配置保存到当前登录账号。 localStorage.setItem(_resizeData.storageKey, JSON.stringify(allW)); saveOrderColumnWidthsToServer(getOrderWidthPage(_resizeData.tableId), allW); } catch(e){} if (_resizeData.tableId === 'domesticOrdersTable') { applyDomesticFrozenPositions(); } else if (_resizeData.tableId === 'overseasOrdersTable') { applyOverseasFrozenPositions(); } else if (_resizeData.tableId === 'packagingOrdersTable') { applyPackagingFrozenPositions(); } document.body.style.cursor = ''; document.body.style.userSelect = ''; _resizeData = null; }); } // ─── 动态计算国内订单冻结列位置(基于实际渲染宽度) ─── function applyDomesticFrozenPositions() { var OFFSET = 1; var table = document.getElementById('domesticOrdersTable'); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; var scope = headerRow.querySelectorAll('th'); var tbody = table.querySelector('tbody'); if (!domesticFrozen || !domesticFrozenFields || domesticFrozenFields.length === 0) { // 取消冻结:清理所有 th 的 sticky for (var i = 1; i < scope.length - 1; i++) { scope[i].style.position = ''; scope[i].style.left = ''; scope[i].style.zIndex = ''; } // 清理所有 td 的 sticky(保留最后一列操作列和第一列勾选框) if (tbody) { for (var ri = 0; ri < tbody.rows.length; ri++) { var tds = tbody.rows[ri].cells; for (var i = 1; i < tds.length - 1; i++) { tds[i].style.position = ''; tds[i].style.left = ''; tds[i].style.zIndex = ''; } } } return; } var frozenSet = {0: true}; for (var fi = 0; fi < domesticFrozenFields.length; fi++) frozenSet[domesticFrozenFields[fi] + OFFSET] = true; // 清除非冻结列 th 的 sticky(保留最后一列的操作列) var lastThIdx = scope.length - 1; for (var i = 0; i < scope.length; i++) { if (i === lastThIdx) continue; if (!frozenSet[i]) { if (!scope[i].querySelector('.col-resize-handle')) { scope[i].style.position = ''; } else { scope[i].style.position = 'relative'; } scope[i].style.left = ''; scope[i].style.zIndex = ''; } } // 一次性测量所有冻结列宽度(勾选框排首位) var widths = []; var acc = 0; var cbW = scope[0].offsetWidth; widths.push(cbW); acc += cbW; for (var fi = 0; fi < domesticFrozenFields.length; fi++) { var idx = domesticFrozenFields[fi] + OFFSET; if (idx >= scope.length) break; var th = scope[idx]; var w = th.offsetWidth; widths.push(w); var leftVal = acc + 'px'; th.style.position = 'sticky'; th.style.top = '0'; th.style.left = leftVal; th.style.zIndex = fi < 2 ? '3' : '11'; th.style.background = 'var(--bg,#f5f5f5)'; acc += w; } // 更新 td var tbody = table.querySelector('tbody'); if (tbody && widths.length > 0) { var rows = tbody.rows; for (var ri = 0; ri < rows.length; ri++) { var tr = rows[ri]; var tds = tr.cells; var lastIdx = tds.length - 1; for (var i = 0; i < tds.length; i++) { // 跳过最后一列(操作列),始终保留 sticky-right if (i === lastIdx) continue; if (!frozenSet[i]) { tds[i].style.position = ''; tds[i].style.left = ''; tds[i].style.zIndex = ''; } } var acc2 = 0; // 勾选框 td 始终冻结 tds[0].style.position = 'sticky'; tds[0].style.left = acc2 + 'px'; tds[0].style.zIndex = '2'; tds[0].style.background = 'var(--card-bg,#fff)'; acc2 += widths[0]; for (var fi = 0; fi < domesticFrozenFields.length; fi++) { var idx = domesticFrozenFields[fi] + OFFSET; if (idx >= tds.length) break; tds[idx].style.position = 'sticky'; tds[idx].style.left = acc2 + 'px'; tds[idx].style.zIndex = '2'; tds[idx].style.background = 'var(--card-bg,#fff)'; acc2 += widths[fi + 1]; } } } } // ─── 动态计算海外订单冻结列位置 ─── function applyOverseasFrozenPositions() { var OFFSET = 1; var table = document.getElementById('overseasOrdersTable'); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; var scope = headerRow.querySelectorAll('th'); var tbody = table.querySelector('tbody'); if (!overseasFrozen || !overseasFrozenFields || overseasFrozenFields.length === 0) { for (var i = 1; i < scope.length - 1; i++) { scope[i].style.position = ''; scope[i].style.left = ''; scope[i].style.zIndex = ''; } if (tbody) { for (var ri = 0; ri < tbody.rows.length; ri++) { var tds = tbody.rows[ri].cells; for (var i = 1; i < tds.length - 1; i++) { tds[i].style.position = ''; tds[i].style.left = ''; tds[i].style.zIndex = ''; } } } return; } var frozenSet = {0: true}; for (var fi = 0; fi < overseasFrozenFields.length; fi++) frozenSet[overseasFrozenFields[fi] + OFFSET] = true; // 清除非冻结列 th 的 sticky(保留拖拽手柄和右侧操作列) for (var i = 0; i < scope.length; i++) { if (!frozenSet[i]) { if (scope[i].style.position === 'sticky' && scope[i].style.right !== '') continue; if (!scope[i].querySelector('.col-resize-handle')) { scope[i].style.position = ''; } else { scope[i].style.position = 'relative'; } scope[i].style.left = ''; scope[i].style.zIndex = ''; } } // 一次性测量所有冻结列宽度(勾选框排首位) var widths = []; var acc = 0; var cbW = scope[0].offsetWidth; widths.push(cbW); acc += cbW; for (var fi = 0; fi < overseasFrozenFields.length; fi++) { var idx = overseasFrozenFields[fi] + OFFSET; if (idx >= scope.length) break; var th = scope[idx]; var w = th.offsetWidth; widths.push(w); var leftVal = acc + 'px'; th.style.position = 'sticky'; th.style.top = '0'; th.style.left = leftVal; th.style.zIndex = fi < 2 ? '3' : '11'; th.style.background = 'var(--bg,#f5f5f5)'; acc += w; } // 更新 td var tbody = table.querySelector('tbody'); if (tbody && widths.length > 0) { var rows = tbody.rows; for (var ri = 0; ri < rows.length; ri++) { var tr = rows[ri]; var tds = tr.cells; for (var i = 0; i < tds.length; i++) { if (!frozenSet[i]) { if (tds[i].style.position === 'sticky' && tds[i].style.right !== '') continue; tds[i].style.position = ''; tds[i].style.left = ''; tds[i].style.zIndex = ''; } } var acc2 = 0; tds[0].style.position = 'sticky'; tds[0].style.left = acc2 + 'px'; tds[0].style.zIndex = '2'; tds[0].style.background = 'var(--card-bg,#fff)'; acc2 += widths[0]; for (var fi = 0; fi < overseasFrozenFields.length; fi++) { var idx = overseasFrozenFields[fi] + OFFSET; if (idx >= tds.length) break; tds[idx].style.position = 'sticky'; tds[idx].style.left = acc2 + 'px'; tds[idx].style.zIndex = '2'; tds[idx].style.background = 'var(--card-bg,#fff)'; acc2 += widths[fi + 1]; } } } } window.domesticEditRow = function(id) { // 简单编辑弹窗 var row = document.querySelector('#domesticOrdersTbody tr:nth-child(' + (id) + ')'); showCenteredPrompt('编辑订单', '输入新值(格式: 字段名=值),例如: 状态=已完结').then(function(newVal) { if (!newVal) return; var parts = newVal.split('='); if (parts.length < 2) { alert('格式错误,请用: 字段名=值'); return; } var field = parts[0].trim(); var value = parts.slice(1).join('=').trim(); fetch('/api/domestic/orders/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: field, value: value}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { domesticLoadOrders(domesticOrderPage); } else { alert(res.msg || '更新失败'); } }); }); }; window.domesticDeleteRow = function(id) { if (!window._hasPagePerm('orders_domestic', 'delete')) { alert('权限不足,当前账号没有删除订单权限'); return; } showCenteredConfirm('确认', '确认删除此记录?').then(function(ok) { if (!ok) return; fetch('/api/domestic/orders/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('domestic'); domesticLoadOrders(domesticOrderPage); } else { alert(res.msg || '删除失败'); } }); }); }; function domesticToggleAll(master) { var cbs = document.querySelectorAll('.domestic-checkbox'); cbs.forEach(function(cb) { cb.checked = master.checked; }); } function domesticBatchGenerateContract() { var cbs = document.querySelectorAll('.domestic-checkbox:checked'); var ids = []; cbs.forEach(function(cb) { ids.push(parseInt(cb.getAttribute('data-id'))); }); if (ids.length === 0) { alert('请先勾选要生成合同的订单'); return; } generateContractForOrders('domestic', ids); } window.domesticAddRow = function() { if (!window._hasPagePerm('orders_domestic', 'create')) { alert('权限不足,当前账号没有新增订单权限'); return; } showCenteredPrompt('新增订单', '请输入货品编号(必填):').then(function(code) { if (!code) return; showCenteredPrompt('新增订单', '请输入货品名称:').then(function(name) { fetch('/api/domestic/orders/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({'货品编号': code, '货品名称': name || ''}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('domestic'); domesticLoadOrders(1); } else { alert(res.msg || '添加失败'); } }); }); }); }; function _orderExportCsvValue(value) { var text = value === null || value === undefined ? '' : String(value); return '"' + text.replace(/"/g, '""').replace(/\r?\n/g, ' ') + '"'; } function _orderExportColumns(type) { var cols = _getOrderColumns(type); var order = type === 'domestic' ? window._domesticDragOrder : type === 'overseas' ? window._overseasDragOrder : window._packagingDragOrder; var visible = type === 'domestic' ? window.domesticColVisible : type === 'overseas' ? window.overseasColVisible : window.packagingColVisible; order = order && order.length ? order : cols.map(function(c) { return c.field; }); return order.map(function(field) { return cols.find(function(c) { return c.field === field; }); }).filter(function(col) { return col && (!visible || visible[col.field] !== false); }); } function _orderExport(type, state, endpoint, filename) { var columns = _orderExportColumns(type); var params = {page: 1, size: 100000}; if (state.search) params.search = state.search; if (state.status) params.status = state.status; if (state.supplier) params.supplier = state.supplier; if (state.orderer) params.orderer = state.orderer; var query = Object.keys(params).map(function(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&'); fetch(endpoint + '?' + query, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: state.orderFilter || {}}) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) throw new Error(res.msg || '导出失败'); var rows = (res.data && res.data.rows) || []; var lines = [columns.map(function(col) { return _orderExportCsvValue(col.label); }).join(',')]; rows.forEach(function(row) { lines.push(columns.map(function(col) { return _orderExportCsvValue(row[col.field]); }).join(',')); }); if (rows.length === 0) return; var blob = new Blob(['\ufeff' + lines.join('\n')], {type: 'text/csv;charset=utf-8'}); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename + '_' + new Date().toISOString().slice(0, 10) + '.csv'; a.click(); setTimeout(function() { URL.revokeObjectURL(a.href); }, 1000); }).catch(function(e) { alert(e.message || '导出失败'); }); } window.domesticExport = function() { _orderExport('domestic', { search: domesticSearch, status: domesticFilterStatus, supplier: domesticFilterSupplier, orderer: domesticFilterOrderer, orderFilter: domesticOrderFilter }, '/api/domestic/orders/list', '国内订单'); }; // ──────────── 海外订单管理 ──────────── var OVERSEAS_PAGE_SIZE = 100; var overseasOrderPage = 1; var overseasSearch = ''; var overseasFilterStatus = ''; var overseasFilterSupplier = ''; var overseasFilterOrderer = ''; var overseasOrderFilter = {}; var overseasOrderFilterActive = {}; var overseasOrderFilterMenu = null; var overseasOrderFilterField = ''; var overseasFrozen = localStorage.getItem('overseasFrozen') !== 'false'; var overseasFrozenFields = (function(){ var arr; try { arr = JSON.parse(localStorage.getItem('overseasFrozenFields')) || [0,1,2]; } catch(e){ arr = [0,1,2]; } return arr.filter(function(v) { return v < 3; }); })(); function loadOverseasOrdersPage(container) { container.innerHTML = `

🌍 海外采购下单明细

序号' + label + filterControl + '做货周期
加载中...
暂无数据
' + escHtml(r['货品编号'] || '') + '' + escHtml(r['货品名称'] || '') + '' + escHtml(r['货品类型'] || '') + '' + escHtml(window._padDateStr(r['日期'] || '')) + '' + escHtml(t) + '' + escHtml(r['新旧品'] || '') + '' + escHtml(r['下单备注'] || '-') + '' + escHtml(r['单价'] == null ? '' : String(r['单价'])) + '' + escHtml(r['是否审核价格'] || '') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (amt ? '¥' + amt.toLocaleString() : '') + '' + escHtml(r['供货商'] || '') + '' + escHtml(r['仓库'] || '') + '' + escHtml(r['金蝶采购单'] || '') + '' + renderNameBadges(r['采购员']) + '' + renderNameBadges(r['下单人'], true) + '' + escHtml(r['进度'] || '') + '' + escHtml(r['旺店通生产单号'] || '') + '' + escHtml(r['是否已录入旺店通'] || '') + '' + renderNameBadges(r['采购开发']) + '' + escHtml(r['实际到货日期'] || '') + '' + (parseInt(r['未到货数']) || 0).toLocaleString() + '' + escHtml(r['预计到货日期'] || '') + '' + escHtml(r['金蝶入库'] || '') + '' + escHtml(r['状态'] || '') + '' + (parseInt(r['实际到货数量']) || 0).toLocaleString() + '
' + rowNum + '' + escHtml(r[field] || '') + '' + manuDays + '
'; html += ''; html += ''; html += ''; html += '
状态
操作
操作
`; // 动态生成海外表头列 (function() { var cols = OVERSEAS_COLUMNS; var order = window._overseasDragOrder || cols.map(function(c) { return c.field; }); order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; if (!window.overseasColVisible[field]) return; var fieldAttr = col.field; var thStyle = ''; if (fieldAttr === '产品编码') thStyle = 'min-width:120px;position:sticky;left:90px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '货品名称') thStyle = 'min-width:200px;position:sticky;left:210px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '货品类型') thStyle = 'min-width:100px;position:sticky;left:410px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '日期') thStyle = 'width:85px;min-width:85px;position:sticky;left:510px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '计划下单时间' || fieldAttr === '新旧品') thStyle = 'width:1%;white-space:nowrap'; else if (fieldAttr === '单价') thStyle = 'min-width:70px'; else if (fieldAttr === '是否审核价格') thStyle = 'min-width:80px'; else if (fieldAttr === '数量') thStyle = 'min-width:60px'; else if (fieldAttr === '金额') thStyle = 'min-width:80px'; else if (fieldAttr === '供应商') thStyle = 'min-width:100px'; else if (fieldAttr === '仓库') thStyle = 'min-width:80px'; else if (fieldAttr === '是否已做采购单') thStyle = 'min-width:110px'; else if (fieldAttr === '是否做入库') thStyle = 'min-width:70px'; else if (fieldAttr === '采购员') thStyle = 'min-width:150px'; else if (fieldAttr === '下单人') thStyle = 'min-width:70px'; else if (fieldAttr === '进度') thStyle = 'min-width:100px'; else if (fieldAttr === '下单备注') thStyle = 'min-width:150px;white-space:normal'; else if (fieldAttr === '采购开发') thStyle = 'min-width:100px'; else if (fieldAttr === '旺店通生产单号') thStyle = 'min-width:120px'; else if (fieldAttr === '是否已录入到旺店通') thStyle = 'min-width:110px'; else if (fieldAttr === '实际到货日期' || fieldAttr === '预计到货日期') thStyle = 'min-width:100px'; else if (fieldAttr === '未到货数') thStyle = 'min-width:80px'; else if (fieldAttr === '状态') thStyle = 'min-width:70px'; else { thStyle = ''; } var filterControl = ' ▾'; container.querySelector('#overseasOrdersHeader').innerHTML += ''; }); })(); container.querySelector('#overseasOrdersHeader').innerHTML += ''; container.querySelector('#overseasOrdersHeader').innerHTML += renderOrderActionHeader('overseas'); container.querySelector('#overseasOrdersTable').innerHTML += ''; applyOrderActionState('overseas'); container.querySelector('#overseasContent').innerHTML += ''; // 海外冻结列弹窗 + 字段设置弹窗 container.innerHTML += ` `; // 初始化字段设置checkbox var ocGrid = document.getElementById('overseasColGrid'); if (ocGrid) { window._overseasDragOrder = OVERSEAS_COLUMNS.map(function(c) { return c.field; }); renderOrderColCheckboxes('overseas'); } overseasLoadFilters(); Promise.all([loadOrderFollowers(), loadPersonnelGroupsForLinkedPages()]).then(function() { overseasLoadOrders(1); }); loadDefaultOrderScheme('overseas'); // 后台预拉取所有列的筛选值 prefetchOrderColumnValues('overseas'); } function overseasLoadFilters() { fetch('/api/overseas/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var so = document.getElementById('overseasOrdererFilter'); (res.data.orderers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; so.appendChild(opt); }); }); } function overseasDoSearch() { overseasApplyFilters(); } function overseasApplyFilters() { overseasSearch = document.getElementById('overseasSearchInput').value.trim(); overseasFilterSupplier = document.getElementById('overseasSupplierFilter').value.trim(); overseasFilterStatus = document.getElementById('overseasStatusFilter').value; overseasFilterOrderer = document.getElementById('overseasOrdererFilter').value; overseasLoadOrders(1); } function overseasResetFilters() { document.getElementById('overseasSearchInput').value = ''; document.getElementById('overseasSupplierFilter').value = ''; document.getElementById('overseasStatusFilter').value = ''; document.getElementById('overseasOrdererFilter').value = ''; overseasSearch = ''; overseasFilterSupplier = ''; overseasFilterStatus = ''; overseasFilterOrderer = ''; overseasOrderFilter = {}; overseasOrderFilterActive = {}; _syncOrderFilterIcons('overseas'); overseasLoadOrders(1); } function overseasLoadOrders(page) { overseasOrderPage = page; var params = '?page=' + page + '&size=' + OVERSEAS_PAGE_SIZE; if (overseasSearch) params += '&search=' + encodeURIComponent(overseasSearch); if (overseasFilterStatus) params += '&status=' + encodeURIComponent(overseasFilterStatus); if (overseasFilterSupplier) params += '&supplier=' + encodeURIComponent(overseasFilterSupplier); if (overseasFilterOrderer) params += '&orderer=' + encodeURIComponent(overseasFilterOrderer); fetch('/api/overseas/orders/list' + params, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: overseasOrderFilter}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('overseasOrdersTbody'); var totalInfo = document.getElementById('overseasTotalInfo'); if (totalInfo) totalInfo.textContent = '共 ' + data.total + ' 条'; if (!data.rows || data.rows.length === 0) { tbody.innerHTML = ''; } else { var startIdx = (page - 1) * OVERSEAS_PAGE_SIZE; var html = ''; data.rows.forEach(function(r, i) { var rowNum = startIdx + i + 1; html += ''; html += ''; html += ''; // 严格按当前页面已生成的表头顺序渲染单元格,避免账号字段方案异步加载后错列。 var order = Array.from(document.querySelectorAll('#overseasOrdersHeader th[data-field]')).map(function(th) { return th.getAttribute('data-field'); }); order.forEach(function(field) { var cellHtml = ''; if (field === '产品编码') { cellHtml = ''; } else if (field === '货品名称') { cellHtml = ''; } else if (field === '货品类型') { cellHtml = ''; } else if (field === '日期') { cellHtml = ''; } else if (field === '计划下单时间') { var planTime = window._padDateStr(r['计划下单时间'] || ''); if (planTime.length > 16) planTime = planTime.slice(0, 16); cellHtml = ''; } else if (field === '单价') { cellHtml = ''; } else if (field === '是否审核价格') { cellHtml = ''; } else if (field === '数量') { cellHtml = ''; } else if (field === '金额') { var priceVal = parseFloat(r['单价']) || 0; var qtyVal = parseInt(r['数量']) || 0; var amtVal = priceVal * qtyVal; cellHtml = ''; } else if (field === '供应商') { cellHtml = ''; } else if (field === '仓库') { cellHtml = ''; } else if (field === '新旧品') { cellHtml = ''; } else if (field === '是否已做采购单') { cellHtml = ''; } else if (field === '是否做入库') { cellHtml = ''; } else if (field === '采购员') { cellHtml = ''; } else if (field === '下单人') { cellHtml = ''; } else if (field === '进度') { cellHtml = ''; } else if (field === '下单备注') { cellHtml = ''; } else if (field === '采购开发') { cellHtml = ''; } else if (field === '旺店通生产单号') { cellHtml = ''; } else if (field === '是否已录入到旺店通') { cellHtml = ''; } else if (field === '实际到货日期') { cellHtml = ''; } else if (field === '预计到货日期') { cellHtml = ''; } else if (field === '实际到货数量') { cellHtml = ''; } else if (field === '未到货数') { cellHtml = ''; } else if (field === '状态') { cellHtml = ''; } else { cellHtml = ''; } html += cellHtml; }); // 做货周期 = 实际到货日期 - 跟单下单日期 var manuDaysOv = ''; if (r['实际到货日期'] && r['日期']) { var d1 = new Date(r['实际到货日期'].replace(/-/g, '/')); var d2 = new Date(r['日期'].replace(/-/g, '/')); if (!isNaN(d1) && !isNaN(d2)) { var diff = Math.round((d1 - d2) / 86400000); if (diff >= 0) manuDaysOv = diff + '天'; } } html += ''; html += ''; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('overseasOrdersTable', 'orders_overseas'); _syncOrderFilterIcons('overseas'); restoreColumnWidths('overseasOrdersTable', 'overseasColWidths'); applyOverseasFrozenPositions(); enableColumnResize('overseasOrdersTable', 'overseasColWidths'); var pel = document.getElementById('overseasOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, OVERSEAS_PAGE_SIZE, 'overseasLoadOrders', 'OVERSEAS_PAGE_SIZE'); } } }); } window.overseasEditRow = function(id) { showCenteredPrompt('编辑订单', '输入新值(格式: 字段名=值),例如: 状态=已完结').then(function(newVal) { if (!newVal) return; var parts = newVal.split('='); if (parts.length < 2) { alert('格式错误,请用: 字段名=值'); return; } var field = parts[0].trim(); var value = parts.slice(1).join('=').trim(); fetch('/api/overseas/orders/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: field, value: value}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { overseasLoadOrders(overseasOrderPage); } else { alert(res.msg || '更新失败'); } }); }); }; window.overseasDeleteRow = function(id) { if (!window._hasPagePerm('orders_overseas', 'delete')) { alert('权限不足,当前账号没有删除订单权限'); return; } showCenteredConfirm('确认', '确认删除此记录?').then(function(ok) { if (!ok) return; fetch('/api/overseas/orders/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('overseas'); overseasLoadOrders(overseasOrderPage); } else { alert(res.msg || '删除失败'); } }); }); }; window.overseasAddRow = function() { if (!window._hasPagePerm('orders_overseas', 'create')) { alert('权限不足,当前账号没有新增订单权限'); return; } showCenteredPrompt('新增订单', '请输入产品编码(必填):').then(function(code) { if (!code) return; showCenteredPrompt('新增订单', '请输入货品名称:').then(function(name) { fetch('/api/overseas/orders/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({'产品编码': code, '货品名称': name || ''}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('overseas'); overseasLoadOrders(1); } else { alert(res.msg || '添加失败'); } }); }); }); }; window.overseasExport = function() { _orderExport('overseas', { search: overseasSearch, status: overseasFilterStatus, supplier: overseasFilterSupplier, orderer: overseasFilterOrderer, orderFilter: overseasOrderFilter }, '/api/overseas/orders/list', '海外订单'); }; window.overseasToggleAll = function(master) { var cbs = document.querySelectorAll('.overseas-checkbox'); cbs.forEach(function(cb) { cb.checked = master.checked; }); }; window.overseasBatchGenerateContract = function() { var cbs = document.querySelectorAll('.overseas-checkbox:checked'); var ids = []; cbs.forEach(function(cb) { ids.push(parseInt(cb.getAttribute('data-id'))); }); if (ids.length === 0) { alert('请先勾选要生成合同的订单'); return; } generateContractForOrders('overseas', ids); }; // ──────────── 包材订单管理 ──────────── var PACKAGING_PAGE_SIZE = 100; var packagingOrderPage = 1; var packagingSearch = ''; var packagingFilterStatus = ''; var packagingFilterSupplier = ''; var packagingFilterOrderer = ''; var packagingOrderFilter = {}; var packagingOrderFilterActive = {}; var packagingOrderFilterMenu = null; var packagingOrderFilterField = ''; var packagingFrozen = localStorage.getItem('packagingFrozen') !== 'false'; // ─── 包材订单管理筛选状态:不再持久化,刷新后自动清空(与国内/海外订单一致) ─── function _savePackagingFilterState() { // 已移除 sessionStorage 持久化:强制刷新后筛选状态应清空,不再自动恢复 } function _restorePackagingFilterState() { return false; } var packagingFrozenFields = (function(){ var arr; try { arr = JSON.parse(localStorage.getItem('packagingFrozenFields')) || [0,1,2]; } catch(e){ arr = [0,1,2]; } return arr.filter(function(v) { return v < 3; }); })(); // ─── 冻结列切换函数 ─── window.openDomesticFrozenModal = function() { // 从 localStorage 加载当前冻结列设置 try { domesticFrozenFields = JSON.parse(localStorage.getItem('domesticFrozenFields')) || [0,1,2]; } catch(e){ domesticFrozenFields = [0,1,2]; } var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结 domesticFrozenFields = domesticFrozenFields.filter(function(v) { return v < labels.length; }); var html = '
'; html += '
'; labels.forEach(function(label, idx) { var checked = domesticFrozenFields.indexOf(idx) >= 0; html += ''; }); html += '
'; document.getElementById('domesticFrozenModalBody').innerHTML = html; document.getElementById('domesticFrozenModal').style.display = 'flex'; }; window.domesticFrozenCbChange = function() { var checked = []; document.querySelectorAll('.domestic-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); }); domesticFrozenFields = checked.sort(function(a,b){return a-b;}); var allCb = document.getElementById('domesticFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.domestic-frozen-cb').length; }; window.domesticFrozenToggleAll = function(checked) { document.querySelectorAll('.domestic-frozen-cb').forEach(function(cb) { cb.checked = checked; }); domesticFrozenCbChange(); }; function closeDomesticFrozenModal() { document.getElementById('domesticFrozenModal').style.display = 'none'; } function confirmDomesticFrozen() { domesticFrozen = domesticFrozenFields.length > 0; localStorage.setItem('domesticFrozen', domesticFrozen); localStorage.setItem('domesticFrozenFields', JSON.stringify(domesticFrozenFields)); closeDomesticFrozenModal(); domesticLoadOrders(1); } // ─── 海外冻结列弹窗 ─── window.openOverseasFrozenModal = function() { try { overseasFrozenFields = JSON.parse(localStorage.getItem('overseasFrozenFields')) || [0,1,2]; } catch(e){ overseasFrozenFields = [0,1,2]; } var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结 overseasFrozenFields = overseasFrozenFields.filter(function(v) { return v < labels.length; }); var html = '
'; html += '
'; labels.forEach(function(label, idx) { var checked = overseasFrozenFields.indexOf(idx) >= 0; html += ''; }); html += '
'; document.getElementById('overseasFrozenModalBody').innerHTML = html; document.getElementById('overseasFrozenModal').style.display = 'flex'; }; window.overseasFrozenCbChange = function() { var checked = []; document.querySelectorAll('.overseas-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); }); overseasFrozenFields = checked.sort(function(a,b){return a-b;}); var allCb = document.getElementById('overseasFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.overseas-frozen-cb').length; }; window.overseasFrozenToggleAll = function(checked) { document.querySelectorAll('.overseas-frozen-cb').forEach(function(cb) { cb.checked = checked; }); overseasFrozenCbChange(); }; function closeOverseasFrozenModal() { document.getElementById('overseasFrozenModal').style.display = 'none'; } function confirmOverseasFrozen() { overseasFrozen = overseasFrozenFields.length > 0; localStorage.setItem('overseasFrozen', overseasFrozen); localStorage.setItem('overseasFrozenFields', JSON.stringify(overseasFrozenFields)); closeOverseasFrozenModal(); overseasLoadOrders(1); } // ─── 包材冻结列弹窗 ─── window.openPackagingFrozenModal = function() { try { packagingFrozenFields = JSON.parse(localStorage.getItem('packagingFrozenFields')) || [0,1,2]; } catch(e){ packagingFrozenFields = [0,1,2]; } var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结 packagingFrozenFields = packagingFrozenFields.filter(function(v) { return v < labels.length; }); var html = '
'; html += '
'; labels.forEach(function(label, idx) { var checked = packagingFrozenFields.indexOf(idx) >= 0; html += ''; }); html += '
'; document.getElementById('packagingFrozenModalBody').innerHTML = html; document.getElementById('packagingFrozenModal').style.display = 'flex'; }; window.packagingFrozenCbChange = function() { var checked = []; document.querySelectorAll('.packaging-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); }); packagingFrozenFields = checked.sort(function(a,b){return a-b;}); var allCb = document.getElementById('packagingFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.packaging-frozen-cb').length; }; window.packagingFrozenToggleAll = function(checked) { document.querySelectorAll('.packaging-frozen-cb').forEach(function(cb) { cb.checked = checked; }); packagingFrozenCbChange(); }; function closePackagingFrozenModal() { document.getElementById('packagingFrozenModal').style.display = 'none'; } function confirmPackagingFrozen() { packagingFrozen = packagingFrozenFields.length > 0; localStorage.setItem('packagingFrozen', packagingFrozen); localStorage.setItem('packagingFrozenFields', JSON.stringify(packagingFrozenFields)); closePackagingFrozenModal(); packagingLoadOrders(1); } // ─── 包材冻结列位置 ─── function applyPackagingFrozenPositions() { // 包装表格: [checkbox(0), 序号(1), 货品编码(2), 货品名称(3), 日期(4), ...] var frozenColMap = function(idx) { return idx + 1; }; var table = document.getElementById('packagingOrdersTable'); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; var scope = headerRow.querySelectorAll('th'); var tbody = table.querySelector('tbody'); // checkbox(0) 和 序号(1) 始终固定 var frozenSet = {0: true, 1: true}; if (packagingFrozen && packagingFrozenFields) { for (var fi = 0; fi < packagingFrozenFields.length; fi++) { frozenSet[frozenColMap(packagingFrozenFields[fi])] = true; } } // 处理 th:清除非冻结列的 sticky,设置冻结列 for (var i = 0; i < scope.length; i++) { // 操作列保留 sticky:right if (scope[i].style.position === 'sticky' && scope[i].style.right !== '') continue; if (frozenSet[i]) { scope[i].style.position = 'sticky'; scope[i].style.top = '0'; scope[i].style.zIndex = '3'; } else { scope[i].style.position = ''; scope[i].style.left = ''; scope[i].style.zIndex = ''; } } scope[0].style.left = '0px'; // checkbox 始终 left:0 // 处理 td if (tbody) { for (var ri = 0; ri < tbody.rows.length; ri++) { var tds = tbody.rows[ri].cells; for (var i = 0; i < tds.length; i++) { if (tds[i].style.position === 'sticky' && tds[i].style.right !== '') continue; if (frozenSet[i]) { tds[i].style.position = 'sticky'; tds[i].style.top = '0'; tds[i].style.zIndex = '2'; } else { tds[i].style.position = ''; tds[i].style.left = ''; tds[i].style.zIndex = ''; } } tds[0].style.left = '0px'; tds[0].style.background = 'var(--card-bg,#fff)'; } } // 测量冻结列宽度并设置 left 偏移 var widths = []; var acc = 0; // 获取所有冻结列的宽度(按 col index 顺序) var frozenIdxList = []; for (var i = 0; i <= 10; i++) { if (frozenSet[i]) frozenIdxList.push(i); } frozenIdxList.forEach(function(idx) { var w = idx < scope.length ? scope[idx].offsetWidth : 0; widths.push(w); }); // 为每个冻结的 th 设置 left for (var fi = 1; fi < frozenIdxList.length; fi++) { var idx = frozenIdxList[fi]; if (idx >= scope.length) break; acc += widths[fi - 1]; scope[idx].style.left = acc + 'px'; } // 为 td 设置 left if (tbody && frozenIdxList.length > 1) { for (var ri = 0; ri < tbody.rows.length; ri++) { var tds = tbody.rows[ri].cells; var acc2 = widths[0]; // checkbox width for (var fi = 1; fi < frozenIdxList.length; fi++) { var idx = frozenIdxList[fi]; if (idx >= tds.length) break; tds[idx].style.left = acc2 + 'px'; tds[idx].style.background = 'var(--card-bg,#fff)'; acc2 += widths[fi]; } } } // 一次性测量所有冻结列宽度(勾选框排首位) var widths = []; var acc = 0; var cbW = scope[0].offsetWidth; widths.push(cbW); acc += cbW; for (var fi = 0; fi < packagingFrozenFields.length; fi++) { var idx = frozenColMap(packagingFrozenFields[fi]); if (idx >= scope.length) break; var th = scope[idx]; var w = th.offsetWidth; widths.push(w); var leftVal = acc + 'px'; th.style.position = 'sticky'; th.style.top = '0'; th.style.left = leftVal; th.style.zIndex = fi < 2 ? '3' : '11'; th.style.background = 'var(--bg,#f5f5f5)'; acc += w; } // 更新 td if (tbody && widths.length > 0) { var rows = tbody.rows; for (var ri = 0; ri < rows.length; ri++) { var tr = rows[ri]; var tds = tr.cells; var acc2 = 0; tds[0].style.position = 'sticky'; tds[0].style.left = acc2 + 'px'; tds[0].style.zIndex = '2'; tds[0].style.background = 'var(--card-bg,#fff)'; acc2 += widths[0]; for (var fi = 0; fi < packagingFrozenFields.length; fi++) { var idx = frozenColMap(packagingFrozenFields[fi]); if (idx >= tds.length) break; tds[idx].style.position = 'sticky'; tds[idx].style.left = acc2 + 'px'; tds[idx].style.zIndex = '2'; tds[idx].style.background = 'var(--card-bg,#fff)'; acc2 += widths[fi + 1]; } } } } function loadPackagingOrdersPage(container) { container.innerHTML = `

📦 包材采购下单明细

序号' + col.label + filterControl + '做货周期
加载中...
暂无数据
' + rowNum + '' + escHtml(r['产品编码'] || '') + '' + escHtml(r['货品名称'] || '') + '' + escHtml(r['货品类型'] || '') + '' + escHtml(window._padDateStr(r['日期'] || '')) + '' + escHtml(planTime) + '' + escHtml(r['单价'] == null ? '' : String(r['单价'])) + '' + escHtml(r['是否审核价格'] || '未审核') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (amtVal ? '¥' + amtVal.toLocaleString() : '') + '' + escHtml(r['供应商'] || '') + '' + escHtml(r['仓库'] || '') + '' + escHtml(r['新旧品'] || '') + '' + escHtml(r['是否已做采购单'] || '') + '' + escHtml(r['是否做入库'] || '') + '' + renderNameBadges(r['采购员']) + '' + renderNameBadges(r['下单人'], true) + '' + escHtml(r['进度'] || '') + '' + escHtml(r['下单备注'] || '-') + '' + renderNameBadges(r['采购开发']) + '' + escHtml(r['旺店通生产单号'] || '') + '' + escHtml(r['是否已录入到旺店通'] || '') + '' + escHtml(r['实际到货日期'] || '') + '' + escHtml(r['预计到货日期'] || '') + '' + (parseInt(r['实际到货数量']) || 0).toLocaleString() + '' + (parseInt(r['未到货数']) || 0).toLocaleString() + '' + escHtml(r['状态'] || '') + '' + escHtml(r[field] || '') + '' + manuDaysOv + '
'; html += ''; if (window._hasPagePerm('orders_overseas', 'edit')) html += ''; if (window._hasPagePerm('orders_overseas', 'delete')) html += ''; html += '
`; // 动态生成包材表头列 (function() { var cols = PACKAGING_COLUMNS; var order = window._packagingDragOrder || cols.map(function(c) { return c.field; }); order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; if (!window.packagingColVisible[field]) return; var fieldAttr = col.field; var thStyle = ''; if (fieldAttr === '计划下单时间') thStyle = 'min-width:110px'; else if (fieldAttr === '日期') thStyle = 'min-width:110px'; else if (fieldAttr === '编码') thStyle = 'min-width:140px'; else if (fieldAttr === '名称') thStyle = 'min-width:250px'; else if (fieldAttr === '单价') thStyle = 'min-width:70px'; else if (fieldAttr === '数量') thStyle = 'min-width:60px'; else if (fieldAttr === '合计') thStyle = 'min-width:80px'; else if (fieldAttr === '供应商') thStyle = 'min-width:100px'; else if (fieldAttr === '下单备注') thStyle = 'min-width:120px'; else if (fieldAttr === '仓库到货') thStyle = 'min-width:100px'; else if (fieldAttr === '进度') thStyle = 'min-width:160px'; else if (fieldAttr === '是否已做采购单') thStyle = 'min-width:110px'; else if (fieldAttr === '是否已做入库') thStyle = 'min-width:70px'; else if (fieldAttr === '采购下单人') thStyle = 'min-width:80px'; else if (fieldAttr === '下单人') thStyle = 'min-width:110px'; else if (fieldAttr === '旺店通编号') thStyle = 'min-width:120px'; else if (fieldAttr === '是否已录入旺店通') thStyle = 'min-width:110px'; else if (fieldAttr === '到货日期') thStyle = 'min-width:100px'; else if (fieldAttr === '状态') thStyle = 'min-width:70px'; else if (fieldAttr === '做货天数') thStyle = 'min-width:70px'; else if (fieldAttr === '包材开发') thStyle = 'min-width:100px'; else { thStyle = ''; } var filterControl = ' ▾'; container.querySelector('#packagingOrdersTable thead tr').innerHTML += ''; }); })(); container.querySelector('#packagingOrdersTable thead tr').innerHTML += renderOrderActionHeader('packaging'); container.querySelector('#packagingOrdersTable').innerHTML += ''; applyOrderActionState('packaging'); container.querySelector('#packagingContent').innerHTML += ''; // 包材冻结列弹窗 + 字段设置弹窗 container.innerHTML += ` `; // 初始化字段设置checkbox var pcGrid = document.getElementById('packagingColGrid'); if (pcGrid) { window._packagingDragOrder = PACKAGING_COLUMNS.map(function(c) { return c.field; }); renderOrderColCheckboxes('packaging'); } packagingLoadFilters().then(function() { loadDefaultOrderScheme('packaging'); // 恢复上次的筛选状态(切页/刷新后保留搜索、筛选、表头筛选、页码) var restoredPage = 1; if (_restorePackagingFilterState()) { restoredPage = packagingOrderPage || 1; var _si = document.getElementById('packagingSearchInput'); if (_si) _si.value = packagingSearch; var _sf = document.getElementById('packagingSupplierFilter'); if (_sf) _sf.value = packagingFilterSupplier; var _ss = document.getElementById('packagingStatusFilter'); if (_ss) _ss.value = packagingFilterStatus; var _so = document.getElementById('packagingOrdererFilter'); if (_so) _so.value = packagingFilterOrderer; _syncOrderFilterIcons('packaging'); } // Load members and personnel groups before rendering the list. Promise.all([loadOrderFollowers(), loadPersonnelGroupsForLinkedPages()]).then(function() { packagingLoadOrders(restoredPage); }); }); // 后台预拉取所有列的筛选值 prefetchOrderColumnValues('packaging'); } function packagingLoadFilters() { return fetch('/api/packaging/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var so = document.getElementById('packagingOrdererFilter'); (res.data.orderers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; so.appendChild(opt); }); }); } function packagingDoSearch() { packagingApplyFilters(); } function packagingApplyFilters() { packagingSearch = document.getElementById('packagingSearchInput').value.trim(); packagingFilterSupplier = document.getElementById('packagingSupplierFilter').value.trim(); packagingFilterStatus = document.getElementById('packagingStatusFilter').value; packagingFilterOrderer = document.getElementById('packagingOrdererFilter').value; _savePackagingFilterState(); packagingLoadOrders(1); } function packagingResetFilters() { document.getElementById('packagingSearchInput').value = ''; document.getElementById('packagingSupplierFilter').value = ''; document.getElementById('packagingStatusFilter').value = ''; document.getElementById('packagingOrdererFilter').value = ''; packagingSearch = ''; packagingFilterSupplier = ''; packagingFilterStatus = ''; packagingFilterOrderer = ''; packagingOrderFilter = {}; packagingOrderFilterActive = {}; _savePackagingFilterState(); _syncOrderFilterIcons('packaging'); packagingLoadOrders(1); } function updatePackagingOrdererSelectColor(selEl) { var selectedOpt = selEl.options[selEl.selectedIndex]; if (selectedOpt && selectedOpt.value) { selEl.style.background = selectedOpt.style.background || '#fff'; selEl.style.color = selectedOpt.style.color || '#333'; } else { selEl.style.background = '#fff'; selEl.style.color = '#333'; } } function packagingLoadOrders(page) { packagingOrderPage = page; var params = '?page=' + page + '&size=' + PACKAGING_PAGE_SIZE; if (packagingSearch) params += '&search=' + encodeURIComponent(packagingSearch); if (packagingFilterStatus) params += '&status=' + encodeURIComponent(packagingFilterStatus); if (packagingFilterSupplier) params += '&supplier=' + encodeURIComponent(packagingFilterSupplier); if (packagingFilterOrderer) params += '&orderer=' + encodeURIComponent(packagingFilterOrderer); _savePackagingFilterState(); fetch('/api/packaging/orders/list' + params, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: packagingOrderFilter}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('packagingOrdersTbody'); if (document.getElementById('packagingTotalInfo')) document.getElementById('packagingTotalInfo').textContent = '共 ' + data.total + ' 条'; if (!data.rows || data.rows.length === 0) { tbody.innerHTML = ''; } else { var startIdx = (page - 1) * PACKAGING_PAGE_SIZE; var html = ''; data.rows.forEach(function(r, i) { var rowNum = startIdx + i + 1; html += ''; html += ''; html += ''; // 严格按当前页面已生成的表头顺序渲染单元格。 // 表头生成后,字段方案或本地列设置可能异步更新全局顺序;若表体继续读取全局状态, // 首次加载及表头筛选后的重绘就会出现表头与内容错列。 var order = Array.from(document.querySelectorAll('#packagingOrdersTable thead th[data-field]')).map(function(th) { return th.getAttribute('data-field'); }); order.forEach(function(field) { var cellHtml = ''; if (field === '计划下单时间') { cellHtml = ''; } else if (field === '日期') { cellHtml = ''; } else if (field === '编码') { cellHtml = ''; } else if (field === '名称') { cellHtml = ''; } else if (field === '货品类型') { cellHtml = ''; } else if (field === '单价') { cellHtml = ''; } else if (field === '是否审核价格') { cellHtml = ''; } else if (field === '数量') { cellHtml = ''; } else if (field === '合计') { var priceVal = parseFloat(r['单价']) || 0; var qtyVal = parseInt(r['数量']) || 0; var amtVal = priceVal * qtyVal; cellHtml = ''; } else if (field === '供应商') { cellHtml = ''; } else if (field === '下单备注') { cellHtml = ''; } else if (field === '仓库到货') { cellHtml = ''; } else if (field === '进度') { cellHtml = ''; } else if (field === '是否已做采购单') { cellHtml = ''; } else if (field === '是否已做入库') { cellHtml = ''; } else if (field === '采购下单人') { cellHtml = ''; } else if (field === '下单人') { cellHtml = ''; } else if (field === '旺店通编号') { cellHtml = ''; } else if (field === '是否已录入旺店通') { cellHtml = ''; } else if (field === '到货日期') { cellHtml = ''; } else if (field === '状态') { cellHtml = ''; } else if (field === '做货天数') { cellHtml = ''; } else if (field === '实际到货数量') { cellHtml = ''; } else if (field === '未到货数') { cellHtml = ''; } else if (field === '包材开发') { cellHtml = ''; } else { cellHtml = ''; } html += cellHtml; }); html += ''; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('packagingOrdersTable', 'orders_packaging'); _syncOrderFilterIcons('packaging'); restoreColumnWidths('packagingOrdersTable', 'packagingColWidths'); applyPackagingFrozenPositions(); enableColumnResize('packagingOrdersTable', 'packagingColWidths'); var pel = document.getElementById('packagingOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, PACKAGING_PAGE_SIZE, 'packagingLoadOrders', 'PACKAGING_PAGE_SIZE'); } } }); } window.packagingEditRow = function(id) { showCenteredPrompt('编辑包材订单', '输入新值(格式: 字段名=值),例如: 状态=已完结').then(function(newVal) { if (!newVal) return; var parts = newVal.split('='); if (parts.length < 2) { alert('格式错误'); return; } var field = parts[0].trim(); var value = parts.slice(1).join('=').trim(); fetch('/api/packaging/orders/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: field, value: value}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { packagingLoadOrders(packagingOrderPage); } else { alert(res.msg || '更新失败'); } }); }); }; // 包材订单状态内联编辑 window.savePackagingStatus = function(sel) { var id = sel.getAttribute('data-id'); var val = sel.value; var oldVal = sel.getAttribute('data-current') || '未完结'; sel.disabled = true; fetch('/api/packaging/orders/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: '状态', value: val}) }) .then(function(r){return r.json()}) .then(function(res){ sel.disabled = false; if (res.code === 0) { sel.setAttribute('data-current', val); sel.style.borderColor = '#52c41a'; setTimeout(function(){ sel.style.borderColor = ''; packagingLoadOrders(packagingOrderPage || 1); }, 300); } else { sel.value = oldVal; alert(res.msg || '保存失败'); } }) .catch(function(){ sel.disabled = false; sel.value = oldVal; alert('网络错误'); }); }; window.packagingDeleteRow = function(id) { if (!window._hasPagePerm('orders_packaging', 'delete')) { alert('权限不足,当前账号没有删除订单权限'); return; } showCenteredConfirm('确认', '确认删除?').then(function(ok) { if (!ok) return; fetch('/api/packaging/orders/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('packaging'); packagingLoadOrders(packagingOrderPage); } else { alert(res.msg || '删除失败'); } }); }); }; window.packagingAddRow = function() { if (!window._hasPagePerm('orders_packaging', 'create')) { alert('权限不足,当前账号没有新增订单权限'); return; } showCenteredPrompt('新增包材订单', '请输入编码(必填):').then(function(code) { if (!code) return; showCenteredPrompt('新增包材订单', '请输入名称:').then(function(name) { fetch('/api/packaging/orders/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({编码: code, 名称: name || ''}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('packaging'); packagingLoadOrders(1); } else { alert(res.msg || '添加失败'); } }); }); }); }; window.packagingExport = function() { _orderExport('packaging', { search: packagingSearch, status: packagingFilterStatus, supplier: packagingFilterSupplier, orderer: packagingFilterOrderer, orderFilter: packagingOrderFilter }, '/api/packaging/orders/list', '包材订单'); }; window.packagingToggleAll = function(master) { var cbs = document.querySelectorAll('.packaging-checkbox'); cbs.forEach(function(cb) { cb.checked = master.checked; }); }; window.packagingBatchGenerateContract = function() { var cbs = document.querySelectorAll('.packaging-checkbox:checked'); var ids = []; cbs.forEach(function(cb) { ids.push(parseInt(cb.getAttribute('data-id'))); }); if (ids.length === 0) { alert('请先勾选要生成合同的订单'); return; } generateContractForOrders('packaging', ids); }; // ──────────── 成品提货表 ──────────── var finishedOrderFilter = {}; // 表头筛选状态 var finishedFilterMenu = null; // 当前筛选弹窗 var finishedFilterField = ''; // 当前筛选字段 var finishedSortField = ''; // 成品提货排序字段 var finishedSortAsc = false; // 成品提货排序方向 var FINISHED_FILTER_COLS = [ {field:'品类', label:'品类', type:'text'}, {field:'货品编号', label:'货品编号', type:'text'}, {field:'货品名称', label:'货品名称', type:'text'}, {field:'货品类型', label:'货品类型', type:'text'}, {field:'供应商', label:'供应商', type:'text'}, {field:'可用库存', label:'可用库存', type:'number'}, {field:'昨日销量', label:'昨日销量', type:'number'}, {field:'3天均销量', label:'3天均销', type:'number'}, {field:'7天均销量', label:'7天均销', type:'number'}, {field:'15天均销量', label:'15天均销', type:'number'}, {field:'支撑天数', label:'支撑天数', type:'number'}, {field:'加工厂库存', label:'加工厂库存', type:'number'}, {field:'采购总数量', label:'采购总数量', type:'number'}, {field:'生产在途', label:'生产在途', type:'number'}, {field:'采购在途', label:'采购在途', type:'number'}, {field:'产品进度', label:'产品进度', type:'text'}, {field:'采购跟单', label:'采购跟单', type:'text'}, {field:'采购计划', label:'采购计划', type:'text'}, {field:'备注', label:'备注', type:'text'}, ]; var FINISHED_PAGE_SIZE = 100; var finishedPage = 1; var finishedSearch = ''; var finishedCategory = ''; var finishedSupplier = ''; // 成品提货字段设置:序号和操作列固定,业务字段支持显隐与拖拽排序。 var FINISHED_COLUMNS = [ {field:'品类', label:'品类', defShow:true, type:'text', minWidth:60}, {field:'货品编号', label:'货品编号', defShow:true, type:'text', minWidth:120}, {field:'货品名称', label:'货品名称', defShow:true, type:'text', minWidth:250}, {field:'货品类型', label:'货品类型', defShow:true, type:'text', minWidth:100}, {field:'供应商', label:'供应商', defShow:true, type:'text', minWidth:100}, {field:'可用库存', label:'可用库存', defShow:true, type:'number', minWidth:70}, {field:'昨日销量', label:'昨日销量', defShow:true, type:'number', minWidth:60}, {field:'3天均销量', label:'3天均销', defShow:true, type:'number', minWidth:65}, {field:'7天均销量', label:'7天均销', defShow:true, type:'number', minWidth:65}, {field:'15天均销量', label:'15天均销', defShow:true, type:'number', minWidth:65}, {field:'支撑天数', label:'支撑天数', defShow:true, type:'number', minWidth:60}, {field:'加工厂库存', label:'加工厂库存', defShow:true, type:'number', minWidth:80}, {field:'采购总数量', label:'采购总数量', defShow:true, type:'number', minWidth:80}, {field:'生产在途', label:'生产在途', defShow:true, type:'number', minWidth:70}, {field:'采购在途', label:'采购在途', defShow:true, type:'number', minWidth:70}, {field:'产品进度', label:'产品进度', defShow:true, type:'text', minWidth:130}, {field:'采购跟单', label:'跟单小组', defShow:true, type:'text', minWidth:150}, {field:'采购计划', label:'采购计划', defShow:true, type:'text', minWidth:100}, {field:'备注', label:'备注', defShow:true, type:'text', minWidth:200} ]; function _finishedColumnDef(field) { return FINISHED_COLUMNS.find(function(c) { return c.field === field; }); } function _finishedEffectiveFields() { // _finishedDragOrder 是弹窗临时顺序;只有保存后写入 appliedOrder,外部表格才读取它。 var order = window._finishedAppliedOrder || window._finishedDragOrder || FINISHED_COLUMNS.map(function(c) { return c.field; }); var visible = window.finishedColVisible || {}; return order.map(_finishedColumnDef).filter(function(c) { return c && visible[c.field] !== false && window._canViewField('orders_finished', c.label); }); } function _finishedHeaderHtml() { var html = ''; _finishedEffectiveFields().forEach(function(c) { var filter = '▾'; html += ''; }); html += renderCollapsibleActionHeader('finished', 'finishedOrdersTable', false) + ''; return html; } function _finishedCellHtml(field, r) { var value = r[field]; if (field === '品类' || field === '货品类型' || field === '供应商' || field === '采购计划') return ''; if (field === '货品编号') return ''; if (field === '货品名称') return ''; if (field === '可用库存') return ''; if (['昨日销量','3天均销量','7天均销量','15天均销量','支撑天数'].indexOf(field) >= 0) return ''; if (['加工厂库存','采购总数量','生产在途','采购在途'].indexOf(field) >= 0) return ''; if (field === '产品进度') return ''; if (field === '采购跟单') return ''; if (field === '备注') return ''; return ''; } function loadFinishedProductsPage(container) { // 在生成表头前初始化已生效状态,避免刷新页面时短暂使用默认列顺序。 initOrderColState('finished'); container.innerHTML = `

📋 成品提货明细

序号' + col.label + filterControl + '
加载中...
暂无数据
' + rowNum + '' + escHtml(r['计划下单时间'] || '') + '' + escHtml(r['日期'] || '') + '' + escHtml(r['编码'] || '') + '' + escHtml(r['名称'] || '') + '' + escHtml(r['货品类型'] || '') + '' + escHtml(r['单价'] == null ? '' : String(r['单价'])) + '' + escHtml(r['是否审核价格'] || '未审核') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (amtVal ? '¥' + amtVal.toLocaleString() : '') + '' + escHtml(r['供应商'] || '') + '' + escHtml(r['下单备注'] || '') + '' + escHtml(r['仓库到货'] || '') + '' + escHtml(r['进度'] || '') + '' + escHtml(r['是否已做采购单'] || '') + '' + escHtml(r['是否已做入库'] || '') + '' + escHtml(r['采购下单人'] || '-') + '' + renderNameBadges(r['下单人'], true) + '' + escHtml(r['旺店通编号'] || '') + '' + escHtml(r['是否已录入旺店通'] || '') + '' + escHtml(r['到货日期'] || '') + '' + escHtml(r['状态'] || '') + '' + escHtml(r['做货天数'] || '') + '' + (parseInt(r['实际到货数量']) || 0).toLocaleString() + '' + (parseInt(r['未到货数']) || 0).toLocaleString() + '' + renderPersonBadges(r['包材开发']) + '' + escHtml(r[field] || '') + '
'; html += ''; if (window._hasPagePerm('orders_packaging', 'edit')) html += ''; if (window._hasPagePerm('orders_packaging', 'delete')) html += ''; html += '
序号' + escHtml(c.label) + ' ' + filter + '
' + escHtml(value || '') + '' + escHtml(value || '') + '' + escHtml(value || '') + '' + (parseInt(value) || 0).toLocaleString() + '' + (parseFloat(value) || 0) + '' + (parseInt(value) || 0).toLocaleString() + '' + escHtml(value || '') + '' + renderNameBadges(value) + '' + escHtml(value || '') + '' + escHtml(value || '') + '
${_finishedHeaderHtml()}
加载中...
`; // 页面每次重建时按已生效字段生成表头;弹窗内调整不会触碰此处,直到点击“保存”。 var finishedThead = document.getElementById('finishedOrdersThead'); if (finishedThead) finishedThead.innerHTML = _finishedHeaderHtml(); finishedLoadFilters(); finishedLoadOrders(1); restoreAdjustableTableColumnWidths('finishedOrdersTable', 'orders_finished'); enableAdjustableTableColumnResize('finishedOrdersTable', 'orders_finished'); applyCollapsibleActionState('finished'); // 后台预拉取成品提货表所有列的筛选值 prefetchOrderColumnValues('finished'); loadDefaultOrderScheme('finished'); } function openFinishedExportModal() { if (!window._hasPagePerm('orders_finished', 'export')) { showCenteredAlert('权限不足', '当前账号没有成品提货导出权限'); return; } var old = document.getElementById('finishedExportModal'); if (old) old.remove(); var fields = _finishedEffectiveFields().map(function(c) { return ''; }).join(''); var overlay = document.createElement('div'); overlay.id = 'finishedExportModal'; overlay.className = 'modal-overlay'; overlay.innerHTML = ''; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); } function finishedExport() { var selected = Array.prototype.map.call(document.querySelectorAll('#finishedExportFields input:checked'), function(el) { return el.value; }); if (!selected.length) { showCenteredAlert('提示', '请至少选择一个导出字段'); return; } fetch('/api/finished/export', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fields: selected, search: finishedSearch, category: finishedCategory, supplier: finishedSupplier, order_filter: finishedOrderFilter, sort_by: finishedSortField, sort_order: finishedSortAsc ? 'asc' : 'desc' }) }).then(function(r) { if (!r.ok) return r.json().then(function(e) { throw new Error(e.msg || '导出失败'); }); return r.blob(); }).then(function(blob) { var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = '成品提货管理.xlsx'; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); var modal = document.getElementById('finishedExportModal'); if (modal) modal.remove(); }).catch(function(e) { showCenteredAlert('导出失败', e.message || '导出失败'); }); } function finishedLoadFilters() { fetch('/api/finished/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var sc = document.getElementById('finishedCategoryFilter'); var ss = document.getElementById('finishedSupplierFilter'); (res.data.categories || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; sc.appendChild(opt); }); (res.data.suppliers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; ss.appendChild(opt); }); }); } function finishedDoSearch() { finishedSearch = document.getElementById('finishedSearchInput').value.trim(); finishedLoadOrders(1); } function finishedApplyFilters() { finishedCategory = document.getElementById('finishedCategoryFilter').value; finishedSupplier = document.getElementById('finishedSupplierFilter').value; finishedLoadOrders(1); } function finishedResetFilters() { document.getElementById('finishedSearchInput').value = ''; document.getElementById('finishedCategoryFilter').value = ''; document.getElementById('finishedSupplierFilter').value = ''; finishedSearch = ''; finishedCategory = ''; finishedSupplier = ''; finishedOrderFilter = {}; _syncFinishedFilterIcons(); finishedLoadOrders(1); } function finishedLoadOrders(page) { finishedPage = page; var params = '?page=' + page + '&size=' + FINISHED_PAGE_SIZE; if (finishedSearch) params += '&search=' + encodeURIComponent(finishedSearch); if (finishedCategory) params += '&category=' + encodeURIComponent(finishedCategory); if (finishedSupplier) params += '&supplier=' + encodeURIComponent(finishedSupplier); // 表头筛选菜单的升降序需要随列表请求传给后端,避免只更新按钮状态而不改变数据顺序。 if (finishedSortField) { params += '&sort_by=' + encodeURIComponent(finishedSortField) + '&sort_order=' + (finishedSortAsc ? 'asc' : 'desc'); } fetch('/api/finished/list' + params, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: finishedOrderFilter}) }) .then(function(r){ if (!r.ok) throw new Error('成品提货列表请求失败:HTTP ' + r.status); return r.json(); }) .then(function(res){ if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('finishedOrdersTbody'); var totalInfo = document.getElementById('finishedTotalInfo'); if (totalInfo) totalInfo.textContent = '共 ' + data.total + ' 条'; if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据'; } else { var startIdx = (page - 1) * FINISHED_PAGE_SIZE; var html = ''; data.rows.forEach(function(r, i) { var rowNum = startIdx + i + 1; html += ''; html += '' + rowNum + ''; _finishedEffectiveFields().forEach(function(c) { html += _finishedCellHtml(c.field, r); }); html += ''; html += '
'; html += ' '; html += ''; html += '
'; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('finishedOrdersTable', 'orders_finished'); var pel = document.getElementById('finishedOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, FINISHED_PAGE_SIZE, 'finishedLoadOrders', 'FINISHED_PAGE_SIZE'); } } }); } window.finishedDeleteRow = function(id) { showCenteredConfirm('确认', '确认删除此记录?').then(function(ok) { if (!ok) return; fetch('/api/finished/orders/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { invalidateOrderColumnCache('finished'); finishedLoadOrders(finishedPage); } else { alert(res.msg || '删除失败'); } }); }); }; // ─── 成品提货表头筛选 ─── function _finishedNormalizePlannerValues(values) { var result = []; (values || []).forEach(function(value) { var text = String(value || '').trim(); if (!text || text === '__EMPTY__') { if (text === '__EMPTY__') result.push(text); return; } text.replace(/,/g, ',').split(',').forEach(function(name) { name = name.trim(); if (name && result.indexOf(name) < 0) result.push(name); }); }); return result.sort(); } function _finishedGetColType(field) { for (var i = 0; i < FINISHED_FILTER_COLS.length; i++) { if (FINISHED_FILTER_COLS[i].field === field) return FINISHED_FILTER_COLS[i].type; } return 'text'; } function _finishedIsSupplierFilter(field) { return field === '供应商'; } function openFinishedFilterMenu(field, event) { event = event || window.event; event.stopPropagation(); if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; } finishedFilterField = field; var th = event.currentTarget; while (th && th.tagName !== 'TH') th = th.parentNode; var rect = th.getBoundingClientRect(); var label = field; var colType = _finishedGetColType(field); // ─── 联动筛选:先检测是否有搜索/筛选条件 ─── var linkedFinishedFilters = Object.assign({}, finishedOrderFilter); delete linkedFinishedFilters[field]; var hasOtherFilters = Object.keys(linkedFinishedFilters).length > 0; var hasSearchCond = !!(document.getElementById('finishedSearchInput')?.value.trim() || document.getElementById('finishedCategoryFilter')?.value || document.getElementById('finishedSupplierFilter')?.value); if (colType === 'number') { _buildFinishedNumericFilter(field, label, rect); } else if (hasOtherFilters || hasSearchCond || finishedOrderFilter[field]) { _buildFinishedFilterShell(field, label, rect); var apiUrl = '/api/finished/column-values?field=' + encodeURIComponent(field); if (finishedSearch) apiUrl += '&search=' + encodeURIComponent(finishedSearch); if (finishedCategory) apiUrl += '&category=' + encodeURIComponent(finishedCategory); if (finishedSupplier) apiUrl += '&supplier=' + encodeURIComponent(finishedSupplier); fetch(apiUrl, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: linkedFinishedFilters}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { _populateFinishedFilterList(field, res.data); } }); // 点击外部关闭 if (finishedFilterMenu) { var h2 = function(e) { if (finishedFilterMenu && !finishedFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) { finishedFilterMenu.remove(); finishedFilterMenu = null; document.removeEventListener('click', h2); } }; setTimeout(function() { document.addEventListener('click', h2); }, 10); setTimeout(function() { var _inp = document.getElementById('finishedFilterSearch'); if (_inp) _inp.focus(); }, 150); } return; } // ─── 无任何筛选条件:显示完整筛选菜单 ─── else { // 文本列无筛选:缓存或API var _ck = 'finished:' + field; var _c = window._orderColumnCache && window._orderColumnCache[_ck]; if (_c) { _buildFinishedListFilter(field, label, rect, _c); } else { _buildFinishedFilterShell(field, label, rect); fetch('/api/finished/column-values?field=' + encodeURIComponent(field)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { window._orderColumnCache = window._orderColumnCache || {}; window._orderColumnCache[_ck] = field === '采购计划' ? _finishedNormalizePlannerValues(res.data) : res.data; _populateFinishedFilterList(field, window._orderColumnCache[_ck]); } }); } } if (finishedFilterMenu) { var handler = function(e) { if (finishedFilterMenu && !finishedFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) { finishedFilterMenu.remove(); finishedFilterMenu = null; document.removeEventListener('click', handler); } }; setTimeout(function() { document.addEventListener('click', handler); }, 10); setTimeout(function() { var inp = document.getElementById('finishedFilterSearch'); if (inp) inp.focus(); }, 150); } } function _buildFinishedFilterShell(field, label, rect) { var menu = document.createElement('div'); menu.id = 'finishedFilterMenu'; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
加载中...
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); finishedFilterMenu = menu; } function _buildFinishedListFilter(field, label, rect, values) { var menu = document.createElement('div'); menu.id = 'finishedFilterMenu'; var isSorted = finishedSortField === field; var currentOrder = isSorted ? (finishedSortAsc ? 'asc' : 'desc') : ''; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var sel = finishedOrderFilter[field] || {}; var supplierFilter = _finishedIsSupplierFilter(field); var allChecked = !supplierFilter || (sel.type === 'list' && sel.values && sel.values.length === values.length); var html = '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var checked = supplierFilter ? (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : '') : (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : 'checked'); html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); finishedFilterMenu = menu; } function _buildFinishedNumericFilter(field, label, rect) { var isSorted = finishedSortField === field; var currentOrder = isSorted ? (finishedSortAsc ? 'asc' : 'desc') : ''; var menu = document.createElement('div'); menu.id = 'finishedFilterMenu'; var current = finishedOrderFilter[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; var html = '
' + escHtml(label) + '
'; html += '
'; html += '内容筛选'; html += '条件筛选
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += ''; html += '
加载中...
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; menu.innerHTML = html; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:260px;max-width:320px;font-size:13px;display:flex;flex-direction:column;max-height:450px'; document.body.appendChild(menu); finishedFilterMenu = menu; // 从后端API获取唯一值 var linkedNumericFilters = Object.assign({}, finishedOrderFilter); delete linkedNumericFilters[field]; var filterParam = ''; if (finishedSearch) filterParam += '&search=' + encodeURIComponent(finishedSearch); if (finishedCategory) filterParam += '&category=' + encodeURIComponent(finishedCategory); if (finishedSupplier) filterParam += '&supplier=' + encodeURIComponent(finishedSupplier); fetch('/api/finished/column-values?field=' + encodeURIComponent(field) + filterParam, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: linkedNumericFilters}) }) .then(function(r) { return r.json(); }) .then(function(res) { var numValues = []; if (res.code === 0 && res.data) { numValues = res.data.map(function(v) { return String(v).trim(); }).filter(function(v) { return v !== '' && v !== '-'; }); } numValues.sort(function(a,b) { return parseFloat(a) - parseFloat(b); }); _finishedRenderNumList(numValues); }); // 回显已保存的数字筛选状态 if (current && current.type === 'num') { var condSel = document.getElementById('finishedNumCond'); var valInput = document.getElementById('finishedNumVal'); if (condSel) condSel.value = current.cond || ''; if (valInput) valInput.value = current.val || ''; finishedNumSwitchTab('cond'); } else if (current && current.type === 'list' && current.values) { var cbs = document.querySelectorAll('#finishedNumList .finished-num-cb'); cbs.forEach(function(cb) { cb.checked = current.values.indexOf(cb.value) >= 0; }); } setTimeout(function() { var inp = document.getElementById('finishedNumVal'); if (inp) inp.focus(); }, 100); } function _populateFinishedFilterList(field, values) { values = field === '采购计划' ? _finishedNormalizePlannerValues(values) : values; var listEl = document.getElementById('finishedFilterList'); if (!listEl) return; var sel = finishedOrderFilter[field] || {}; var supplierFilter = _finishedIsSupplierFilter(field); var allChecked = !supplierFilter || (sel.type === 'list' && sel.values && sel.values.length === values.length); var html = '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = supplierFilter ? (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : '') : (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : 'checked'); var displayValue = v === '__EMPTY__' ? '空' : v; html += ''; }); listEl.innerHTML = html; } function finishedFilterSearch(q) { var items = document.querySelectorAll('#finishedFilterList label'); q = (q || '').toLowerCase(); items.forEach(function(item) { item.style.display = item.textContent.toLowerCase().indexOf(q) >= 0 ? 'flex' : 'none'; }); } function finishedFilterToggleAll(checked) { document.querySelectorAll('#finishedFilterList .finished-filter-cb').forEach(function(cb) { cb.checked = checked; }); } function finishedFilterCbChange() { var all = document.querySelectorAll('#finishedFilterList .finished-filter-cb'); var checked = document.querySelectorAll('#finishedFilterList .finished-filter-cb:checked'); var sa = document.getElementById('finishedFilterSelectAll'); if (sa) sa.checked = checked.length === all.length; } function applyFinishedFilter() { var field = finishedFilterField; var searchInput = document.getElementById('finishedFilterSearch'); var searchText = searchInput ? searchInput.value.trim() : ''; // 供应商列搜索输入会同步勾选匹配项;确定时始终提交复选框多选值。 if (searchText && !_finishedIsSupplierFilter(field)) { finishedOrderFilter[field] = {type: 'search', value: searchText}; } else { var cbs = document.querySelectorAll('#finishedFilterList .finished-filter-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); var allCbs = document.querySelectorAll('#finishedFilterList .finished-filter-cb'); if (checked.length === 0 || checked.length === allCbs.length) { delete finishedOrderFilter[field]; } else { finishedOrderFilter[field] = {type: 'list', values: checked}; } } if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; } _syncFinishedFilterIcons(); finishedLoadOrders(1); } function applyFinishedNumFilter() { var field = finishedFilterField; var condPanel = document.getElementById('finishedNumCondPanel'); var isCondTab = condPanel && condPanel.style.display !== 'none'; if (isCondTab) { var cond = document.getElementById('finishedNumCond')?.value || ''; var val = document.getElementById('finishedNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } finishedOrderFilter[field] = {type: 'num', cond: cond, val: val}; } else { var cbs = document.querySelectorAll('#finishedNumList .finished-num-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (checked.length === 0 || checked.length === document.querySelectorAll('#finishedNumList .finished-num-cb').length) { delete finishedOrderFilter[field]; } else { finishedOrderFilter[field] = {type: 'list', values: checked}; } } if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; } _syncFinishedFilterIcons(); finishedLoadOrders(1); } // ─── 成品提货数字列双模式筛选辅助函数 ─── function finishedNumSwitchTab(tab) { var contentPanel = document.getElementById('finishedNumContentPanel'); var condPanel = document.getElementById('finishedNumCondPanel'); var tabContent = document.getElementById('finishedTabContent'); var tabCond = document.getElementById('finishedTabCond'); var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; if (tab === 'content') { if (contentPanel) contentPanel.style.display = ''; if (condPanel) condPanel.style.display = 'none'; if (tabContent) tabContent.style.cssText = tabAct; if (tabCond) tabCond.style.cssText = tabDef; } else { if (contentPanel) contentPanel.style.display = 'none'; if (condPanel) condPanel.style.display = ''; if (tabContent) tabContent.style.cssText = tabDef; if (tabCond) tabCond.style.cssText = tabAct; } } function finishedNumSearchList(q) { var items = document.querySelectorAll('#finishedNumList label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function finishedNumToggleAll(checked) { document.querySelectorAll('#finishedNumList .finished-num-cb').forEach(function(cb) { cb.checked = checked; }); } function finishedNumUpdateAll() { var all = document.querySelectorAll('#finishedNumList .finished-num-cb'); var checked = document.querySelectorAll('#finishedNumList .finished-num-cb:checked'); var cb = document.getElementById('finishedNumSelAll'); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } function _finishedRenderNumList(vals) { var list = document.getElementById('finishedNumList'); if (!list) return; if (vals.length === 0) { list.innerHTML = '
暂无数据
'; return; } var html = ''; vals.forEach(function(v) { html += ''; }); list.innerHTML = html; var savedListFilter = finishedOrderFilter[finishedFilterField]; if (savedListFilter && savedListFilter.type === 'list' && savedListFilter.values) { list.querySelectorAll('.finished-num-cb').forEach(function(cb) { cb.checked = savedListFilter.values.indexOf(cb.value) >= 0; }); finishedNumUpdateAll(); } } function finishedNumFilterApply() { applyFinishedNumFilter(); } function clearFinishedHeaderFilter() { delete finishedOrderFilter[finishedFilterField]; if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; } _syncFinishedFilterIcons(); finishedLoadOrders(1); } function _syncFinishedFilterIcons() { var icons = document.querySelectorAll('#finishedOrdersTable .fn-filt-icon'); icons.forEach(function(icon) { var field = icon.getAttribute('data-field') || icon.parentNode.textContent.trim().replace(/▾/, '').trim(); var hasFilter = finishedOrderFilter[field]; icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = hasFilter ? 'bold' : 'normal'; }); } // 成品提货排序 function finishedFilterSort(field, order) { if (order === 'asc') { finishedSortField = field; finishedSortAsc = true; } else if (order === 'desc') { finishedSortField = field; finishedSortAsc = false; } else { finishedSortField = ''; } if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; } _syncFinishedFilterIcons(); finishedLoadOrders(1); } // ─── 下单备注可编辑通用函数 ─── // ─── 订单表头列筛选功能 ─── // 根据类型获取筛选状态对象 function _orderFilterState(type) { if (type === 'domestic') return domesticOrderFilter; if (type === 'overseas') return overseasOrderFilter; if (type === 'packaging') return packagingOrderFilter; return {}; } // 检查订单表是否有搜索栏筛选条件(使用JS变量而非DOM值) function _orderHasSearch(type) { if (type === 'domestic') { return !!(domesticSearch || domesticFilterStatus || domesticFilterSupplier || domesticFilterOrderer); } if (type === 'overseas') { return !!(overseasSearch || overseasFilterStatus || overseasFilterSupplier || overseasFilterOrderer); } if (type === 'packaging') { return !!(packagingSearch || packagingFilterStatus || packagingFilterSupplier || packagingFilterOrderer); } return false; } // 从当前表格DOM中提取指定列的所有唯一值(通过data-field属性精确定位列) function _extractColumnValuesFromDOM(type, field) { var tableId = type === 'domestic' ? 'domesticOrdersTable' : type === 'overseas' ? 'overseasOrdersTable' : 'packagingOrdersTable'; var tbodyId = type === 'domestic' ? 'domesticOrdersTbody' : type === 'overseas' ? 'overseasOrdersTbody' : 'packagingOrdersTbody'; var table = document.getElementById(tableId); if (!table) return []; // 从表头找到该字段的列索引 var headerRow = table.querySelector('thead tr'); if (!headerRow) return []; var colIdx = -1; headerRow.querySelectorAll('th').forEach(function(th, idx) { if (th.getAttribute('data-field') === field) { colIdx = idx; } }); if (colIdx < 0) return []; // 从每行提取该列的值 var vals = []; var seen = {}; var tbody = document.getElementById(tbodyId); if (tbody) { tbody.querySelectorAll('tr').forEach(function(tr) { var td = tr.cells[colIdx]; if (td) { // 优先从 input/select 元素读取值(编辑类列),否则取 textContent var input = td.querySelector('input'); var select = td.querySelector('select'); var txt; if (input) { txt = (input.value || '').trim(); } else if (select) { txt = (select.options[select.selectedIndex]?.text || select.value || '').trim(); } else { txt = td.textContent.trim(); } if (txt && txt !== '-' && !seen[txt]) { seen[txt] = 1; vals.push(txt); } } }); } vals.sort(); return vals; } function _orderFilterActive(type) { if (type === 'domestic') return domesticOrderFilterActive; if (type === 'overseas') return overseasOrderFilterActive; if (type === 'packaging') return packagingOrderFilterActive; return {}; } function _orderFilterMenu(type) { if (type === 'domestic') return domesticOrderFilterMenu; if (type === 'overseas') return overseasOrderFilterMenu; if (type === 'packaging') return packagingOrderFilterMenu; return null; } function _setOrderFilterMenu(type, v) { if (type === 'domestic') domesticOrderFilterMenu = v; else if (type === 'overseas') overseasOrderFilterMenu = v; else if (type === 'packaging') packagingOrderFilterMenu = v; } function _orderFilterField(type) { if (type === 'domestic') return domesticOrderFilterField; if (type === 'overseas') return overseasOrderFilterField; if (type === 'packaging') return packagingOrderFilterField; return ''; } // 供应商列使用复选框支持同时选择多个供应商;搜索框仅用于定位选项。 function _isSupplierOrderFilter(type, field) { return (type === 'domestic' && field === '供货商') || ((type === 'overseas' || type === 'packaging') && field === '供应商'); } function _setOrderFilterField(type, v) { if (type === 'domestic') domesticOrderFilterField = v; else if (type === 'overseas') overseasOrderFilterField = v; else if (type === 'packaging') packagingOrderFilterField = v; } function _getOrderColumns(type) { if (type === 'domestic') return DOMESTIC_COLUMNS; if (type === 'overseas') return OVERSEAS_COLUMNS; if (type === 'packaging') return PACKAGING_COLUMNS; if (type === 'finished') return FINISHED_FILTER_COLS; return []; } function _getOrderColLabel(type, field) { var cols = _getOrderColumns(type); for (var i = 0; i < cols.length; i++) { if (cols[i].field === field) return cols[i].label || field; } return field; } function _getOrderColType(type, field) { var cols = _getOrderColumns(type); for (var i = 0; i < cols.length; i++) { if (cols[i].field === field) { if (cols[i].type) return cols[i].type; // 根据字段名自动判断数字列 var numFields = ['数量','金额','单价','实际到货数量','未到货数','做货天数','待提数量']; if (numFields.indexOf(field) >= 0) return 'number'; return 'text'; } } return 'text'; } // 打开列筛选菜单 function openOrderFilterMenu(type, field, event) { event = event || window.event; event.stopPropagation(); // 关闭已有的 var existing = _orderFilterMenu(type); if (existing) { existing.remove(); _setOrderFilterMenu(type, null); } _setOrderFilterField(type, field); var th = event.currentTarget.parentNode; var rect = th.getBoundingClientRect(); var label = _getOrderColLabel(type, field); var colType = _getOrderColType(type, field); // ─── 联动筛选:先检测是否有搜索/筛选条件 ─── var filterState = _orderFilterState(type); var linkedOrderFilters = {}; Object.keys(filterState || {}).forEach(function(key) { if (key !== field && key.charAt(0) !== '_') linkedOrderFilters[key] = filterState[key]; }); var hasOtherFilters = Object.keys(linkedOrderFilters).length > 0; var hasSearchCond = _orderHasSearch(type); if (colType === 'number') { _buildOrderNumericFilter(type, field, label, rect); var numericMenu = _orderFilterMenu(type); if (numericMenu) { var closeNumericMenu = function(e) { if (!numericMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) { numericMenu.remove(); _setOrderFilterMenu(type, null); document.removeEventListener('click', closeNumericMenu); } }; setTimeout(function() { document.addEventListener('click', closeNumericMenu); }, 10); } return; } if (hasOtherFilters || hasSearchCond) { // 任何筛选条件激活时:请求API时带上搜索条件(从数据库层面联动) _buildOrderFilterShell(type, field, label, rect); var _apiMap = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'}; var _apiUrl = _apiMap[type] + '?field=' + encodeURIComponent(field); // 传搜索栏条件 if (type === 'domestic') { if (domesticSearch) _apiUrl += '&search=' + encodeURIComponent(domesticSearch); if (domesticFilterStatus) _apiUrl += '&status=' + encodeURIComponent(domesticFilterStatus); if (domesticFilterSupplier) _apiUrl += '&supplier=' + encodeURIComponent(domesticFilterSupplier); if (domesticFilterOrderer) _apiUrl += '&orderer=' + encodeURIComponent(domesticFilterOrderer); } else if (type === 'overseas') { if (overseasSearch) _apiUrl += '&search=' + encodeURIComponent(overseasSearch); if (overseasFilterStatus) _apiUrl += '&status=' + encodeURIComponent(overseasFilterStatus); if (overseasFilterSupplier) _apiUrl += '&supplier=' + encodeURIComponent(overseasFilterSupplier); if (overseasFilterOrderer) _apiUrl += '&orderer=' + encodeURIComponent(overseasFilterOrderer); } else if (type === 'packaging') { if (packagingSearch) _apiUrl += '&search=' + encodeURIComponent(packagingSearch); if (packagingFilterStatus) _apiUrl += '&status=' + encodeURIComponent(packagingFilterStatus); if (packagingFilterSupplier) _apiUrl += '&supplier=' + encodeURIComponent(packagingFilterSupplier); if (packagingFilterOrderer) _apiUrl += '&orderer=' + encodeURIComponent(packagingFilterOrderer); } // 传表头筛选条件 fetch(_apiUrl, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: linkedOrderFilters}) }) .then(function(r) { if (!r.ok) throw new Error('筛选值请求失败:HTTP ' + r.status); return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { _populateOrderFilterList(type, field, res.data); } else { _populateOrderFilterList(type, field, []); } }) .catch(function(err) { console.error('订单表头筛选值加载失败:', err); _populateOrderFilterList(type, field, []); }); // 外部点击关闭 + 搜索聚焦 var _m3 = _orderFilterMenu(type); if (_m3) { var _h3 = function(e) { if (_m3 && !_m3.contains(e.target) && e.target !== th && !th.contains(e.target)) { _m3.remove(); _setOrderFilterMenu(type, null); document.removeEventListener('click', _h3); } }; setTimeout(function() { document.addEventListener('click', _h3); }, 10); setTimeout(function() { var _inp3 = document.getElementById('orderFilterSearch_' + type); if (_inp3) _inp3.focus(); }, 150); } return; } // ─── 无任何筛选条件:显示完整文本筛选菜单 ─── { // 文本列:缓存或API var _cacheKey = type + ':' + field; var _cached = window._orderColumnCache && window._orderColumnCache[_cacheKey]; if (_cached) { _buildOrderFilterList(type, field, label, rect, _cached); } else { _buildOrderFilterShell(type, field, label, rect); var _apiMap = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'}; fetch(_apiMap[type] + '?field=' + encodeURIComponent(field)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { window._orderColumnCache = window._orderColumnCache || {}; var normalizedValues = _normalizeOrderPersonnelFilterValues(field, res.data); window._orderColumnCache[_cacheKey] = normalizedValues.slice(); _populateOrderFilterList(type, field, normalizedValues); } }); } } // 点击外部关闭 var menu = _orderFilterMenu(type); if (menu) { var handler = function(e) { if (!menu.contains(e.target) && e.target !== th && !th.contains(e.target)) { menu.remove(); _setOrderFilterMenu(type, null); document.removeEventListener('click', handler); } }; setTimeout(function() { document.addEventListener('click', handler); }, 10); // 搜索自动聚焦 setTimeout(function() { var inp = document.getElementById('orderFilterSearch_' + type); if (inp) inp.focus(); }, 150); } } // 构建列表筛选菜单 function _buildOrderFilterList(type, field, label, rect, values) { // 人员列与计划预测一致:缓存和接口返回都统一按人员信息管理归并为小组。 values = _normalizeOrderPersonnelFilterValues(field, values); var menu = document.createElement('div'); menu.id = 'orderFilterMenu_' + type; var filterState = _orderFilterState(type); var sortField = filterState._sortField || ''; var sortAsc = filterState._sortAsc || false; var isSorted = sortField === field; var currentOrder = isSorted ? (sortAsc ? 'asc' : 'desc') : ''; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; // 加锁防止快速多次点击 if (window._orderFilterLock) return; window._orderFilterLock = true; setTimeout(function() { window._orderFilterLock = false; }, 300); var sel = filterState[field] || {}; var supplierFilter = _isSupplierOrderFilter(type, field); var allChecked = !supplierFilter || (sel.type === 'list' && sel.values && sel.values.length === values.length); var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var key = escHtml(v).replace(/'/g, "\\'"); var checked = supplierFilter ? (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : '') : (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : 'checked'); var displayValue = v === '__EMPTY__' ? '空' : v; html += ''; }); html += '
'; html += '
'; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); _setOrderFilterMenu(type, menu); } // 构建订单筛选菜单外壳(用于异步加载的字段) function _buildOrderFilterShell(type, field, label, rect) { var existing = _orderFilterMenu(type); if (existing) { existing.remove(); _setOrderFilterMenu(type, null); } _setOrderFilterField(type, field); var menu = document.createElement('div'); menu.id = 'orderFilterMenu_' + type; var filterState = _orderFilterState(type); var sortField = filterState._sortField || ''; var sortAsc = filterState._sortAsc || false; var isSorted = sortField === field; var currentOrder = isSorted ? (sortAsc ? 'asc' : 'desc') : ''; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column'; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += '' + ''; html += '
'; html += '
加载中...
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); _setOrderFilterMenu(type, menu); } // 填充订单筛选列表(从API返回的数据) function _populateOrderFilterList(type, field, values) { values = _normalizeOrderPersonnelFilterValues(field, values); var listEl = document.getElementById('orderFilterList_' + type); if (!listEl) return; var filterState = _orderFilterState(type); var sel = filterState[field] || {}; var supplierFilter = _isSupplierOrderFilter(type, field); var allChecked = !supplierFilter || (sel.type === 'list' && sel.values && sel.values.length === values.length); var html = ''; html += '
'; html += ''; html += '清除筛选
'; if (values.length === 0) { html += '
暂无数据
'; } values.forEach(function(v) { var checked = supplierFilter ? (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : '') : (sel.type === 'list' && sel.values ? (sel.values.indexOf(String(v)) >= 0 ? 'checked' : '') : 'checked'); var labelText = (v === '__EMPTY__') ? '空' : escHtml(v); html += ''; }); listEl.innerHTML = html; // 搜索自动聚焦 setTimeout(function() { var inp = document.getElementById('orderFilterSearch_' + type); if (inp) inp.focus(); }, 150); } // 构建数字筛选菜单 function _buildOrderNumericFilter(type, field, label, rect) { var menu = document.createElement('div'); menu.id = 'orderFilterMenu_' + type; var filterState = _orderFilterState(type); var current = filterState[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var sortField = filterState._sortField || ''; var sortAsc = filterState._sortAsc || false; var isSorted = sortField === field; var currentOrder = isSorted ? (sortAsc ? 'asc' : 'desc') : ''; var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '内容筛选'; html += '条件筛选
'; html += '
'; html += '' + ''; html += '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += ''; html += '
加载中...
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; menu.innerHTML = html; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:260px;max-width:320px;font-size:13px;display:flex;flex-direction:column;max-height:450px'; document.body.appendChild(menu); _setOrderFilterMenu(type, menu); // 从后端API获取唯一值 var apiMap = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'}; var linkedNumericFilters = {}; Object.keys(filterState || {}).forEach(function(key) { if (key !== field && key.charAt(0) !== '_') linkedNumericFilters[key] = filterState[key]; }); var fp = ''; if (type === 'domestic') { if (domesticSearch) fp += '&search=' + encodeURIComponent(domesticSearch); if (domesticFilterStatus) fp += '&status=' + encodeURIComponent(domesticFilterStatus); if (domesticFilterSupplier) fp += '&supplier=' + encodeURIComponent(domesticFilterSupplier); if (domesticFilterOrderer) fp += '&orderer=' + encodeURIComponent(domesticFilterOrderer); } else if (type === 'overseas') { if (overseasSearch) fp += '&search=' + encodeURIComponent(overseasSearch); if (overseasFilterStatus) fp += '&status=' + encodeURIComponent(overseasFilterStatus); if (overseasFilterSupplier) fp += '&supplier=' + encodeURIComponent(overseasFilterSupplier); if (overseasFilterOrderer) fp += '&orderer=' + encodeURIComponent(overseasFilterOrderer); } else if (type === 'packaging') { if (packagingSearch) fp += '&search=' + encodeURIComponent(packagingSearch); if (packagingFilterStatus) fp += '&status=' + encodeURIComponent(packagingFilterStatus); if (packagingFilterSupplier) fp += '&supplier=' + encodeURIComponent(packagingFilterSupplier); if (packagingFilterOrderer) fp += '&orderer=' + encodeURIComponent(packagingFilterOrderer); } fetch(apiMap[type] + '?field=' + encodeURIComponent(field) + fp, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_filter: linkedNumericFilters}) }) .then(function(r) { return r.json(); }) .then(function(res) { var numValues = []; if (res.code === 0 && res.data) { numValues = res.data.map(function(v) { return String(v).trim(); }).filter(function(v) { return v !== '' && v !== '-'; }); } numValues.sort(function(a,b) { return parseFloat(a) - parseFloat(b); }); _orderRenderNumList(type, numValues); }); // 回显已保存的数字筛选状态 if (current && current.type === 'num') { var condSel = document.getElementById('orderNumCond_' + type); var valInput = document.getElementById('orderNumVal_' + type); if (condSel) condSel.value = current.cond || ''; if (valInput) valInput.value = current.val || ''; orderNumSwitchTab(type, 'cond'); } else if (current && current.type === 'list' && current.values) { var cbs = document.querySelectorAll('#orderNumList_' + type + ' .order-num-cb'); cbs.forEach(function(cb) { cb.checked = current.values.indexOf(cb.value) >= 0; }); } setTimeout(function() { var inp = document.getElementById('orderNumVal_' + type); if (inp) inp.focus(); }, 100); } function closeOrderFilterMenu(type) { var menu = _orderFilterMenu(type); if (menu) { menu.remove(); _setOrderFilterMenu(type, null); } } // 应用列表筛选 function applyOrderListFilter(type) { var field = _orderFilterField(type); var searchInput = document.getElementById('orderFilterSearch_' + type); var searchText = searchInput ? searchInput.value.trim() : ''; var filterState = _orderFilterState(type); // 供应商列搜索输入会同步勾选匹配项;确定时始终提交复选框多选值。 if (searchText && !_isSupplierOrderFilter(type, field)) { // 搜索框有内容 → 文本模糊匹配 filterState[field] = {type: 'search', value: searchText}; } else { // 搜索框为空 → 用复选框选择 var cbs = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); var allCbs = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb'); if (checked.length === 0 || checked.length === allCbs.length) { delete filterState[field]; } else { filterState[field] = {type: 'list', values: checked}; } } closeOrderFilterMenu(type); _syncOrderFilterIcons(type); // 重新加载列表 if (type === 'domestic') domesticLoadOrders(1); else if (type === 'overseas') overseasLoadOrders(1); else if (type === 'packaging') packagingLoadOrders(1); } // 应用数字筛选 function applyOrderNumFilter(type) { var field = _orderFilterField(type); var condPanel = document.getElementById('orderNumCondPanel_' + type); var isCondTab = condPanel && condPanel.style.display !== 'none'; var filterState = _orderFilterState(type); if (isCondTab) { var cond = document.getElementById('orderNumCond_' + type)?.value || ''; var val = document.getElementById('orderNumVal_' + type)?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } filterState[field] = {type: 'num', cond: cond, val: val}; } else { // 内容筛选Tab:复选框值创建列表筛选 var cbs = document.querySelectorAll('#orderNumList_' + type + ' .order-num-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (checked.length === 0 || checked.length === document.querySelectorAll('#orderNumList_' + type + ' .order-num-cb').length) { delete filterState[field]; } else { filterState[field] = {type: 'list', values: checked}; } } closeOrderFilterMenu(type); _syncOrderFilterIcons(type); if (type === 'domestic') domesticLoadOrders(1); else if (type === 'overseas') overseasLoadOrders(1); else if (type === 'packaging') packagingLoadOrders(1); } // ─── 订单页数字列双模式筛选辅助函数 ─── function orderNumSwitchTab(type, tab) { var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none'; var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none'; if (tab === 'content') { var cp = document.getElementById('orderNumContentPanel_' + type); var cdp = document.getElementById('orderNumCondPanel_' + type); var tc = document.getElementById('orderNumTabContent_' + type); var tcd = document.getElementById('orderNumTabCond_' + type); if (cp) cp.style.display = ''; if (cdp) cdp.style.display = 'none'; if (tc) tc.style.cssText = tabAct; if (tcd) tcd.style.cssText = tabDef; } else { var cpp = document.getElementById('orderNumContentPanel_' + type); var cdp2 = document.getElementById('orderNumCondPanel_' + type); var tc2 = document.getElementById('orderNumTabContent_' + type); var tcd2 = document.getElementById('orderNumTabCond_' + type); if (cpp) cpp.style.display = 'none'; if (cdp2) cdp2.style.display = ''; if (tc2) tc2.style.cssText = tabDef; if (tcd2) tcd2.style.cssText = tabAct; } } function orderNumSearchList(type, q) { var items = document.querySelectorAll('#orderNumList_' + type + ' label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function orderNumToggleAll(type, checked) { document.querySelectorAll('#orderNumList_' + type + ' .order-num-cb').forEach(function(cb) { cb.checked = checked; }); } function orderNumUpdateAll(type) { var all = document.querySelectorAll('#orderNumList_' + type + ' .order-num-cb'); var checked = document.querySelectorAll('#orderNumList_' + type + ' .order-num-cb:checked'); var cb = document.getElementById('orderNumSelAll_' + type); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } function _orderRenderNumList(type, vals) { var list = document.getElementById('orderNumList_' + type); if (!list) return; if (vals.length === 0) { list.innerHTML = '
暂无数据
'; return; } var html = ''; vals.forEach(function(v) { html += ''; }); list.innerHTML = html; var savedListFilter = _orderFilterState(type)[_orderFilterField(type)]; if (savedListFilter && savedListFilter.type === 'list' && savedListFilter.values) { list.querySelectorAll('.order-num-cb').forEach(function(cb) { cb.checked = savedListFilter.values.indexOf(cb.value) >= 0; }); orderNumUpdateAll(type); } } // 订单页排序 function orderFilterSort(type, field, order) { var filterState = _orderFilterState(type); if (order === 'asc') { filterState._sortField = field; filterState._sortAsc = true; } else if (order === 'desc') { filterState._sortField = field; filterState._sortAsc = false; } else { filterState._sortField = ''; } closeOrderFilterMenu(type); if (type === 'domestic') domesticLoadOrders(1); else if (type === 'overseas') overseasLoadOrders(1); else if (type === 'packaging') packagingLoadOrders(1); } function orderFilterSearchList(type, q) { var items = document.querySelectorAll('#orderFilterList_' + type + ' label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? 'flex' : 'none'; }); } function orderFilterToggleAll(type, checked) { document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb').forEach(function(cb) { cb.checked = checked; }); } function orderFilterCbChange(type) { var all = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb'); var checked = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb:checked'); var cb = document.getElementById('orderFilterSelectAll_' + type); if (cb) { cb.checked = all.length === checked.length; cb.indeterminate = checked.length > 0 && checked.length < all.length; } } // 清除某一列的筛选 function orderFilterClear(type) { var field = _orderFilterField(type); var filterState = _orderFilterState(type); delete filterState[field]; closeOrderFilterMenu(type); _syncOrderFilterIcons(type); if (type === 'domestic') domesticLoadOrders(1); else if (type === 'overseas') overseasLoadOrders(1); else if (type === 'packaging') packagingLoadOrders(1); } // 重置所有筛选 function orderFilterResetAll(type) { var filterState = _orderFilterState(type); Object.keys(filterState).forEach(function(k) { delete filterState[k]; }); closeOrderFilterMenu(type); _syncOrderFilterIcons(type); if (type === 'domestic') domesticLoadOrders(1); else if (type === 'overseas') overseasLoadOrders(1); else if (type === 'packaging') packagingLoadOrders(1); } // 同步筛选图标高亮 function _syncOrderFilterIcons(type) { var filterState = _orderFilterState(type); var active = {}; Object.keys(filterState).forEach(function(f) { if (filterState[f]) active[f] = true; }); var activeObj = _orderFilterActive(type); Object.keys(activeObj).forEach(function(k) { delete activeObj[k]; }); Object.keys(active).forEach(function(k) { activeObj[k] = true; }); // 更新表头图标颜色 var tableId = type + 'OrdersTable'; var table = document.getElementById(tableId); if (!table) return; var ths = table.querySelectorAll('thead th[data-field]'); ths.forEach(function(th) { var field = th.getAttribute('data-field'); var icon = th.querySelector('.order-filt-icon'); if (icon) { if (active[field]) { icon.style.color = '#1890ff'; icon.style.fontWeight = 'bold'; icon.textContent = '▼'; } else { icon.style.color = 'var(--text-light)'; icon.style.fontWeight = 'normal'; icon.textContent = '▾'; } } }); } // ─── 字段设置功能 ─── const DOMESTIC_COLUMNS = [ {field:'货品编号',label:'货品编号',defShow:true}, {field:'货品名称',label:'货品名称',defShow:true}, {field:'货品类型',label:'货品类型',defShow:true}, {field:'日期',label:'跟单下单日期',defShow:true}, {field:'计划下单时间',label:'计划下单日期',defShow:true}, {field:'新旧品',label:'新旧品',defShow:true}, {field:'下单备注',label:'下单备注',defShow:true}, {field:'单价',label:'单价',defShow:true,type:'number'}, {field:'是否审核价格',label:'审核价格',defShow:true}, {field:'数量',label:'实际下单数量',defShow:true,type:'number'}, {field:'金额',label:'金额',defShow:true,type:'number'}, {field:'供货商',label:'供应商名称',defShow:true}, {field:'仓库',label:'仓库',defShow:true}, {field:'金蝶采购单',label:'金蝶采购单',defShow:true}, {field:'采购员',label:'跟单小组',defShow:true}, {field:'下单人',label:'下单人',defShow:true}, {field:'进度',label:'进度',defShow:true}, {field:'旺店通生产单号',label:'旺店通单号',defShow:true}, {field:'是否已录入旺店通',label:'旺店通是否已录入',defShow:true}, {field:'采购开发',label:'采购开发',defShow:true}, {field:'实际到货日期',label:'实际到货日期',defShow:true}, {field:'实际到货数量',label:'实际到货数量',defShow:true,type:'number'}, {field:'未到货数',label:'未到货数',defShow:true,type:'number'}, {field:'预计到货日期',label:'预计到货日期',defShow:true}, {field:'金蝶入库',label:'金蝶是否入库',defShow:true}, {field:'状态',label:'状态',defShow:true}, ] const OVERSEAS_COLUMNS = [ {field:'产品编码',label:'货品编号',defShow:true}, {field:'货品名称',label:'货品名称',defShow:true}, {field:'货品类型',label:'货品类型',defShow:true}, {field:'日期',label:'跟单下单日期',defShow:true}, {field:'计划下单时间',label:'计划下单日期',defShow:true}, {field:'单价',label:'单价',defShow:true,type:'number'}, {field:'是否审核价格',label:'审核价格',defShow:true}, {field:'数量',label:'实际下单数量',defShow:true,type:'number'}, {field:'金额',label:'金额',defShow:true,type:'number'}, {field:'供应商',label:'供应商名称',defShow:true}, {field:'仓库',label:'仓库',defShow:true}, {field:'新旧品',label:'新旧品',defShow:true}, {field:'是否已做采购单',label:'已做采购单',defShow:true}, {field:'是否做入库',label:'金蝶是否入库',defShow:true}, {field:'采购员',label:'跟单小组',defShow:true}, {field:'下单人',label:'下单人',defShow:true}, {field:'进度',label:'进度',defShow:true}, {field:'下单备注',label:'下单备注',defShow:true}, {field:'采购开发',label:'采购开发',defShow:true}, {field:'旺店通生产单号',label:'旺店通单号',defShow:true}, {field:'是否已录入到旺店通',label:'旺店通是否已录入',defShow:true}, {field:'实际到货日期',label:'实际到货日期',defShow:true}, {field:'实际到货数量',label:'实际到货数量',defShow:true,type:'number'}, {field:'预计到货日期',label:'预计到货日期',defShow:true}, {field:'未到货数',label:'未到货数',defShow:true,type:'number'}, {field:'状态',label:'状态',defShow:true}, ]; const PACKAGING_COLUMNS = [ {field:'编码',label:'货品编号',defShow:true}, {field:'名称',label:'货品名称',defShow:true}, {field:'货品类型',label:'货品类型',defShow:true}, {field:'新旧品',label:'新旧品',defShow:true}, {field:'计划下单时间',label:'计划下单日期',defShow:true}, {field:'日期',label:'跟单下单日期',defShow:true}, {field:'单价',label:'单价',defShow:true,type:'number'}, {field:'是否审核价格',label:'审核价格',defShow:true}, {field:'数量',label:'实际下单数量',defShow:true,type:'number'}, {field:'合计',label:'金额',defShow:true,type:'number'}, {field:'供应商',label:'供应商名称',defShow:true}, {field:'下单备注',label:'下单备注',defShow:true}, {field:'仓库到货',label:'到货仓库',defShow:true}, {field:'进度',label:'进度',defShow:true}, {field:'是否已做采购单',label:'已做采购单',defShow:true}, {field:'是否已做入库',label:'金蝶是否入库',defShow:true}, {field:'采购下单人',label:'采购小组',defShow:true}, {field:'下单人',label:'下单人',defShow:true}, {field:'旺店通编号',label:'旺店通单号',defShow:true}, {field:'是否已录入旺店通',label:'旺店通是否已录入',defShow:true}, {field:'到货日期',label:'到货日期',defShow:true}, {field:'状态',label:'状态',defShow:true}, {field:'做货天数',label:'做货天数',defShow:true,type:'number'}, {field:'包材开发',label:'包材开发',defShow:true} ]; // ─── 通用字段设置系统(支持拖拽排序 + 方案管理) ─── // 按类型获取列数据 function getOrderColumns(type) { if (type === 'domestic') return DOMESTIC_COLUMNS; if (type === 'overseas') return OVERSEAS_COLUMNS; if (type === 'packaging') return PACKAGING_COLUMNS; if (type === 'finished') return FINISHED_COLUMNS; return []; } // 获取可见性映射(兼容localStorage) function getOrderColVisible(type) { var key = type + 'ColVisible'; var stored = localStorage.getItem(key); if (stored) { try { var m = JSON.parse(stored); if (type === 'domestic') { if (m['货品编号'] === undefined && m['编码'] !== undefined) m['货品编号'] = m['编码']; if (m['货品名称'] === undefined && m['名称'] !== undefined) m['货品名称'] = m['名称']; delete m['编码']; delete m['名称']; localStorage.setItem(key, JSON.stringify(m)); } cols = getOrderColumns(type); cols.forEach(function(c) { if (m[c.field] === undefined) m[c.field] = c.defShow; }); localStorage.setItem(key, JSON.stringify(m)); return m; } catch(e) {} } var cols = getOrderColumns(type); var m = {}; cols.forEach(function(c) { m[c.field] = c.defShow; }); return m; } function saveOrderColVisible(type, m) { localStorage.setItem(type + 'ColVisible', JSON.stringify(m)); } // 初始化各页面状态 function initOrderColState(type) { var cols = getOrderColumns(type); var visVar = type + 'ColVisible'; var pendingVar = '_' + type + 'PendingVisible'; var dragVar = '_' + type + 'DragOrder'; if (!window[visVar]) { window[visVar] = getOrderColVisible(type); } if (!window[pendingVar]) { window[pendingVar] = {}; cols.forEach(function(c) { window[pendingVar][c.field] = window[visVar][c.field]; }); } if (!window[dragVar]) { var initialOrder = cols.map(function(c) { return c.field; }); if (type === 'finished') { try { var storedOrder = JSON.parse(localStorage.getItem('finishedColOrder') || 'null'); if (Array.isArray(storedOrder)) { var allowed = {}; cols.forEach(function(c) { allowed[c.field] = true; }); var seen = {}; var validStored = storedOrder.filter(function(field) { if (!allowed[field] || seen[field]) return false; seen[field] = true; return true; }); cols.forEach(function(c) { if (!seen[c.field]) validStored.push(c.field); }); if (validStored.length === cols.length) initialOrder = validStored; } } catch (e) {} window._finishedAppliedOrder = initialOrder.slice(); } window[dragVar] = initialOrder; } else if (type === 'finished' && !window._finishedAppliedOrder) { window._finishedAppliedOrder = window[dragVar].slice(); } } // 确保所有页面已初始化 initOrderColState('domestic'); initOrderColState('overseas'); initOrderColState('packaging'); initOrderColState('finished'); // ─── 拖拽checkbox渲染 ─── function renderOrderColCheckboxes(type) { var grid = document.getElementById(type + 'ColGrid'); if (!grid) return; var cols = getOrderColumns(type); var dragVar = '_' + type + 'DragOrder'; var pendingVar = '_' + type + 'PendingVisible'; var order = window[dragVar] || cols.map(function(c) { return c.field; }); grid.innerHTML = ''; order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; var checked = window[pendingVar][field] ? 'checked' : ''; var label = document.createElement('label'); label.draggable = true; label.dataset.field = field; label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px'; label.innerHTML = '⠿ ' + col.label; label.addEventListener('dragstart', function(e) { e.dataTransfer.setData('text/plain', field); this.style.opacity = '0.4'; }); label.addEventListener('dragend', function(e) { this.style.opacity = '1'; document.querySelectorAll('#' + type + 'ColGrid label').forEach(function(el) { el.style.border = ''; }); }); label.addEventListener('dragover', function(e) { e.preventDefault(); document.querySelectorAll('#' + type + 'ColGrid label').forEach(function(el) { el.style.border = ''; }); this.style.border = '2px dashed var(--primary)'; }); label.addEventListener('dragleave', function(e) { this.style.border = ''; }); label.addEventListener('drop', function(e) { e.preventDefault(); document.querySelectorAll('#' + type + 'ColGrid label').forEach(function(el) { el.style.border = ''; }); var fromField = e.dataTransfer.getData('text/plain'); var toField = this.dataset.field; if (fromField === toField) return; var order = window[dragVar]; var fromIdx = order.indexOf(fromField); var toIdx = order.indexOf(toField); if (fromIdx === -1 || toIdx === -1) return; order.splice(fromIdx, 1); order.splice(toIdx, 0, fromField); renderOrderColCheckboxes(type); }); grid.appendChild(label); }); } // ─── 模态弹窗 ─── function openOrderColModal(type) { var cols = getOrderColumns(type); var dragVar = '_' + type + 'DragOrder'; var pendingVar = '_' + type + 'PendingVisible'; var visVar = type + 'ColVisible'; // 初始化拖拽顺序:以当前已生效顺序为基础,补齐新增字段。 var currentOrder = type === 'finished' && window._finishedAppliedOrder ? window._finishedAppliedOrder.slice() : (window[dragVar] || []); var knownOrder = {}; currentOrder = currentOrder.filter(function(field) { if (!cols.some(function(c) { return c.field === field; }) || knownOrder[field]) return false; knownOrder[field] = true; return true; }); cols.forEach(function(c) { if (!knownOrder[c.field]) currentOrder.push(c.field); }); window[dragVar] = currentOrder; // 把当前生效状态复制到临时状态 cols.forEach(function(c) { window[pendingVar][c.field] = window[visVar][c.field]; }); // 渲染grid var grid = document.getElementById(type + 'ColGrid'); if (grid) renderOrderColCheckboxes(type); // 全选checkbox同步 var allCb = document.getElementById(type + '-cb-all'); if (allCb) { allCb.checked = cols.every(function(c) { return window[visVar][c.field]; }); } // 重置搜索 var searchInput = document.getElementById(type + 'ColSearchInput'); if (searchInput) searchInput.value = ''; filterOrderColCheckboxes(type); document.getElementById(type + 'ColModal').style.display = 'flex'; refreshOrderSchemeList(type); } // ─── 通用辅助函数 ─── function toggleOrderCol(type, field) { var pendingVar = '_' + type + 'PendingVisible'; window[pendingVar][field] = !window[pendingVar][field]; var show = window[pendingVar][field]; var cb = document.getElementById(type + '-cb-' + field); if (cb) cb.checked = show; } function filterOrderColCheckboxes(type) { var keyword = (document.getElementById(type + 'ColSearchInput')?.value || '').trim(); var labels = document.querySelectorAll('#' + type + 'ColCheckboxes label'); labels.forEach(function(label) { if (!keyword) { label.style.display = ''; } else { var text = label.textContent || ''; label.style.display = text.includes(keyword) ? '' : 'none'; } }); } function toggleOrderColAll(type, checked) { var cols = getOrderColumns(type); var pendingVar = '_' + type + 'PendingVisible'; cols.forEach(function(c) { window[pendingVar][c.field] = checked; var cb = document.getElementById(type + '-cb-' + c.field); if (cb) cb.checked = checked; }); } function closeOrderColModal(type) { document.getElementById(type + 'ColModal').style.display = 'none'; } // ─── 确认/应用字段设置 ─── function confirmOrderCol(type) { var cols = getOrderColumns(type); var dragVar = '_' + type + 'DragOrder'; var pendingVar = '_' + type + 'PendingVisible'; var visVar = type + 'ColVisible'; // 应用拖拽后的新顺序 var newOrder = window[dragVar]; if (newOrder) { var reordered = []; newOrder.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (col) reordered.push(col); }); cols.forEach(function(c) { if (newOrder.indexOf(c.field) === -1) reordered.push(c); }); // 修改原数组(按引用) cols.length = 0; reordered.forEach(function(c) { cols.push(c); }); } // 更新可见性 cols.forEach(function(c) { window[visVar][c.field] = window[pendingVar][c.field]; }); saveOrderColVisible(type, window[visVar]); if (type === 'finished') { window._finishedAppliedOrder = (window[dragVar] || []).slice(); localStorage.setItem('finishedColOrder', JSON.stringify(window._finishedAppliedOrder)); } document.getElementById(type + 'ColModal').style.display = 'none'; // 全量刷新页面,确保表头与表体列顺序一致(同计划预测 confirmPlanCol 做法) var pageMap = { 'domestic': 'orders_domestic', 'overseas': 'orders_overseas', 'packaging': 'orders_packaging', 'finished': 'orders_finished' }; var pageKey = pageMap[type]; if (pageKey && window._pageCache) { delete window._pageCache[pageKey]; loadPage(pageKey); } } // ─── 方案管理 ─── // 构建当前列配置JSON function buildOrderColumnsJSON(type) { var cols = getOrderColumns(type); var dragVar = '_' + type + 'DragOrder'; var pendingVar = '_' + type + 'PendingVisible'; var order = window[dragVar] || cols.map(function(c) { return c.field; }); var result = []; order.forEach(function(field) { var colDef = cols.find(function(c) { return c.field === field; }); if (colDef) { result.push({ field: colDef.field, label: colDef.label, visible: window[pendingVar][field] !== undefined ? window[pendingVar][field] : true }); } }); cols.forEach(function(c) { var found = result.some(function(col) { return col.field === c.field; }); if (!found) { result.push({ field: c.field, label: c.label, visible: window[pendingVar][c.field] !== undefined ? window[pendingVar][c.field] : c.defShow }); } }); return JSON.stringify(result); } // 保存方案 window.saveOrderScheme = function(type) { var nameInput = document.getElementById(type + 'SchemeNameInput'); if (!nameInput) return; var schemeName = nameInput.value.trim(); if (!schemeName) { alert('请输入方案名称'); nameInput.focus(); return; } var columnsJson = buildOrderColumnsJSON(type); fetch('/api/planning/scheme/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ scheme_name: schemeName, columns_json: columnsJson, page: type, is_default: 0 }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { nameInput.value = ''; refreshOrderSchemeList(type); alert('方案已保存'); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败,请检查网络'); }); }; // 加载方案 window.loadOrderScheme = function(type, schemeId) { if (schemeId === undefined || schemeId === null) return; var cols = getOrderColumns(type); var pendingVar = '_' + type + 'PendingVisible'; var dragVar = '_' + type + 'DragOrder'; fetch('/api/planning/schemes?page=' + type) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var target = null; for (var i = 0; i < schemes.length; i++) { if (schemes[i].id == schemeId) { target = schemes[i]; break; } } if (!target) { alert('方案不存在或已被删除'); refreshOrderSchemeList(type); return; } var parsed; try { parsed = JSON.parse(target.columns_json); } catch(e) { alert('方案数据异常'); return; } if (!parsed || !Array.isArray(parsed) || parsed.length === 0) { alert('方案数据为空'); return; } var newOrder = []; var pendingVisible = {}; parsed.forEach(function(c) { if (c && c.field) { newOrder.push(c.field); pendingVisible[c.field] = c.visible !== false; } }); cols.forEach(function(c) { if (pendingVisible[c.field] === undefined) { newOrder.push(c.field); pendingVisible[c.field] = false; } }); window[dragVar] = newOrder.slice(); Object.keys(pendingVisible).forEach(function(field) { window[pendingVar][field] = pendingVisible[field]; }); var grid = document.getElementById(type + 'ColGrid'); if (grid) renderOrderColCheckboxes(type); var allCb = document.getElementById(type + '-cb-all'); if (allCb) { var allChecked = cols.every(function(c) { return window[pendingVar][c.field]; }); allCb.checked = allChecked; } refreshOrderSchemeList(type); }) .catch(function(err) { console.error('loadOrderScheme error:', err); }); }; // 删除方案 window.deleteOrderScheme = function(type, schemeId) { showCenteredConfirm('确认', '确定要删除此方案吗?').then(function(ok) { if (!ok) return; fetch('/api/planning/scheme/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { refreshOrderSchemeList(type); } else { alert(res.msg || '删除失败'); } }); }); }; // 设为默认方案 window.setDefaultOrderScheme = function(type, schemeId) { fetch('/api/planning/scheme/set_default', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: schemeId, page: type}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadOrderScheme(type, schemeId); refreshOrderSchemeList(type); } else { alert(res.msg || '设置失败'); } }) .catch(function(err) { console.error('setDefaultOrderScheme error:', err); }); }; // 刷新方案列表UI function refreshOrderSchemeList(type) { var container = document.getElementById(type + 'SchemeList'); if (!container) return; fetch('/api/planning/schemes?page=' + type) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; // 只显示当前页面的方案 var pageSchemes = []; schemes.forEach(function(s) { if (s.page === type) pageSchemes.push(s); }); if (pageSchemes.length === 0) { container.innerHTML = '暂无保存方案'; return; } var html = ''; pageSchemes.forEach(function(s) { var isDefault = s.is_default == 1; html += ''; html += '' + escHtml(s.scheme_name) + ''; if (isDefault) { html += '★默认'; } html += '📌'; html += '✕'; html += ''; }); container.innerHTML = html; }); } // 加载默认方案(页面初始化时调用) function loadDefaultOrderScheme(type) { fetch('/api/planning/schemes?page=' + type) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var schemes = res.data || []; var defaultScheme = null; schemes.forEach(function(s) { if (s.is_default == 1 && s.page === type) defaultScheme = s; }); if (defaultScheme) { loadOrderScheme(type, defaultScheme.id); } }); } // ─── 保持向后兼容 ─── function getDomesticVisible() { return getOrderColVisible('domestic'); } function getOverseasVisible() { return getOrderColVisible('overseas'); } function getPackagingVisible() { return getOrderColVisible('packaging'); } function saveDomesticVisible(m) { saveOrderColVisible('domestic', m); } function saveOverseasVisible(m) { saveOrderColVisible('overseas', m); } function savePackagingVisible(m) { saveOrderColVisible('packaging', m); } window.openDomesticColModal = function() { openOrderColModal('domestic'); }; window.openOverseasColModal = function() { openOrderColModal('overseas'); }; window.openPackagingColModal = function() { openOrderColModal('packaging'); }; window.editRemark = function(td, id, field, apiEndpoint) { var textSpan = td.querySelector('.remark-text'); var current = textSpan ? textSpan.textContent.trim() : td.textContent.trim(); var input = document.createElement('input'); input.type = 'text'; input.className = 'remark-input'; input.value = current; input.style.cssText = 'width:100%;border:1px solid #1890ff;padding:2px 6px;font-size:12px;border-radius:3px;box-sizing:border-box'; td.innerHTML = ''; td.appendChild(input); input.focus(); function revert() { if (td.querySelector('input')) { td.innerHTML = '' + escHtml(current) + ' ✏️'; } } function doSave() { var newVal = input.value.trim(); if (newVal === current) { revert(); return; } showCenteredConfirm('确认', '是否保存修改?').then(function(ok) { if (ok) { fetch(apiEndpoint, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: field, value: newVal}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { td.innerHTML = '' + escHtml(newVal) + ' ✏️'; } else { alert('保存失败: ' + (res.msg || '')); revert(); } }) .catch(function(){ alert('网络错误'); revert(); }); } else { revert(); } }); } input.onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); doSave(); } else if (e.key === 'Escape') { revert(); } }; input.onblur = function() { setTimeout(function() { if (td.querySelector('input')) revert(); }, 250); }; }; // ─── 订单详情仓库模糊搜索 ─── var orderWarehouseListPromise = null; window.orderWarehouseSearch = function(input) { var searchValue = input.value.trim().toLowerCase(); if (!orderWarehouseListPromise) { orderWarehouseListPromise = fetch('/api/warehouse/all') .then(function(r) { return r.json(); }) .then(function(res) { return (res.data || []).filter(function(w) { return w && w.warehouse_name; }); }); } orderWarehouseListPromise.then(function(rows) { var matched = rows.filter(function(w) { return !searchValue || String(w.warehouse_name).toLowerCase().indexOf(searchValue) >= 0; }); showOrderWarehouseDropdown(input, matched); }).catch(hideOrderWarehouseDropdown); }; function showOrderWarehouseDropdown(input, rows) { var dd = document.getElementById('orderWarehouseDropdown'); if (!dd) { dd = document.createElement('div'); dd.id = 'orderWarehouseDropdown'; dd.style.cssText = 'position:fixed;z-index:100000;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:220px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);min-width:220px'; document.body.appendChild(dd); } if (!rows.length) { dd.style.display = 'none'; return; } window._orderWarehouseTarget = input; var rect = input.getBoundingClientRect(); dd.style.left = rect.left + 'px'; dd.style.top = (rect.bottom + 2) + 'px'; dd.style.minWidth = Math.max(rect.width, 220) + 'px'; dd.innerHTML = rows.map(function(w, index) { return '
' + '' + escHtml(w.warehouse_name) + '' + (w.warehouse_code ? ' ' + escHtml(w.warehouse_code) + '' : '') + '
'; }).join(''); Array.from(dd.children).forEach(function(item) { item.onmouseenter = function() { this.style.background = '#e6f7ff'; }; item.onmouseleave = function() { this.style.background = ''; }; item.onmousedown = function(e) { e.preventDefault(); var selected = rows[parseInt(this.getAttribute('data-index'), 10)]; var target = window._orderWarehouseTarget; if (target && selected) { target.value = selected.warehouse_name || ''; hideOrderWarehouseDropdown(); target.focus(); } }; }); dd.style.display = 'block'; } function hideOrderWarehouseDropdown() { var dd = document.getElementById('orderWarehouseDropdown'); if (dd) dd.style.display = 'none'; } if (!window._orderWarehouseDelegated) { window._orderWarehouseDelegated = true; document.addEventListener('input', function(e) { if (e.target.classList.contains('order-warehouse-search')) orderWarehouseSearch(e.target); }); document.addEventListener('focusin', function(e) { if (e.target.classList.contains('order-warehouse-search')) orderWarehouseSearch(e.target); }); document.addEventListener('focusout', function(e) { if (e.target.classList.contains('order-warehouse-search')) setTimeout(hideOrderWarehouseDropdown, 200); }); } // ─── 订单列表供应商/进度编辑与供应商模糊搜索 ─── function renderOrderInlineInput(row, field, api, kind) { var value = row[field] || ''; var extraClass = kind === 'supplier' ? ' order-supplier-search' : ''; var placeholder = kind === 'supplier' ? '输入简称或全称' : '填写进度'; return ''; } window.orderSupplierSearch = function(input) { var searchValue = input.value.trim(); clearTimeout(input._supplierSearchTimer); if (!searchValue) { hideOrderSupplierDropdown(); return; } input._supplierSearchTimer = setTimeout(function() { fetch('/api/suppliers/list?search=' + encodeURIComponent(searchValue) + '&size=20') .then(function(r) { return r.json(); }) .then(function(res) { showOrderSupplierDropdown(input, (res.data && res.data.rows) || []); }) .catch(hideOrderSupplierDropdown); }, 150); }; function showOrderSupplierDropdown(input, rows) { var dd = document.getElementById('orderSupplierDropdown'); if (!dd) { dd = document.createElement('div'); dd.id = 'orderSupplierDropdown'; dd.style.cssText = 'position:fixed;z-index:100000;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:220px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);min-width:260px'; document.body.appendChild(dd); } if (!rows.length) { dd.style.display = 'none'; return; } window._orderSupplierTarget = input; var rect = input.getBoundingClientRect(); dd.style.left = rect.left + 'px'; dd.style.top = (rect.bottom + 2) + 'px'; dd.style.minWidth = Math.max(rect.width, 260) + 'px'; dd.innerHTML = rows.map(function(s, index) { return '
' + '' + escHtml(s.supplier_name || '') + '' + (s.short_name ? ' ' + escHtml(s.short_name) + '' : '') + (s.supplier_code ? ' ' + escHtml(s.supplier_code) + '' : '') + '
'; }).join(''); Array.from(dd.children).forEach(function(item) { item.onmouseenter = function() { this.style.background = '#e6f7ff'; }; item.onmouseleave = function() { this.style.background = ''; }; item.onmousedown = function(e) { e.preventDefault(); var selected = rows[parseInt(this.getAttribute('data-index'), 10)]; var inp = window._orderSupplierTarget; if (inp && selected) { var newValue = selected.supplier_name || ''; var oldValue = inp.value.trim(); inp.value = newValue; inp.defaultValue = newValue; hideOrderSupplierDropdown(); // 点击选中即自动保存,无需回车 if (newValue !== oldValue && inp.hasAttribute('data-papi')) { inp.style.borderColor = '#1890ff'; fetch(inp.getAttribute('data-papi'), { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: parseInt(inp.getAttribute('data-pid'), 10), field: inp.getAttribute('data-pfield'), value: newValue}) }).then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 1000); } else { inp.style.borderColor = '#ff4d4f'; setTimeout(function() { inp.style.borderColor = ''; }, 1200); } }).catch(function() { inp.style.borderColor = '#ff4d4f'; setTimeout(function() { inp.style.borderColor = ''; }, 1200); }); } } else if (inp) { inp.focus(); hideOrderSupplierDropdown(); } }; }); dd.style.display = 'block'; } function hideOrderSupplierDropdown() { var dd = document.getElementById('orderSupplierDropdown'); if (dd) dd.style.display = 'none'; } window.saveOrderInlineEdit = function(input) { var oldValue = input.defaultValue; var newValue = input.value.trim(); if (newValue === oldValue) return; var label = input.getAttribute('data-kind') === 'supplier' ? '供应商名称' : '进度'; showCenteredConfirm('确认', '确认保存' + label + '为:' + (newValue || '空') + '?').then(function(ok) { if (!ok) { input.value = oldValue; return; } fetch(input.getAttribute('data-papi'), { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: parseInt(input.getAttribute('data-pid'), 10), field: input.getAttribute('data-pfield'), value: newValue}) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) throw new Error(res.msg || '保存失败'); input.defaultValue = newValue; var api = input.getAttribute('data-papi') || ''; if (api.indexOf('overseas') >= 0) overseasLoadOrders(overseasOrderPage || 1); else if (api.indexOf('packaging') >= 0) packagingLoadOrders(packagingOrderPage || 1); else domesticLoadOrders(domesticOrderPage || 1); }).catch(function(err) { alert(err.message || '保存失败'); input.value = oldValue; }); }); }; if (!window._orderInlineEditDelegated) { window._orderInlineEditDelegated = true; document.addEventListener('keydown', function(e) { if (e.key === 'Enter' && e.target.classList.contains('order-inline-edit')) { e.preventDefault(); hideOrderSupplierDropdown(); saveOrderInlineEdit(e.target); } }); document.addEventListener('input', function(e) { if (e.target.classList.contains('order-supplier-search')) orderSupplierSearch(e.target); }); document.addEventListener('focusin', function(e) { if (e.target.classList.contains('order-supplier-search')) orderSupplierSearch(e.target); }); document.addEventListener('focusout', function(e) { if (e.target.classList.contains('order-supplier-search')) setTimeout(hideOrderSupplierDropdown, 200); }); } // ─── 单价保存函数 ─── // ─── 供应商模糊搜索联动 ─── // ─── 供应商模糊搜索下拉(按简称匹配)─── window.domesticSupSearch = function(input) { var dd = document.getElementById('domesticSupDD'); if (!dd) { dd = document.createElement('div'); dd.id = 'domesticSupDD'; dd.style.cssText = 'position:fixed;z-index:99999;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12);min-width:200px'; document.body.appendChild(dd); } var val = input.value.trim().toLowerCase(); if (!val || !window._supplierList) { dd.style.display = 'none'; return; } var matched = []; window._supplierList.forEach(function(s) { var sn = (s.short_name || '').toLowerCase(); if (sn.indexOf(val) >= 0) matched.push(s); }); if (matched.length === 0) { dd.style.display = 'none'; return; } var rect = input.getBoundingClientRect(); dd.style.left = rect.left + 'px'; dd.style.top = (rect.bottom + 2) + 'px'; dd.style.minWidth = Math.max(rect.width, 200) + 'px'; var html = ''; matched.slice(0, 20).forEach(function(s) { var name = escHtml(s.supplier_name || ''); var sn = escHtml(s.short_name || ''); var code = escHtml(s.supplier_code || ''); html += '
'; html += '' + name + ''; html += ' ' + sn + ''; html += ' ' + code + ''; html += '
'; }); dd.innerHTML = html; dd.style.display = 'block'; }; window.domesticSupSelect = function(pid, name) { var input = document.querySelector('.supplier-inline[data-pid="' + pid + '"]'); if (input) { input.value = name; input.focus(); supplierSave(input); } var dd = document.getElementById('domesticSupDD'); if (dd) dd.style.display = 'none'; }; window.supplierSave = function(input) { var oldVal = input.defaultValue; var newVal = input.value.trim(); if (newVal === oldVal) return; var id = input.getAttribute('data-pid'); var field = input.getAttribute('data-pfield'); var api = input.getAttribute('data-papi'); showCenteredConfirm('确认', '是否保存修改?将供应商修改为:' + newVal).then(function(ok) { if (ok) { fetch(api, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: parseInt(id), field: field, value: newVal || ''}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { input.defaultValue = newVal; if (window.domesticLoadOrders) domesticLoadOrders(domesticOrderPage || 1); } else { alert('保存失败: ' + (res.msg || '')); input.value = oldVal; } }) .catch(function(){ alert('网络错误'); input.value = oldVal; }); } else { input.value = oldVal; } }); }; // 单价修改时自动重算同行金额 function recalcAmount(input) { var row = input.closest('tr'); if (!row) return; var price = parseFloat(input.value) || 0; var qty = 0; var amtCell = null; Array.from(row.cells).forEach(function(cell) { if (cell.classList.contains('qty-cell')) { qty = parseInt(cell.textContent.replace(/[^0-9-]/g,'')) || 0; } if (cell.classList.contains('amt-cell')) { amtCell = cell; } }); if (amtCell) { var amount = price * qty; amtCell.textContent = amount ? '¥' + amount.toLocaleString() : ''; amtCell.dataset.amount = amount; } } window.priceSave = function(input) { var oldVal = input.defaultValue; var newVal = input.value.trim(); if (newVal === oldVal) return; var id = input.getAttribute('data-pid'); var field = input.getAttribute('data-pfield'); var api = input.getAttribute('data-papi'); showCenteredConfirm('确认', '是否保存修改?').then(function(ok) { if (ok) { fetch(api, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: parseInt(id), field: field, value: newVal || '0'}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { input.defaultValue = newVal; // 根据API路径判定刷新哪个列表 if (api && api.indexOf('overseas') >= 0 && window.overseasLoadOrders) { overseasLoadOrders(overseasOrderPage || 1); } else if (api && api.indexOf('packaging') >= 0 && window.packagingLoadOrders) { packagingLoadOrders(packagingOrderPage || 1); } else if (window.domesticLoadOrders) { domesticLoadOrders(domesticOrderPage || 1); } } else { alert('保存失败: ' + (res.msg || '')); input.value = oldVal; } }) .catch(function(){ alert('网络错误'); input.value = oldVal; }); } else { input.value = oldVal; } }); }; // 表格事件委托:单价输入框 if (!window._priceDelegated) { window._priceDelegated = true; document.addEventListener('focusin', function(e) { if (e.target.classList.contains('price-inline')) e.target.style.borderColor = '#1890ff'; }); document.addEventListener('focusout', function(e) { if (e.target.classList.contains('price-inline')) { e.target.style.borderColor = '#d9d9d9'; } }); document.addEventListener('keydown', function(e) { if (e.key === 'Enter' && e.target.classList.contains('price-inline')) { e.preventDefault(); priceSave(e.target); } if (e.key === 'Enter' && e.target.classList.contains('supplier-inline')) { e.preventDefault(); supplierSave(e.target); } }); document.addEventListener('input', function(e) { if (e.target.classList.contains('supplier-inline')) { domesticSupSearch(e.target); } }); document.addEventListener('focusout', function(e) { if (e.target.classList.contains('supplier-inline')) { setTimeout(function() { var dd = document.getElementById('domesticSupDD'); if (dd) dd.style.display = 'none'; }, 200); } }); } // ─── 日期编辑函数 ─── window.editDateField = function(td, id, field, apiEndpoint) { var textSpan = td.querySelector('.date-text'); var current = textSpan ? textSpan.textContent.trim() : td.textContent.trim(); var dateOnly = current.split(' ')[0]; var input = document.createElement('input'); input.type = 'date'; input.className = 'date-input'; input.value = dateOnly || ''; input.style.cssText = 'width:100%;border:1px solid #1890ff;padding:2px 4px;font-size:12px;border-radius:3px;box-sizing:border-box'; td.innerHTML = ''; td.appendChild(input); input.focus(); // 主动弹出日期选择器(标记pickering防止blur误杀) input._picking = true; if (input.showPicker) input.showPicker(); setTimeout(function() { input._picking = false; }, 500); // 点击外部关闭编辑器 function outsideClick(e) { if (!td.contains(e.target)) { document.removeEventListener('click', outsideClick, true); if (!saved) { saved = true; restoreView(current); } } } setTimeout(function() { document.addEventListener('click', outsideClick, true); }, 100); function makeIconTd(val) { var v = val || ''; return '' + escHtml(v) + ' 📅'; } function restoreView(val) { td.innerHTML = makeIconTd(val); td.querySelector('.date-edit-btn').onclick = function() { editDateField(td, parseInt(this.getAttribute('data-did')), this.getAttribute('data-dfield'), this.getAttribute('data-dapi')); }; } var saved = false; function doSave() { if (saved) return; saved = true; var newVal = input.value; var isClear = !newVal && !!dateOnly; // 无变化 → 直接退出 if (newVal === dateOnly || (!newVal && !dateOnly)) { restoreView(current); return; } // 清空日期 → 直接保存不弹框 if (isClear) { fetch(apiEndpoint, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: field, value: ''}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { if (window.domesticLoadOrders) domesticLoadOrders(domesticOrderPage || 1); else if (window.overseasLoadOrders) overseasLoadOrders(overseasOrderPage || 1); } else { alert('保存失败: ' + (res.msg || '')); restoreView(current); } }) .catch(function(){ alert('网络错误'); restoreView(current); }); return; } // 修改日期 → 弹确认 showCenteredConfirm('确认', '是否保存修改?').then(function(ok) { if (ok) { fetch(apiEndpoint, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: field, value: newVal}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { if (window.domesticLoadOrders) domesticLoadOrders(domesticOrderPage || 1); else if (window.overseasLoadOrders) overseasLoadOrders(overseasOrderPage || 1); } else { alert('保存失败: ' + (res.msg || '')); restoreView(current); } }) .catch(function(){ alert('网络错误'); restoreView(current); }); } else { restoreView(current); } }); } input.onchange = function() { doSave(); }; input.onblur = function() { if (saved || input._picking) return; saved = true; restoreView(current); }; }; // ──────────── 订单管理(原采购订单) ──────────── function loadOrdersPage(container) { container.innerHTML = `

📋 采购订单明细

序号下单时间下单单号货品编号货品名称单价数量总金额供应商名称货期状态操作
加载中...
`; loadOrders(1); } function loadOrders(page) { ordersCurrentPage = page; fetch('/api/purchase/orders/list?page=' + page + '&size=' + ORDERS_PAGE_SIZE) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('ordersTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无采购订单明细'; } else { var startIdx = (page - 1) * ORDERS_PAGE_SIZE; var statusMap = {'pending':'🟡 待确认','confirmed':'🟢 已确认'}; tbody.innerHTML = data.rows.map(function(o, i) { var amount = (parseFloat(o.order_qty) || 0) * (parseFloat(o.unit_price) || 0); var statLabel = statusMap[o.status] || o.status || '🟡 待确认'; var isPending = (o.status || 'pending') === 'pending'; return '' + '' + (startIdx + i + 1) + '' + '' + escHtml(o.order_time || '-') + '' + '' + escHtml(o.order_no) + '' + '' + escHtml(o.product_code) + '' + '' + escHtml(o.product_name || '-') + '' + '¥' + (parseFloat(o.unit_price) || 0).toFixed(2) + '' + '' + (parseInt(o.order_qty) || 0).toLocaleString() + '' + '¥' + (parseFloat(o.amount || amount) || 0).toLocaleString() + '' + '' + escHtml(o.supplier_name) + '' + '' + (o.lead_time || '-') + '天' + '' + statLabel + '' + '' + ' ' + ' ' + (isPending ? ' ' : '') + '' + '' + ''; }).join(''); } var pel = document.getElementById('ordersPagination'); if (pel) { renderPaginationWithSize(pel, page, data.total || 0, ORDERS_PAGE_SIZE, 'loadOrders', 'ORDERS_PAGE_SIZE'); } }); } window.viewOrder = function(orderId) { document.getElementById('orderDetailModal').style.display = 'flex'; document.getElementById('orderDetailTitle').textContent = '订单详情'; var content = document.getElementById('orderDetailContent'); content.innerHTML = '
'; fetch('/api/purchase/orders/detail?id=' + orderId) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { content.innerHTML = '
' + (res.msg || '加载失败') + '
'; return; } var order = res.data.order; var items = res.data.items || []; var statusMap = {'pending':'🟡 待确认','confirmed':'🟢 已确认'}; var html = '
'; html += '
'; html += '订单号:' + escHtml(order.order_no) + ''; html += '供应商:' + escHtml(order.supplier_name) + ''; html += '总数量:' + (order.total_qty || 0).toLocaleString() + ''; html += '总金额:¥' + (parseFloat(order.total_amount) || 0).toLocaleString() + ''; html += '状态:' + (statusMap[order.status] || order.status) + ''; html += '创建人:' + escHtml(order.created_by || '-') + ''; html += '创建时间:' + (order.created_at || '-') + ''; html += '
'; if (items.length > 0) { html += ''; html += ''; items.forEach(function(item) { html += ''; }); html += '
货品编号货品名称规格建议下单下单数量单价金额货期操作
' + escHtml(item.product_code) + '' + escHtml(item.product_name) + '' + escHtml(item.specification || '-') + '' + (item.suggested_qty || 0) + '' + (item.order_qty || 0).toLocaleString() + '¥' + parseFloat(item.unit_price || 0).toFixed(2) + '¥' + (parseFloat(item.amount) || 0).toLocaleString() + '' + (item.lead_time || '-') + '天
'; } content.innerHTML = html; }); }; window.editOrderItem = function(itemId, code, name, price, qty) { document.getElementById('editItemId').value = itemId; document.getElementById('editItemCode').value = code; document.getElementById('editItemName').value = name; document.getElementById('editUnitPrice').value = price; document.getElementById('editOrderQty').value = qty; document.getElementById('orderEditModal').style.display = 'flex'; }; window.saveOrderItem = function() { var itemId = document.getElementById('editItemId').value; var price = parseFloat(document.getElementById('editUnitPrice').value) || 0; var qty = parseInt(document.getElementById('editOrderQty').value) || 0; fetch('/api/purchase/orders/update_item', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({item_id: itemId, order_qty: qty, unit_price: price}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { document.getElementById('orderEditModal').style.display = 'none'; loadOrders(ordersCurrentPage); } else { alert(res.msg || '保存失败'); } }); }; window.submitOrder = function(orderId) { showCenteredConfirm('确认', '确认提交此订单?提交后不可撤回。').then(function(ok) { if (!ok) return; fetch('/api/purchase/orders/update_status', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: orderId, status: 'confirmed'}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadOrders(ordersCurrentPage); } else { alert(res.msg || '操作失败'); } }); }); }; window.deleteOrder = function(orderId) { showCenteredConfirm('确认', '确定删除此订单?删除后不可恢复。').then(function(ok) { if (!ok) return; fetch('/api/purchase/orders/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: orderId}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadOrders(ordersCurrentPage); } else { alert(res.msg || '操作失败'); } }); }); }; // ─── 物料清单管理 (树形折叠视图,以货品编号分页) ─── var BOM_PAGE_SIZE = 20; var BOM_PAGE_SIZE_OPTIONS = [10, 20, 50]; var bomCurrentPage = 1; var bomExpanded = {}; // { "productCode": true/false } function loadBomPage(container) { container.innerHTML = `

📋 物料清单

物料编号 物料名称 用量 类型 成本价 供应商 可用库存 生产在途 采购在途 维护 操作
加载中...
`; loadBom(1); } function loadBom(page) { bomCurrentPage = page; var search = document.getElementById('bomSearchInput')?.value || ''; var maintFilter = document.getElementById('bomMaintFilter')?.value || ''; var supplierFilter = document.getElementById('bomSupplierFilter')?.value || ''; var url = '/api/bom/list?page=' + page + '&size=' + BOM_PAGE_SIZE; if (search) url += '&search=' + encodeURIComponent(search); if (maintFilter) url += '&maint_filter=' + encodeURIComponent(maintFilter); if (supplierFilter) url += '&supplier=' + encodeURIComponent(supplierFilter); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('bomTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据,请先导入BOM文件'; } else { // Group by product_code (already batched by product) var groups = {}; var groupOrder = []; data.rows.forEach(function(r) { var key = r.product_code; if (!groups[key]) { groups[key] = { product_code: r.product_code, product_name: r.product_name, product_cost_price: r.product_cost_price, product_supplier: r.product_supplier, materials: [] }; groupOrder.push(key); } groups[key].materials.push(r); }); var html = ''; groupOrder.forEach(function(key) { var g = groups[key]; var isExpanded = bomExpanded[key] === true; // Product header row // 成品成本价 = 绑定的包材成本价之和(非货款之和,不乘用量) var totalMatCost = 0; g.materials.forEach(function(m) { var mc = parseFloat(m.material_cost_price) || 0; totalMatCost += mc; }); var productCost = totalMatCost > 0 ? '¥' + totalMatCost.toFixed(2) : '-'; var productSupplier = g.product_supplier || '-'; var foldIcon = isExpanded ? '▼' : '▶'; // 成品库存(从第一行取,所有行同产品数据一致) var prodAvail = g.materials[0] ? (parseInt(g.materials[0].product_available_qty) || 0) : 0; var prodProdTr = g.materials[0] ? (parseInt(g.materials[0].product_production_in_transit) || 0) : 0; var prodPurchTr = g.materials[0] ? (parseInt(g.materials[0].product_purchase_in_transit) || 0) : 0; var prodInvClass = prodAvail > 0 ? '' : ' style="color:#ff4d4f;font-weight:600"'; // 判断是否有实际包材 var hasRealMats = g.materials.some(function(r) { return r.material_code && r.material_code.trim(); }); var maintLabel = hasRealMats ? '有BOM' : '无BOM'; html += '' + '' + foldIcon + '' + '' + escHtml(g.product_code) + '' + '' + escHtml(g.product_name) + '' + '-' + '成品' + '' + productCost + '' + '' + escHtml(productSupplier) + '' + '' + prodAvail.toLocaleString() + '' + '' + prodProdTr.toLocaleString() + '' + '' + prodPurchTr.toLocaleString() + '' + '' + maintLabel + '' + '' + ' ' + ' ' + '' + '' + ''; // Material detail rows (hidden unless expanded) if (isExpanded) { // 过滤掉占位空物料行 var realMats = g.materials.filter(function(r) { return r.material_code && r.material_code.trim(); }); if (realMats.length === 0) { html += '' + '暂无包材数据,请点击编辑维护'; } else { realMats.forEach(function(r) { var qty = parseInt(r.quantity) + ''; var matCost = r.material_cost_price ? '¥' + parseFloat(r.material_cost_price).toFixed(2) : '-'; var matSupplier = r.material_supplier || '-'; var avail = r.mat_available_qty || 0; var prodTr = r.mat_prod_transit || 0; var purchTr = r.mat_purch_transit || 0; html += '' + '' + '' + escHtml(r.material_code) + '' + '' + escHtml(r.material_name) + '' + '' + qty + '' + '包材' + '' + matCost + '' + '' + escHtml(matSupplier) + '' + '' + (avail || 0) + '' + '' + (prodTr || 0) + '' + '' + (purchTr || 0) + '' + '' + ''; }); } } }); tbody.innerHTML = html; } var pel = document.getElementById('bomPagination'); if (pel) { renderPaginationWithSize(pel, page, data.total || 0, BOM_PAGE_SIZE, 'loadBom', 'BOM_PAGE_SIZE'); } }); } window.toggleBomMaterial = function(productCode) { bomExpanded[productCode] = !bomExpanded[productCode]; loadBom(bomCurrentPage); }; // 绑定回车搜索 window.bomSearchKeydown = function(evt) { if (evt.key === 'Enter') bomSearch(); }; window.bomSearch = function() { bomCurrentPage = 1; loadBom(1); }; // 重置所有筛选条件 window.bomReset = function() { var si = document.getElementById('bomSearchInput'); var sf = document.getElementById('bomSupplierFilter'); var mf = document.getElementById('bomMaintFilter'); if (si) si.value = ''; if (sf) sf.value = ''; if (mf) mf.value = ''; bomCurrentPage = 1; loadBom(1); }; window.bomMaintFilterChange = function() { bomCurrentPage = 1; loadBom(1); }; // ─── 物料清单编辑弹窗 ─── window.showBomEdit = function(productCode) { if (!productCode) return; // 物料清单管理编辑弹窗需要展示完整 BOM(包括内料);计划预测/下单场景仍使用默认过滤结果。 // 缓存弹窗打开时的旧物料数据,用于检测编号变更 window._oldBomMaterials = null; fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode) + '&include_inner=1') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { alert(res.msg || '获取BOM数据失败'); return; } var materials = (res.data && res.data.bom_materials) || []; // 缓存旧物料(深拷贝,用于后续检测编号变化) window._oldBomMaterials = JSON.parse(JSON.stringify(materials)); var old = document.getElementById('bomEditModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'bomEditModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; // Build rows var bodyHtml = ''; if (materials.length === 0) { bodyHtml = window._bomEditRowHtml(1); } else { materials.forEach(function(m, i) { bodyHtml += window._bomEditRowHtml(i + 1, m); }); } var productName = (res.data && res.data.product && res.data.product.product_name) || productCode; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); window._bindBomSupEvents(document.querySelectorAll('#bomEditTbody .bom-sup-name')); var bomEditTbody = document.getElementById('bomEditTbody'); if (bomEditTbody) bomEditTbody.addEventListener('input', window.fitBomEditColWidths); window.fitBomEditColWidths(); }); }; // 编辑弹窗:按表头和当前输入内容计算各列宽度,避免编号、名称、供应商等内容被截断。 window.fitBomEditColWidths = function() { var table = document.getElementById('bomEditTable'); var tbody = document.getElementById('bomEditTbody'); if (!table || !tbody) return; var colWidths = []; var textWidth = function(value) { var width = 0; String(value == null ? '' : value).split('\n').forEach(function(line) { var lineWidth = 0; for (var i = 0; i < line.length; i++) { lineWidth += line.charCodeAt(i) > 255 ? 13 : 7.5; } width = Math.max(width, lineWidth); }); return width; }; var minWidths = [42, 160, 180, 100, 110, 180, 46]; var controls = [null, 'input.bom-mat-code', 'input.bom-mat-name', 'input.bom-mat-qty', 'input.bom-mat-cost', 'input.bom-sup-name', null]; Array.prototype.forEach.call(table.querySelectorAll('thead th'), function(th, index) { colWidths[index] = Math.max(minWidths[index] || 0, textWidth(th.textContent) + 24); }); Array.prototype.forEach.call(tbody.querySelectorAll('tr'), function(row) { Array.prototype.forEach.call(row.cells, function(cell, index) { var control = controls[index] ? cell.querySelector(controls[index]) : null; var value = control ? control.value : cell.textContent; var minWidth = minWidths[index] || 0; colWidths[index] = Math.max(colWidths[index] || 0, textWidth(value) + (index === 3 || index === 4 ? 34 : 24), minWidth); }); }); Array.prototype.forEach.call(table.rows, function(row) { Array.prototype.forEach.call(row.cells, function(cell, index) { var width = colWidths[index]; if (!width) return; cell.style.width = width + 'px'; cell.style.minWidth = width + 'px'; cell.style.boxSizing = 'border-box'; }); }); table.style.width = 'max-content'; table.style.minWidth = '100%'; }; window._bomEditRowHtml = function(idx, data) { data = data || {}; var code = escHtml(String(data.material_code || '')); var name = escHtml(String(data.material_name || '')); var qty = data.bom_qty || 1; var cost = data.material_cost || data.material_cost_price || 0; var supName = escHtml(String(data.material_supplier || '')); var html = ''; html += '' + idx + ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '✕'; html += ''; return html; }; window.addBomEditRow = function() { var tbody = document.getElementById('bomEditTbody'); if (!tbody) return; var idx = tbody.querySelectorAll('.bom-bind-row').length + 1; tbody.insertAdjacentHTML('beforeend', window._bomEditRowHtml(idx)); window._bindBomSupEvents(tbody.querySelectorAll('.bom-bind-row:last-child .bom-sup-name')); window.fitBomEditColWidths(); }; window.submitBomEdit = function(productCode) { var tbody = document.getElementById('bomEditTbody'); if (!tbody) return; var materials = []; [].forEach.call(tbody.querySelectorAll('.bom-bind-row'), function(row) { var code = (row.querySelector('.bom-mat-code')?.value || '').trim(); var name = (row.querySelector('.bom-mat-name')?.value || '').trim(); var supName = (row.querySelector('.bom-sup-name')?.value || '').trim(); var qty = parseFloat(row.querySelector('.bom-mat-qty')?.value) || 1; var cost = parseFloat(row.querySelector('.bom-mat-cost')?.value) || 0; if (code) { materials.push({material_code: code, material_name: name, quantity: qty, cost_price: cost, supplier_name: supName}); } }); if (materials.length === 0) { alert('请填写至少一行物料信息'); return; } // 检测物料编号变更,同步到产品表 var codeChanges = {}; if (window._oldBomMaterials && window._oldBomMaterials.length > 0) { window._oldBomMaterials.forEach(function(oldMat) { var oldCode = oldMat.material_code; if (!oldCode) return; // 在当前提交的物料列表中找相同编号 var matched = materials.some(function(newMat) { return newMat.material_code === oldCode; }); if (!matched) { // 旧编号不存在了,尝试按物料名称匹配 var oldName = oldMat.material_name; materials.forEach(function(newMat) { if (newMat.material_code !== oldCode && newMat.material_name === oldName) { codeChanges[oldCode] = newMat.material_code; } }); } }); // 如果按名称没匹配到,再尝试按顺序 if (Object.keys(codeChanges).length === 0 && window._oldBomMaterials.length === materials.length) { window._oldBomMaterials.forEach(function(oldMat, idx) { var oldCode = oldMat.material_code; var newCode = materials[idx].material_code; if (oldCode && newCode && oldCode !== newCode) { codeChanges[oldCode] = newCode; } }); } window._oldBomMaterials = null; } var btn = document.querySelector('.btn-primary[onclick*="submitBomEdit"]'); if (btn) { btn.disabled = true; btn.textContent = '保存中...'; } // 先删除旧的,再新增 fetch('/api/bom/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(delRes) { if (delRes.code !== 0) { throw new Error(delRes.msg || '删除旧数据失败'); } return fetch('/api/bom/create_with_materials', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, materials: materials}) }); }) .then(function(r) { return r.json(); }) .then(function(res) { if (btn) { btn.disabled = false; btn.textContent = '💾 保存'; } if (res.code === 0) { // 同步物料编号变更到产品表 var codeChangeEntries = Object.keys(codeChanges); if (codeChangeEntries.length > 0) { (function syncCode(idx) { if (idx >= codeChangeEntries.length) { alert('✅ ' + res.msg + ';' + codeChangeEntries.length + '个物料编号已同步到产品信息'); document.getElementById('bomEditModal').remove(); loadBom(bomCurrentPage); return; } var oldCode = codeChangeEntries[idx]; var newCode = codeChanges[oldCode]; fetch('/api/product/batch_update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: oldCode, fields: {product_code: newCode}}) }) .then(function(r) { return r.json(); }) .then(function(sr) { if (sr.code !== 0) { alert('物料编号 [' + oldCode + '→' + newCode + '] 同步失败: ' + (sr.msg || '')); } syncCode(idx + 1); }) .catch(function() { alert('物料编号 [' + oldCode + '→' + newCode + '] 同步失败: 网络错误'); syncCode(idx + 1); }); })(0); } else { alert('✅ ' + res.msg); document.getElementById('bomEditModal').remove(); loadBom(bomCurrentPage); } } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function(err) { if (btn) { btn.disabled = false; btn.textContent = '💾 保存'; } alert('❌ ' + (err.message || '操作失败')); }); }; // ─── 删除成品BOM(含所有物料行) ─── window.deleteBomProduct = function(productCode) { if (!productCode) return; showCenteredConfirm('确认', '确认删除 [' + productCode + '] 的物料清单数据吗?此操作不可恢复!').then(function(ok) { if (!ok) return; fetch('/api/bom/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ ' + (res.msg || '删除成功')); loadBom(bomCurrentPage); } else { alert('❌ ' + (res.msg || '删除失败')); } }) .catch(function() { alert('❌ 网络错误'); }); }); }; // ─── BOM新增弹窗 ─── window.openBomAddModal = function() { var old = document.getElementById('bomAddModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'bomAddModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); // 初始给一行 + 绑定供应商搜索 setTimeout(function() { var tb = document.getElementById('bomNewMatTbody'); if (tb && !tb.querySelectorAll('tr').length) { tb.innerHTML = window._bomAddMatRowHtml(1); } // 绑定供应商自动搜索 window._bindBomSupEvents(document.querySelectorAll('#bomAddModal .bom-sup-name')); }, 50); }; // 新增弹窗 - 物料行HTML window._bomAddMatRowHtml = function(idx, data) { data = data || {}; var code = escHtml(String(data.material_code || '')); var name = escHtml(String(data.material_name || '')); var qty = data.quantity || 1; var sup = escHtml(String(data.supplier_name || '')); var ptype = data.product_type || '包材'; var cost = data.cost_price || 0; return '' + '' + idx + '' + '' + '' + '' + '' + '' + '' + '✕' + ''; }; // 新增弹窗 - 添加行 window.bomAddNewRow = function() { var tb = document.getElementById('bomNewMatTbody'); if (!tb) return; var idx = tb.querySelectorAll('tr').length + 1; tb.insertAdjacentHTML('beforeend', window._bomAddMatRowHtml(idx)); window._bindBomSupEvents(tb.querySelectorAll('tr:last-child .bom-sup-name')); }; // 新增弹窗 - 删除行 window.bomAddRemoveRow = function(el) { var tr = el.closest('tr'); if (!tr) return; var tb = tr.closest('tbody'); tr.remove(); // 重新编号 if (tb) { [].forEach.call(tb.querySelectorAll('tr'), function(r, i) { r.querySelector('td:first-child').textContent = i + 1; }); } }; // 关闭弹窗 window.closeBomAddModal = function() { var m = document.getElementById('bomAddModal'); if (m) m.remove(); }; // 提交新增BOM window.submitBomAdd = function() { var productCode = document.getElementById('bomNewProdCode').value.trim(); if (!productCode) { alert('请输入成品编号'); return; } var tbody = document.getElementById('bomNewMatTbody'); if (!tbody) return; var materials = []; [].forEach.call(tbody.querySelectorAll('.bom-add-row'), function(row) { var code = (row.querySelector('.bom-add-mat-code')?.value || '').trim(); var name = (row.querySelector('.bom-add-mat-name')?.value || '').trim(); var qty = parseFloat(row.querySelector('.bom-add-mat-qty')?.value) || 1; var supName = (row.querySelector('.bom-sup-name')?.value || '').trim(); var cost = parseFloat(row.querySelector('.bom-add-mat-cost')?.value) || 0; var ptype = (row.querySelector('.bom-add-mat-type')?.value || '包材').trim(); if (code) { materials.push({material_code: code, material_name: name, quantity: qty, supplier_name: supName, cost_price: cost, product_type: ptype}); } }); if (materials.length === 0) { alert('请填写至少一行包材信息'); return; } var btn = document.querySelector('.btn-primary[onclick*="submitBomAdd"]'); if (btn) { btn.disabled = true; btn.textContent = '保存中...'; } var prodName = document.getElementById('bomNewProdName')?.value?.trim() || ''; var prodSup = document.getElementById('bomNewProdSup')?.value?.trim() || ''; fetch('/api/bom/create_with_materials', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, product_name: prodName, product_supplier: prodSup, materials: materials}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (btn) { btn.disabled = false; btn.textContent = '💾 保存 BOM'; } if (res.code === 0) { alert('✅ ' + res.msg); closeBomAddModal(); loadBom(1); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function(err) { if (btn) { btn.disabled = false; btn.textContent = '💾 保存 BOM'; } alert('❌ ' + (err.message || '网络错误')); }); }; // ─── BOM导入弹窗 ─── window.openBomImportModal = function() { var old = document.getElementById('bomImportModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'bomImportModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); }; window.previewBomImportFile = function() { var f = document.getElementById('bomImportFile'); var p = document.getElementById('bomImportPreview'); var b = document.getElementById('bomImportBtn'); if (f.files && f.files.length > 0) { p.textContent = '已选择:' + f.files[0].name + '(' + (f.files[0].size / 1024).toFixed(1) + 'KB)'; b.disabled = false; } else { p.textContent = ''; b.disabled = true; } }; window.submitBomImport = function() { var f = document.getElementById('bomImportFile'); if (!f.files || !f.files[0]) return; var btn = document.getElementById('bomImportBtn'); var reader = new FileReader(); reader.readAsArrayBuffer(f.files[0]); reader.onload = function(e) { try { var buf = new Uint8Array(e.target.result); // 尝试 UTF-8 解码,若包含乱码则回退 GBK var text = new TextDecoder('utf-8').decode(buf); if (text.indexOf('\ufffd') >= 0) { text = new TextDecoder('gbk').decode(buf); } if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1); text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); var lines = text.split('\n').filter(function(l) { return l.trim(); }); var items = []; // 第一行始终视为表头,直接跳过 var startIdx = 1; for (var i = startIdx; i < lines.length; i++) { var line = lines[i]; var cols = line.split(','); if (cols.length >= 4) { var pc = cols[0].trim(); if (!pc || pc.includes('示例')) continue; items.push({ product_code: pc, product_name: (cols[1] || '').trim(), material_code: cols[2].trim(), material_name: (cols[3] || '').trim(), quantity: parseFloat(cols[4]) || 1, supplier: (cols[5] || '').trim(), cost_price: parseFloat(cols[6]) || 0 }); } } if (items.length === 0) { alert('未解析到有效数据'); btn.disabled = false; return; } btn.disabled = true; btn.textContent = '导入中...'; fetch('/api/bom/import', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({items: items}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert(res.msg); document.getElementById('bomImportModal').remove(); loadBom(1); } else { alert(res.msg || '导入失败'); document.getElementById('bomImportBtn').disabled = false; } }) .catch(function(err) { alert('请求失败:' + err.message); document.getElementById('bomImportBtn').disabled = false; }); } catch(err) { alert('解析错误:' + err.message); btn.disabled = false; } }; }; // ─── BOM导出 ─── window.exportBom = function() { safeOpenUrl('/api/bom/export'); }; // ─── 统一产品详情弹窗(固定字段,参考弹窗展示内容1方案)─── var POPUP_VISIBLE_FIELDS = [ 'product_type', '产品种类', 'product_code', 'product_name', '产品分级', '昨日销量', '销售趋势', '库存', '加工厂库存', '生产在途', '采购在途', '日销量预测', '供应商名称', '3天平均销量', '7天平均销量', '14天平均销量', '库存可支撑天数', '加工厂库存支撑天数', '总支撑天数', '30天平均销量', '极值判断', '生产货期', '物流时间', '创建时间' ]; /** 字段显示名称映射 */ var POPUP_FIELD_LABELS = { 'product_type': '产品类型', '产品种类': '产品种类', 'product_code': '货品编号', 'product_name': '货品名称', '产品分级': '产品分级', 'brand': '品牌', 'specification': '规格', 'cost_price': '成本价', '昨日销量': '昨日销量', '3天平均销量': '3天平均销量', '7天平均销量': '7天平均销量', '14天平均销量': '14天平均销量', '30天平均销量': '30天平均销量', '60天平均销量': '60天平均销量', '销售趋势': '销售趋势', '库存': '库存', '加工厂库存': '加工厂库存', '生产在途': '生产在途', '采购在途': '采购在途', '日销量预测': '日销量预测', '库存可支撑天数': '库存可支撑天数', '库存工厂可支撑天数': '现货采购在途', '加工厂库存支撑天数': '加工厂库存支撑', '库存工厂待生产可支撑天数': '定制品在途', '总支撑天数': '总支撑天数', '库存健康指数': '库存健康指数', '风险把控': '风险把控', '建议下单数量': '建议下单数量', '实际下单数量': '实际下单数量', '建议回货数量': '建议回货数量', '供应商名称': '供应商名称', 'supplier_code': '供应商编码', '采购跟单': '采购跟单', '采购计划': '采购计划', '运营专员': '运营专员', '开发专员': '开发专员', '归属平台': '归属平台', '生产货期': '生产货期', '物流时间': '物流时间', 'lead_time': '生产货期', 'logistics_time': '物流时间', '最低起订量': '最低起订量', '创建时间': '创建时间', '是否新品': '是否新品', '一级类目': '一级类目', '二级类目': '二级类目', '三级类目': '三级类目', '批注': '批注', '极值判断': '极值判断', '14天波动率': '14天波动率', '波动风险': '波动风险', '3天最高销量': '3天最高销量', '7天最高销量': '7天最高销量', '14天最高销量': '14天最高销量', '30天最高销量': '30天最高销量', '14天最低销量': '14天最低销量', '14天标准差': '14天标准差', '最低销量偏差率': '最低销量偏差率', '最高销量偏差率': '最高销量偏差率', }; /** 格式化字段值 */ function formatPopupFieldValue(field, val, p) { if (val === null || val === undefined || val === '') return '-'; if (field === 'cost_price') return '¥' + parseFloat(val || 0).toFixed(4); if (field === 'product_type') { return escHtml(val); } if (field === 'product_code' || field === 'product_name') { return escHtml(val); } if (field === '产品分级') { var gcolors = {'S级':'#f50','A级':'#1890ff','B级':'#52c41a','C级':'#faad14','不参与评级':'#999'}; var gc = gcolors[val] || '#999'; return '' + escHtml(val) + ''; } if (field === '库存健康指数') { var v = parseFloat(val); if (isNaN(v) || v === 999) return '999'; if (v >= 1.5) return '' + v.toFixed(1) + ''; if (v >= 0.8) return '' + v.toFixed(1) + ''; return '' + v.toFixed(1) + ''; } if (field === '风险把控') { var rc = {'正常':'tag-green','新品':'tag-blue','呆滞品':'tag-orange','需启动清库存':'tag-red'}; return '' + escHtml(val) + ''; } if (field === '销售趋势') { var tc = { '开始上升':'tag-blue','开始下跌':'tag-orange','平稳趋势':'tag-green', '持续上升':'tag-red','持续下跌':'tag-purple','上升趋稳':'tag-blue', '下跌趋稳':'tag-orange','急增':'tag-red','急跌':'tag-red','近期无销售':'tag-grey' }; var c = tc[val] || 'tag-grey'; return '' + escHtml(val) + ''; } if (field === '日销量预测') { if (val === '近期无销售') return '近期无销售'; return '' + escHtml(val) + ''; } if (field === '建议下单数量' || field === '实际下单数量') { var v = parseInt(val); if (isNaN(v) || v === 0) return '0'; return '' + v.toLocaleString() + ''; } if (field === '建议回货数量') { var v = parseFloat(val); if (isNaN(v) || v === 0) return '0'; return '' + v.toFixed(1) + ''; } if (field === '库存可支撑天数' || field === '总支撑天数' || field === '加工厂库存支撑天数' || field === '库存工厂可支撑天数' || field === '库存工厂待生产可支撑天数') { var v = parseFloat(val); if (isNaN(v)) return '-'; if (v === 999) return '999'; return v.toFixed(1); } if (typeof val === 'number') { return val.toLocaleString(); } return escHtml(val); } // 占位:下单功能(待完善) // ─── 正式下单数量编辑 ─── function editFormalQty(productCode) { if (window._canEditField && !window._canEditField('planning', '正式下单数量')) return; var valSpan = document.getElementById('fqv-' + productCode); if (!valSpan) return; var currentVal = parseInt(valSpan.dataset.val) || 0; var code = productCode; // 替换为输入框 var input = document.createElement('input'); input.type = 'number'; input.className = 'form-input'; input.style.width = '80px'; input.value = currentVal; input.dataset.original = currentVal; valSpan.replaceWith(input); input.focus(); input.select(); // Enter 确认 input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); // 已有确认弹窗打开,忽略重复回车 if (input.dataset.confirming) return; var newVal = parseInt(input.value); if (isNaN(newVal) || newVal < 0) newVal = 0; if (newVal === parseInt(input.dataset.original)) { replaceBack(code, newVal); return; } // 标记正在确认,阻止 blur 恢复原值 input.dataset.confirming = '1'; showCenteredConfirm('修改确认', '确认将正式下单数量修改为 ' + newVal + '?').then(function(confirmed) { delete input.dataset.confirming; if (confirmed) { // 保存到后端 - 只更新数量,不创建待下单 fetch('/api/purchase/update_formal_qty', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, formal_order_qty: newVal}) }); replaceBack(code, newVal); } else { // 取消 → 恢复原值 replaceBack(code, parseInt(input.dataset.original)); } }); } else if (e.key === 'Escape') { replaceBack(code, parseInt(input.dataset.original)); } }); // 没有回车确认就失焦 → 恢复原值 input.addEventListener('blur', function() { if (input.dataset.confirming) return; setTimeout(function() { replaceBack(code, parseInt(input.dataset.original)); }, 200); }); } function replaceBack(code, val) { var input = document.querySelector('.formal-qty-wrap input'); if (!input) return; var wrap = input.closest('.formal-qty-wrap'); if (!wrap) return; var newSpan = document.createElement('span'); newSpan.className = 'formal-qty-val'; newSpan.id = 'fqv-' + code; newSpan.dataset.code = code; newSpan.dataset.val = val; newSpan.textContent = val; input.replaceWith(newSpan); } function planBatchOrder() { var checkedCodes = Object.keys(window._planSelectedProductCodes); if (checkedCodes.length === 0) { alert('请先勾选要下单的商品'); return; } showCenteredConfirm('确认', '确认将选中的 ' + checkedCodes.length + ' 个商品加入待下单列表?').then(function(ok) { if (!ok) return; // 清除待下单页面缓存 if (window._pageCache) delete window._pageCache['purchase']; fetch('/api/purchase/batch_add_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: checkedCodes}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { var d = res.data || {}; var msg = []; if (d.added > 0) msg.push('✅ ' + d.added + ' 个已加入待下单列表'); if (d.skipped > 0) msg.push('⚠️ ' + d.skipped + ' 个已在列表中,已跳过'); alert(msg.join('\n') || '操作完成'); } else { alert('❌ ' + (res.msg || '操作失败')); } }) .catch(function() { alert('❌ 网络错误'); }); }); } function showBatchResult(success, skipped) { var msg = []; if (success > 0) msg.push('✅ ' + success + ' 个已加入待下单列表'); if (skipped > 0) msg.push('⚠️ ' + skipped + ' 个已在列表中,已跳过'); alert(msg.join('\n') || '操作完成'); } // 进度详情弹窗 window.showProgressDetail = function(el) { var progressText = el.getAttribute('data-progress') || '-'; showCenteredAlert('进度详情', escHtml(progressText)); }; // 通用居中确认弹窗(返回Promise,resolve(true)=确认, resolve(false)=取消) window.showCenteredAlert = function(title, message, options) { if (typeof options === 'function') options = {onConfirm: options}; options = options || {}; if (options.id) { var existing = document.getElementById(options.id); if (existing) return existing; } var modal = document.createElement('div'); if (options.id) modal.id = options.id; var zIndex = options.zIndex || 100001; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:' + zIndex + ';display:flex;align-items:center;justify-content:center;overflow-y:auto'; modal.innerHTML = '
' + '
' + '

' + escHtml(title) + '

' + '

' + message + '

' + '
' + '
' + '' + '
'; document.body.appendChild(modal); var okBtn = modal.querySelector('#alertOkBtn'); if (okBtn) okBtn.onclick = function() { modal.remove(); if (typeof options.onConfirm === 'function') options.onConfirm(); }; // 点击遮罩层不关闭弹窗(仅通过按钮关闭) modal.addEventListener('click', function(e) { if (e.target === modal) {} }); return modal; }; // 覆盖原生 alert,全部走自定义弹窗,避免浏览器"不允许再次提示"限制 (function() { var origAlert = window.alert; window.alert = function(msg) { if (typeof window.showCenteredAlert === 'function') { window.showCenteredAlert('提示', String(msg)); } else { origAlert(msg); } }; })(); window.showCenteredConfirm = function(title, message) { return new Promise(function(resolve) { var modal = document.createElement('div'); modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:100000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; modal.innerHTML = '
' + '
' + '

' + escHtml(title) + '

' + '

' + message + '

' + '
' + '
' + '' + '' + '
'; document.body.appendChild(modal); document.getElementById('confirmCancelBtn').onclick = function() { modal.remove(); resolve(false); }; document.getElementById('confirmOkBtn').onclick = function() { modal.remove(); resolve(true); }; // 点击遮罩层不关闭弹窗(仅通过按钮关闭) modal.addEventListener('click', function(e) { if (e.target === modal) {} }); }); }; // 通用居中输入弹窗(返回Promise,resolve(输入值)=确认, resolve(null)=取消) window.showCenteredPrompt = function(title, message, defaultValue) { return new Promise(function(resolve) { var modal = document.createElement('div'); modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:100000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; modal.innerHTML = '
' + '
' + '

' + escHtml(title) + '

' + (message ? '

' + message + '

' : '') + '' + '
' + '
' + '' + '' + '
'; document.body.appendChild(modal); var input = modal.querySelector('#promptInput'); if (input) { input.focus(); input.select(); } function doOk() { modal.remove(); resolve(input ? input.value : ''); } function doCancel() { modal.remove(); resolve(null); } var cancelBtn = modal.querySelector('#promptCancelBtn'); var okBtn = modal.querySelector('#promptOkBtn'); if (cancelBtn) cancelBtn.onclick = doCancel; if (okBtn) okBtn.onclick = doOk; if (input) input.onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); doOk(); } else if (e.key === 'Escape') { doCancel(); } }; // 点击遮罩层不关闭弹窗(仅通过按钮关闭) modal.addEventListener('click', function(e) { if (e.target === modal) {} }); }); }; // 详情弹窗输入框回车确认(自定义弹窗版) window._confirmSaveDetailInput = function(inp) { if (!inp) return; var label = inp.previousElementSibling ? (inp.previousElementSibling.textContent || '') : ''; var v = inp.value; showCenteredConfirm('确认保存', '确认保存 ' + label + ' 为 ' + v + '?').then(function(ok) { if (!ok) return; inp.blur(); var btn = inp.closest('.modal-overlay') ? inp.closest('.modal-overlay').querySelector('.pd-footer .btn-primary') : null; if (btn) btn.click(); }); }; // 订单表格仓库输入框回车保存(自定义弹窗版) window._confirmSaveOrderField = function(inp) { if (!inp) return; var v = inp.value; showCenteredConfirm('确认保存', '确认保存仓库为 ' + v + '?').then(function(ok) { if (!ok) return; fetch('/api/domestic/orders/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: parseInt(inp.dataset.pid || inp.dataset.id, 10), field: '仓库', value: v}) }).then(function(r){return r.json()}).then(function(res){ if (res.code===0) { inp.blur(); inp.style.borderColor='#52c41a'; setTimeout(function(){inp.style.borderColor=''},800); } else { alert(res.msg||'保存失败'); } }).catch(function(){ alert('网络错误'); }); }); }; // 从计划预测中移除产品 window.planDeleteProduct = function(productCode) { if (!productCode) return; showCenteredConfirm('确认移除', '确认将 [' + escHtml(productCode) + '] 从计划预测中移除?
移除后该产品不再出现在计划预测列表中。' ).then(function(confirmed) { if (!confirmed) return; return showCenteredConfirm('⚠️ 二次确认', '确定要移除 [' + escHtml(productCode) + '] 吗?'); }).then(function(confirmed2) { if (!confirmed2) return; return fetch('/api/planning/hide', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode})}); }).then(function(resp) { return resp.json(); }).then(function(res) { if (res.code === 0) { if (typeof loadPlanPage === 'function') loadPlanPage(planCurrentPage || 1); } else { alert('❌ ' + (res.msg || '操作失败')); } }).catch(function(e) { console.warn('移除失败:', e); }); }; window.planOrder = function(data) { // 支持两种调用方式:传入 product_code 字符串(从缓存取数据)或直接传入数据对象 if (typeof data === 'string') { var cached = window._planOrderDataCache && window._planOrderDataCache[data]; if (cached) { try { data = JSON.parse(cached); } catch(e) { data = null; } } else { data = null; } } if (!data || !data.product_code) { alert('数据异常,无法下单'); return; } // 从页面读取当前最新的正式下单数量(用户可能已通过 ✏️ 编辑修改过) var fqvSpan = document.getElementById('fqv-' + data.product_code); if (fqvSpan) { var liveQty = parseInt(fqvSpan.dataset.val) || 0; data.formal_order_qty = liveQty; } // 先检查是否已在待下单列表 var pc = data.product_code; fetch('/api/purchase/check_pending?code=' + encodeURIComponent(pc)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.exists) { alert('❌ [' + pc + '] 已在待下单列表中,请勿重复添加'); return; } showPlanOrderConfirm(data); }) .catch(function() { showPlanOrderConfirm(data); }); }; function showPlanOrderConfirm(data) { if (!data || !data.product_code) { alert('数据异常,无法下单'); return; } var pc = data.product_code; // 先显示 loading 弹窗,同时加载BOM数据 var modal = document.createElement('div'); modal.id = 'planOrderConfirmModal'; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; modal.innerHTML = '
' + '
加载BOM包材数据...
'; document.body.appendChild(modal); // 异步加载BOM数据(后端按品牌自动决定是否含内料) fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(pc) + '&include_inner=auto') .then(function(r) { return r.json(); }) .then(function(res) { var bomMaterials = (res && res.code === 0 && res.data && res.data.bom_materials) ? res.data.bom_materials : []; renderPlanConfirmDialog(modal, data, bomMaterials); }) .catch(function() { // 加载失败也继续,无BOM显示 renderPlanConfirmDialog(modal, data, []); }); } function renderPlanConfirmDialog(modal, data, bomMaterials) { var formalQty = data.formal_order_qty || 0; // 检查BOM弹窗中是否已保存成品实际下单数量和备注 var savedProductQty = 0; var savedRemark = ''; if (window._pendingBomData && window._pendingBomData[data.product_code]) { window._pendingBomData[data.product_code].forEach(function(m) { if (m.material_code === '成品_' + data.product_code) { if (m.actual_quantity >= 0) savedProductQty = m.actual_quantity; if (m.remark) savedRemark = m.remark; } }); } var displayQty = savedProductQty >= 0 ? savedProductQty : formalQty; // 内料是否展示由接口按品牌分类判定(include_inner=auto):海外品牌含内料,其余品牌接口已过滤 var hasBom = bomMaterials.length > 0; // 商品基础信息:2列布局 var confirmHtml = '
'; confirmHtml += '

确认加入待下单列表?

'; confirmHtml += '
'; confirmHtml += ''; var fieldTriples = [ [['货品编号', data.product_code], ['供应商', data.supplier_name], ['采购跟单', data.procurement_follower]], [['货品名称', data.product_name], ['货期(天)', data.lead_time], ['采购计划', data.procurement_planner]], [['产品类型', data.product_type], ['物流(天)', data.logistics_time], ['实际下单数量', displayQty]], [['产品分级', data.product_grade], ['总支撑天数', data.total_support_days], ['下单备注', savedRemark || data.remark || '']] ]; fieldTriples.forEach(function(triple) { confirmHtml += ''; for (var t = 0; t < 3; t++) { var item = triple[t]; if (item) { confirmHtml += ''; confirmHtml += ''; } else { confirmHtml += ''; confirmHtml += ''; } } confirmHtml += ''; }); confirmHtml += '
' + item[0] + '' + escHtml(String(item[1] || '-')) + '
'; // BOM包材信息 if (hasBom) { confirmHtml += '
'; confirmHtml += '
'; confirmHtml += '📦 包材下单数据'; confirmHtml += '共 ' + bomMaterials.length + ' 项'; confirmHtml += '
'; confirmHtml += '
'; // 获取之前BOM弹窗保存的包材下单数据 var savedBomMap = {}; var savedRemarkMap = {}; var savedManuscriptMap = {}; if (window._pendingBomData && window._pendingBomData[data.product_code]) { window._pendingBomData[data.product_code].forEach(function(s) { savedBomMap[s.material_code] = s.actual_quantity; savedRemarkMap[s.material_code] = s.remark || ''; savedManuscriptMap[s.material_code] = s.change_manuscript || ''; }); } var cellBorder = 'border:1px solid #d9d9d9'; var thBase = 'padding:7px 8px;' + cellBorder + ';text-align:center;white-space:nowrap'; var thTight = 'padding:7px 3px;' + cellBorder + ';text-align:center;white-space:nowrap'; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; var tdCellBase = 'padding:7px 8px;' + cellBorder + ';text-align:center'; var tdTight = 'padding:7px 3px;' + cellBorder + ';text-align:center'; bomMaterials.forEach(function(b, i) { var orderQty = Math.ceil(formalQty * (b.bom_qty || 0)); var savedActual = (b.material_code in savedBomMap) ? savedBomMap[b.material_code] : orderQty; var bg2 = i % 2 === 0 ? '#fff' : '#f5f5f5'; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; var manuscriptVal = savedManuscriptMap[b.material_code] || ''; var manuscriptDisplay = manuscriptVal === '是' ? '是' : '否'; confirmHtml += ''; confirmHtml += ''; confirmHtml += ''; }); confirmHtml += '
物料编号物料名称用量建议下单数量实际下单数量库存采购在途生产在途是否修改稿件下单备注
' + escHtml(b.material_code || '') + '' + escHtml(b.material_name || '') + '' + (b.bom_qty || 0) + '' + orderQty.toLocaleString() + '' + (savedActual > 0 ? savedActual.toLocaleString() : '0') + '' + (b.available_qty || 0).toLocaleString() + '' + (b.purchase_in_transit || 0).toLocaleString() + '' + (b.production_in_transit || 0).toLocaleString() + '' + manuscriptDisplay + '' + escHtml(savedRemarkMap[b.material_code] || '') + '
'; } else { // 无包材商品仍保留与其他商品一致的包材区域和弹窗结构。 confirmHtml += '
'; confirmHtml += '
'; confirmHtml += '📦 包材下单数据'; confirmHtml += '共 0 项'; confirmHtml += '
'; confirmHtml += '
暂无数据
'; } confirmHtml += '
'; confirmHtml += '
'; confirmHtml += ''; confirmHtml += '
'; // 计划预测下所有商品统一使用同一尺寸和结构,不按产品类型或是否有包材切换版式。 modal.innerHTML = '
' + confirmHtml + '
'; document.getElementById('planOrderCancelBtn').onclick = function() { modal.remove(); }; document.getElementById('planOrderConfirmBtn').onclick = function() { modal.remove(); if (window._pageCache) delete window._pageCache['purchase']; var orderData = JSON.parse(JSON.stringify(data)); if (window._pendingBomData && window._pendingBomData[data.product_code]) { orderData.bom_items = JSON.stringify(window._pendingBomData[data.product_code]); // 从已保存的BOM数据中获取成品实际下单数量和备注 window._pendingBomData[data.product_code].forEach(function(m) { if (m.material_code === '成品_' + data.product_code) { if (m.actual_quantity >= 0) orderData.formal_order_qty = m.actual_quantity; if (m.remark) orderData.remark = m.remark; } }); } fetch('/api/purchase/add_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(orderData) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { showCenteredAlert('操作成功', '✅ 已加入待下单列表'); } else { showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败')); } }) .catch(function() { showCenteredAlert('网络错误', '❌ 网络错误'); }); }; }; window.showProductDetail = function(productCode) { if (!productCode) return; const old = document.getElementById('productDetailModal'); if (old) old.remove(); const modal = document.createElement('div'); modal.id = 'productDetailModal'; modal.className = 'pd-overlay'; modal.style.display = 'flex'; modal.innerHTML = `
产品详情 \u2014 加载中...
加载中...
`; document.body.appendChild(modal); // \u5e76\u884c\u8bf7\u6c42\uff1a\u8ba1\u5212\u6570\u636e + BOM/\u8ba2\u5355/\u9500\u91cf Promise.all([ fetch('/api/product/planning_detail?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }), fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }) ]).then(function(results) { var body = document.getElementById('pdBody'); if (!body) return; var planRes = results[0]; var bomRes = results[1]; if (planRes.code !== 0) { body.innerHTML = '
' + (planRes.msg || '\u67e5\u8be2\u5931\u8d25') + '
'; return; } var p = planRes.data; // \u66f4\u65b0\u6807\u9898\u4e3a\u4ea7\u54c1\u540d\u79f0 var titleEl = modal.querySelector('.pd-title'); if (titleEl) { titleEl.innerHTML = '\u4ea7\u54c1\u8be6\u60c5 \u2014 ' + escHtml(p.product_name || productCode) + ''; } // \u5206\u7ec4\u5b9a\u4e49\uff1a\u57fa\u7840\u4fe1\u606f\uff082\u5217\uff09/ \u9500\u91cf\u4fe1\u606f\uff084\u5217\uff09 var basicFields = ['product_type', '\u4ea7\u54c1\u79cd\u7c7b', 'product_code', 'product_name', '\u4f9b\u5e94\u5546\u540d\u79f0', '\u751f\u4ea7\u8d27\u671f', '\u7269\u6d41\u65f6\u95f4', '\u521b\u5efa\u65f6\u95f4', '\u6279\u6ce8']; var salesFields = ['\u4ea7\u54c1\u5206\u7ea7', '\u6628\u65e5\u9500\u91cf', '\u9500\u552e\u8d8b\u52bf', '\u5e93\u5b58', '\u52a0\u5de5\u5382\u5e93\u5b58', '\u751f\u4ea7\u5728\u9014', '\u91c7\u8d2d\u5728\u9014', '\u65e5\u9500\u91cf\u9884\u6d4b', '3\u5929\u5e73\u5747\u9500\u91cf', '7\u5929\u5e73\u5747\u9500\u91cf', '14\u5929\u5e73\u5747\u9500\u91cf', '\u5e93\u5b58\u53ef\u652f\u6491\u5929\u6570', '\u52a0\u5de5\u5382\u5e93\u5b58\u652f\u6491\u5929\u6570', '\u603b\u652f\u6491\u5929\u6570', '30\u5929\u5e73\u5747\u9500\u91cf', '\u6781\u503c\u5224\u65ad']; // \u751f\u6210\u57fa\u7840\u4fe1\u606f\u5b57\u6bb5HTML\uff082\u5217\u7b49\u8ddd\uff09 var basicHtml = ''; for (var i = 0; i < basicFields.length; i++) { var field = basicFields[i]; var label = POPUP_FIELD_LABELS[field] || field; var val = p[field] !== undefined ? p[field] : '-'; var valHtml = formatPopupFieldValue(field, val, p); // 批注字段整行显示(跨两列),内容过长时自动换行,避免挤压右侧字段 var isRemarkRow = (field === '批注'); basicHtml += '
' + '' + escHtml(label) + '' + '' + valHtml + '' + '
'; } // \u751f\u6210\u9500\u91cf\u4fe1\u606f\u5b57\u6bb5HTML\uff084\u5217\u7b49\u8ddd\uff09 var salesHtml = ''; for (var i = 0; i < salesFields.length; i++) { var field = salesFields[i]; var label = POPUP_FIELD_LABELS[field] || field; var val = p[field] !== undefined ? p[field] : '-'; var valHtml = formatPopupFieldValue(field, val, p); salesHtml += '
' + '' + escHtml(label) + '' + '' + valHtml + '' + '
'; } // 云仓数据与默认数据使用相同字段顺序和四列布局。 var cloudSalesHtml = ''; var cloudSales = p['云仓数据'] || {}; for (var i = 0; i < salesFields.length; i++) { var cloudField = salesFields[i]; var cloudLabel = POPUP_FIELD_LABELS[cloudField] || cloudField; var cloudVal = cloudSales[cloudField] !== undefined ? cloudSales[cloudField] : '-'; var cloudValHtml = formatPopupFieldValue(cloudField, cloudVal, cloudSales); cloudSalesHtml += '
' + '' + escHtml(cloudLabel) + '' + '' + cloudValHtml + '' + '
'; } // 外仓数据与默认数据使用相同字段顺序和四列布局。 var externalSalesHtml = ''; var externalSales = p['外仓数据'] || {}; for (var i = 0; i < salesFields.length; i++) { var externalField = salesFields[i]; var externalLabel = POPUP_FIELD_LABELS[externalField] || externalField; var externalVal = externalSales[externalField] !== undefined ? externalSales[externalField] : '-'; var externalValHtml = formatPopupFieldValue(externalField, externalVal, externalSales); externalSalesHtml += '
' + '' + escHtml(externalLabel) + '' + '' + externalValHtml + '' + '
'; } // BOM\u7269\u6599\u6e05\u5355 var bomHtml = ''; var order = null; var bomMaterials = []; var sales = []; if (bomRes.code === 0) { order = bomRes.data.order; bomMaterials = bomRes.data.bom_materials || []; sales = bomRes.data.sales || []; } if (bomMaterials.length > 0) { var totalMatCost = 0; bomHtml = bomMaterials.map(function(m) { return '' + '' + escHtml(m.material_code) + '' + '' + escHtml(m.material_name) + '' + '' + m.available_qty.toLocaleString() + '' + '' + m.production_in_transit.toLocaleString() + '' + '' + m.purchase_in_transit.toLocaleString() + '' + '' + m.material_cost.toFixed(4) + '' + ''; }).join(''); } body.innerHTML = `
\u57fa\u7840\u4fe1\u606f
${basicHtml}
\u9500\u91cf\u4fe1\u606f
默认数据 云仓数据 外仓数据
${salesHtml}
BOM\u7269\u6599\u6e05\u5355 ${order && order.total_order_qty ? '\u6210\u54c1\u4e0b\u5355\u91cf: ' + (order.total_order_qty || 0).toLocaleString() + ' \u4ef6\uff08\u5efa\u8bae: ' + (order.total_suggested_qty || 0).toLocaleString() + '\uff09' : ''}
${bomMaterials.length > 0 ? '' + bomHtml + '
\u7269\u6599\u7f16\u53f7\u7269\u6599\u540d\u79f0\u53ef\u7528\u5e93\u5b58\u751f\u4ea7\u5728\u9014\u91c7\u8d2d\u5728\u9014\u5355\u4ef7
' : '
\u6682\u65e0BOM\u7269\u6599\u6e05\u5355
' }
\u8ba2\u5355\u5728\u9014 \u52a0\u8f7d\u4e2d...
\u67e5\u8be2\u4e2d...
\u9500\u91cf\u8d8b\u52bf
\u65e5\u671f\u8303\u56f4 \u2014 | \u67f1\u72b6\u56fe / \u6298\u7ebf\u56fe
${sales.length === 0 ? '
\u6682\u65e0\u9500\u91cf\u6570\u636e
' : ''}
`; // 使用 detail_with_bom 返回的 orders_in_transit 数据显示订单在途(国内/海外/包材订单) var transitOrders = bomRes.data && bomRes.data.orders_in_transit || []; document.getElementById('orderInTransitLoading').textContent = ''; var container = document.getElementById('orderInTransitBody'); if (container) { if (transitOrders.length === 0) { container.innerHTML = '
暂无在途订单
'; } else { var totalQty = 0; var html = ''; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; transitOrders.forEach(function(o) { totalQty += parseInt(o['下单数量']) || 0; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
下单时间货品编码货品名称下单数量未到数量下单进度预计到货时间
' + (o['下单时间'] || '-') + '' + (o['编码'] || '') + '' + (o['货品名称'] || '-') + '' + (parseInt(o['下单数量']) || 0).toLocaleString() + '' + (parseInt(o['未到数量']) || 0).toLocaleString() + '' + (o['下单进度'] || '-') + '' + (o['预计到货时间'] || '-') + '
'; html += '
' + '共 ' + transitOrders.length + ' 笔在途订单,' + '合计数量: ' + totalQty.toLocaleString() + '' + '
'; container.innerHTML = html; } } // \u9500\u91cf\u56fe\u8868 if (sales.length > 0) { window._salesData = sales; window._currentSalesChartType = 'bar'; setTimeout(function() { var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); if (startEl) startEl.value = sales[0].date; if (endEl) endEl.value = sales[sales.length - 1].date; renderSalesChartByRange('bar'); }, 100); } }); }; // ─── BOM管理模式下的简化详情弹窗(仅基础信息 + BOM物料清单)─── window.showBomProductDetail = function(productCode) { if (!productCode) return; const old = document.getElementById('productDetailModal'); if (old) old.remove(); const modal = document.createElement('div'); modal.id = 'productDetailModal'; modal.className = 'pd-overlay'; modal.style.display = 'flex'; modal.innerHTML = `
产品详情 \u2014 加载中...
加载中...
`; document.body.appendChild(modal); Promise.all([ fetch('/api/product/planning_detail?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }), fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }) ]).then(function(results) { var body = document.getElementById('pdBody'); if (!body) return; var planRes = results[0]; var bomRes = results[1]; if (planRes.code !== 0) { body.innerHTML = '
' + (planRes.msg || '\u67e5\u8be2\u5931\u8d25') + '
'; return; } var p = planRes.data; var titleEl = modal.querySelector('.pd-title'); if (titleEl) { titleEl.innerHTML = '\u4ea7\u54c1\u8be6\u60c5 \u2014 ' + escHtml(p.product_name || productCode) + ''; } var basicFields = ['product_type', '\u4ea7\u54c1\u79cd\u7c7b', 'product_code', 'product_name', '\u4f9b\u5e94\u5546\u540d\u79f0', '\u751f\u4ea7\u8d27\u671f', '\u7269\u6d41\u65f6\u95f4', '\u521b\u5efa\u65f6\u95f4', '\u6279\u6ce8']; var basicHtml = ''; for (var i = 0; i < basicFields.length; i++) { var field = basicFields[i]; var label = POPUP_FIELD_LABELS[field] || field; var val = p[field] !== undefined ? p[field] : '-'; var valHtml = formatPopupFieldValue(field, val, p); // 批注字段整行显示(跨两列),内容过长时自动换行,避免挤压右侧字段 var isRemarkRow = (field === '批注'); basicHtml += '
' + '' + escHtml(label) + '' + '' + valHtml + '' + '
'; } var bomHtml = ''; var order = null; var bomMaterials = []; if (bomRes.code === 0) { order = bomRes.data.order; bomMaterials = bomRes.data.bom_materials || []; } if (bomMaterials.length > 0) { bomHtml = bomMaterials.map(function(m) { return '' + '' + escHtml(m.material_code) + '' + '' + escHtml(m.material_name) + '' + '' + m.available_qty.toLocaleString() + '' + '' + m.production_in_transit.toLocaleString() + '' + '' + m.purchase_in_transit.toLocaleString() + '' + '' + m.material_cost.toFixed(4) + '' + ''; }).join(''); } body.innerHTML = `
\u57fa\u7840\u4fe1\u606f
${basicHtml}
BOM\u7269\u6599\u6e05\u5355 ${order && order.total_order_qty ? '\u6210\u54c1\u4e0b\u5355\u91cf: ' + (order.total_order_qty || 0).toLocaleString() + ' \u4ef6\uff08\u5efa\u8bae: ' + (order.total_suggested_qty || 0).toLocaleString() + '\uff09' : ''}
${bomMaterials.length > 0 ? '' + bomHtml + '
\u7269\u6599\u7f16\u53f7\u7269\u6599\u540d\u79f0\u53ef\u7528\u5e93\u5b58\u751f\u4ea7\u5728\u9014\u91c7\u8d2d\u5728\u9014\u5355\u4ef7
' : '
\u6682\u65e0BOM\u7269\u6599\u6e05\u5355
' }
`; }); }; // 切换销量信息tab页 window.switchSalesTab = function(tab) { var tabs = ['default', 'cloud', 'external']; tabs.forEach(function(t) { var tabEl = document.querySelector('.sales-tab[data-tab="' + t + '"]'); var contentEl = document.getElementById('salesTab' + t.charAt(0).toUpperCase() + t.slice(1)); if (tab === t) { if (tabEl) { tabEl.style.background = '#e6f7ff'; tabEl.style.color = '#1890ff'; tabEl.style.fontWeight = '600'; } if (contentEl) contentEl.style.display = ''; } else { if (tabEl) { tabEl.style.background = ''; tabEl.style.color = '#888'; tabEl.style.fontWeight = ''; } if (contentEl) contentEl.style.display = 'none'; } }); }; // ─── 编辑弹窗(动态跟随列显隐)─── // 列字段名 → 可编辑字段映射 const PLAN_EDITABLE_FIELDS = { 'cost_price': {label:'成本价', field:'cost_price', type:'number'}, 'product_type': {label:'产品类型', field:'product_type', type:'select', options:['成品','包材','现货','赠品','清库存','其他']}, '产品种类': {label:'产品种类', field:'product_type', type:'text'}, '品牌': {label:'品牌', field:'brand', type:'text'}, 'specification': {label:'规格', field:'specification', type:'text'}, '一级类目': {label:'一级类目', field:'category1', type:'text'}, '二级类目': {label:'二级类目', field:'category2', type:'text'}, '三级类目': {label:'三级类目', field:'category3', type:'text'}, '供应商名称': {label:'供应商名称', field:'supplier_name_match', type:'text'}, '供应商编码': {label:'供应商编码', field:'supplier_code', type:'text'}, '生产货期': {label:'货期', field:'lead_time', type:'number'}, '物流时间': {label:'物流时间', field:'logistics_time', type:'number'}, '采购跟单': {label:'采购跟单', field:'procurement_follower', type:'text'}, '采购计划': {label:'采购计划', field:'procurement_planner', type:'text'}, '开发专员': {label:'开发专员', field:'developer', type:'text'}, '归属平台': {label:'归属平台', field:'platform', type:'text'}, '运营专员': {label:'运营专员', field:'operator', type:'text'}, 'custom_field1': {label:'自定义字段1', field:'custom_field1', type:'text'}, 'custom_field2': {label:'自定义字段2', field:'custom_field2', type:'text'}, 'custom_field3': {label:'自定义字段3', field:'custom_field3', type:'text'}, 'custom_field4': {label:'自定义字段4', field:'custom_field4', type:'text'}, 'custom_field5': {label:'自定义字段5', field:'custom_field5', type:'text'}, '批注': {label:'批注', field:'remark', type:'text'} }; // 产品信息管理 - 成品回收 window.recycleProduct = function(productCode) { if (!productCode) return; showCenteredConfirm('确认', '确认将产品 ' + productCode + ' 移入产品回收站吗?
关联包材将同步移入回收站,该成品的BOM将同步删除,且无法随产品恢复。').then(function(ok) { if (!ok) return; fetch('/api/product/recycle', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { if (window._prodRowCache) delete window._prodRowCache[productCode]; loadProducts(prodCurrentPage); } else { alert(res.msg || '回收失败'); } }); }); }; // 产品信息管理 - 删除产品(包材沿用原有删除逻辑) window.deleteProduct = function(productCode) { if (!productCode) return; // 弹窗二次确认 var existing = document.getElementById('prodDeleteConfirmOverlay'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.id = 'prodDeleteConfirmOverlay'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var modalBodyHtml = [ '

确认删除产品 ' + escHtml(productCode) + ' 吗?

', '

删除后将移入回收站,可在回收站中恢复

' ]; if (window.prodActiveTab !== '成品' && window.prodActiveTab !== undefined) { modalBodyHtml.push('

⚠️ 若该产品被其他成品引用,对应的物料清单记录将被同时删除

'); } if (window.prodActiveTab === '成品' || window.prodActiveTab === undefined) { modalBodyHtml.push('

⚠️ 删除后将级联删除:
• 物料清单(BOM)中的关联记录
• 关联的包材产品
• 计划预测中的关联数据

'); } overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); }; window.confirmDeleteProduct = function(productCode) { // 关闭确认弹窗 var overlay = document.getElementById('prodDeleteConfirmOverlay'); if (overlay) overlay.remove(); fetch('/api/product/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { delete window._prodRowCache[productCode]; loadProducts(prodCurrentPage); } else { alert(res.msg || '删除失败'); } }); }; // ─── 回收站弹窗功能 ─── window._recyclePageSize = 20; window._recycleCurrentPage = 1; window.openRecycleBin = function() { // 创建弹窗 var old = document.getElementById('recycleBinModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'recycleBinModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); loadRecycleBin(1); }; window.loadRecycleBin = function(page) { _recycleCurrentPage = page; // 构建全字段表头 var thead = document.getElementById('rbThead'); if (thead) { var thHtml = '#'; PROD_COLUMNS.forEach(function(c) { if (c.field === 'cost_price') return; // 成本价不显示在回收站 thHtml += '' + c.label + ''; }); thHtml += '删除时间'; thHtml += '操作'; thead.innerHTML = thHtml; } var code = (document.getElementById('rbCode')?.value || '').trim(); var name = (document.getElementById('rbName')?.value || '').trim(); var url = '/api/products/recycle_bin?page=' + page + '&size=' + _recyclePageSize; if (codeName) url += '&search=' + encodeURIComponent(codeName); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('rbTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '' + '
🗑️
' + '
回收站为空
'; } else { var theadEl = document.getElementById('rbThead'); var colCount = (theadEl ? theadEl.querySelectorAll('th').length : PROD_COLUMNS.length + 2); var typeColors = {'成品':'tag-blue','包材':'tag-orange','赠品':'tag-green','小样':'tag-purple','物料':'tag-orange'}; var html = ''; data.rows.forEach(function(r, i) { html += ''; html += ''; html += '' + ((page - 1) * _recyclePageSize + i + 1) + ''; PROD_COLUMNS.forEach(function(c) { if (c.field === 'cost_price') return; var raw = r[c.field]; var val = raw != null ? String(raw) : '-'; if (c.field === 'created_at') val = raw ? String(raw).slice(0, 10) : '-'; if (c.field === 'import_date') val = raw ? String(raw).slice(0, 10) : '-'; if (c.field === 'product_type') { var cls = typeColors[raw] || 'tag-grey'; val = '' + escHtml(raw || '-') + ''; } else { val = escHtml(val); } html += '' + val + ''; }); html += '' + (r.deleted_at ? r.deleted_at.substring(0, 16) : '-') + ''; html += ''; html += ''; html += ''; html += ''; }); tbody.innerHTML = html; } // 分页 var totalPages = Math.max(1, Math.ceil(data.total / _recyclePageSize)); var ph = ''; if (page > 1) ph += ''; ph += '共 ' + data.total + ' 条,第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) ph += ''; document.getElementById('rbPagination').innerHTML = ph; document.getElementById('rbCount').textContent = '共 ' + data.total + ' 条'; }); }; window.searchRecycleBin = function(page) { loadRecycleBin(page || 1); }; window.resetRecycleSearch = function() { var c = document.getElementById('rbCode'); if (c) c.value = ''; var n = document.getElementById('rbName'); if (n) n.value = ''; loadRecycleBin(1); }; window.restoreProduct = function(productCode) { showCenteredConfirm('确认', '确认恢复产品 ' + productCode + ' 吗?').then(function(ok) { if (!ok) return; fetch('/api/product/restore', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadRecycleBin(_recycleCurrentPage); window._recycleChanged = true; } else { alert(res.msg || '恢复失败'); } }); }); }; window.permanentDeleteProduct = function(productCode) { showCenteredConfirm('确认', '确认彻底删除产品 ' + productCode + ' 吗?
此操作不可恢复!').then(function(ok) { if (!ok) return; fetch('/api/product/permanent_delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { loadRecycleBin(_recycleCurrentPage); window._recycleChanged = true; } else { alert(res.msg || '删除失败'); } }); }); }; // ─── 回收站全选/批量操作 ─── window.rbToggleAll = function(checked) { [].forEach.call(document.querySelectorAll('.rb-row-cb'), function(cb) { cb.checked = checked; }); }; window.getSelectedRbCodes = function() { var codes = []; [].forEach.call(document.querySelectorAll('.rb-row-cb:checked'), function(cb) { codes.push(cb.value); }); return codes; }; window.rbBatchRestore = function() { var codes = window.getSelectedRbCodes(); if (codes.length === 0) { alert('请先勾选需要恢复的商品'); return; } showCenteredConfirm('确认', '确认批量恢复 ' + codes.length + ' 个商品吗?').then(function(ok) { if (!ok) return; var done = 0; codes.forEach(function(code) { fetch('/api/product/restore', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) done++; if (done === codes.length || done + (codes.length - codes.indexOf(code) - 1) === codes.length) { // 已有写入保护,取最后一次处理结果 } }); }); // 延迟刷新列表 setTimeout(function() { loadRecycleBin(_recycleCurrentPage); window._recycleChanged = true; }, 500); }); }; window.rbBatchDelete = function() { var codes = window.getSelectedRbCodes(); if (codes.length === 0) { alert('请先勾选需要删除的商品'); return; } showCenteredConfirm('确认', '确认彻底删除 ' + codes.length + ' 个商品吗?
此操作不可恢复!').then(function(ok) { if (!ok) return; var done = 0; codes.forEach(function(code) { fetch('/api/product/permanent_delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) done++; }); }); setTimeout(function() { loadRecycleBin(_recycleCurrentPage); window._recycleChanged = true; }, 500); }); }; window.closeRecycleBin = function() { var modal = document.getElementById('recycleBinModal'); if (modal) modal.remove(); // 如果回收站有改动(恢复/删除),刷新主列表 if (window._recycleChanged) { window._recycleChanged = false; loadProducts(prodCurrentPage || 1); } }; // 产品信息管理 - 查看详情 window.viewProductDetail = function(productCode) { if (!productCode) return; var row = window._prodRowCache && window._prodRowCache[productCode]; if (!row) { showProductDetailEdit(productCode); return; } var old = document.getElementById('productViewModal'); if (old) old.remove(); var isPackaging = prodActiveTab === '非成品'; var packHide = ['品牌','采购跟单','采购计划','平台','运营','开发专员']; var fields = [ {label:'货品编号', val:row.product_code}, {label:'货品名称', val:row.product_name}, {label:'品牌', val:row.brand}, {label:'货品类型', val:row.product_type}, {label:'供应商编码', val:row.supplier_code}, {label:'供应商名称', val:row.supplier_name_match}, {label:'成本价', val:row.cost_price}, {label:'货期(天)', val:row.lead_time}, {label:'物流时间(天)', val:row.logistics_time}, {label:'最低起订量', val:row.min_order_qty != null ? parseInt(row.min_order_qty) : '-'}, {label:'采购跟单', val:row.procurement_follower}, {label:'采购计划', val:row.procurement_planner}, {label:'平台', val:row.platform}, {label:'运营', val:row.operator}, {label:'开发专员', val:row.developer}, {label:'创建时间', val:(row.created_at||'').toString().slice(0,10)}, {label:'导入日期', val:row.import_date}, {label:'自定义字段1', val:row.custom_field1}, {label:'自定义字段2', val:row.custom_field2}, {label:'自定义字段3', val:row.custom_field3}, {label:'自定义字段4', val:row.custom_field4}, {label:'自定义字段5', val:row.custom_field5}, ]; if (isPackaging) fields = fields.filter(function(f) { return packHide.indexOf(f.label) < 0; }); var html = '' + '' + '' + ''; for (var i = 0; i < fields.length; i += 2) { var left = fields[i]; var right = fields[i + 1]; var lv = left.val != null && left.val !== '' ? escHtml(String(left.val)) : '-'; html += ''; html += ''; html += ''; if (right) { var rv = right.val != null && right.val !== '' ? escHtml(String(right.val)) : '-'; html += ''; html += ''; } else { html += ''; } html += ''; } html += '
' + escHtml(left.label) + '' + lv + '' + escHtml(right.label) + '' + rv + '
'; var modal = document.createElement('div'); modal.id = 'productViewModal'; modal.className = 'pd-overlay'; modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center'; modal.innerHTML = '' + '
' + '
📋 产品详情' + '——' + '' + escHtml(row.product_name || productCode) + '' + '
' + '
' + html + '
'; document.body.appendChild(modal); }; // 供应商名称→编码查询缓存 if (!window._supplierLookup) { window._supplierLookup = {}; window._supplierList = []; window._overseasBrands = []; window._plannerMode = null; window._lastBound = { key: '', data: null }; // 海外品品牌清单:取自品牌管理的品牌分类 fetch('/api/brands/overseas') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && Array.isArray(res.data)) window._overseasBrands = res.data; if (document.getElementById('pa_planner')) window.applyPlannerLogic(); }); fetch('/api/suppliers/list?size=9999') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data && res.data.rows) { window._supplierList = res.data.rows; res.data.rows.forEach(function(s) { window._supplierLookup[s.supplier_name] = s.supplier_code; }); // 新增弹窗已打开时,供应商列表到位后补一次跟单小组自动带入 if (document.getElementById('pa_follower')) { window.applySupplierFollower((document.getElementById('pa_supcode') || {}).value || '', (document.getElementById('pa_supname') || {}).value || ''); } } }); } window.lookupSupplierCode = function(name) { if (!name || !window._supplierList) return; var matched = null; if (window._supplierLookup[name]) { matched = window._supplierLookup[name]; } else { for (var i = 0; i < window._supplierList.length; i++) { var sn = window._supplierList[i].supplier_name || ''; if (sn.indexOf(name) >= 0 || name.indexOf(sn) >= 0) { matched = window._supplierList[i].supplier_code; break; } } } var codeEl = document.getElementById('pe_supplier_code'); if (codeEl && matched) codeEl.value = matched; }; // ─── 新增弹窗:供应商模糊搜索 ─── window.lookupAddSupplierName = function(val) { var dd = document.getElementById('pa_supDropdown'); if (!dd) return; if (!val || !window._supplierList) { dd.style.display = 'none'; return; } var matched = []; var v = val.toLowerCase(); window._supplierList.forEach(function(s) { var name = (s.supplier_name || '').toLowerCase(); if (name.indexOf(v) >= 0) matched.push(s); }); if (matched.length === 0 || matched.length > 50) { dd.style.display = 'none'; return; } var html = ''; matched.slice(0, 20).forEach(function(s) { html += '
'; html += '' + escHtml(s.supplier_name) + ''; if (s.supplier_code) html += ' ' + escHtml(s.supplier_code) + ''; html += '
'; }); dd.innerHTML = html; dd.style.display = 'block'; }; // ─── 新增产品:品牌下拉选择 + 模糊查询 ─── window._brandList = null; window.loadBrandList = function(cb) { if (window._brandList) { if (cb) cb(window._brandList); return; } fetch('/api/brands/all') .then(function(r) { return r.json(); }) .then(function(res) { window._brandList = (res && res.code === 0 && Array.isArray(res.data)) ? res.data : []; if (cb) cb(window._brandList); }) .catch(function() { window._brandList = window._brandList || []; if (cb) cb(window._brandList); }); }; window.renderAddBrandDropdown = function(keyword) { var dd = document.getElementById('pa_brandDropdown'); if (!dd) return; window.loadBrandList(function(list) { var k = String(keyword || '').trim().toLowerCase(); var matched = list.filter(function(b) { return !k || String(b).toLowerCase().indexOf(k) >= 0; }); if (!matched.length) { dd.innerHTML = '
无匹配品牌,可直接输入新品牌
'; dd.style.display = 'block'; return; } dd.innerHTML = matched.map(function(b) { return '
' + escHtml(b) + '
'; }).join(''); dd.style.display = 'block'; }); }; window.showAddBrandList = function() { var input = document.getElementById('pa_brand'); window.renderAddBrandDropdown(input ? input.value : ''); }; window.lookupAddBrand = function(val) { window.renderAddBrandDropdown(val); }; window.selectAddBrand = function(name) { var input = document.getElementById('pa_brand'); if (input) input.value = name || ''; var dd = document.getElementById('pa_brandDropdown'); if (dd) dd.style.display = 'none'; // 品牌决定采购计划是否可手动编辑 window.applyPlannerLogic(); }; window.hideAddBrandList = function() { var dd = document.getElementById('pa_brandDropdown'); if (dd) dd.style.display = 'none'; window.applyPlannerLogic(); }; window.addBrandKeydown = function(e) { if (e.key === 'Escape') { window.hideAddBrandList(); return; } if (e.key !== 'Enter') return; var dd = document.getElementById('pa_brandDropdown'); if (dd && dd.style.display !== 'none') { var first = dd.querySelector('.pa-brand-item'); if (first) { e.preventDefault(); window.selectAddBrand(first.textContent); return; } } window.applyPlannerLogic(); }; window.selectAddSupplier = function(code, name) { document.getElementById('pa_supcode').value = code || ''; document.getElementById('pa_supname').value = name || ''; document.getElementById('pa_supDropdown').style.display = 'none'; window.applySupplierFollower(code, name); }; // 跟单小组:根据供应商名称/编码从供应商管理自动获取(新增弹窗内不可手工编辑) window.applySupplierFollower = function(code, name) { var el = document.getElementById('pa_follower'); if (!el) return; var list = window._supplierList || []; var hit = null; var c = (code || '').trim(); var n = (name || '').trim(); if (c) { for (var i = 0; i < list.length; i++) { if ((list[i].supplier_code || '') === c) { hit = list[i]; break; } } } if (!hit && n) { var exact = list.filter(function(s) { return (s.supplier_name || '') === n; }); if (exact.length === 1) { hit = exact[0]; } else { // 仅当唯一模糊命中时才带入,避免取错供应商 var partial = list.filter(function(s) { return n && (s.supplier_name || '').indexOf(n) >= 0; }); if (partial.length === 1) hit = partial[0]; } } el.value = hit ? (hit.procurement_follower || '') : ''; el.title = (hit && !hit.procurement_follower) ? '该供应商在供应商管理中未维护跟单小组' : ''; // 采购计划:非海外品时同样按供应商自动带入,海外品保留手动填写值 if (!window.isOverseasBrand((document.getElementById('pa_brand') || {}).value)) { var pel = document.getElementById('pa_planner'); if (pel) { pel.value = hit ? (hit.procurement_planner || '') : ''; pel.title = (hit && !hit.procurement_planner) ? '该供应商在供应商管理中未维护采购计划' : ''; } } // 供应商管理未绑定时,向后端要一次(回退到该供应商已有产品的值) window.resolveSupplierBound((hit && hit.supplier_code) || c, (hit && hit.supplier_name) || n); }; // 将后端解析结果回填到跟单小组/采购计划 window.applyBoundFields = function(d) { if (!d) return; var fel = document.getElementById('pa_follower'); if (fel && d.follower) { fel.value = d.follower; fel.title = (d.follower_source === 'product_fallback') ? '取自该供应商已有产品的跟单小组' : ''; } var pel = document.getElementById('pa_planner'); if (pel && !window.isOverseasBrand((document.getElementById('pa_brand') || {}).value) && d.planner) { pel.value = d.planner; pel.title = (d.planner_source === 'product_fallback') ? '取自该供应商已有产品的采购计划' : ''; } }; // 向后端解析跟单小组+采购计划并回填(同一供应商重复调用走缓存) window.resolveSupplierBound = function(code, name) { var c = (code || '').trim(); var n = (name || '').trim(); if (!c && !n) return Promise.resolve(); var key = c + '||' + n; if (window._lastBound && window._lastBound.key === key && window._lastBound.data) { window.applyBoundFields(window._lastBound.data); return Promise.resolve(); } return fetch('/api/supplier/bound_fields?supplier_code=' + encodeURIComponent(c) + '&supplier_name=' + encodeURIComponent(n)) .then(function(r) { return r.json(); }) .then(function(res) { if (!res || res.code !== 0 || !res.data) return; window._lastBound = { key: key, data: res.data }; window.applyBoundFields(res.data); }) .catch(function() {}); }; // 采购计划两种逻辑:品牌为海外品时手动填写,否则按供应商自动带入 window.isOverseasBrand = function(brand) { var b = String(brand || '').trim().toLowerCase(); if (!b) return false; return (window._overseasBrands || []).some(function(x) { return String(x || '').trim().toLowerCase() === b; }); }; window.applyPlannerLogic = function() { var pel = document.getElementById('pa_planner'); if (!pel) return false; var brandEl = document.getElementById('pa_brand'); var overseas = window.isOverseasBrand(brandEl ? brandEl.value : ''); var modeChanged = (window._plannerMode !== overseas); window._plannerMode = overseas; if (overseas) { pel.readOnly = false; pel.style.background = ''; pel.style.color = ''; pel.placeholder = '必填'; pel.title = ''; // 海外品牌:采购计划默认填写为张莹纯(仍可人工修改) if (modeChanged || !pel.value) { pel.value = '张莹纯'; pel.dataset.overseasDefault = '1'; } } else { pel.readOnly = true; pel.style.background = '#f5f5f5'; pel.style.color = '#999'; pel.placeholder = '根据供应商名称自动获取'; pel.title = ''; if (modeChanged) { pel.value = ''; window._lastBound = { key: '', data: null }; } window.applySupplierFollower((document.getElementById('pa_supcode') || {}).value || '', (document.getElementById('pa_supname') || {}).value || ''); } return overseas; }; // 点击其他地方关闭下拉 window.addEventListener('click', function(e) { var dd = document.getElementById('pa_supDropdown'); if (dd && !e.target.closest('#pa_supname') && !e.target.closest('#pa_supDropdown')) dd.style.display = 'none'; var fd = document.getElementById('pa_folDropdown'); if (fd && !e.target.closest('#pa_follower') && !e.target.closest('#pa_folDropdown')) fd.style.display = 'none'; var bd = document.getElementById('pa_brandDropdown'); if (bd && !e.target.closest('#pa_brand') && !e.target.closest('#pa_brandDropdown')) bd.style.display = 'none'; }); // ─── 采购跟单记忆 ─── window.getFollowerHistory = function() { var raw = localStorage.getItem('_pa_follower_history'); return raw ? JSON.parse(raw) : []; }; window.saveFollowerHistory = function(val) { if (!val) return; var list = window.getFollowerHistory(); list = list.filter(function(v) { return v !== val; }); list.unshift(val); if (list.length > 20) list = list.slice(0, 20); localStorage.setItem('_pa_follower_history', JSON.stringify(list)); }; window.showFollowerHistory = function() { var dd = document.getElementById('pa_folDropdown'); if (!dd) return; var input = document.getElementById('pa_follower'); var val = (input.value || '').toLowerCase(); var list = window.getFollowerHistory(); if (val) list = list.filter(function(v) { return v.toLowerCase().indexOf(val) >= 0; }); if (list.length === 0) { dd.style.display = 'none'; return; } var html = ''; list.forEach(function(v) { html += '
' + escHtml(v) + '
'; }); dd.innerHTML = html; dd.style.display = 'block'; }; window.selectFollower = function(val) { document.getElementById('pa_follower').value = val; document.getElementById('pa_folDropdown').style.display = 'none'; }; // 产品信息管理快捷编辑(使用缓存数据,无需等待) window.quickEditProduct = function(productCode) { if (!productCode) return; var row = window._prodRowCache && window._prodRowCache[productCode]; if (!row) { // 缓存未命中,回退到原始编辑 showProductDetailEdit(productCode); return; } // 关闭已有弹窗 var oldEdit = document.getElementById('productEditModal'); if (oldEdit) oldEdit.remove(); var modal = document.createElement('div'); modal.id = 'productEditModal'; modal.className = 'pd-overlay'; modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center'; modal.innerHTML = '
' + '
✏️ 编辑产品信息' + '
' + '
' + _buildProductEditForm(row) + '
'; document.body.appendChild(modal); }; // 构建产品编辑表单 function _buildProductEditForm(row) { var isNonProd = prodActiveTab === '非成品'; var hideFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer']; var fields = [ {key:'product_code', label:'货品编号', val:row.product_code, type:'text'}, {key:'product_name', label:'货品名称', val:row.product_name, type:'text'}, {key:'brand', label:'品牌', val:row.brand, type:'text'}, {key:'product_type', label:'货品类型', val:row.product_type, type:'select', opts:['成品','现货','清库存','赠品','原材料','包材','其他']}, {key:'supplier_code', label:'供应商编码', val:row.supplier_code, type:'text', ro:true}, {key:'supplier_name_match', label:'供应商名称', val:row.supplier_name_match, type:'text', supSearch:true}, {key:'cost_price', label:'成本价', val:row.cost_price, type:'number'}, {key:'lead_time', label:'货期(天)', val:row.lead_time, type:'number'}, {key:'logistics_time', label:'物流时间(天)', val:row.logistics_time, type:'number', max:999}, {key:'min_order_qty', label:'最低起订量', val:row.min_order_qty != null ? parseInt(row.min_order_qty) : 0, type:'number'}, {key:'procurement_follower', label:'采购跟单', val:row.procurement_follower, type:'text'}, {key:'procurement_planner', label:'采购计划', val:row.procurement_planner, type:'text'}, {key:'platform', label:'平台', val:row.platform, type:'text'}, {key:'operator', label:'运营', val:row.operator, type:'text'}, {key:'developer', label:'开发专员', val:row.developer, type:'text'}, {key:'created_at', label:'创建时间', val:row.created_at ? String(row.created_at).slice(0,10) : '', type:'date'}, {key:'custom_field1', label:'自定义字段1', val:row.custom_field1, type:'text'}, {key:'custom_field2', label:'自定义字段2', val:row.custom_field2, type:'text'}, {key:'custom_field3', label:'自定义字段3', val:row.custom_field3, type:'text'}, {key:'custom_field4', label:'自定义字段4', val:row.custom_field4, type:'text'}, {key:'custom_field5', label:'自定义字段5', val:row.custom_field5, type:'text'}, ]; if (isNonProd) fields = fields.filter(function(f) { return hideFields.indexOf(f.key) < 0; }); var html = '
'; fields.forEach(function(f) { var val = escHtml(String(f.val != null ? f.val : '')); var roAttr = f.ro ? ' readonly style="background:#f5f5f5;color:#999"' : ''; if (f.type === 'select') { var opts = ''; (f.opts || []).forEach(function(o) { var sel = o === f.val ? ' selected' : ''; opts += ''; }); html += '
'; } else if (f.supSearch) { html += '
'; } else { html += '
'; } }); html += '
'; html += '
' + '' + '
'; return html; } // ─── 编辑弹窗供应商模糊搜索 ─── window.editSupSearch = function(input) { var dd = input.parentElement.querySelector('.pe-sup-dd'); if (!dd) return; var val = input.value.trim(); if (!val || !window._supplierList) { dd.style.display = 'none'; return; } var v = val.toLowerCase(); var matched = window._supplierList.filter(function(s) { return (s.supplier_name || '').toLowerCase().indexOf(v) >= 0; }); if (matched.length === 0 || matched.length > 50) { dd.style.display = 'none'; return; } dd.innerHTML = ''; dd.style.display = 'block'; matched.slice(0, 20).forEach(function(s) { var item = document.createElement('div'); item.style.cssText = 'padding:4px 8px;cursor:pointer;font-size:12px;border-bottom:1px solid #f0f0f0'; item.className = 'pe-sup-dd-item'; item.dataset.code = s.supplier_code || ''; item.dataset.name = s.supplier_name || ''; item.innerHTML = '' + escHtml(s.supplier_name) + '' + (s.supplier_code ? ' ' + escHtml(s.supplier_code) + '' : ''); dd.appendChild(item); }); }; window.saveQuickEditProduct = function(productCode) { var isNonProd = prodActiveTab === '非成品'; var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer','created_at']; var fields = {}; var newCodeEl = document.getElementById('pe_product_code'); var newCode = newCodeEl ? newCodeEl.value.trim() : productCode; if (newCode && newCode !== productCode) { fields['product_code'] = newCode; } var fieldNames = ['product_name','brand','product_type','supplier_code','supplier_name_match', 'cost_price','lead_time','logistics_time','min_order_qty', 'procurement_follower','procurement_planner', 'platform','operator','developer','created_at', 'custom_field1','custom_field2','custom_field3','custom_field4','custom_field5']; fieldNames.forEach(function(f) { if (isNonProd && skipFields.indexOf(f) >= 0) return; var el = document.getElementById('pe_' + f); if (el) { var val = el.value; if (f === 'lead_time' || f === 'logistics_time' || f === 'min_order_qty') val = parseInt(val) || 0; if (f === 'cost_price') val = parseFloat(val) || 0; fields[f] = val; } }); fetch('/api/product/batch_update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, fields: fields}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { document.getElementById('productEditModal').remove(); if (newCode && newCode !== productCode) { // 编号已变更,更新缓存 if (window._prodRowCache && window._prodRowCache[productCode]) { var oldRow = window._prodRowCache[productCode]; oldRow.product_code = newCode; window._prodRowCache[newCode] = oldRow; delete window._prodRowCache[productCode]; } } loadProducts(prodCurrentPage); } else { alert(res.msg || '保存失败'); } }); }; window.showProductDetailEdit = function(productCode) { if (!productCode) return; // 关闭已有弹窗 var oldEdit = document.getElementById('productEditModal'); if (oldEdit) oldEdit.remove(); var modal = document.createElement('div'); modal.id = 'productEditModal'; modal.className = 'pd-overlay'; modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center'; modal.innerHTML = '
' + '
✏️ 编辑产品信息' + '
' + '
加载中...
'; document.body.appendChild(modal); fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { document.getElementById('pdEditBody').innerHTML = '
' + (res.msg || '查询失败') + '
'; return; } var p = res.data.product; // 获取当前可见列字段名(来自计划预测页面) var visibleFields = []; if (window.planColVisible) { PLAN_COLUMNS.forEach(function(c) { if (window.planColVisible[c.field]) { visibleFields.push(c.field); } }); } else { PLAN_COLUMNS.forEach(function(c) { if (c.defaultShow) { visibleFields.push(c.field); } }); } // 过滤出可编辑的字段(按 visibleFields 顺序排列) var editFields = []; visibleFields.forEach(function(colField) { var cfg = PLAN_EDITABLE_FIELDS[colField]; if (cfg) { // 从产品数据取值 var val = null; if (colField === '成本价' || colField === 'cost_price') val = p.cost_price; else if (colField === 'product_type') val = p.product_type; else if (colField === '产品种类') val = p.产品种类; else if (colField === '品牌') val = p.brand; else if (colField === 'specification') val = p.specification; else if (colField === '一级类目') val = p.category1; else if (colField === '二级类目') val = p.category2; else if (colField === '三级类目') val = p.category3; else if (colField === '供应商名称') val = p.supplier_name; else if (colField === '供应商编码') val = p.supplier_code; else if (colField === '生产货期') val = p.lead_time; else if (colField === '物流时间') val = p.logistics_time; else if (colField === '采购跟单') val = p.procurement_follower; else if (colField === '采购计划') val = p.procurement_planner; else if (colField === '开发专员') val = p.developer; else if (colField === '归属平台') val = p.platform; else if (colField === '运营专员') val = p.operator; else if (colField === 'custom_field1') val = p.custom_field1; else if (colField === 'custom_field2') val = p.custom_field2; else if (colField === 'custom_field3') val = p.custom_field3; else if (colField === 'custom_field4') val = p.custom_field4; else if (colField === 'custom_field5') val = p.custom_field5; else if (colField === '批注') val = p.remark; editFields.push({cfg: cfg, colField: colField, val: val}); } }); // 批注字段固定放最后一个 var remarkFieldIdx = -1; editFields.forEach(function(ef, i) { if (ef.colField === '批注') remarkFieldIdx = i; }); if (remarkFieldIdx >= 0) { var remarkField = editFields.splice(remarkFieldIdx, 1)[0]; editFields.push(remarkField); } var html = '
'; html += '
'; html += '
基础信息
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += '
' + escHtml(p.product_name || '-') + '
'; html += '
'; if (editFields.length > 0) { html += '
'; html += '
可编辑字段
'; html += '
'; editFields.forEach(function(ef) { var f = ef.cfg; var inputId = 'edit_' + f.field; var isRemark = f.field === 'remark'; // 批注字段跨两列显示,文本框更大 html += '
'; html += ''; var val = (ef.val !== null && ef.val !== undefined) ? ef.val : ''; if (isRemark) { // 根据内容自适应高度,能全部显示 var remarkLineCount = String(val).split('\n').length; var remarkRows = Math.max(4, remarkLineCount, Math.ceil(String(val).length / 30)); html += ''; } else if (f.type === 'select' && f.options) { html += ''; } else if (f.type === 'number') { html += ''; } else { html += ''; } html += '
'; }); html += '
'; } html += '
'; html += ''; if (editFields.length > 0) { html += ''; } html += '
'; document.getElementById('pdEditBody').innerHTML = html; }); }; window.confirmProductEdit = function(productCode) { var fields = {}; var changed = false; // 读取货品编号 var newCodeInput = document.getElementById('edit_product_code'); var newCode = newCodeInput ? newCodeInput.value.trim() : productCode; if (newCode !== productCode && newCode) { fields['product_code'] = newCode; changed = true; } // 查找所有以 edit_ 开头的 input/textarea 元素(排除 product_code,因为已单独处理) document.querySelectorAll('#productEditModal input[id^="edit_"], #productEditModal textarea[id^="edit_"]').forEach(function(el) { var fieldName = el.id.replace('edit_', ''); if (fieldName === 'product_code') return; var val = el.value.trim(); if (el.type === 'number') val = parseFloat(val) || 0; fields[fieldName] = val; changed = true; }); if (!changed) { var nbEl = document.getElementById('editNoChangeTip'); if (!nbEl) { nbEl = document.createElement('div'); nbEl.id = 'editNoChangeTip'; nbEl.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;padding:24px 32px;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:10001;font-size:15px;color:var(--text-secondary)'; nbEl.textContent = '没有可保存的字段'; document.body.appendChild(nbEl); setTimeout(function() { nbEl.remove(); }, 1500); } return; } var fieldLabels = { 'cost_price':'成本价', 'product_type':'产品类型', 'brand':'品牌', 'specification':'规格', 'category1':'一级类目', 'category2':'二级类目', 'category3':'三级类目', 'supplier_name_match':'供应商名称', 'supplier_code':'供应商编码', 'lead_time':'货期(天)', 'logistics_time':'物流时间(天)', 'procurement_follower':'采购跟单', 'procurement_planner':'采购计划', 'developer':'开发专员', 'operator':'运营专员', 'platform':'归属平台', 'custom_field1':'自定义字段1', 'custom_field2':'自定义字段2', 'custom_field3':'自定义字段3', 'custom_field4':'自定义字段4', 'custom_field5':'自定义字段5', 'remark':'批注' }; // 自定义确认弹窗(替代原生 confirm) var oldConfirm = document.getElementById('editConfirmModal'); if (oldConfirm) oldConfirm.remove(); var confirmHtml = ''; confirmHtml += '
'; confirmHtml += '

确认保存以下修改?

'; Object.keys(fields).forEach(function(f) { confirmHtml += '
'; confirmHtml += '' + (fieldLabels[f] || f) + ''; confirmHtml += '' + fields[f] + ''; confirmHtml += '
'; }); confirmHtml += '
'; var confirmModal = document.createElement('div'); confirmModal.id = 'editConfirmModal'; confirmModal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; confirmModal.innerHTML = '
' + confirmHtml + '
' + '' + '
'; document.body.appendChild(confirmModal); // 绑定确认按钮 document.getElementById('editConfirmSaveBtn').onclick = function() { confirmModal.remove(); doSaveProductEdit(productCode, fields); }; }; window.doSaveProductEdit = function(productCode, fields) { if (typeof loadPlanPage !== 'function' && typeof planCurrentPage === 'undefined') { planCurrentPage = 1; } // 如果货品编号变更,使用新编号刷新页面 var refreshCode = fields['product_code'] || productCode; fetch('/api/product/batch_update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, fields: fields}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 保存成功'); var editModal = document.getElementById('productEditModal'); if (editModal) editModal.remove(); if (typeof loadPlanPage === 'function') loadPlanPage(planCurrentPage || 1); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function() { alert('❌ 网络错误'); }); }; window.showProductDetailFull = function(productCode) { if (!productCode) return; const old = document.getElementById('productDetailFullModal'); if (old) old.remove(); const modal = document.createElement('div'); modal.id = 'productDetailFullModal'; modal.className = 'pd-overlay'; modal.style.display = 'flex'; modal.innerHTML = `
完整产品详情 — ${escHtml(productCode)}
加载中...
`; document.body.appendChild(modal); fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)) .then(r => r.json()) .then(res => { const body = document.getElementById('pdFullBody'); if (res.code !== 0) { body.innerHTML = '
' + (res.msg || '查询失败') + '
'; return; } const p = res.data.product; const order = res.data.order; const bomMaterials = res.data.bom_materials || []; const sales = res.data.sales || []; // BOM表格 var bomHtml = ''; if (bomMaterials.length > 0) { var totalMatCost = 0; bomHtml = bomMaterials.map(function(m) { totalMatCost += m.total_cost; var shortage = m.required_qty - m.available_qty; var shortageClass = shortage > 0 ? 'pd-warn' : 'pd-ok'; return '' + '' + escHtml(m.material_code) + '' + '' + escHtml(m.material_name) + '' + '' + m.bom_qty + '' + '' + (m.required_qty || 0).toLocaleString() + '' + '' + m.available_qty.toLocaleString() + '' + '' + m.production_in_transit.toLocaleString() + '' + '' + m.purchase_in_transit.toLocaleString() + '' + '¥' + m.material_cost.toFixed(4) + '' + '¥' + m.total_cost.toLocaleString() + '' + '' + (shortage > 0 ? '+' : '') + shortage.toLocaleString() + '' + ''; }).join(''); var summaryHtml = '' + '合计' + '' + bomMaterials.reduce(function(s,m){return s+m.required_qty;},0).toLocaleString() + '' + '' + bomMaterials.reduce(function(s,m){return s+m.available_qty;},0).toLocaleString() + '' + '' + bomMaterials.reduce(function(s,m){return s+m.production_in_transit;},0).toLocaleString() + '' + '' + bomMaterials.reduce(function(s,m){return s+m.purchase_in_transit;},0).toLocaleString() + '' + '' + '¥' + totalMatCost.toLocaleString() + '' + '' + ''; } body.innerHTML = ''; // BOM物料清单 if (bomMaterials.length > 0) { body.innerHTML += '
' + '
' + '' + '' + '' + 'BOM物料清单' + '' + '成品下单量: ' + (order.total_order_qty || 0).toLocaleString() + ' 件' + '(建议: ' + (order.total_suggested_qty || 0).toLocaleString() + ')' + '' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + bomHtml + '' + summaryHtml + '
物料编号物料名称BOM用量需用量可用库存生产在途采购在途操作单价总成本缺口
' + '
' + '
'; } else { body.innerHTML += '
暂无BOM物料清单
'; } // 订单在途 body.innerHTML += '
' + '' + '' + '' + '订单在途' + '加载中...' + '
' + '
' + '
查询中...
' + '
'; // 使用 detail_with_bom 返回的 orders_in_transit 数据显示订单在途(国内/海外/包材订单) var transitOrders = bomRes.data && bomRes.data.orders_in_transit || []; document.getElementById('orderInTransitLoading').textContent = ''; var container = document.getElementById('orderInTransitBody'); if (container) { if (transitOrders.length === 0) { container.innerHTML = '
暂无在途订单
'; } else { var totalQty = 0; var html = ''; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; transitOrders.forEach(function(o) { totalQty += parseInt(o['下单数量']) || 0; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
下单时间货品编码货品名称下单数量未到数量下单进度预计到货时间
' + (o['下单时间'] || '-') + '' + (o['编码'] || '') + '' + (o['货品名称'] || '-') + '' + (parseInt(o['下单数量']) || 0).toLocaleString() + '' + (parseInt(o['未到数量']) || 0).toLocaleString() + '' + (o['下单进度'] || '-') + '' + (o['预计到货时间'] || '-') + '
'; html += '
' + '共 ' + transitOrders.length + ' 笔在途订单,' + '合计数量: ' + totalQty.toLocaleString() + '' + '
'; container.innerHTML = html; container.innerHTML = html; } } // 销量趋势 body.innerHTML += '
' + '
' + '' + '' + '' + '销量趋势' + '
' + '日期范围' + '' + '—' + '' + '' + '|' + '柱状图' + '/' + '折线图' + '
' + '
' + '
' + '
' + '
' + '' + '
' + '
' + (sales.length === 0 ? '
暂无销量数据
' : '') + '
' + '
'; // 底部信息 body.innerHTML += ''; if (sales.length > 0) { // 设置日期范围输入框的默认值(数据的起止日期) setTimeout(function() { if (sales.length > 0) { var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); if (startEl) startEl.value = sales[0].date; if (endEl) endEl.value = sales[sales.length - 1].date; } renderSalesChartByRange('bar'); }, 100); } }); }; window.closeProductDetailFull = function() { const modal = document.getElementById('productDetailFullModal'); if (modal) modal.remove(); if (window._salesChart) { window._salesChart.destroy(); window._salesChart = null; } }; window.switchSalesChartType = function(type) { if (!window._salesData) return; window._currentSalesChartType = type; var barEl = document.getElementById('chartTypeBar'); var lineEl = document.getElementById('chartTypeLine'); if (barEl && lineEl) { if (type === 'bar') { barEl.style.color = '#1890ff'; barEl.style.fontWeight = '600'; lineEl.style.color = '#8c8c8c'; lineEl.style.fontWeight = 'normal'; } else { barEl.style.color = '#8c8c8c'; barEl.style.fontWeight = 'normal'; lineEl.style.color = '#1890ff'; lineEl.style.fontWeight = '600'; } } renderSalesChartByRange(type); }; function getSalesFilteredData() { var allSales = window._salesData || []; if (allSales.length === 0) return []; var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); var startDate = startEl ? startEl.value : ''; var endDate = endEl ? endEl.value : ''; if (!startDate && !endDate) return allSales; return allSales.filter(function(s) { var d = s.date; if (startDate && d < startDate) return false; if (endDate && d > endDate) return false; return true; }); } function updateSalesChartRange() { renderSalesChartByRange(window._currentSalesChartType || 'bar'); } function resetSalesChartRange() { var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); if (startEl) startEl.value = ''; if (endEl) endEl.value = ''; renderSalesChartByRange(window._currentSalesChartType || 'bar'); } function renderSalesChartByRange(chartType) { chartType = chartType || 'bar'; var filtered = getSalesFilteredData(); renderSalesChart(filtered, chartType); } function renderSalesChart(sales, chartType) { chartType = chartType || 'bar'; const canvas = document.getElementById('salesTrendChart'); if (!canvas) return; if (window._salesChart) { window._salesChart.destroy(); } const labels = sales.map(function(s) { return s.date; }); const data = sales.map(function(s) { return s.qty; }); const barColor = 'rgba(24, 144, 255, 0.6)'; const lineColor = '#096dd9'; // 滚动模式:根据数据量动态设置图表宽度(每天约44px,最少占满容器) var wrap = document.getElementById('salesTrendChartWrap'); if (wrap) { var containerWidth = wrap.parentElement ? wrap.parentElement.clientWidth || 1000 : 1000; var targetWidth = Math.max(containerWidth, labels.length * 44); wrap.style.width = targetWidth + 'px'; } var datasets = []; if (chartType === 'bar') { datasets.push({ label: ' ', data: data, backgroundColor: barColor, borderColor: barColor, borderWidth: 1, borderRadius: 2, order: 1, yAxisID: 'y' }); } else { datasets.push({ label: ' ', data: data, borderColor: lineColor, backgroundColor: 'rgba(9, 109, 217, 0.1)', borderWidth: 2, pointBackgroundColor: lineColor, pointRadius: 4, pointHoverRadius: 6, tension: 0.3, fill: true, yAxisID: 'y' }); } // 自定义插件:在柱状/折线图上显示数值 var valueDisplayPlugin = { id: 'valueDisplay', afterDraw: function(chart) { var ctx = chart.ctx; var chartArea = chart.chartArea; var meta = chart.getDatasetMeta(0); if (!meta || !meta.data || meta.data.length === 0) return; ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.font = '11px sans-serif'; var maxVal = Math.max.apply(null, data); meta.data.forEach(function(element, idx) { var val = data[idx]; if (val === null || val === undefined) return; var x = element.x; var y = element.y; ctx.fillStyle = '#333'; ctx.fillText(val.toLocaleString(), x, y - (chartType === 'line' ? 8 : 4)); }); ctx.restore(); } }; window._salesChart = new Chart(canvas, { type: chartType === 'bar' ? 'bar' : 'line', data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, callbacks: { title: function(items) { return ''; }, label: function(context) { if (context.dataset.label === '趋势线') return null; const idx = context.dataIndex; const date = labels[idx] || ''; const val = context.raw || 0; return '日期: ' + date + ' 销量: ' + val.toLocaleString(); } } } }, scales: { x: { grid: { display: false }, ticks: { display: true, maxRotation: 45, autoSkip: true, maxTicksLimit: 40, font: { size: 10 } } }, y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.06)' }, ticks: { font: { size: 11 } } } } }, plugins: [valueDisplayPlugin] }); } window.closeProductDetail = function() { const modal = document.getElementById('productDetailModal'); if (modal) modal.remove(); }; // 通用分页+页码选择器 function changePageSize(newSize, loadFn) { window._pageSize = parseInt(newSize); eval(loadFn)(1); } function renderPaginationWithSize(el, currentPage, total, pageSize, loadFnName, sizeVarName) { if (!el) return; const totalPages = Math.ceil(total / pageSize); if (totalPages <= 1 && total <= pageSize) { el.innerHTML = '
共 ' + total + ' 条
'; return; } let html = '
'; html += ''; html += ''; const start = Math.max(1, currentPage - 2); const end = Math.min(totalPages, currentPage + 2); for (let i = start; i <= end; i++) { html += ''; } html += ''; html += ''; html += '' + currentPage + '/' + totalPages + '页 共' + total + '条'; html += '跳转'; html += '=1&&v<=' + totalPages + ')' + loadFnName + '(v);}" '; html += 'onblur="if(this.value===\'\'||parseInt(this.value)<1||parseInt(this.value)>' + totalPages + ')this.value=' + currentPage + ';" '; html += 'onfocus="this.select()">'; html += '页'; html += '每页 '; html += ' 条'; html += '
'; el.innerHTML = html; } // ─── 供应商编辑弹窗 ─── window.editSupplier = function(code) { if (!code) return; fetch('/api/suppliers/list?size=1&code=' + encodeURIComponent(code)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data.rows || res.data.rows.length === 0) return; var s = res.data.rows[0]; var old = document.getElementById('editSupplierModal'); if (old) old.remove(); var html = '
'; html += '

编码: ' + escHtml(code) + '

'; html += '
'; var fields = [ {label:'供应商名称', id:'es_name', val:s.supplier_name, type:'text'}, {label:'做货类型', id:'es_ptype', val:s.production_type, type:'text'}, {label:'简称', id:'es_sname', val:s.short_name, type:'text'}, {label:'合同简称', id:'es_csname', val:s.contract_short_name, type:'text'}, {label:'供应商分组', id:'es_group', val:s.supplier_group, type:'text'}, {label:'跟单小组', id:'es_follower', val:s.procurement_follower, type:'text'}, {label:'采购计划', id:'es_planner', val:s.procurement_planner, type:'text'}, {label:'审核人', id:'es_reviewer', val:s.reviewer, type:'text'}, {label:'审核日期', id:'es_rdate', val:s.review_date || '', type:'text'}, {label:'数据状态', id:'es_dstatus', val:s.data_status, type:'select', options:['已审核','未审核']}, {label:'禁用状态', id:'es_bstatus', val:s.disabled_status, type:'select', options:['否','是']} ]; fields.forEach(function(f) { html += '
'; if (f.type === 'select') { var opts = (f.options || []).map(function(o) { var sel = (String(f.val || '') === o) ? ' selected' : ''; return ''; }).join(''); html += '
'; } else { html += '
'; } }); html += '
'; html += '
'; html += ''; html += '
'; var modal = document.createElement('div'); modal.id = 'editSupplierModal'; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center'; modal.innerHTML = '
' + '
✏️ 编辑供应商' + '
' + html + '
'; document.body.appendChild(modal); }); }; window.confirmSupplierEdit = function(code) { window._editingSupplierCode = code; var overlay = document.createElement('div'); overlay.className = 'sup-confirm-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center'; overlay.innerHTML = '

💾 确认保存

确认保存供应商修改?

'; document.body.appendChild(overlay); }; window.doSaveSupplier = function() { var overlays = document.querySelectorAll('.sup-confirm-overlay'); overlays.forEach(function(o) { o.remove(); }); var supplierCode = window._editingSupplierCode || ''; window._editingSupplierCode = null; if (!supplierCode) return; var fields = { supplier_name: document.getElementById('es_name')?.value?.trim() || '', production_type: document.getElementById('es_ptype')?.value?.trim() || '', short_name: document.getElementById('es_sname')?.value?.trim() || '', contract_short_name: document.getElementById('es_csname')?.value?.trim() || '', supplier_group: document.getElementById('es_group')?.value?.trim() || '', procurement_follower: document.getElementById('es_follower')?.value?.trim() || '', procurement_planner: document.getElementById('es_planner')?.value?.trim() || '', reviewer: document.getElementById('es_reviewer')?.value?.trim() || '', review_date: document.getElementById('es_rdate')?.value?.trim() || '', data_status: document.getElementById('es_dstatus')?.value?.trim() || '', disabled_status: document.getElementById('es_bstatus')?.value?.trim() || '' }; fetch('/api/supplier/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({supplier_code: supplierCode, fields: fields}) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code === 0) { alert('✅ 保存成功'); document.getElementById('editSupplierModal').remove(); loadSuppliers(supCurrentPage); } else { alert('❌ ' + (res.msg || '保存失败')); } }).catch(function() { alert('❌ 网络错误'); }); }; // ─── 新增供应商 ─── window.addSupplier = function() { var oldEl = document.getElementById('addSupplierModal'); if (oldEl) oldEl.remove(); var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'addSupplierModal'; overlay.style.display = 'flex'; overlay.onclick = function(e) { if (e.target === overlay) {} }; var html = ''; overlay.innerHTML = html; document.body.appendChild(overlay); }; window.saveNewSupplier = function() { var code = document.getElementById('newSupCode').value.trim(); var name = document.getElementById('newSupName').value.trim(); if (!code || !name) { alert('供应商编码和名称不能为空'); return; } var data = { supplier_code: code, supplier_name: name, production_type: document.getElementById('newSupProdType').value.trim(), short_name: document.getElementById('newSupShortName').value.trim(), contract_short_name: document.getElementById('newSupContractShortName').value.trim(), supplier_group: document.getElementById('newSupGroup').value.trim(), procurement_follower: document.getElementById('newSupFollower').value.trim(), procurement_planner: document.getElementById('newSupPlanner').value.trim(), reviewer: document.getElementById('newSupReviewer').value.trim(), review_date: document.getElementById('newSupReviewDate').value || '', data_status: document.getElementById('newSupDataStatus').value.trim(), disabled_status: document.getElementById('newSupDisabledStatus').value.trim() }; fetch('/api/supplier/create', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)}) .then(function(r) { return r.json(); }).then(function(res) { if (res.code === 0) { alert('新增成功'); document.getElementById('addSupplierModal').remove(); loadSuppliers(supCurrentPage || 1); } else { alert(res.msg || '新增失败'); } }); }; // ─── 删除供应商 ─── window.deleteSupplier = function(code) { if (!code) return; // 自定义确认弹窗 var overlay = document.createElement('div'); overlay.className = 'sup-confirm-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center'; overlay.setAttribute('data-sup-code', code); overlay.innerHTML = '

🗑️ 确认删除

确认删除供应商 ' + escHtml(code) + ' 吗?
此操作不可恢复!

'; document.body.appendChild(overlay); }; window.confirmSupDelete = function(btn) { var overlay = btn.closest('.sup-confirm-overlay'); var code = overlay.getAttribute('data-sup-code'); overlay.remove(); doDeleteSupplier(code); }; window.doDeleteSupplier = function(code) { var overlays = document.querySelectorAll('.sup-confirm-overlay'); overlays.forEach(function(o) { o.remove(); }); fetch('/api/supplier/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({supplier_code: code}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ ' + (res.msg || '删除成功')); loadSuppliers(supCurrentPage); } else { alert('❌ ' + (res.msg || '删除失败')); } }) .catch(function() { alert('❌ 网络错误'); }); };