diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..611af08 --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,29829 @@ +// 蝶雅采购智能体平台 - 前端逻辑 +let charts = {}; +// 页面缓存:切换菜单时不销毁页面状态 +var CACHE_VERSION = 93; +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) { + if (title === '现货看板') return loadSpotDashboardPage(container); + container.innerHTML = '
📦

' + escHtml(title) + '

功能页面建设中

'; +} + +function loadSpotDashboardPage(container) { + container.innerHTML = '

📦 现货看板

现货申请、采样、上架及销量汇总 · 销量截至 —
'; + loadSpotDashboardData(); +} +function loadSpotDashboardData(){var m=document.getElementById('spotDashboardMetrics'),c=document.getElementById('spotDashboardCharts');if(!m||!c)return;m.innerHTML='
正在加载现货看板数据...
';fetch('/api/spot/dashboard',{credentials:'same-origin'}).then(function(r){return r.json();}).then(function(res){if(res.code!==0)throw new Error(res.msg||'加载失败');var d=res.data||{},x=d.metrics||{};document.getElementById('spotDashboardLatest').textContent=d.latest_sales_date||'—';document.getElementById('spotDashboardUpdated').textContent='更新于 '+new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});var cards=[['总填表数量',x.total_forms,'blue'],['总采样数量',x.total_sample_quantity,'cyan'],['不授权数量',x.unauthorized_count,'red'],['只填表未采样',x.form_only_unsampled,'orange'],['上架数量',x.listed_count,'green'],['销量汇总',x.sales_total,'purple']];m.innerHTML=cards.map(function(a){return '
'+a[0]+'
'+Number(a[1]||0).toLocaleString('zh-CN')+'
';}).join('');c.innerHTML=renderSpotChartCard('运营各小组采样情况','按负责运营归组,展示采样数量',d.operator_sampling||[],'sample_quantity','blue','采样数量')+renderSpotChartCard('首批无销量','现货产品首批入库后暂无累计销量,按负责运营归组',d.first_no_sales||[],'value','orange','商品数量')+renderSpotChartCard('现货销量前十','按累计销量排序',d.top10_sales||[],'total_sales','purple','累计销量');}).catch(function(e){m.innerHTML='
加载失败:'+escHtml(e.message||'网络异常')+'
';c.innerHTML='';});} +function renderSpotChartCard(title,subtitle,rows,key,color,label){var max=0;rows.forEach(function(r){max=Math.max(max,Number(r[key]||0));});var body=rows.length?'
'+rows.map(function(r){var v=Number(r[key]||0),w=max?Math.max(2,v/max*100):2,n=r.name||r.product_name||r.product_code||'未命名';return '
'+escHtml(n)+'
'+v.toLocaleString('zh-CN')+'
';}).join('')+'
':'
暂无数据
';return '
'+title+''+subtitle+' · '+label+'
'+body+'
';} + +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;window._spotAppTab='application';loadSpotApplicationList(); +} +window.switchSpotApplicationTab=function(tab){window._spotAppTab=tab;window._spotAppPage=1;document.querySelectorAll('.spot-app-tab').forEach(function(btn){btn.classList.toggle('active',btn.dataset.tab===tab);});var add=document.getElementById('spotAppAddBtn');if(add)add.style.display=tab==='application'?'':'none';loadSpotApplicationList();}; +function renderSpotApplicationHead(tab){var h='';if(tab==='application')h='申请人日期部门品牌产品名称厂家图片产品链接寄样数量条形码收件信息状态原因操作';else if(tab==='arrived')h='品牌产品名称条形码是否到货到货时间';else h='品牌产品名称条形码已驳回驳回原因';var head=document.getElementById('spotAppHead');if(head)head.innerHTML=h;var table=document.getElementById('spotAppTable');if(table)table.classList.toggle('compact',tab!=='application');} +function loadSpotApplicationList(){var body=document.getElementById('spotAppRows');if(!body)return;var tab=window._spotAppTab||'application';renderSpotApplicationHead(tab);var page=window._spotAppPage||1,searchEl=document.getElementById('spotAppSearch'),search=searchEl?searchEl.value.trim():'';var url='/api/spot/application/list?page='+page+'&size=20&tab='+encodeURIComponent(tab)+'&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 if(tab==='application'){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','edit'))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('');}else if(tab==='arrived'){body.innerHTML=data.rows.map(function(row){return ''+escHtml(row.brand)+''+escHtml(row.product_name)+''+escHtml(row.barcode)+''+escHtml(row.arrived||'是')+''+escHtml(row.arrival_time||'')+'';}).join('');}else{body.innerHTML=data.rows.map(function(row){return ''+escHtml(row.brand)+''+escHtml(row.product_name)+''+escHtml(row.barcode)+''+escHtml(row.rejected||'是')+''+escHtml(row.reject_reason||'')+'';}).join('');}renderSpotApplicationPagination(data.total,data.size,data.page);}).catch(function(){body.innerHTML='网络异常,请稍后重试';});} + +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; }); + }); + window.openSpotApplicationModal = openSpotApplicationModal; + var imageInput = document.getElementById('spotApplicationImage'); + var imagePreview = document.getElementById('spotApplicationImagePreview'); + var imageRemove = document.getElementById('spotApplicationImageRemove'); + if (imageInput) imageInput.addEventListener('change', function() { + var file = this.files && this.files[0]; + if (!file) return; + if (!file.type || file.type.indexOf('image/') !== 0) { this.value=''; alert('只能上传图片文件'); return; } + var reader = new FileReader(); + reader.onload = function(e){ imagePreview.src=e.target.result; imagePreview.style.display='block'; imageRemove.style.display='inline-block'; }; + reader.readAsDataURL(file); + }); + if (imageRemove) imageRemove.addEventListener('click', function(){ imageInput.value=''; imagePreview.src=''; imagePreview.style.display='none'; imageRemove.style.display='none'; }); +} + +function decideSpotApplication(id, decision) { + var reason = ''; + if (decision === '驳回') { reason = window.prompt('请输入驳回原因:', '') || ''; if (!reason.trim()) { alert('驳回原因不能为空'); return; } } + showCenteredConfirm('确认审核', '确定将该现货申请标记为“' + decision + '”吗?').then(function(ok){ if (!ok) return; fetch('/api/spot/application/decision',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({id:id,decision:decision,reason:reason.trim()})}).then(function(r){return r.json();}).then(function(res){if(res.code===0){alert(res.msg||'处理成功');loadSpotApplicationList();}else alert(res.msg||'处理失败');}).catch(function(){alert('网络异常,请稍后重试');}); }); +} + +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 = [ + '分析建议下单数量最多的商品,并说明库存、销量和在途风险', + '哪些商品未来最容易缺货,应该优先处理什么?', + '分析一个商品是否需要继续下单,以及对应BOM包材是否够用', + '当前未到货最多的商品有哪些,分别关联了哪些订单?', + '哪些供应商未到货数量和未完结订单最多?', + '库存金额最高的10个商品中,哪些动销较差?', + '最近30天各品牌出库表现有什么异常?', + '哪些商品需要调拨到云仓或外仓?', + '分析当前库存总额及主要构成', + '查询某个产品的基础信息、货期、供应商和负责人' +]; + +// 示例问题弹窗(对话中途也能随时打开) +function aiToggleSuggest() { + var existing = document.getElementById('aiSuggestOverlay'); + if (existing) { existing.remove(); return; } + var overlay = document.createElement('div'); + overlay.id = 'aiSuggestOverlay'; + 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 = '' + + '
' + + '
' + + '💡 示例问题' + + '' + + '
' + + '
' + + AI_SUGGEST_QUESTIONS.map(function(q) { + return ''; + }).join('') + + '
' + + '
'; + document.body.appendChild(overlay); +} + +// 选不到数据源时:按推荐数据源再问一次 +window.aiAskWithSource = function(question, key, name) { + aiAsk(question + '(请使用数据源 ' + key + ')'); +}; + +// 初始空状态 HTML(重置时恢复) +function aiEmptyHtml() { + return '
' + + '
🧠
' + + '
向AI描述你的经营问题
' + + '
例如:分析某个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 = []; + window._aiLastResult = null; +} + +// 动态计算 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 = '' + + '' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
正在扫描经营数据…
' + + '
' + + '' + + '' + + '' + + ''; + setTimeout(aiFitHeight, 50); + aiLoadInsights(false); + if (!window._aiFitBound) { + window._aiFitBound = true; + window.addEventListener('resize', aiFitHeight); + } +} + +function aiSwitchMode(mode) { + ['insights','chat','decision','reports'].forEach(function(name) { + var panel = document.getElementById('ai' + name.charAt(0).toUpperCase() + name.slice(1) + 'Panel'); + var btn = document.getElementById('aiMode' + name.charAt(0).toUpperCase() + name.slice(1)); + if (panel) panel.style.display = name === mode ? 'flex' : 'none'; + if (btn) btn.className = 'btn btn-sm' + (name === mode ? ' btn-primary' : ''); + }); + if (mode === 'insights' && !window._aiInsightsLoaded) aiLoadInsights(false); + if (mode === 'decision' && !window._aiDecisionLoaded) aiLoadDecision(); + setTimeout(aiFitHeight, 20); +} + +function aiInsightSeverityBadge(level) { + var map = {高:['#fff1f0','#cf1322','#ffa39e'],中:['#fff7e6','#d46b08','#ffd591'],低:['#f6ffed','#389e0d','#b7eb8f']}; + var c = map[level] || ['#f5f5f5','#666','#ddd']; + return '' + escHtml(level || '信息') + '风险'; +} + +function aiLoadInsights(force) { + var target = document.getElementById('aiInsightsContent'); + if (!target) return; + target.innerHTML = '
⏳ 正在' + (force ? '重新' : '') + '扫描库存、销量、订单、供应商和BOM数据…
'; + fetch('/api/ai/insights?limit=100' + (force ? '&refresh=1' : '')) + .then(function(r){return r.json();}) + .then(function(res){ + if (!res || res.code !== 0) throw new Error((res && res.msg) || '加载失败'); + window._aiInsightData = res.data || {}; + window._aiInsightRows = (res.data || {}).items || []; + window._aiInsightsLoaded = true; + aiRenderInsights(); + }) + .catch(function(err){target.innerHTML='
洞察加载失败:'+escHtml(err.message||'网络错误')+'
';}); +} + +function aiRenderInsights() { + var target = document.getElementById('aiInsightsContent'); + if (!target) return; + var data = window._aiInsightData || {}, all = window._aiInsightRows || []; + var severity = (document.getElementById('aiInsightSeverity') || {}).value || ''; + var category = (document.getElementById('aiInsightCategory') || {}).value || ''; + var keyword = ((document.getElementById('aiInsightKeyword') || {}).value || '').trim().toLowerCase(); + var rows = all.filter(function(x){ + if (severity && x.severity !== severity) return false; + if (category && x.category !== category) return false; + if (keyword && JSON.stringify(x).toLowerCase().indexOf(keyword) === -1) return false; + return true; + }); + var counts = data.counts || {}, profile = data.profile || {}; + var categories = []; + all.forEach(function(x){if(categories.indexOf(x.category)===-1)categories.push(x.category);}); + var questions = (profile.questions || []).map(function(q){return '';}).join(''); + var html = '
' + + '
' + escHtml(profile.name || '业务人员') + ' · 主动经营洞察
扫描时间:' + escHtml(data.generated_at || '-') + ' 数据日期:' + escHtml(Object.values(data.data_dates || {}).filter(Boolean)[0] || '-') + '
' + + '
当前按岗位优先展示与你相关的风险
' + + '
' + + '
全部异常
'+(counts.total||0)+'
' + + '
高风险
'+(counts.high||0)+'
' + + '
中风险
'+(counts.medium||0)+'
' + + '
已展示
'+rows.length+'
' + + '
' + + '
💡 适合你的分析问题
'+questions+'
' + + '
' + + '' + + '' + + '' + + '' + + '筛选结果 '+rows.length+' 项' + + '
'; + if (!rows.length) { + html += '
当前筛选条件下暂无异常
'; + } else { + html += '
'+rows.map(function(x,idx){ + var metrics = Object.keys(x.metrics||{}).slice(0,4).map(function(k){return ''+escHtml(k)+':'+escHtml(x.metrics[k])+'';}).join(''); + return '
' + + '
'+aiInsightSeverityBadge(x.severity)+''+escHtml(x.category||'')+''+(x.focused?'岗位重点':'')+'
'+escHtml(x.title||'')+'
'+escHtml(x.detail||'')+'
'+metrics+'
' + + '
'+(x.source_page?'查看数据':'')+'
'; + }).join('')+'
'; + } + target.innerHTML = html; + var sev=document.getElementById('aiInsightSeverity'),cat=document.getElementById('aiInsightCategory'),kw=document.getElementById('aiInsightKeyword'); + if(sev)sev.value=severity;if(cat)cat.value=category;if(kw)kw.value=keyword; + window._aiFilteredInsightRows = rows; +} + +function aiAskInsightByIndex(idx) { + var row = (window._aiFilteredInsightRows || [])[idx]; + if (row) aiAskInsight(row.question || ('分析' + row.title)); +} +function aiAskInsight(question) { + aiSwitchMode('chat'); + var input=document.getElementById('aiQuestionInput'); if(input)input.value=''; + aiAsk(question); +} + +function aiLoadDecision() { + var target=document.getElementById('aiDecisionContent'); + if(!target)return; + var days=Number((document.getElementById('aiForecastDays')||{}).value||30); + target.innerHTML='
⏳ 正在预测未来'+days+'天供应风险并生成行动草稿…
'; + Promise.all([ + fetch('/api/ai/forecast?days='+days+'&limit=80').then(function(r){return r.json();}), + fetch('/api/ai/action-drafts?days='+days+'&limit=50').then(function(r){return r.json();}) + ]).then(function(results){ + var f=results[0],a=results[1]; + if(!f||f.code!==0)throw new Error((f&&f.msg)||'风险预测失败'); + if(!a||a.code!==0)throw new Error((a&&a.msg)||'行动草稿加载失败'); + window._aiForecastData=f.data||{}; window._aiActionDraftData=a.data||{}; window._aiDecisionLoaded=true; + aiRenderDecision(); + }).catch(function(err){target.innerHTML='
决策数据加载失败:'+escHtml(err.message||'网络错误')+'
';}); +} + +function aiRenderDecision() { + var target=document.getElementById('aiDecisionContent'); if(!target)return; + var forecast=window._aiForecastData||{}, drafts=window._aiActionDraftData||{}, counts=forecast.counts||{}; + var html='
🧮 AI决策模拟中心
所有计算均为只读模拟,不会修改库存、订单或待下单数据
预测周期
' + + '
🔬 单品情景模拟
%
天
' + + '
📈 未来'+escHtml(forecast.horizon_days||30)+'天供应风险预测
预测日期:'+escHtml(forecast.generated_at||'-')+'
高风险
'+(counts.high||0)+'
中风险
'+(counts.medium||0)+'
低风险
'+(counts.low||0)+'
风险SKU总数
'+(forecast.total||0)+'
'+aiRenderForecastTable(forecast.items||[])+'
' + + '
📋 待人工确认的行动草稿
'+escHtml(drafts.notice||'所有草稿必须人工确认后执行')+'
共'+(drafts.total||0)+'项,当前展示'+((drafts.items||[]).length)+'项
'+aiRenderActionDrafts(drafts.items||[])+'
'; + target.innerHTML=html; + var dayEl=document.getElementById('aiForecastDays');if(dayEl)dayEl.value=String(forecast.horizon_days||30); +} + +function aiRenderForecastTable(rows) { + if(!rows.length)return '
当前周期内暂无供应风险
'; + return '
'+rows.map(function(x){return '';}).join('')+'
风险产品编码产品名称日销量预测供应数量支撑天数补货阈值最晚下单预计见底建议下单BOM缺口操作
'+aiInsightSeverityBadge(x.risk_level)+''+escHtml(x.product_code||'')+''+escHtml(x.product_name||'')+''+escHtml(x.daily_sales)+''+escHtml(x.supply_qty)+''+escHtml(x.support_days)+''+escHtml(x.threshold_days)+''+escHtml(x.latest_order_date||'-')+''+escHtml(x.estimated_shortage_date||'-')+''+escHtml(x.suggested_order_qty||0)+''+(x.bom_shortage_count?''+x.bom_shortage_count+'项':'无')+'
'; +} + +function aiRenderActionDrafts(rows) { + if(!rows.length)return '
暂无行动草稿
'; + return '
'+rows.map(function(x){return '
['+escHtml(x.priority||'P1')+'] '+escHtml(x.title||'')+''+escHtml(x.action_type||'')+'
'+escHtml(x.reason||'')+'
建议负责人:'+escHtml(x.owner||'-')+(x.suggested_qty? ' 建议数量:'+escHtml(x.suggested_qty):'')+' 状态:'+escHtml(x.status||'待人工确认')+'
'+(x.source_page?'进入原菜单确认':'')+'
';}).join('')+'
'; +} + +function aiPrefillSimulation(code) { + var el=document.getElementById('aiSimProduct');if(el)el.value=code; + var panel=document.getElementById('aiDecisionContent');if(panel)panel.scrollTop=0; +} + +function aiRunSimulation() { + var code=((document.getElementById('aiSimProduct')||{}).value||'').trim(),target=document.getElementById('aiSimulationResult'); + if(!code){alert('请输入产品编码');return;} + var body={product_code:code,sales_change_pct:Number((document.getElementById('aiSimSalesPct')||{}).value||0),supplier_delay_days:Number((document.getElementById('aiSimDelay')||{}).value||0),planned_order_qty:Number((document.getElementById('aiSimOrderQty')||{}).value||0),horizon_days:Number((document.getElementById('aiSimHorizon')||{}).value||30)}; + target.innerHTML='
正在计算模拟结果…
'; + fetch('/api/ai/simulate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(function(r){return r.json();}).then(function(res){if(!res||res.code!==200)throw new Error((res&&res.msg)||'模拟失败');target.innerHTML=aiRenderSimulation(res.data||{});}).catch(function(err){target.innerHTML='
模拟失败:'+escHtml(err.message||'网络错误')+'
';}); +} + +function aiRenderSimulation(d) { + var b=d.base||{},s=d.scenario||{},riskColor=s.risk_level==='高'?'#cf1322':s.risk_level==='中'?'#d46b08':'#389e0d'; + var html='
'+escHtml(d.product_code||'')+''+escHtml(d.product_name||'')+'
模拟风险:'+escHtml(s.risk_level||'低')+'
'+escHtml(d.conclusion||'')+'
'+[['原日销量',b.daily_sales],['模拟日销量',s.daily_sales],['原支撑天数',b.support_days],['模拟支撑天数',s.support_days],['模拟补货阈值',s.threshold_days],['模拟建议下单',s.recommended_order_qty],['下单后支撑',s.support_after_order_days],['最晚下单日期',s.latest_order_date||'-'],['预计见底日期',s.estimated_shortage_date||'-']].map(function(x){return '
'+x[0]+'
'+escHtml(x[1])+'
';}).join('')+'
'; + if(d.bom_gaps&&d.bom_gaps.length){html+='
⚠️ BOM物料缺口
'+d.bom_gaps.map(function(x){return '';}).join('')+'
物料可支撑成品目标成品成品缺口物料缺口
'+escHtml(x.material_name||x.material_code)+''+x.support_finished_qty+''+x.target_finished_qty+''+x.finished_gap+''+x.material_gap+'
';} + html+='
'+(d.assumptions||[]).map(escHtml).join(' · ')+'
'; + return html; +} + +function aiGenerateReport(reportType) { + aiSwitchMode('reports'); + var target=document.getElementById('aiReportContent'); + if(!target)return; + target.innerHTML='
🤔 正在汇总异常并生成经营报告…
'; + fetch('/api/ai/report',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({report_type:reportType})}) + .then(function(r){return r.json();}).then(function(res){if(!res||res.code!==200)throw new Error((res&&res.msg)||'生成失败');target.innerHTML=aiRenderReport(res.data||{});}) + .catch(function(err){target.innerHTML='
报告生成失败:'+escHtml(err.message||'网络错误')+'
';}); +} + +function aiRenderReport(d) { + var html='
📊 '+escHtml(d.title||'经营报告')+'
生成时间:'+escHtml(d.generated_at||'-')+'
'+escHtml(d.summary||'')+'
'; + (d.sections||[]).forEach(function(sec){html+='
'+escHtml(sec.title||'')+'
'+(sec.content?'
'+escHtml(sec.content)+'
':'')+((sec.items||[]).length?'
    '+sec.items.map(function(x){return '
  • '+escHtml(typeof x==='string'?x:JSON.stringify(x))+'
  • ';}).join('')+'
':'')+'
';}); + if(d.priorities&&d.priorities.length){html+='
✅ 优先行动清单
'+d.priorities.map(function(x){return '
['+escHtml(x.priority||'P1')+'] '+escHtml(x.action||'')+'建议责任人:'+escHtml(x.owner||'-')+'
'+escHtml(x.reason||'')+'
';}).join('')+'
';} + if(d.follow_ups&&d.follow_ups.length){html+='
继续分析:
'+d.follow_ups.slice(0,6).map(function(q){return '';}).join('')+'
';} + return html+'
'; +} + +// 添加一条聊天气泡 +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, totalRows, canExport) { + if (!rows || !rows.length) return '
查询无数据
'; + if (canExport === undefined || canExport === null) canExport = true; + 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(''); + var rid = 'aiRes' + (window._aiResSeq = (window._aiResSeq || 0) + 1); + window._aiResults = window._aiResults || {}; + window._aiResults[rid] = {columns: columns, rows: rows}; + var total = totalRows || rows.length; + var hint = total > rows.slice(0, 100).length + ? '
共 ' + total + ' 行,当前仅展示前 ' + rows.slice(0, 100).length + ' 行,可点「导出CSV」查看全部
' + : ''; + var toolbar = '
' + + (canExport + ? '' + + '' + : '(无导出权限)') + + '共 ' + total + ' 行' + + '
'; + return toolbar + '
' + + '' + thead + '' + tbody + '
' + hint + + (sql ? '
SQL:' + escHtml(sql) + '
' : ''); +} + +// 导出 CSV(带 BOM,Excel 直接打开不乱码) +window.aiExportCsv = function(rid) { + var res = (window._aiResults || {})[rid]; + if (!res) return; + var columns = res.columns || [], rows = res.rows || []; + var cell = function(v) { + if (v === null || v === undefined) v = ''; + var s = String(v); + return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; + }; + var lines = [columns.map(cell).join(',')]; + rows.forEach(function(r) { lines.push(columns.map(function(c) { return cell(r[c]); }).join(',')); }); + var blob = new Blob(['\ufeff' + lines.join('\r\n')], {type: 'text/csv;charset=utf-8'}); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'AI分析结果_' + new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-') + '.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(function() { URL.revokeObjectURL(a.href); }, 2000); +}; + +// 复制结果(制表符分隔,可直接粘贴到 Excel) +window.aiCopyResult = function(rid) { + var res = (window._aiResults || {})[rid]; + if (!res) return; + var lines = [(res.columns || []).join('\t')]; + (res.rows || []).forEach(function(r) { + lines.push((res.columns || []).map(function(c) { + var v = r[c]; + return (v === null || v === undefined) ? '' : String(v); + }).join('\t')); + }); + var text = lines.join('\n'); + var done = function() { showCenteredAlert('已复制', '✅ 结果已复制,可直接粘贴到 Excel'); }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(done).catch(function() { aiCopyFallback(text, done); }); + } else { + aiCopyFallback(text, done); + } +}; + +function aiCopyFallback(text, done) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;top:-1000px'; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); done(); } catch (e) { alert('复制失败,请手动选择表格内容'); } + document.body.removeChild(ta); +} + +// 结果图表:日期列→折线趋势;其他→前 12 名横向柱状图 +function aiRenderChart(columns, rows) { + if (typeof Chart === 'undefined') return ''; + if (!columns || columns.length < 2 || !rows || rows.length < 2) return ''; + var labelCol = columns[0]; + var prefer = ['库存金额', '总数量', '建议下单数量', '在途未到合计', '总成本', '库存', '可用库存', '昨日销量', '30天平均销量']; + var numCols = columns.slice(1).filter(function(c) { + return rows.some(function(r) { return typeof r[c] === 'number'; }); + }); + var valueCol = null; + for (var i = 0; i < prefer.length && !valueCol; i++) { + if (numCols.indexOf(prefer[i]) !== -1) valueCol = prefer[i]; + } + if (!valueCol) valueCol = numCols[0]; + if (!valueCol) return ''; + var MAX = 12; + var data = rows.slice(0, MAX); + var isDate = data.every(function(r) { return /^\d{4}-\d{2}-\d{2}/.test(String(r[labelCol] || '')); }); + var labels = data.map(function(r) { return String(r[labelCol] === null || r[labelCol] === undefined ? '' : r[labelCol]); }); + var values = data.map(function(r) { return Number(r[valueCol]) || 0; }); + var cid = 'aiChart' + (window._aiChartSeq = (window._aiChartSeq || 0) + 1); + var height = isDate ? 180 : Math.max(140, data.length * 26); + var title = escHtml(valueCol) + (isDate ? '(趋势)' : '(前 ' + data.length + ' 名)') + (rows.length > MAX ? ' · 共 ' + rows.length + ' 行' : ''); + var html = '
' + + '
📊 ' + title + '
' + + '
'; + setTimeout(function() { + var el = document.getElementById(cid); + if (!el) return; + try { + new Chart(el.getContext('2d'), { + type: isDate ? 'line' : 'bar', + data: { + labels: labels, + datasets: [{ + label: valueCol, data: values, + borderColor: '#722ed1', backgroundColor: 'rgba(114,46,209,0.45)', + borderWidth: 2, fill: false, tension: 0.25 + }] + }, + options: { + responsive: true, maintainAspectRatio: false, + indexAxis: isDate ? 'x' : 'y', + plugins: {legend: {display: false}, tooltip: {callbacks: {label: function(ctx) { return valueCol + ':' + ctx.parsed[isDate ? 'y' : 'x']; }}}}, + scales: { + x: {beginAtZero: isDate, ticks: {font: {size: 10}}}, + y: {beginAtZero: true, ticks: {font: {size: 10}}} + } + } + }); + } catch (e) {} + }, 30); + return html; +} + +// 统一渲染:口径/数据日期 + 图表 + 表格 +function aiRenderResult(columns, rows, meta, sql, canExport) { + var html = ''; + if (meta && (meta.name || meta.data_date)) { + html += '
🗂 数据源:' + escHtml(meta.name || '-') + + (meta.data_date ? ' · 数据日期:' + escHtml(meta.data_date) : '') + '
'; + } + html += aiRenderChart(columns || [], rows || []); + html += aiRenderTable(columns || [], rows || [], sql, (meta && meta.total_rows) || (rows || []).length, canExport); + return html; +} + +// P0:按AI返回的图表意图渲染,不再只按第一列猜图表。 +function aiRenderConfiguredChart(spec, datasets) { + if (!spec || spec.type === 'none' || typeof Chart === 'undefined') return ''; + var source = null; + (datasets || []).some(function(ds) { + if (ds.key === spec.datasource) { source = ds; return true; } + return false; + }); + if (!source || !source.rows || !source.rows.length) return ''; + var labelField = spec.label_field; + var valueFields = spec.value_fields || []; + if (!labelField || !valueFields.length) return ''; + var data = source.rows.slice(0, spec.type === 'pie' ? 10 : 20); + var labels = data.map(function(r) { return String(r[labelField] === null || r[labelField] === undefined ? '' : r[labelField]); }); + var palette = ['#722ed1','#1677ff','#13c2c2','#52c41a','#faad14','#fa8c16','#f5222d','#eb2f96','#2f54eb','#a0d911']; + var sets = valueFields.map(function(field, idx) { + return { + label: field, + data: data.map(function(r) { return Number(r[field]) || 0; }), + borderColor: palette[idx % palette.length], + backgroundColor: spec.type === 'pie' ? palette.slice(0, data.length) : palette[idx % palette.length] + '99', + borderWidth: 2, + fill: false, + tension: 0.25 + }; + }); + var cid = 'aiConfiguredChart' + (window._aiChartSeq = (window._aiChartSeq || 0) + 1); + var html = '
' + + '
📊 ' + escHtml(spec.title || '分析图表') + '
' + + '
'; + setTimeout(function() { + var canvas = document.getElementById(cid); + if (!canvas) return; + try { + new Chart(canvas.getContext('2d'), { + type: spec.type, + data: {labels: labels, datasets: sets}, + options: { + responsive: true, maintainAspectRatio: false, + indexAxis: spec.type === 'bar' && data.length > 8 ? 'y' : 'x', + plugins: {legend: {display: spec.type === 'pie' || sets.length > 1, position: 'bottom'}}, + scales: spec.type === 'pie' ? {} : {x: {beginAtZero: false}, y: {beginAtZero: true}} + } + }); + } catch (e) {} + }, 40); + return html; +} + +function aiAnalysisList(items, kind) { + if (!items || !items.length) return ''; + var cfg = { + finding: {title:'关键发现', icon:'🔎', color:'#1677ff', bg:'#f0f7ff'}, + risk: {title:'风险与异常', icon:'⚠️', color:'#d46b08', bg:'#fff7e6'}, + action: {title:'建议动作', icon:'✅', color:'#389e0d', bg:'#f6ffed'} + }[kind]; + var body = items.map(function(item) { + var heading = kind === 'action' ? ((item.priority ? '[' + item.priority + '] ' : '') + (item.action || '')) : (item.title || ''); + var detail = kind === 'action' ? item.reason : item.detail; + var level = item.level ? '' + escHtml(item.level) + '' : ''; + return '
' + + '
' + escHtml(heading) + level + '
' + + (detail ? '
' + escHtml(detail) + '
' : '') + '
'; + }).join(''); + return '
' + + '
' + cfg.icon + ' ' + cfg.title + '
' + + '
' + body + '
'; +} + +function aiRenderAnalysis(d) { + var confidenceColor = d.confidence === '高' ? '#389e0d' : (d.confidence === '低' ? '#cf1322' : '#d46b08'); + var html = '
' + + '
' + + '🧠 ' + escHtml(d.title || 'AI经营分析') + '' + + '可信度:' + escHtml(d.confidence || '中') + '' + + '
' + + '
' + escHtml(d.summary || '') + '
'; + + html += aiAnalysisList(d.findings || [], 'finding'); + html += aiAnalysisList(d.risks || [], 'risk'); + html += aiAnalysisList(d.actions || [], 'action'); + + if (d.evidence && d.evidence.length) { + html += '
📌 核心依据
' + + '
' + + d.evidence.map(function(e) { + return '
' + + '
' + escHtml(e.label || '') + '
' + + '
' + escHtml(e.value === undefined ? '' : e.value) + '
' + + '
' + escHtml(e.source || '') + (e.date ? ' · ' + escHtml(e.date) : '') + '
' + + (e.formula ? '
口径:' + escHtml(e.formula) + '
' : '') + '
'; + }).join('') + '
'; + } + + html += aiRenderConfiguredChart(d.chart || {}, d.datasets || []); + + if (d.datasets && d.datasets.length) { + html += '
🗂 分析数据依据(' + d.datasets.length + '个数据源)
'; + d.datasets.forEach(function(ds, idx) { + var sourceInfo = escHtml(ds.name || ds.key || '') + (ds.data_date ? ' · 数据日期:' + escHtml(ds.data_date) : '') + ' · 共' + (ds.total_rows || 0) + '行'; + var jump = ds.source_page ? '查看原菜单 ↗' : ''; + html += '
' + + '' + sourceInfo + jump + '' + + '
' + + (ds.purpose ? '
分析用途:' + escHtml(ds.purpose) + '
' : '') + + aiRenderTable(ds.columns || [], ds.rows || [], null, ds.total_rows, d.can_export !== false) + + '
'; + }); + html += '
'; + } + + if ((d.denied_sources && d.denied_sources.length) || (d.failed_sources && d.failed_sources.length) || (d.limitations && d.limitations.length) || d.planning_warning) { + var notes = (d.limitations || []).slice(); + if (d.denied_sources && d.denied_sources.length) notes.push('因权限未读取:' + d.denied_sources.join('、')); + if (d.failed_sources && d.failed_sources.length) notes.push('查询失败:' + d.failed_sources.map(function(x){return x.datasource;}).join('、')); + if (d.planning_warning) notes.push(d.planning_warning); + html += '
ℹ️ ' + notes.map(escHtml).join('
') + '
'; + } + if (d.follow_ups && d.follow_ups.length) { + html += '
继续分析:
' + + d.follow_ups.slice(0, 5).map(function(q) { + return ''; + }).join('') + '
'; + } + return html; +} + +// 发送问题 +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; + var kwEl = document.getElementById('aiHistoryKeyword'); + var dsEl = document.getElementById('aiHistoryDs'); + var keyword = kwEl ? kwEl.value.trim() : ''; + var ds = dsEl ? dsEl.value : ''; + list.innerHTML = '
加载中…
'; + var url = '/api/ai/history?limit=100' + + (keyword ? '&keyword=' + encodeURIComponent(keyword) : '') + + (ds ? '&datasource=' + encodeURIComponent(ds) : ''); + fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + .then(function(r) { return r.json(); }) + .then(function(res) { + var rows = (res && res.data) || []; + // 数据源下拉(保留当前选择) + if (dsEl && res && res.datasources) { + var cur = dsEl.value; + dsEl.innerHTML = '' + res.datasources.map(function(d) { + return ''; + }).join(''); + dsEl.value = cur && res.datasources.some(function(d) { return d.key === cur; }) ? cur : ''; + } + if (!rows.length) { + list.innerHTML = '
' + + (keyword || ds ? '没有匹配的历史记录' : '暂无历史记录,提问后自动保存') + '
'; + 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 dsName = escHtml(h.datasource_name || h.datasource || ''); + var dsTag = dsName ? ' [' + dsName + ']' : ''; + return '
' + + '
' + + '
🔍 ' + q + dsTag + '
' + + '
' + t + '
' + + (s ? '
' + s + '
' : '') + + '
' + + '' + + '
'; + }).join(''); + }) + .catch(function() { + list.innerHTML = '
历史记录加载失败
'; + }); +} + +// 删除单条历史记录 +window.aiDeleteHistory = function(id) { + if (!confirm('确定删除这条历史记录?')) return; + fetch('/api/ai/history/' + id, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + .then(function(r) { return r.json(); }) + .then(function(res) { + if (res && res.code === 0) { aiLoadHistory(); } + else { alert((res && res.msg) || '删除失败'); } + }) + .catch(function() { alert('网络错误,请重试'); }); +}; + +// 点击历史记录:展开弹窗显示记录内容 +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 === 'analysis') { + bodyHtml = aiRenderAnalysis(content); + } else if (content.type === 'report') { + bodyHtml = aiRenderReport(content); + } else if (content.type === 'table' && content.columns && content.rows && content.rows.length) { + bodyHtml = aiRenderResult(content.columns || [], content.rows || [], content.meta || { + total_rows: content.total_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, + last_result: window._aiLastResult || null + }) + }) + .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.type === 'analysis') { + html = aiRenderAnalysis(d); + // 保存主数据集摘要,下一轮追问可继续沿用本次分析上下文。 + window._aiLastResult = { + datasource: d.datasource || '', + columns: d.columns || [], + rows: (d.rows || []).slice(0, 5), + total_rows: d.total_rows || (d.rows || []).length, + datasets: (d.datasets || []).map(function(ds) { return {key: ds.key, name: ds.name, columns: ds.columns}; }) + }; + } else { + if (d.summary) html += '
' + escHtml(d.summary) + '
'; + if (d.type === 'table') { + html += aiRenderResult(d.columns || [], d.rows || [], { + name: d.datasource_name || d.datasource, data_date: d.data_date, total_rows: d.total_rows + }, d.sql, d.can_export !== false); + window._aiLastResult = { + datasource: d.datasource || '', + columns: d.columns || [], + rows: (d.rows || []).slice(0, 5), + total_rows: d.total_rows || (d.rows || []).length + }; + } + } + // 选不到数据源:把最接近的数据源列出来,点一下就用它再问 + if (d.suggestions && d.suggestions.length) { + html += '
💡 可能是想问这些(点击重问):
'; + html += '
' + + d.suggestions.map(function(sg) { + return ''; + }).join('') + '
'; + } + 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: 'freight_included', label: '是否含运', width: '90px'}, + {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: 'freight_included', label: '是否含运', type: 'select', options: ['是', '否']}, + {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.filter(function(f) { return !window._hasFieldPerm || window._hasFieldPerm('product_packaging', f.label, 'view'); }).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.filter(function(f) { + return !window._hasFieldPerm || window._hasFieldPerm('product_packaging', f.label, 'view'); + }).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' +}; +// ─── 权限工具函数 ─── +// 权限接口返回的是运行时页面ID(如 orders_domestic),而详情弹窗/字段注册表 +// 使用标准页面ID(如 order_domestic)。统一在前端解析两套ID,避免页面权限存在 +// 但详情弹窗误判为“无权限”后把基础字段全部隐藏。 +window._getPagePermissions = function(pageId) { + pageId = String(pageId || ''); + var perms = (window._currentUser && window._currentUser.permissions) || {}; + var runtimeAliases = { + 'purchase_pending': 'purchase', + 'purchase_backup': 'purchase_ready', + 'order_domestic': 'orders_domestic', + 'order_overseas': 'orders_overseas', + 'order_packaging': 'orders_packaging', + 'order_finished': 'orders_finished' + }; + var pagePerms = perms[pageId]; + if (!pagePerms && runtimeAliases[pageId]) pagePerms = perms[runtimeAliases[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 || {}; + return pagePerms || {}; +}; +// 检查当前用户是否有页面查看权限 +window._canViewPage = function(pageId) { + if (!window._currentUser) return true; + if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; + var pagePerms = window._getPagePermissions(pageId); + 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 pagePerms = window._getPagePermissions(pageId); + var key = 'field:' + fieldName + ':view'; + if (pagePerms[key] !== undefined) return pagePerms[key] == 1; + return pagePerms['view'] !== 0; +}; +// 字段权限检查别名:详情弹窗、表格和导出共用同一套规则。 +window._hasFieldPerm = function(pageId, fieldName, operation) { + return operation === 'edit' ? window._canEditField(pageId, fieldName) : window._canViewField(pageId, fieldName); +}; +// 检查当前用户是否有某个字段的编辑权限 +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 pagePerms = window._getPagePermissions(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 pagePerms = window._getPagePermissions(pageId); + if (pagePerms[op] !== undefined) return pagePerms[op] == 1; + // 新增按钮级权限未配置时兼容旧权限:详情继承查看,合同继承编辑。 + var fallback = {detail: 'view', contract: 'edit'}[op]; + if (fallback && pagePerms[fallback] !== undefined) return pagePerms[fallback] == 1; + return false; // 未配置且无兼容旧权限时无权限 +}; + +// 仅当用户明确拥有“成本价”字段查看权限且该字段实际被授权时,才限制为海外品。 +// 旧逻辑把“无法查看成本价”(敏感字段默认隐藏)误当成页面范围限制,导致 +// 仅配置了 planning.view 的岗位也被强制隐藏“计划预测”和“计划预测-国内品”两个 Tab。 +window._planOverseasOnly = false; + +// 隐藏无权限的菜单 +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, '''); +} + +// 成本价、单价在业务页面统一固定显示 4 位小数;空值仍显示“-”。 +function formatPrice4(value, currency) { + if (value === null || value === undefined || value === '') return '-'; + var number = Number(value); + if (!Number.isFinite(number)) return '-'; + return (currency ? '¥' : '') + number.toFixed(4); +} + +function formatPriceInput4(value) { + var number = Number(value); + return Number.isFinite(number) ? number.toFixed(4) : '0.0000'; +} + +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) { + if (window._inventoryBoardRefreshTimer) { + clearTimeout(window._inventoryBoardRefreshTimer); + window._inventoryBoardRefreshTimer = null; + } + window._inventoryBoardRefreshAttempts = 0; + // 页面高度占满视口: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 scheduleInventoryBoardFreshPoll(d) { + if (window._inventoryBoardRefreshTimer) { + clearTimeout(window._inventoryBoardRefreshTimer); + window._inventoryBoardRefreshTimer = null; + } + if (!d || d.cache_state !== 'stale_refreshing') { + window._inventoryBoardRefreshAttempts = 0; + return; + } + window._inventoryBoardRefreshAttempts = (window._inventoryBoardRefreshAttempts || 0) + 1; + if (window._inventoryBoardRefreshAttempts > 12) return; + var requestedMonth = d.month || inventoryBoardMonth(); + window._inventoryBoardRefreshTimer = setTimeout(function() { + if (!document.getElementById('invBoardContent') || inventoryBoardMonth() !== requestedMonth) return; + fetch('/api/dashboard/inventory-board?month=' + encodeURIComponent(requestedMonth), { + headers: {'X-Requested-With': 'XMLHttpRequest'} + }).then(function(r) { return r.json(); }).then(function(res) { + if (res.code === 0 && document.getElementById('invBoardContent')) { + renderInventoryBoard(res.data); + } + }).catch(function() { + // 后台刷新失败不清空当前已展示数据;下一次进入页面仍可继续尝试。 + }); + }, 1500); +} +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; + var cacheStatus = document.getElementById('invBoardCacheStatus'); + if (cacheStatus) { + if (d.cache_state === 'stale_refreshing') { + cacheStatus.textContent = '● 正在后台更新,当前先展示上次统计'; + cacheStatus.style.color = '#d48806'; + } else { + cacheStatus.textContent = d.data_updated_at ? '● 统计更新时间:' + d.data_updated_at : ''; + cacheStatus.style.color = '#52c41a'; + } + } + scheduleInventoryBoardFreshPoll(d); + + // 金额统一以“万元”展示 + 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); + }); +} + +var DASHBOARD_COST_LOGIC = { + '总成本': { + formula: '成品成本 + 包材成本。', + scope: '使用最新库存日期的库存数量和同日成本价,汇总成品与包材的库存、在途成本。', + note: '排除内料库存;特殊物料使用系统维护的修正成本价。' + }, + '成品成本': { + formula: '成品库存成本 + 成品在途成本。', + scope: '成品范围包含成品、清库存、现货、赠品;未匹配产品资料且不属于20号仓的库存也按成品归类。', + note: '已删除产品不计入;系统指定的特殊编码即使在20号仓也固定归为成品。' + }, + '包材成本': { + formula: '包材库存成本 + 包材在途成本。', + scope: '包材范围为产品类型“包材”,以及未匹配产品资料但位于20号仓的库存。', + note: '名称或编码属于内料的记录不计入包材成本。' + }, + '成品库存成本': { + formula: 'Σ(最新库存日可用库存 × 同日成品成本价)。', + scope: '按商品汇总成品、清库存、现货、赠品的可用库存后计算。', + note: '库存数量与成本价严格取同一天;20号仓的指定排除商品不统计。' + }, + '成品在途成本': { + formula: 'Σ(生产在途 × BOM内料成本价)+ Σ(采购在途 × 成品成本价)。', + scope: '生产在途的内料通过物料名称含“内料”或物料编码含“-NL”识别。', + note: '生产在途不计算彩盒、瓶子等其他BOM物料,也不乘BOM用量。' + }, + '包材库存成本': { + formula: 'Σ(最新库存日包材可用库存 × 同日包材成本价)。', + scope: '有产品资料的按“包材”类型归类;无产品资料的20号仓库存默认归为包材。', + note: '排除内料及系统指定的20号仓例外商品。' + }, + '包材在途成本': { + formula: 'Σ(包材采购在途数量 × 同日包材成本价)。', + scope: '统计包材及无产品资料但位于20号仓商品的采购在途。', + note: '当前口径只统计采购在途,不统计包材生产在途。' + } +}; + +function renderCostLogicEye(label) { + var logic = DASHBOARD_COST_LOGIC[label]; + if (!logic) return ''; + // 只渲染图标,不把说明正文嵌在卡片布局里。说明由统一的 body 层浮层按需生成, + // 避免部分用户浏览器/扩展样式覆盖隐藏规则后把整段说明撑进卡片,导致页面错位。 + return '' + + ''; +} + +function bindDashboardCostLogicTips() { + var hosts = document.querySelectorAll('#dashContent .stat-cost-help'); + if (!hosts.length) return; + var tooltip = document.getElementById('dashboardCostTooltip'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.id = 'dashboardCostTooltip'; + tooltip.className = 'dashboard-cost-tooltip'; + tooltip.setAttribute('role', 'tooltip'); + tooltip.setAttribute('aria-hidden', 'true'); + document.body.appendChild(tooltip); + } + var hideTimer = null; + var hide = function() { + clearTimeout(hideTimer); + hideTimer = setTimeout(function() { + tooltip.classList.remove('is-visible'); + tooltip.setAttribute('aria-hidden', 'true'); + }, 80); + }; + var show = function(host) { + clearTimeout(hideTimer); + var label = host.getAttribute('data-cost-label') || ''; + var logic = DASHBOARD_COST_LOGIC[label]; + if (!logic) return; + tooltip.innerHTML = '
' + escHtml(label) + '统计逻辑
' + + '
计算公式:' + escHtml(logic.formula) + '
' + + '
统计范围:' + escHtml(logic.scope) + '
' + + '
特别说明:' + escHtml(logic.note) + '
' + + '
卡片金额在页面上按整数四舍五入显示。
'; + tooltip.classList.add('is-visible'); + tooltip.setAttribute('aria-hidden', 'false'); + var rect = host.getBoundingClientRect(); + var gap = 10; + var width = Math.min(360, Math.max(240, window.innerWidth - 36)); + tooltip.style.width = width + 'px'; + tooltip.style.left = Math.max(18, Math.min(window.innerWidth - width - 18, rect.right - width + 10)) + 'px'; + tooltip.style.top = Math.min(window.innerHeight - 18, rect.bottom + gap) + 'px'; + }; + hosts.forEach(function(host) { + host.addEventListener('mouseenter', function() { show(host); }); + host.addEventListener('mouseleave', hide); + host.addEventListener('focus', function() { show(host); }); + host.addEventListener('blur', hide); + }); + tooltip.onmouseenter = function() { clearTimeout(hideTimer); }; + tooltip.onmouseleave = hide; +} + +function renderCostBreakdown(fields, values) { + fields = fields || []; + values = values || {}; + return '
' + fields.map(function(label) { + return '
' + escHtml(label) + '' + renderCostLogicEye(label) + '' + (values[label] == null ? '待补数据' : Math.round(values[label]).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 canViewCost = !window._canViewField || window._canViewField('data_summary', '总成本'); + 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'} + ]; + if (canViewCost && d.total_cost !== undefined) { + statItems.push({label:'总成本', value:renderCostBreakdown(['总成本', '成品成本', '包材成本'], costValues), sub:'', icon:'💎', color:'#eb2f96', cost:true}); + statItems.push({label:'成品成本', value:renderCostBreakdown(['成品成本', '成品库存成本', '成品在途成本'], costValues), sub:'', icon:'🏷️', color:'#722ed1', cost:true}); + statItems.push({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 += '
'; + if (canViewCost && d.cost_data_warning) { + cards += '
⚠️ ' + escHtml(d.cost_data_warning) + '
'; + } + + // 第一行图表:日出库趋势 + 品牌占比 + var row1 = '
'; + row1 += '

📈 日出库趋势

'; + row1 += '

🥧 品牌出库占比 TOP8

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

🏪 店铺出库排行

'; + row2 += '

📦 单品销量 TOP10

'; + row2 += '
'; + + el.innerHTML = cards + row1 + row2; + bindDashboardCostLogicTips(); + + // 绘制所有图表 + 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 permissionLabel = {supplier_code:'供应商编码',supplier_name_match:'供应商名称'}[c.field] || c.label; + var permitted = window._canViewField('products', permissionLabel); + var vis = window.prodColVisible[c.field] && permitted ? '' : ' 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) { + var permissionLabel = {supplier_code:'供应商编码',supplier_name_match:'供应商名称'}[c.field] || c.label; + if (!window.prodColVisible[c.field] || !window._canViewField('products', permissionLabel)) 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) ? '-' : formatPrice4(p, true); + } + 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) ? '-' : formatPrice4(price, true); + } + 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、XLSX或XLS文件'); return; } + + const extension = (file.name.split('.').pop() || '').toLowerCase(); + if (!['csv', 'xlsx', 'xls'].includes(extension)) { + alert('仅支持CSV、XLSX、XLS格式的用户文件'); + input.value = ''; + return; + } + + const btn = document.getElementById('importBtn'); + if (btn) { btn.disabled = true; btn.textContent = '导入中...'; } + + const finish = function() { + if (btn) { btn.disabled = false; btn.textContent = '导入用户'; } + input.value = ''; + }; + const reader = new FileReader(); + reader.onload = function(e) { + const result = String(e.target.result || ''); + const commaIndex = result.indexOf(','); + if (commaIndex < 0) { + alert('导入失败:无法读取文件内容'); + finish(); + return; + } + fetch('/api/users/import', { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + filename: file.name, + file_base64: result.slice(commaIndex + 1) + }) + }) + .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(finish); + }; + reader.onerror = function() { + alert('导入失败:文件读取失败,请重新选择文件'); + finish(); + }; + reader.readAsDataURL(file); +} + +function loadUsersPage(container) { + container.innerHTML = ` + + +
+
+
+ 🔐 权限角色: + 超级管理员 全部权限 + 管理员 管理权限 + 查看者 只读权限 +
+
+
+
+
+

用户列表

+
+ + + + 支持 CSV / XLSX / XLS + + +
+
+ +
+ + + + + + + +
+
+ + + + + + + + +
序号用户名姓名角色 ↕级别岗位手机邮箱状态最后登录操作
+
+
+
+ + + + `; + + 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, ''); + // 操作列由 detail/edit/contract/delete/rollback 等按钮权限控制,不再按字段权限隐藏。 + if (fieldName === 'action') return; + var fieldAliases = { + purchase: {product_type:'产品类型',product_code:'货品编号',product_name:'货品名称',inventory_support_days:'库存支撑天数',stocking_support_days:'备货支撑天数',avg_3d_sales:'3天平均销量',avg_7d_sales:'7天平均销量',avg_14d_sales:'14天平均销量',sales_trend:'销售趋势',confirm_order:'确认下单',product_grade:'产品分级',supplier_name:'供应商名称',suggested_qty:'建议下单数量',formal_qty:'正式下单数量',remark:'下单备注',lead_time:'货期',logistics_time:'物流',total_support_days:'总支撑天数',platform:'平台',operator:'运营',audit_price:'审核价格',created_by:'下单人员',created_time:'下单时间',procurement_follower:'采购跟单',procurement_planner:'采购计划',created_at:'加入时间',action:'操作'}, + products: {supplier_code:'供应商编码',supplier_name_match:'供应商名称',supplier_name:'供应商名称'}, + orders_domestic: {action:'操作'}, orders_overseas: {action:'操作'}, orders_packaging: {'编码':'产品编码','名称':'货品名称','合计':'金额','供应商':'供应商','仓库到货':'仓库','采购下单人':'采购员','包材开发':'采购开发','旺店通编号':'旺店通单号',action:'操作'}, orders_finished: {action:'操作'} + }; + var permissionField = (fieldAliases[pageId] && fieldAliases[pageId][fieldName]) || fieldName; + if (permissionField && !window._canViewField(pageId, permissionField)) { + th.style.display = 'none'; + table.querySelectorAll('tbody tr').forEach(function(tr) { + var td = tr.children[idx]; + if (td) td.style.display = 'none'; + }); + } + }); +}; + +// ─── 操作日志 P0 审计查询中心 ─── +var logCurrentPage=1, LOG_PAGE_SIZE=30, _logQuickRisk=''; +function loadLogsPage(container){ + container.innerHTML=` +
加载中...
+
+
至
+
${window._hasPagePerm('logs','export')?'':''}${(window._hasPagePerm('logs','verify')||window._hasPagePerm('logs','manage'))?'':''}
+
+

日志列表

操作时间操作人角色/岗位操作页面操作类型操作字段业务对象结果风险IP地址操作内容操作
`; + loadLogOptions();loadLogSummary();loadLogs(1); +} +function _logParams(includePage){var p=new URLSearchParams();if(includePage!==false){p.set('page',logCurrentPage);p.set('size',LOG_PAGE_SIZE);}var map={logUser:'user',logPage:'page_name',logAction:'action',logField:'field',logKeyword:'keyword',logIp:'ip',logDateFrom:'date_from',logDateTo:'date_to',logRisk:'risk',logResult:'result'};Object.keys(map).forEach(function(id){var e=document.getElementById(id);if(e&&e.value)p.set(map[id],e.value);});return p;} +function loadLogOptions(){fetch('/api/logs/options').then(r=>r.json()).then(res=>{if(res.code!==0)return;[['logPage','pages'],['logAction','actions'],['logField','fields']].forEach(function(x){var el=document.getElementById(x[0]);if(!el)return;var first=el.options[0].outerHTML;el.innerHTML=first+(res.data[x[1]]||[]).map(v=>'').join('');});});} +function loadLogSummary(){fetch('/api/logs/summary').then(r=>r.json()).then(res=>{if(res.code!==0)return;var d=res.data||{},el=document.getElementById('logSummaryCards');if(!el)return;el.innerHTML='
'+d.active_users+'今日活跃用户
';});} +function humanizeLogContent(row){ + var raw=String(row.detail||''),path=row.request_path||'',match=raw.match(/^(POST|PUT|PATCH|DELETE)\s+(\/api\/[^;\s]+)/);if(!path&&match)path=match[2]; + var maps=[['/api/finished/list','查询成品提货列表'],['/api/packaging/orders/list','查询包材订单列表'],['/api/domestic/orders/list','查询国内订单列表'],['/api/overseas/orders/list','查询海外订单列表'],['/api/sales_management/list','查询动销管理列表'],['/api/packaging/orders/column-values','查询包材订单筛选项'],['/api/domestic/orders/column-values','查询国内订单筛选项'],['/api/finished/column-values','查询成品提货筛选项'],['/api/domestic/orders/update','编辑国内订单'],['/api/packaging/orders/update','编辑包材订单'],['/api/purchase/add_pending','加入待下单'],['/api/purchase/confirm_order','确认下单'],['/api/new_products/bom/save','保存新品BOM'],['/api/bom/create_with_materials','保存物料清单'],['/api/bom/delete','删除物料清单'],['/api/users/update','编辑用户'],['/api/product/remark/save','保存产品批注']]; + var title='';maps.some(function(x){if(path.indexOf(x[0])===0){title=x[1];return true;}return false;}); + if(!title&&row.action&&row.action!=='数据变更')title=row.action; + if(!title)title=path?'系统数据操作':'数据变更'; + var rest=raw.replace(/^(POST|PUT|PATCH|DELETE)\s+\/api\/[^;\s]+[;]?/,'').replace(/order_filter:0个字段/g,'筛选条件:无').replace(/filters:0个字段/g,'筛选条件:无').replace(/:0个字段/g,':无').replace(/:0项/g,':无'); + return rest?title+';'+rest:title; +} +function loadLogs(page){logCurrentPage=page;fetch('/api/logs/list?'+_logParams(true).toString()).then(r=>r.json()).then(res=>{if(res.code!==0)return;var d=res.data||{},tb=document.getElementById('logTbody');if(!tb)return;if(!d.rows||!d.rows.length){tb.innerHTML='暂无日志';}else{tb.innerHTML=d.rows.map(function(r){var page=r.page||r.action||'-',detail=r.detail||'-',readable=humanizeLogContent(r),short=readable.length>100?readable.slice(0,100)+'…':readable,role=(r.role||'-')+(r.position_name?' / '+r.position_name:'');return ''+escHtml(r.created_at||'-')+''+escHtml(r.real_name||'-')+'
'+escHtml(r.username||'')+'
'+escHtml(role)+''+escHtml(page)+''+escHtml(r.action||'-')+''+escHtml(r.field||'-')+''+escHtml([r.object_type,r.object_id].filter(Boolean).join(' / ')||'-')+''+logResultBadge(r.result_status)+''+logRiskBadge(r.risk_level)+''+escHtml(r.ip_address||'-')+''+escHtml(short)+'';}).join('');}document.getElementById('logCount').textContent='共 '+(d.total||0)+' 条';renderPaginationWithSize(document.getElementById('logPagination'),page,d.total||0,LOG_PAGE_SIZE,'loadLogs','LOG_PAGE_SIZE');});} +function logResultBadge(r){var m={success:['成功','green'],failed:['失败','red'],denied:['已拒绝','orange']},x=m[r]||m.success;return ''+x[0]+'';} +function logRiskBadge(r){var m={high:['高风险','red'],medium:['中风险','orange'],normal:['普通','grey']},x=m[r]||m.normal;return ''+x[0]+'';} +window.setLogQuickDate=function(days){var now=new Date(),from=new Date();from.setDate(now.getDate()-days);document.getElementById('logDateFrom').value=from.toISOString().slice(0,10);document.getElementById('logDateTo').value=now.toISOString().slice(0,10);loadLogs(1);}; +window.setLogQuickRisk=function(risk){document.getElementById('logRisk').value=risk;loadLogs(1);}; +window.setLogQuickResult=function(result){document.getElementById('logResult').value=result;loadLogs(1);}; +window.resetLogFilters=function(){['logUser','logPage','logAction','logField','logKeyword','logIp','logDateFrom','logDateTo','logRisk','logResult'].forEach(id=>{var e=document.getElementById(id);if(e)e.value='';});loadLogs(1);}; +window.exportOperationLogs=function(){window.location.href='/api/logs/export?'+_logParams(false).toString();}; +window.openLogGovernance=function(){var id='logGovernance-'+Date.now();document.body.insertAdjacentHTML('beforeend','');Promise.all(['/api/logs/governance/summary','/api/logs/alerts','/api/logs/anomalies','/api/logs/reports','/api/logs/retention'].map(u=>fetch(u).then(r=>r.json()))).then(function(rs){var sum=rs[0].data||{},alerts=rs[1].data||[],ano=rs[2].data||{},reports=rs[3].data||{},ret=rs[4].data||{},body=document.getElementById(id+'-body');body.innerHTML='
'+sum.open_alerts+'未处理预警
'+sum.high_7d+'7天高风险
'+sum.failed_7d+'7天失败/拒绝
'+sum.active_users+'7天活跃用户
'+(window._hasPagePerm('logs','verify')?'':'')+(window._hasPagePerm('logs','manage')?'':'')+'

异常行为检测

5分钟高频操作:'+((ano.rapid_operations||[]).length)+'
重复权限拒绝:'+((ano.repeated_denied||[]).length)+'
非工作时间高风险:'+((ano.off_hours_high_risk||[]).length)+'

最近预警

'+alerts.slice(0,20).map(a=>'').join('')+'
时间用户类型风险内容
'+escHtml(a.created_at||'-')+''+escHtml(a.real_name||a.user_id||'-')+''+escHtml(a.title||'-')+''+logRiskBadge(a.risk_level)+''+escHtml((a.detail||'').slice(0,100))+'

最近30天操作排行

'+(reports.by_action||[]).slice(0,10).map(x=>'
'+escHtml(x.label)+''+x.value+'
').join('')+'
在线保留 '+(ret.config?.online_days||180)+' 天,归档保留 '+(ret.config?.archive_days||730)+' 天;当前待归档 '+(ret.preview?.archive_candidates||0)+' 条。本阶段只预览,不自动删除。
';});}; +window.verifyOperationLogs=function(id){fetch('/api/logs/integrity/verify',{method:'POST'}).then(r=>r.json()).then(res=>{if(res.code===0)showCenteredAlert('完整性校验','已校验 '+res.data.checked+' 条,异常 '+res.data.invalid+' 条');else alert(res.msg||'校验失败');});}; +window.editLogRetention=function(id,online,archive){var a=prompt('在线保留天数(最少30天)',online);if(a===null)return;var b=prompt('归档保留天数',archive);if(b===null)return;fetch('/api/logs/retention',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({online_days:parseInt(a),archive_days:parseInt(b),auto_archive:false})}).then(r=>r.json()).then(res=>{if(res.code===0){alert('保留策略已保存,仅用于归档预览,不会自动删除日志');document.getElementById(id)?.remove();openLogGovernance();}else alert(res.msg||'保存失败');});}; +window.showOperationLogDetail=function(id){fetch('/api/logs/detail?id='+id).then(r=>r.json()).then(res=>{if(res.code!==0){alert(res.msg||'读取失败');return;}var d=res.data,m='operationLogDetail-'+Date.now();document.body.insertAdjacentHTML('beforeend','');});}; + +// ─── 访问记录(登录日志) ─── +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 _permPositions = []; +var _positionOpPerms = []; +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(); }), + fetch('/api/permissions/positions').then(function(r) { return r.json(); }), + fetch('/api/permissions/position-operations').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]; + var positionsRes = results[7]; + var positionOpRes = results[8]; + + if (usersRes.code !== 0) { + el.innerHTML = '
❌ 加载失败
'; + return; + } + + _permUsers = usersRes.data || []; + _permPages = pagesRes.data || []; + _permOps = opsRes.data || []; + _permRoles = rolesRes.data || []; + _roleOpPerms = roleOpRes.data || []; + _permPositions = positionsRes && positionsRes.code === 0 ? (positionsRes.data || []) : []; + _positionOpPerms = positionOpRes && positionOpRes.code === 0 ? (positionOpRes.data || []) : []; + _permFields = (fieldsRes && fieldsRes.code === 0 && fieldsRes.data) ? fieldsRes.data : {}; + // 权限配置菜单树由后端按侧边栏自动生成,新增子菜单无需改动前端 + if (menuTreeRes && menuTreeRes.code === 0 && (menuTreeRes.data || []).length) { + _permMenuTree = menuTreeRes.data; + } + + // 收集去重的岗位和级别列表用于筛选下拉 + var posMap = {}; + var levelSet = {}; + _permUsers.forEach(function(u) { + if (u.position) { + posMap[u.position] = u.position_name || POSITION_MAP[u.position] || '其他岗位'; + } + if (u.user_level) levelSet[u.user_level] = true; + }); + var posList = Object.keys(posMap).map(function(code) { + return {code: code, name: posMap[code]}; + }).sort(function(a, b) { return String(a.name).localeCompare(String(b.name), 'zh-CN'); }); + var levelList = Object.keys(levelSet).sort(); + var levelNames = {'employee': '员工', 'leader': '组长', 'director': '主管'}; + + // P0:角色概览 + 用户筛选。角色权限是默认值,用户权限仅用于例外。 + var filterHtml = '
'; + filterHtml += '
👤 用户权限与例外点击用户可编辑三态权限:继承岗位和角色 / 允许 / 禁止
'; + filterHtml += '
'; + filterHtml += '
'; + filterHtml += ''; + filterHtml += ''; + filterHtml += ''; + filterHtml += '
'; + filterHtml += '
'; + + // 角色操作权限卡片 + el.innerHTML = filterHtml; + renderPermRoleOverview(); + renderPermPositionOverview(); + renderPermUserGrid(); + }) + .catch(function() { + el.innerHTML = '
❌ 加载失败
'; + }); +} + +function renderPermRoleOverview() { + var el = document.getElementById('permRoleOverview'); + if (!el) return; + var roleColors = { + 'super_admin': ['#fff1f0','#cf1322'], 'admin': ['#e6f7ff','#096dd9'], + 'procurement_planner': ['#f9f0ff','#531dab'], 'procurement_follower': ['#e6fffb','#08979c'], + 'product_developer': ['#fff0f6','#c41d7f'], 'operator': ['#fff7e6','#d46b08'], 'viewer': ['#f5f5f5','#595959'] + }; + var html = '
'; + html += '
🛡️ 角色权限矩阵角色权限是默认值,用户权限仅用于个别例外
共 ' + _permRoles.length + ' 个角色
'; + html += '
'; + (_permRoles || []).forEach(function(role) { + var colors = roleColors[role.id] || ['#f5f5f5','#595959']; + var configured = (_roleOpPerms || []).filter(function(p){ return p.role === role.id && Number(p.permission) === 1; }).length; + var users = (_permUsers || []).filter(function(u){ return u.role === role.id || (role.id === 'viewer' && u.role === '查看者'); }).length; + var locked = role.id === 'super_admin' || (window._currentRole === 'admin' && role.id === 'admin'); + html += '
'; + html += '
🛡️
'; + html += '
' + escHtml(role.name || role.id) + '
已开启 ' + configured + ' 项 · ' + users + ' 人使用
'; + html += '
'; + html += '
'; + }); + html += '
'; + el.innerHTML = html; +} + +function renderPermPositionOverview() { + var el=document.getElementById('permPositionOverview'); if(!el)return; + var html='
👔 岗位权限矩阵普通用户自动继承:岗位权限 > 查看者角色权限
共 '+_permPositions.length+' 个岗位
'; + (_permPositions||[]).forEach(function(position){var enabled=(_positionOpPerms||[]).filter(function(p){return p.role===position.id&&Number(p.permission)===1;}).length;html+='
👔
'+escHtml(position.name)+'
已开启 '+enabled+' 项 · '+(position.user_count||0)+' 人使用
';}); + html+='
';el.innerHTML=html; +} + +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 _roleOpEditingType = 'role'; +var _roleOpData = {}; // {role_page_operation: 1|0} +// 按功能ID给出偏安全的默认值(查看者以外的角色) +var featureDefaults = { + // 新增权限默认只开放查看,其余操作必须由管理员明确配置。 + 'view': 1 +}; + +window.openRoleOpPermModal = function(roleId, subjectType, subjectLabel) { + subjectType = subjectType || 'role'; + _roleOpEditingType = subjectType; + if (subjectType === 'role' && roleId === 'super_admin') { + alert('超级管理员固定拥有全部权限,无需配置'); + return; + } + if (subjectType === 'role' && window._currentRole === 'admin' && roleId === 'admin') { + alert('管理员不能修改管理员角色权限'); + return; + } + _roleOpEditingRole = roleId; + // 构建权限索引,未保存的按角色默认值填充 + _roleOpData = {}; + if (subjectType === 'position') { + _roleOpPerms.filter(function(p){ return p.role === 'viewer'; }).forEach(function(p) { + _roleOpData[roleId + '_' + normalizePermPage(p.page) + '_' + p.operation] = p.permission; + }); + } + var sourcePerms = subjectType === 'position' ? _positionOpPerms : _roleOpPerms; + sourcePerms.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 if (feat.id === 'detail') { + _roleOpData[key] = _roleOpData[roleId + '_' + item.page + '_view'] == 1 ? 1 : 0; + } else if (feat.id === 'contract') { + _roleOpData[key] = _roleOpData[roleId + '_' + item.page + '_edit'] == 1 ? 1 : 0; + } 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 subjectName = subjectLabel || roleNames[roleId] || roleId; + 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.toggleRolePermGroups = function(expand) { + var wrap = document.getElementById('roleOpModalCardWrap'); + if (!wrap) return; + wrap.querySelectorAll('.perm-group-body').forEach(function(body){ body.style.display = expand ? '' : 'none'; }); +}; + +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.openPermissionVersionModal = function(targetType, targetId, label) { + var modalId='permissionVersionModal-'+Date.now(); + var html=''; + document.body.insertAdjacentHTML('beforeend',html); + loadPermissionVersions(modalId,targetType,String(targetId),label||String(targetId)); +}; + +window.loadPermissionVersions = function(modalId,targetType,targetId,label) { + var body=document.getElementById(modalId+'-body');if(!body)return; + fetch('/api/permissions/versions?target_type='+encodeURIComponent(targetType)+'&target_id='+encodeURIComponent(targetId),{credentials:'same-origin'}).then(function(r){return r.json();}).then(function(res){ + if(res.code!==0){body.innerHTML='
'+escHtml(res.msg||'加载失败')+'
';return;} + var rows=res.data||[]; + var opts=rows.map(function(v){return '';}).join(''); + var html='
版本对比:→
'; + html+=''; + rows.forEach(function(v,index){html+='';}); + html+='
版本说明创建人时间来源操作
V'+v.version_no+''+(index===0?' 当前':'')+''+escHtml(v.summary||'-')+''+escHtml(v.creator_name||v.created_by||'-')+''+escHtml(v.created_at||'-')+''+(v.source_version_id?'回滚自 #'+v.source_version_id:'-')+' '+(index===0?'':'')+'
';body.innerHTML=html; + var right=document.getElementById(modalId+'-right');if(right&&rows.length>1)right.selectedIndex=1; + }).catch(function(e){body.innerHTML='
'+escHtml(e.message||'网络错误')+'
';}); +}; + +function _permissionVersionPageInfo(page) { + var normalized = normalizePermPage(page || ''); + var found = null; + (_permMenuTree || []).some(function(group) { + return (group.items || []).some(function(item) { + if (normalizePermPage(item.page) === normalized) { + found = {page: normalized, name: item.name || normalized, group: group.name || '其他'}; + return true; + } + return false; + }); + }); + return found || {page: normalized, name: normalized || '未知页面', group: '其他'}; +} + +function _permissionVersionOperationName(page, operation) { + var pageInfo = _permissionVersionPageInfo(page); + var name = ''; + (_permMenuTree || []).some(function(group) { + return (group.items || []).some(function(item) { + if (normalizePermPage(item.page) !== pageInfo.page) return false; + var feature = (item.features || []).find(function(f) { return f.id === operation; }); + if (feature) name = feature.name || operation; + return !!feature; + }); + }); + var fallback = { + view:'查看', create:'新增', add:'新增/加入', edit:'编辑', delete:'删除', + export:'导出', import:'导入', query:'查询', batch_confirm:'确认下单', + rollback:'回退订单', field_setting:'字段设置', frozen:'冻结列', + edit_product:'编辑产品', reset:'恢复默认', sync:'同步产品信息' + }; + return name || fallback[operation] || operation || '未知操作'; +} + +function _permissionVersionScopeText(scope) { + if (!scope) return '未配置'; + var names = {all:'全部数据',self:'本人数据',follower:'所属采购跟单',planner:'所属采购计划',product_type:'指定产品类型'}; + var text = names[scope.scope_type] || scope.scope_type || '未配置'; + if (scope.scope_value) text += '(' + scope.scope_value + ')'; + return text; +} + +function _permissionStatusBadge(value, inheritedText) { + if (value === null || value === undefined) return '' + escHtml(inheritedText || '未配置') + ''; + var allowed = Number(value) === 1 || value === true; + return '' + (allowed ? '允许' : '禁止') + ''; +} + +function _permissionVersionTargetName(meta) { + if (!meta) return '-'; + if (meta.target_type === 'role') { + var role = (_permRoles || []).find(function(r) { return String(r.id) === String(meta.target_id); }); + return role ? role.name : meta.target_id; + } + if (meta.target_type === 'position') { + var position = (_permPositions || []).find(function(r) { return String(r.id) === String(meta.target_id); }); + return position ? position.name : meta.target_id; + } + var user = (_permUsers || []).find(function(u) { return String(u.id) === String(meta.target_id); }); + return user ? (user.real_name || user.username) : ('用户 #' + meta.target_id); +} + +window.showPermissionVersionDetail = function(versionId) { + fetch('/api/permissions/versions/detail?id=' + encodeURIComponent(versionId), {credentials:'same-origin'}) + .then(function(r) { return r.json(); }) + .then(function(res) { + if (res.code !== 0 || !res.data) { alert(res.msg || '读取失败'); return; } + var meta = res.data.meta || {}; + var snapshot = res.data.snapshot || {}; + var ordinary = {}; + var fields = {}; + var hasView = {}; + + (snapshot.operations || []).forEach(function(item) { + var page = normalizePermPage(item.page || ''); + var operation = String(item.operation || ''); + if (operation.indexOf('field:') === 0) { + var parts = operation.split(':'); + var action = parts.pop(); + var field = parts.slice(1).join(':'); + var key = page + '\u0000' + field; + if (!fields[key]) fields[key] = {page:page, field:field, view:null, edit:null}; + if (action === 'view' || action === 'edit') fields[key][action] = Number(item.permission || 0); + } else { + if (!ordinary[page]) ordinary[page] = []; + ordinary[page].push({operation:operation, permission:Number(item.permission || 0), source:'操作权限'}); + if (operation === 'view') hasView[page] = true; + } + }); + (snapshot.legacy_pages || []).forEach(function(item) { + var page = normalizePermPage(item.page || ''); + if (!hasView[page]) { + if (!ordinary[page]) ordinary[page] = []; + ordinary[page].push({operation:'view', permission:Number(item.permission || 0), source:'兼容页面权限'}); + } + }); + + var ordinaryRows = []; + Object.keys(ordinary).sort(function(a,b){return _permissionVersionPageInfo(a).name.localeCompare(_permissionVersionPageInfo(b).name,'zh-CN');}).forEach(function(page) { + var pageInfo = _permissionVersionPageInfo(page); + ordinaryRows.push('
' + escHtml(pageInfo.name) + '' + escHtml(pageInfo.group) + ' · ' + escHtml(pageInfo.page) + '
' + ordinary[page].map(function(op) { + return '
' + escHtml(_permissionVersionOperationName(page, op.operation)) + '' + _permissionStatusBadge(op.permission) + '
'; + }).join('') + '
'); + }); + + var fieldRows = Object.keys(fields).sort().map(function(key) { + var item = fields[key], pageInfo = _permissionVersionPageInfo(item.page); + return '' + escHtml(pageInfo.name) + '
' + escHtml(pageInfo.page) + '
' + escHtml(item.field) + '' + _permissionStatusBadge(item.view, '继承角色/页面') + '' + _permissionStatusBadge(item.edit, '继承角色/页面') + ''; + }).join(''); + + var scopeRows = (snapshot.data_scopes || []).map(function(scope) { + var pageInfo = _permissionVersionPageInfo(scope.page); + return '' + escHtml(pageInfo.name) + '
' + escHtml(pageInfo.page) + '
' + escHtml(_permissionVersionScopeText(scope)) + '' + _permissionStatusBadge(scope.can_view) + '' + _permissionStatusBadge(scope.can_edit) + '' + _permissionStatusBadge(scope.can_export) + ''; + }).join(''); + + var allowCount = 0, denyCount = 0; + Object.keys(ordinary).forEach(function(page) { ordinary[page].forEach(function(item) { Number(item.permission) === 1 ? allowCount++ : denyCount++; }); }); + var fieldCount = Object.keys(fields).length; + var scopeCount = (snapshot.data_scopes || []).length; + var targetName = _permissionVersionTargetName(meta); + var targetTypeName = meta.target_type === 'role' ? '角色' : (meta.target_type === 'position' ? '岗位' : '用户'); + var modalId = 'versionDetail-' + Date.now(); + var notice = meta.target_type === 'user' ? '
个人版本只记录该用户的例外权限;未列出的权限继续继承所属岗位和角色。
' : ''; + var html = ''; + document.body.insertAdjacentHTML('beforeend', html); + }) + .catch(function(e) { alert('读取权限版本失败:' + (e.message || '网络错误')); }); +}; + +function _permissionVersionChangeLabel(key) { + var text = String(key || ''); + if (text.indexOf('页面:') === 0) { + var page = text.slice(3), info = _permissionVersionPageInfo(page); + return {category:'页面权限', name:info.name + ' / 查看', code:info.page}; + } + if (text.indexOf('操作:') === 0) { + var rest = text.slice(3), pos = rest.indexOf(':'), page = pos >= 0 ? rest.slice(0,pos) : rest, operation = pos >= 0 ? rest.slice(pos+1) : ''; + var info = _permissionVersionPageInfo(page); + if (operation.indexOf('field:') === 0) { + var parts = operation.split(':'), action = parts.pop(), field = parts.slice(1).join(':'); + return {category:'字段权限', name:info.name + ' / ' + field + ' / ' + (action === 'view' ? '查看' : '编辑'), code:info.page}; + } + return {category:'操作权限', name:info.name + ' / ' + _permissionVersionOperationName(page, operation), code:info.page}; + } + if (text.indexOf('范围:') === 0) { + var page = text.slice(3), info = _permissionVersionPageInfo(page); + return {category:'数据范围', name:info.name, code:info.page}; + } + return {category:'其他', name:text, code:''}; +} + +function _permissionVersionChangeValue(key, value) { + if (value === null || value === undefined) return '未配置'; + if (String(key).indexOf('范围:') === 0 && typeof value === 'object') { + return '
' + escHtml(_permissionVersionScopeText(value)) + '查看 ' + (Number(value.can_view) === 1 ? '允许' : '禁止') + ' · 编辑 ' + (Number(value.can_edit) === 1 ? '允许' : '禁止') + ' · 导出 ' + (Number(value.can_export) === 1 ? '允许' : '禁止') + '
'; + } + return _permissionStatusBadge(value); +} + +window.comparePermissionVersions = function(modalId) { + var left = (document.getElementById(modalId + '-left') || {}).value; + var right = (document.getElementById(modalId + '-right') || {}).value; + if (!left || !right || left === right) { alert('请选择两个不同版本'); return; } + fetch('/api/permissions/versions/compare?left=' + encodeURIComponent(left) + '&right=' + encodeURIComponent(right), {credentials:'same-origin'}) + .then(function(r) { return r.json(); }) + .then(function(res) { + if (res.code !== 0) { alert(res.msg || '对比失败'); return; } + var changes = res.data.changes || []; + var id = 'versionCompare-' + Date.now(); + var rows = changes.map(function(change) { + var label = _permissionVersionChangeLabel(change.key); + return '' + escHtml(label.category) + '' + escHtml(label.name) + '
' + escHtml(label.code) + '
' + _permissionVersionChangeValue(change.key, change.before) + '' + _permissionVersionChangeValue(change.key, change.after) + ''; + }).join(''); + var html = ''; + document.body.insertAdjacentHTML('beforeend', html); + }) + .catch(function(e) { alert('版本对比失败:' + (e.message || '网络错误')); }); +}; + + +window.rollbackPermissionVersion = function(versionId,targetType,targetId,modalId,label) { + showCenteredConfirm('⚠️ 权限版本回滚','回滚会覆盖当前该角色/用户的操作权限、字段权限和数据范围。系统会先自动备份当前权限,是否继续?').then(function(ok){if(!ok)return;fetch('/api/permissions/versions/rollback',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({version_id:versionId})}).then(function(r){return r.json();}).then(function(res){if(res.code===0){alert('✅ 权限回滚成功');loadPermissionVersions(modalId,targetType,targetId,label);loadPermissionsUsers();}else alert(res.msg||'回滚失败');}).catch(function(e){alert('回滚失败:'+(e.message||'网络错误'));});}); +}; + +window.openRoleScopeModal = function(roleId, subjectType, subjectLabel) { + subjectType=subjectType||'role'; + var role = subjectType==='position' ? ((_permPositions||[]).find(function(item){return item.id===roleId;})||{id:roleId,name:subjectLabel||roleId}) : ((_permRoles || []).find(function(item){ return item.id === roleId; }) || {id:roleId,name:roleId}); + if (subjectType==='role' && roleId === 'super_admin' || (window._currentRole === 'admin' && roleId === 'admin')) { alert('系统角色数据范围不可修改'); return; } + fetch('/api/permissions/data-scopes?subject_type='+encodeURIComponent(subjectType)+'&subject_id=' + encodeURIComponent(roleId)) + .then(function(r){ return r.json(); }) + .then(function(res){ + if (res.code !== 0) { alert(res.msg || '读取数据范围失败'); return; } + var saved = {}; (res.data || []).forEach(function(item){ saved[item.page] = item; }); + var pages = []; + (_permMenuTree || []).forEach(function(group){ (group.items || []).forEach(function(item){ pages.push({page:item.page,name:item.name,group:group.name}); }); }); + var modalId = 'roleScopeModal-' + Date.now(); + var html = ''; + document.body.insertAdjacentHTML('beforeend', html); + }).catch(function(e){ alert('读取数据范围失败:' + (e.message || '网络错误')); }); +}; + +window.saveRoleScope = function(modalId, roleId, subjectType) { + var modal = document.getElementById(modalId); if (!modal) return; + var items = []; + modal.querySelectorAll('tr[data-scope-page]').forEach(function(row){ + items.push({page:row.dataset.scopePage,scope_type:row.querySelector('.scope-type').value,scope_value:row.querySelector('.scope-value').value.trim(),can_view:row.querySelector('.scope-view').checked,can_edit:row.querySelector('.scope-edit').checked,can_export:row.querySelector('.scope-export').checked}); + }); + fetch('/api/permissions/data-scopes/save',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({subject_type:subjectType,subject_id:roleId,items:items})}).then(function(r){return r.json();}).then(function(res){if(res.code===0){alert('✅ 数据范围保存成功');modal.remove();}else alert(res.msg||'保存失败');}).catch(function(e){alert('保存失败:'+(e.message||'网络错误'));}); +}; + +window.openPermissionAuditModal = function() { + var modalId='permissionAuditModal-' + Date.now(); + var html=''; + document.body.insertAdjacentHTML('beforeend',html); loadPermissionAudit(modalId); +}; +window.loadPermissionAudit = function(modalId) { + var body=document.getElementById(modalId+'-body'), kw=(document.getElementById(modalId+'-kw')||{}).value||''; if(!body)return; + fetch('/api/permissions/audits?size=100&keyword='+encodeURIComponent(kw),{credentials:'same-origin'}).then(function(r){return r.json();}).then(function(res){if(res.code!==0){body.innerHTML='
'+escHtml(res.msg||'加载失败')+'
';return;}var rows=res.data.rows||[];if(!rows.length){body.innerHTML='
暂无权限变更记录
';return;}body.innerHTML=''+rows.map(function(x){return '';}).join('')+'
时间操作人对象动作说明
'+escHtml(x.created_at||'-')+''+escHtml(x.actor_name||x.actor_id||'-')+''+escHtml(x.target_type+' / '+x.target_id)+''+escHtml(x.action||'-')+''+escHtml(x.detail||'-')+'
';}).catch(function(e){body.innerHTML='
'+escHtml(e.message||'网络错误')+'
';}); +}; + +// 用户最终生效权限预览:只读展示,不产生任何业务副作用。 +window.openEffectivePermPreview = function(userId) { + fetch('/api/permissions/user/effective?user_id=' + encodeURIComponent(userId), {credentials:'same-origin'}) + .then(function(r){ return r.json(); }) + .then(function(res) { + if (res.code !== 0 || !res.data) { alert(res.msg || '无法读取最终权限'); return; } + var data = res.data, user = data.user || {}; + var modalId = 'effectivePermModal-' + Date.now(); + var html = ''; + document.body.insertAdjacentHTML('beforeend', html); + var body = document.getElementById(modalId + '-body'); + var rows = []; + (data.pages || []).forEach(function(page) { + (page.operations || []).forEach(function(op) { + rows.push('
' + escHtml(page.name) + '' + escHtml(op.name) + '' + (op.allowed ? '允许' : '禁止') + '来源:' + escHtml(op.source) + '
'); + }); + }); + var fieldRows = []; + Object.keys(data.fields || {}).forEach(function(page) { + (data.fields[page] || []).forEach(function(field) { + fieldRows.push('
' + escHtml(page) + '' + escHtml(field.key) + (field.sensitive ? ' 🔒' : '') + '查看:' + (field.view.allowed ? '允许' : '禁止') + '编辑:' + (field.edit.allowed ? '允许' : '禁止') + '' + escHtml(field.view.source) + ' / ' + escHtml(field.edit.source) + '
'); + }); + }); + body.innerHTML = '
功能操作权限
' + rows.join('') + '
字段权限
' + fieldRows.join('') + '
'; + }).catch(function(e){ alert('读取最终权限失败:' + (e.message || '网络错误')); }); +}; + +window.filterEffectivePermPreview = function(modalId, keyword) { + var wrap = document.getElementById(modalId); + if (!wrap) return; + var kw = String(keyword || '').trim().toLowerCase(); + wrap.querySelectorAll('.effective-perm-row').forEach(function(row){ row.style.display = !kw || (row.dataset.search || '').toLowerCase().indexOf(kw) >= 0 ? 'flex' : 'none'; }); +}; + +// ─── 字段权限矩阵(角色/用户共用辅助) ─── +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; +}; + +// 页面内的“全选”仅切换该页同列字段;既不立即保存,也不改变其他页面/操作权限。 +window.toggleRoleFieldColumn = function(master) { + var table = master && master.closest('.permission-field-table'); + if (!table) return; + table.querySelectorAll('.role-field-cb[data-op="' + master.dataset.op + '"]').forEach(function(cb) { + cb.checked = master.checked; + }); + master.indeterminate = false; +}; + +window.syncRoleFieldSelectAll = function(input) { + var table = input && input.closest('.permission-field-table'); + if (!table) return; + var op = input.dataset.op; + var master = table.querySelector('.role-field-all[data-op="' + op + '"]'); + if (!master) return; + var cells = table.querySelectorAll('.role-field-cb[data-op="' + op + '"]'); + var selected = Array.prototype.filter.call(cells, function(cb) { return cb.checked; }).length; + master.checked = cells.length > 0 && selected === cells.length; + master.indeterminate = selected > 0 && selected < cells.length; +}; + +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) + ''; + bodyHtml += '
'; + feats.forEach(function(f) { + var key = roleId + '_' + item.page + '_' + f.id; + var checked = _roleOpData[key] == 1 ? 'checked' : ''; + bodyHtml += ''; + }); + 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; + cardWrap.querySelectorAll('.permission-field-table').forEach(function(table) { + ['view', 'edit'].forEach(function(op) { + var input = table.querySelector('.role-field-cb[data-op="' + op + '"]'); + if (input) syncRoleFieldSelectAll(input); + }); + }); + var summary = document.getElementById('rolePermSummary'); + if (summary) { + var enabled = Object.keys(_roleOpData).filter(function(k){ return k.indexOf(roleId + '_') === 0 && Number(_roleOpData[k]) === 1; }).length; + summary.textContent = '当前开启 ' + enabled + ' 项'; + } + } +} + +function _permissionConflictMessage(conflicts) { + if (!conflicts || !conflicts.length) return ''; + return '
检测到 ' + conflicts.length + ' 项职责冲突:
' + conflicts.map(function(c){ + return '
[' + escHtml(c.level) + '风险] ' + escHtml(c.name) + '
' + escHtml(c.left) + ' + ' + escHtml(c.right) + '
建议:' + escHtml(c.advice || '') + '
'; + }).join('') + '
继续保存会记录风险确认和权限版本。
'; +} + +function saveRoleOpPerms(modalId) { + var items = [], 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 oldValue=_roleOpData[item.role+'_'+item.page+'_'+item.operation]==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 oldValue=_roleOpData[item.role+'_'+item.page+'_'+item.operation]==1?1:0; + if(oldValue!==item.permission)changes.push((item.permission?'✅ 开启 ':'⛔ 关闭 ')+escHtml(item.page+' / '+cb.dataset.field+'·'+(cb.dataset.op==='view'?'查看':'编辑'))); + }); + if(!changes.length){alert('没有权限变更');return;} + var roleId=items.length?items[0].role:_roleOpEditingRole; + var subjectType=_roleOpEditingType||'role'; + var preview='本次共变更 '+changes.length+' 项:
'+changes.slice(0,50).join('
')+(changes.length>50?'
……其余 '+(changes.length-50)+' 项':'')+'
'; + function submit(confirmConflicts){ + fetch(subjectType==='position'?'/api/permissions/position-operations/save':'/api/permissions/role-operations/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items:items,confirm_conflicts:!!confirmConflicts})}).then(function(r){return r.json();}).then(function(res){ + if(res.code===409&&res.data&&res.data.conflicts){showCenteredConfirm('⚠️ 职责冲突确认',_permissionConflictMessage(res.data.conflicts)).then(function(ok){if(ok)submit(true);});return;} + if(res.code===0){var target=subjectType==='position'?_positionOpPerms:_roleOpPerms;for(var i=target.length-1;i>=0;i--){if(target[i].role===roleId)target.splice(i,1);}items.forEach(function(item){target.push(item);});alert('✅ '+(subjectType==='position'?'岗位':'角色')+'权限已保存,并生成权限版本');if(modalId)document.getElementById(modalId)?.remove();renderPermRoleOverview();renderPermPositionOverview();}else alert(res.msg||'保存失败'); + }).catch(function(e){alert('保存失败: '+(e.message||'网络错误'));}); + } + showCenteredConfirm('权限变更预览',preview).then(function(confirmed){ + if(!confirmed)return; + fetch('/api/permissions/conflicts/check',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({subject_type:subjectType||'role',subject_id:roleId,items:items})}).then(function(r){return r.json();}).then(function(res){ + var conflicts=res.code===0&&res.data?(res.data.conflicts||[]):[]; + if(conflicts.length)showCenteredConfirm('⚠️ 职责冲突确认',_permissionConflictMessage(conflicts)).then(function(ok){if(ok)submit(true);});else submit(false); + }).catch(function(){submit(false);}); + }); +} + +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: '查看者'}, + {role: 'procurement_planner', icon: '📋', label: '采购计划'}, + {role: 'procurement_follower', icon: '📦', label: '采购跟单'}, + {role: 'product_developer', icon: '🧩', label: '产品开发'}, + {role: 'operator', 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 + ''; + if (roleId) h += ''; + 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 levelNames = {'employee': '员工', 'leader': '组长', 'director': '主管'}; + // 岗位中文名称统一来自用户管理的岗位主数据,不直接展示英文/自定义编码。 + var posName = u.position_name || POSITION_MAP[u.position] || (u.position ? '其他岗位' : '未设置岗位'); + var levelName = levelNames[u.user_level] || u.user_level || ''; + + h += '
'; + // 头像 + h += '
' + escHtml(initial) + '
'; + // 信息 + h += ''; + 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=[],fieldItems=[],changes=[]; + document.querySelectorAll('.user-perm-state').forEach(function(sel){var state=sel.value,original=sel.dataset.original||'inherit';if(state!==original)changes.push(escHtml(sel.dataset.page+' / '+sel.dataset.op)+':'+escHtml(original)+' → '+escHtml(state));if(state!=='inherit')items.push({page:sel.dataset.page,operation:sel.dataset.op,permission:state==='allow'?1:0});}); + document.querySelectorAll('.user-field-state').forEach(function(sel){var state=sel.value,original=sel.dataset.original||'inherit';if(state!==original)changes.push(escHtml(sel.dataset.page+' / '+sel.dataset.field+'·'+(sel.dataset.op==='view'?'查看':'编辑'))+':'+escHtml(original)+' → '+escHtml(state));fieldItems.push({page:sel.dataset.page,field:sel.dataset.field,operation:sel.dataset.op,permission:state==='inherit'?null:(state==='allow'?1:0)});}); + if(!changes.length){alert('没有权限变更');return;} + var preview='本次共变更 '+changes.length+' 项:
'+changes.slice(0,50).join('
')+'
'; + function submit(confirmConflicts){ + var saveBtn=document.getElementById('userPermSaveBtn');if(saveBtn){saveBtn.disabled=true;saveBtn.textContent='保存中...';} + var opRes = null; + fetch('/api/permissions/user/save',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({user_id:parseInt(userId),items:items,confirm_conflicts:!!confirmConflicts})}).then(function(r){return r.json();}).then(function(result){ + opRes=result; + if(opRes.code!==0)return Promise.reject(new Error(opRes.msg||'操作权限保存失败')); + return 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,confirm_conflicts:!!confirmConflicts})}).then(function(r){return r.json();}); + }).then(function(fieldRes){ + if(fieldRes.code===409&&fieldRes.data&&fieldRes.data.conflicts){if(saveBtn){saveBtn.disabled=false;saveBtn.textContent='💾 保存';}showCenteredConfirm('⚠️ 职责冲突确认',_permissionConflictMessage(fieldRes.data.conflicts)).then(function(ok){if(ok)submit(true);});return;} + if(fieldRes.code===0){alert('✅ 保存成功,并生成权限版本');document.querySelector('.modal-overlay')?.remove();loadPermissionsUsers();}else alert('❌ '+(fieldRes.msg||'字段权限保存失败')); + }).catch(function(e){alert('保存失败: '+e.message);}).finally(function(){if(saveBtn){saveBtn.disabled=false;saveBtn.textContent='💾 保存';}}); + } + showCenteredConfirm('个人权限变更预览',preview).then(function(confirmed){if(!confirmed)return;fetch('/api/permissions/conflicts/check',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({subject_type:'user',subject_id:String(userId),items:items,field_items:fieldItems})}).then(function(r){return r.json();}).then(function(res){var conflicts=res.code===0&&res.data?(res.data.conflicts||[]):[];if(conflicts.length)showCenteredConfirm('⚠️ 职责冲突确认',_permissionConflictMessage(conflicts)).then(function(ok){if(ok)submit(true);});else submit(false);}).catch(function(){submit(false);});}); +}; + +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:库存可支撑天数÷(货期+物流)', + '建议下单数量': '与(安全库存天数+货期+物流)比较总/云仓/外仓支撑天数:三仓都不足→日销量预测;云仓不足且外仓充足→云仓日销量预测;总仓不足且云仓充足、外仓不足→外仓日销量预测;总仓充足且云仓不足、外仓充足→云仓日销量预测;其余不计算;数量=所选预测×(安全库存天数+货期+物流)', + '实际下单数量': '建议下单数量向上取整到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}`; + }); + if (window._hasPagePerm('planning', 'detail') || window._hasPagePerm('planning', 'edit') || window._hasPagePerm('planning', 'delete')) 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) ? '-' : formatPrice4(val, true); + } + 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['下单备注'] || '' + }); + if (!window._planOrderDataCache) window._planOrderDataCache = {}; + window._planOrderDataCache[code] = orderData; + if (window._hasPagePerm('planning', 'detail') || window._hasPagePerm('planning', 'edit') || window._hasPagePerm('planning', 'delete')) { + colsHtml += ''; + colsHtml += '
'; + if (window._hasPagePerm('planning', 'detail')) colsHtml += ''; + if (window._hasPagePerm('planning', 'edit')) colsHtml += ''; + 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 linkedPlannerFilters = _getPlanLinkedFilters(field); + var plannerFilterParam = Object.keys(linkedPlannerFilters).length > 0 + ? '&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; + // 产品类型是数据库字段,必须从后端按其他已选条件计算全量联动选项。 + // 不能从当前页 tbody 提取:当前页只有 20/50 条,跨页的类型会被错误地隐藏。 + if (field === 'product_type') { + var productTypeMenu = _createFilterMenuShell(field, label, rect); + document.body.appendChild(productTypeMenu); + var linkedProductTypeFilters = _getPlanLinkedFilters(field); + var productTypeFilterParam = Object.keys(linkedProductTypeFilters).length > 0 + ? '&plan_filter=' + encodeURIComponent(JSON.stringify(linkedProductTypeFilters)) : ''; + var productTypeViewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : ''; + fetch('/api/planning/column-values?field=product_type' + productTypeViewParam + productTypeFilterParam) + .then(function(r) { return r.json(); }) + .then(function(res) { + var productTypeValues = (res.code === 0 && Array.isArray(res.data)) + ? res.data : (predefined || []); + _populateFilterMenuList(field, productTypeValues, false); + }) + .catch(function() { _populateFilterMenuList(field, predefined || [], false); }); + setupFilterMenuClose(productTypeMenu, th); + return; + } + // 其他预定义计算字段仍使用本地预定义/当前页逻辑。 + 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 || document.getElementById('planFollowerFilter')?.value || document.getElementById('planPlannerFilter')?.value) { + var linkedPlanFilters = _getPlanLinkedFilters(field); + if (Object.keys(linkedPlanFilters).length > 0) { + planFilterParam2 = '&plan_filter=' + encodeURIComponent(JSON.stringify(linkedPlanFilters)); + } + } + var hasLinkedPF = planFilterParam2 !== ''; + var cacheKey2 = 'plan:field:' + field + ':' + (planActiveView || 'default') + (hasLinkedPF ? ':filtered' : ''); + var cached2 = !hasLinkedPF && 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 (!hasLinkedPF) { + 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 _getPlanLinkedFilters(field) { + var linked = {}; + Object.keys(window._planFilters || {}).forEach(function(f) { + if (f !== field && window._planFilters[f]) { + linked[f] = Object.assign({}, window._planFilters[f]); + } + }); + var follower = document.getElementById('planFollowerFilter')?.value || ''; + var planner = document.getElementById('planPlannerFilter')?.value || ''; + if (field !== '采购跟单' && field !== 'procurement_follower' && follower) { + linked['采购跟单'] = {type: 'list', values: [follower]}; + } + if (field !== '采购计划' && field !== 'procurement_planner' && planner) { + linked['采购计划'] = {type: 'list', values: [planner]}; + } + return linked; +} + +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'; + // 导出当前 Tab 的字段设置:顺序与当前字段设置一致,只导出当前可见且有查看权限的字段。 + var visibleMap = (window._planViewVisibility && window._planViewVisibility[view]) || window.planColVisible || {}; + var order = window._planColDragOrder || PLAN_COLUMNS.map(function(c) { return c.field; }); + var byField = {}; + PLAN_COLUMNS.forEach(function(c) { byField[c.field] = c; }); + var columns = order.map(function(field) { return byField[field]; }).filter(function(c) { + return c && c.field !== 'bom' && visibleMap[c.field] !== false && window._canViewField('planning', c.label); + }).map(function(c) { return {field: c.field, label: c.label}; }); + // 兼容历史方案中未记录顺序的字段,补上当前字段设置中可见的新增字段。 + PLAN_COLUMNS.forEach(function(c) { + if (c.field !== 'bom' && visibleMap[c.field] !== false && window._canViewField('planning', c.label) && !columns.some(function(x) { return x.field === c.field; })) { + columns.push({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); + function applyNames(optionNames) { + sel.innerHTML = '' + + optionNames.map(function(n) { return ''; }).join(''); + sel.value = currentVal; + } + if (field === '采购跟单' || field === '采购计划') { + var linkedFilters = _getPlanLinkedFilters(field); + var linkedParam = Object.keys(linkedFilters).length > 0 + ? '&plan_filter=' + encodeURIComponent(JSON.stringify(linkedFilters)) : ''; + var viewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : ''; + fetch('/api/planning/column-values?field=' + encodeURIComponent(field) + viewParam + linkedParam) + .then(function(r) { return r.json(); }) + .then(function(res) { + var optionNames = res.code === 0 && res.data ? res.data : names; + if (field === '采购跟单') { + optionNames = normalizePlanPersonnelGroupValues(field, optionNames); + PLAN_FILTER_OPTIONS['采购跟单'] = optionNames.slice(); + } + applyNames(optionNames); + }) + .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:库存可支撑天数÷(货期+物流)','建议下单数量':'与(安全库存天数+货期+物流)比较总/云仓/外仓支撑天数:三仓都不足→日销量预测;云仓不足且外仓充足→云仓日销量预测;总仓不足且云仓充足、外仓不足→外仓日销量预测;总仓充足且云仓不足、外仓充足→云仓日销量预测;其余不计算;数量=所选预测×(安全库存天数+货期+物流)','实际下单数量':'建议下单数量向上取整到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 + ''; + }); + if (window._hasPagePerm('planning', 'detail') || window._hasPagePerm('planning', 'edit') || window._hasPagePerm('planning', 'delete')) 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 = ` + +
+ + ${window._canViewField('purchase','供应商名称') ? '' : ''} + + + + +
+
+
+

📋 待下单商品

+ + +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
序号产品类型 ▾货品编号 ▾货品名称 ▾库存支撑天数 ▾备货支撑天数 ▾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) || '-') + '' + + '' + + (window._hasPagePerm('purchase','detail') ? ' ' : '') + + (window._hasPagePerm('purchase','delete') ? '' : '') + + '' + + ''; + }).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)); + applyPendingColSettings(); + window._applyTableFieldPerms && window._applyTableFieldPerms('pendingTable', 'purchase'); + 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) { + if (field === 'index') return true; + if (field === 'action') return window._hasPagePerm('purchase_ready', 'detail') || window._hasPagePerm('purchase_ready', 'rollback') || window._hasPagePerm('purchase_ready', 'delete'); + return 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() : ''; + 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'); + var matName = ''; + var bomQty = 0; + var row = inp.closest('tr'); + if (row) { + var nameCell = row.querySelector('td:nth-child(2)'); + if (nameCell) matName = nameCell.textContent.trim(); + var qtyCell = row.querySelector('td:nth-child(3)'); + if (qtyCell) bomQty = parseFloat(qtyCell.textContent.trim()) || 0; + } + var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); + var pkgRemarkInput = document.querySelector('.bom-pkg-remark[data-material="' + matCode + '"]'); + var manuscriptSelect = document.querySelector('.bom-manuscript[data-material="' + matCode + '"]'); + var prePackInput = document.querySelector('.bom-pre-pack-qty[data-material="' + matCode + '"]'); + materials.push({ + material_code: matCode, + material_name: matName, + quantity: bomQty, + actual_quantity: qty, + remark: remarkInput ? remarkInput.value.trim() : '', + pkg_remark: pkgRemarkInput ? pkgRemarkInput.value.trim() : '', + change_manuscript: manuscriptSelect ? manuscriptSelect.value : '', + pre_pack_qty: prePackInput ? (parseInt(prePackInput.value) || 0) : 0, + post_pack_qty: 0 + }); + }); + 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; + var modal = modalSourceInput ? (modalSourceInput.closest('[style*="z-index:10000"]') || modalSourceInput.closest('[style*="z-index: 10000"]') || modalSourceInput.closest('[id^="bom-modal-"]')) : null; + var saveBtn = modal ? modal.querySelector('button.btn-primary') : null; + if (saveBtn) { + saveBtn.disabled = true; + saveBtn.textContent = '保存中...'; + } + + var updatePayload = {product_code: productCode, bom_items: materials, remark: productRemark}; + var pendingRequest = fetch('/api/purchase/update_pending', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(updatePayload) + }).then(function(r) { return r.json(); }); + var packagingRequest = nonProductMaterials.length > 0 ? fetch('/api/bom_packaging/save', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({product_code: productCode, materials: nonProductMaterials}) + }).then(function(r) { return r.json(); }) : Promise.resolve({code: 0}); + + Promise.all([pendingRequest, packagingRequest]).then(function(results) { + var pendingRes = results[0] || {}; + var packagingRes = results[1] || {}; + if (packagingRes.code !== 0) { + throw new Error(packagingRes.msg || '下单前包材保存失败'); + } + if (pendingRes.code !== 0) { + throw new Error(pendingRes.msg || '待下单明细同步失败'); + } + if (modal) modal.remove(); + var saveMessage = nonProductMaterials.length > 0 + ? '✅ 成品及包材下单信息已保存并同步到数据库' + : '✅ 成品下单信息已保存,无包材商品可直接下单成品'; + showCenteredAlert('保存成功', saveMessage); + }).catch(function(error) { + showCenteredAlert('保存失败', '❌ ' + (error.message || '保存失败,请重试')); + }).finally(function() { + if (saveBtn && document.body.contains(saveBtn)) { + saveBtn.disabled = false; + saveBtn.textContent = '💾 保存'; + } + }); +}; +// ─── 计划预测 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) { + var row = orderInput.closest('tr'); + if (row) { + var nameCell = row.querySelector('td:nth-child(2)'); + if (nameCell) m.material_name = nameCell.textContent.trim(); + // BOM用量取「用量」列,不能用实际下单数量覆盖 + var qtyCell = row.querySelector('td:nth-child(3)'); + if (qtyCell) m.quantity = parseFloat(qtyCell.textContent.trim()) || 0; + } + } + 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 = 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(); + // BOM用量取「用量」列 + var qtyCell = row.querySelector('td:nth-child(3)'); + if (qtyCell) bomQty = parseFloat(qtyCell.textContent.trim()) || 0; + } + } + // 读取备注 + 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 ? '📦 订单管理' : '📋 订单备份管理'}

+ +
+ 拖拽选择单元格,按 Ctrl+C 复制 + + + + + + + + + + ${!isOrder ? '' : ''} +
+
+
+
+ + + ${_backupHeaderHtml(isOrder)} + + +
+
+ +
+ `; +} + +function loadPurchaseReadyPage(container) { + container.innerHTML = ` +
+ + +
+
+
${_renderBackupPanelHtml('order')}
+ +
+ `; + enableTableCellSelection('orderTable'); + enableTableCellSelection('backupTable'); + 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; + // 回退接口需要与数据库 DATETIME 精确匹配;保留后端下发的完整时间值,显示时再截取。 + var orderedAtRaw = (item.ordered_at || '').toString(); + var orderedAt = orderedAtRaw.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: '' + formatPrice4(item['单价'], false) + '', + 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]; + }); + var backupButtons = ''; + if (window._hasPagePerm('purchase_ready', 'detail')) backupButtons += ''; + if (window._hasPagePerm('purchase_ready', 'rollback')) backupButtons += ''; + html += !showActions + ? '' + (item.status === 'rolled_back' ? '已回退' : '正常') + '' + : '
' + backupButtons + '
'; + 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); + clearCopyTableSelection(backupActiveTab + 'Table'); + // 字段显隐/顺序变化后重建 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], + ['单价', formatPrice4(item['单价'], false)], + ['下单人', 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(4) + '' + 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 !== null && d.cost_price !== undefined && d.cost_price !== '') fields.push(['成本价', formatPrice4(d.cost_price, true)]); + 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: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:'否'}, + {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, 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} + ]; + } 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, 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, 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:true}, + {label:'计划下单日期', field:'计划下单时间', editable:true, type:'date'}, + {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:'否'}, + {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, type:'date'}, + {label:'实际下单数量', field:"数量", editable:true}, + {label:'采购计划', field:'procurement_planner', editable:false}, + {label:'金蝶是否入库', field:'金蝶入库', editable:true, type:'select', options:['是','否'], defaultValue:'否'}, + {label:'旺店通单号', field:'旺店通生产单号', editable:true} + ]; + } + + var detailPermissionPage = orderType === 'packaging' ? 'order_packaging' : + (orderType === 'overseas' ? 'order_overseas' : + (orderType === 'domestic' ? 'order_domestic' : 'order_finished')); + var detailFieldPermission = function(cfg) { + var aliases = { + '供应商': '供应商', '供货商': '供货商', '成品供应商': '供应商', + '单价': '单价', '审核价格': '审核价格', '数量': '数量', '金额': '金额' + }; + return !window._canViewField || window._canViewField(detailPermissionPage, aliases[cfg.field] || cfg.label); + }; + var renderValue = function(cfg, val) { + if (val === null || val === undefined) val = ''; + var sv = String(val); + if (!cfg.editable || !window._hasPagePerm(detailPermissionPage, 'edit')) { + return '' + escHtml(sv || '-') + ''; + } + // 保存时要比较“用户看到的规范化值”,不能把后端的时间戳/默认空值误判为用户修改。 + // 例如计划下单时间返回“2026-06-24 18:17:19”,date 控件显示“2026-06-24”; + // 例如金蝶/旺店通字段为空时控件显示默认“否”。 + 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; + // 默认展示值不是用户修改:同步 data-orig,避免保存时把空值默认成“否”提交。 + origAttr = ' data-orig="' + escHtml(sv) + '"'; + } + 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]); + // type=date 只能保存 yyyy-mm-dd;用规范化后的日期作为原值,避免未编辑的时间字段被误提交。 + var dateOrigAttr = ' data-orig="' + escHtml(dateVal || '') + '"'; + return ''; + } + if (cfg.type === 'warehouse') { + return ''; + } + if (cfg.type === 'supplier') { + return ''; + } + if (cfg.type === 'pendingQty') { + return ''; + } + return ''; + }; + + var html = '
'; + html += '
'; + col1.forEach(function(cfg) { + if (!detailFieldPermission(cfg)) return; + var val = row[cfg.field]; + html += '
' + escHtml(cfg.label) + '
' + renderValue(cfg, val) + '
'; + }); + html += '
'; + html += '
'; + col2.forEach(function(cfg) { + if (!detailFieldPermission(cfg)) return; + 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 += '
'; + var showDefectiveQty = orderType === 'packaging'; + html += ''; + html += ''; + html += ''; + html += ''; + if (showDefectiveQty) html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + if (showDefectiveQty) html += ''; + html += ''; + html += ''; + html += '
批次编号到货日期到货数量不良品数量订单未完结数量备注
合计000
'; + } + 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 totalDefectiveQty = 0; + var showDefectiveQty = orderType === 'packaging'; + var batchCount = data.arrival_batches ? data.arrival_batches.length : 0; + // 数据行 + data.arrival_batches.forEach(function(b, idx) { + var qty = parseInt(b.arrival_qty) || 0; + var defectiveQty = parseInt(b.defective_qty) || 0; + totalQty += qty - defectiveQty; + totalDefectiveQty += defectiveQty; + var dateVal = (b.arrival_date || '').substring(0, 10); + var batchId = b.id || ''; + abHtml += ''; + abHtml += '' + (idx + 1) + ''; + abHtml += '' + + '' + + (showDefectiveQty ? '' : '') + + '0' + + '
' + + '' + + '' + + '
'; + abHtml += ''; + }); + // 补齐最少3行空白占位(可编辑) + var placeholderRows = Math.max(0, 3 - batchCount); + for (var i = 0; i < placeholderRows; i++) { + abHtml += ''; + abHtml += '' + (batchCount + i + 1) + ''; + abHtml += ''; + abHtml += ''; + if (showDefectiveQty) abHtml += ''; + abHtml += '-'; + abHtml += '
'; + abHtml += ''; + } + abBody.innerHTML = abHtml; + if (totalQtyEl) totalQtyEl.textContent = totalQty; + var defectiveTotalEl = overlay.querySelector('#arrivalDefectiveTotal'); + if (defectiveTotalEl) defectiveTotalEl.textContent = totalDefectiveQty; + 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 + '' + + '' + + '' + + (orderType === 'packaging' ? '' : '') + + '' + + '
' + + '' + + '' + + '' + + '
'; + // 追加到表格末尾(合计行上方) + 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 defectiveQty = parseInt(tr.querySelector('.batch-defective-qty')?.value) || 0; + var remark = tr.querySelector('.batch-remark')?.value || ''; + if (!date && !qty) { alert('请输入到货日期或数量'); return; } + if (defectiveQty < 0 || defectiveQty > 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, defective_qty: defectiveQty, 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 + '' + + '' + + '' + + (orderType === 'packaging' ? '' : '') + + '0' + + '
'; + 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 defectiveQty = parseInt(tr.querySelector('.batch-defective-qty')?.value) || 0; + var remark = tr.querySelector('.batch-remark')?.value || ''; + if (defectiveQty < 0 || defectiveQty > qty) { alert('不良品数量不能为负数或大于到货数量'); return; } + 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, defective_qty: defectiveQty, 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 df = tr.querySelector('.batch-defective-qty'); + var dr = tr.querySelector('.batch-remark'); + if (dd) dd.setAttribute('data-orig', dd.value || ''); + if (dq) dq.setAttribute('data-orig', dq.value || ''); + if (df) df.setAttribute('data-orig', df.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); + } 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 defectiveInput = tr.querySelector('.batch-defective-qty'); + var unFinishedTd = tr.querySelector('.batch-unfinished'); + if (qtyInput && unFinishedTd) { + var arrivalQty = parseInt(qtyInput.value) || 0; + var defectiveQty = parseInt(defectiveInput && defectiveInput.value) || 0; + cumulative += arrivalQty - defectiveQty; + var remaining = orderQty - cumulative; + if (qtyInput.getAttribute('placeholder') === '数量' && !qtyInput.value && (!defectiveInput || !defectiveInput.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' : ''; + } + // 到货数量合计按净到货计算:到货数量 - 不良品数量。 + var totalQtyEl = table.querySelector('#arrivalTotalQty'); + var defectiveTotalEl = table.querySelector('#arrivalDefectiveTotal'); + var sumQty = 0; + var sumDefectiveQty = 0; + body.querySelectorAll('tr').forEach(function(tr) { + var arrivalInput = tr.querySelector('.batch-qty'); + if (!arrivalInput) return; + var arrivalQty = parseInt(arrivalInput.value) || 0; + var defectiveInput = tr.querySelector('.batch-defective-qty'); + var defectiveQty = parseInt(defectiveInput && defectiveInput.value) || 0; + sumQty += arrivalQty - defectiveQty; + sumDefectiveQty += defectiveQty; + }); + if (totalQtyEl) totalQtyEl.textContent = sumQty; + if (defectiveTotalEl) defectiveTotalEl.textContent = sumDefectiveQty; +} + +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; + var saveErrorMessage = ''; + + // 只收集实际发生变化的字段(对比 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, defective_qty, remark} + var batchUpdates = []; // {id, arrival_date, arrival_qty, defective_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 f = parseInt(tr.querySelector('.batch-defective-qty')?.value) || 0; + var r = tr.querySelector('.batch-remark')?.value || ''; + if (d || q || f) { + batchAdds.push({arrival_date: d, arrival_qty: q, defective_qty: f, 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 f = tr.querySelector('.batch-defective-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 of = tr.querySelector('.batch-defective-qty')?.getAttribute('data-orig') || ''; + var or_ = tr.querySelector('.batch-remark')?.getAttribute('data-orig') || ''; + if (String(d) !== String(od) || String(q) !== String(oq) || String(f) !== String(of) || String(r) !== String(or_)) { + batchUpdates.push({id: bid, arrival_date: d, arrival_qty: parseInt(q) || 0, defective_qty: parseInt(f) || 0, remark: r}); + } + } + }); + + var invalidBatch = batchAdds.concat(batchUpdates).some(function(b) { + return b.defective_qty < 0 || b.defective_qty > b.arrival_qty; + }); + if (invalidBatch) { + showCenteredAlert('提示', '不良品数量不能为负数或大于到货数量'); + return; + } + + 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); + // 单字段沿用内联编辑接口;多字段优先走批量接口,失败时逐字段回退。 + if (changedKeys.length === 1) { + promises.push( + fetch(apiUrl, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({id: orderId, field: changedKeys[0], value: changedFields[changedKeys[0]]}) + }).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 || ('字段“' + changedKeys[0] + '”保存失败')); + }).catch(function(err) { + hasError = true; + saveErrorMessage = err.message || ''; + }) + ); + } else { + // 批量接口失败时逐字段回退到已有单字段接口,避免某个字段/日志链路异常导致整窗无法保存。 + // 回退操作幂等:批量接口即使已经部分写入,重复写入同样的值不会破坏数据。 + var saveFieldsIndividually = function() { + return changedKeys.reduce(function(chain, field) { + return chain.then(function() { + return fetch(apiUrl, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({id: orderId, field: field, value: changedFields[field]}) + }).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 || ('字段“' + field + '”保存失败')); + }); + }); + }, Promise.resolve()); + }; + var batchUrl = apiUrl.replace(/\/update$/, '/update_fields'); + promises.push( + fetch(batchUrl, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({id: orderId, product_code: productCode, fields: changedFields}) + }).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 || '批量保存失败'); + }).catch(function(err) { + return saveFieldsIndividually().catch(function(fallbackErr) { + hasError = true; + saveErrorMessage = fallbackErr.message || err.message || ''; + }); + }) + ); + } + } + + // 新增批次 + 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, defective_qty: b.defective_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, defective_qty: b.defective_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(saveErrorMessage ? ('部分字段保存失败:' + saveErrorMessage) : '部分字段保存失败,请重试'); + } 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(' '); +}; + +// 计划下单时间仅统一前端显示;不改变接口返回的原始值,确保回退订单仍按原值匹配。 +window._formatPlanOrderTime = function(value) { + var normalized = window._padDateStr(String(value || '').trim()); + if (!normalized) return ''; + var parts = normalized.split(' '); + var datePart = parts[0] || ''; + var timePart = parts[1] || ''; + if (!timePart) return datePart + ' 00:00'; + var timeParts = timePart.split(':'); + var hh = ('0' + (timeParts[0] || '0')).slice(-2); + var mm = ('0' + (timeParts[1] || '0')).slice(-2); + return datePart + ' ' + hh + ':' + mm; +}; + +function loadDomesticOrdersPage(container) { + container.innerHTML = ` + +
+
+
+

📦 国内采购下单明细

+
+ 拖拽选择单元格,按 Ctrl+C 复制 + + ${window._hasPagePerm('orders_domestic', 'batch_contract') ? `` : ''} + + + + +
+
+
+
+ + + + `; + // 动态生成表头列 + (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._formatPlanOrderTime(r['计划下单时间'] || ''); + 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]:not([data-field="action"])')).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 += ''; + if (canShowOrderActionColumn('domestic')) { + html += ''; + } + html += ''; + }); + tbody.innerHTML = html; + clearCopyTableSelection('domesticOrdersTable'); + } + window._applyTableFieldPerms && window._applyTableFieldPerms('domesticOrdersTable', 'orders_domestic'); + applyOrderActionState('domestic'); + // 先恢复保存的列宽,再计算冻结列位置(基于实际宽度) + restoreColumnWidths('domesticOrdersTable', 'domesticColWidths'); + applyDomesticFrozenPositions(); + enableColumnResize('domesticOrdersTable', 'domesticColWidths'); + enableTableCellSelection('domesticOrdersTable'); + 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 getOrderActionPermissionPage(type) { + return {domestic:'orders_domestic', overseas:'orders_overseas', packaging:'orders_packaging'}[type] || ''; +} +function canShowOrderActionColumn(type) { + var page = getOrderActionPermissionPage(type); + return !!page && (window._hasPagePerm(page, 'detail') || window._hasPagePerm(page, 'contract') || window._hasPagePerm(page, 'delete')); +} +function renderOrderActionHeader(type) { + if (!canShowOrderActionColumn(type)) return ''; + 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); + } + // 数据行是异步渲染的,显式恢复按钮容器显示状态,避免展开后仍保留 display:none。 + table.querySelectorAll('tbody .order-action-buttons').forEach(function(buttons) { + buttons.style.setProperty('display', collapsed ? 'none' : 'flex', 'important'); + }); + // 操作列折叠时只调整操作列自身和表格总宽度,其他列保持原宽度。 + 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 = ` + +
+
+
+

🌍 海外采购下单明细

+
+ 拖拽选择单元格,按 Ctrl+C 复制 + + ${window._hasPagePerm('orders_overseas', 'batch_contract') ? `` : ''} + + + + +
+
+
+
+
序号' + 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 + '
'; + if (window._hasPagePerm('orders_domestic', 'detail')) html += ''; + if (window._hasPagePerm('orders_domestic', 'contract')) html += ''; + if (window._hasPagePerm('orders_domestic', 'delete')) 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]:not([data-field="action"])')).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._formatPlanOrderTime(r['计划下单时间'] || ''); + 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 += ''; + if (canShowOrderActionColumn('overseas')) { + html += ''; + } + }); + tbody.innerHTML = html; + clearCopyTableSelection('overseasOrdersTable'); + } + window._applyTableFieldPerms && window._applyTableFieldPerms('overseasOrdersTable', 'orders_overseas'); + applyOrderActionState('overseas'); + _syncOrderFilterIcons('overseas'); + restoreColumnWidths('overseasOrdersTable', 'overseasColWidths'); + applyOverseasFrozenPositions(); + enableColumnResize('overseasOrdersTable', 'overseasColWidths'); + enableTableCellSelection('overseasOrdersTable'); + 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 = ` + +
+
+
+

📦 包材采购下单明细

+
+ 拖拽选择单元格,按 Ctrl+C 复制 + + ${window._hasPagePerm('orders_packaging', 'batch_contract') ? `` : ''} + + + + +
+
+
+
+
序号' + 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 + '
'; + if (window._hasPagePerm('orders_overseas', 'detail')) html += ''; + if (window._hasPagePerm('orders_overseas', 'contract')) 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 permissionLabel = fieldAttr === '合计' ? '金额' : (fieldAttr === '编码' ? '产品编码' : (fieldAttr === '名称' ? '货品名称' : col.label)); + if (!window._canViewField('orders_packaging', permissionLabel)) return; + 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:180px'; + else if (fieldAttr === '成品供应商') thStyle = 'min-width:180px'; + 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 = col.filterable === false ? '' : ' ▾'; + 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]:not([data-field="action"])')).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 if (field === '包材开发') { + cellHtml = ''; + } else { + cellHtml = ''; + } + html += cellHtml; + }); + if (canShowOrderActionColumn('packaging')) { + html += ''; + } + }); + tbody.innerHTML = html; + clearCopyTableSelection('packagingOrdersTable'); + } + window._applyTableFieldPerms && window._applyTableFieldPerms('packagingOrdersTable', 'orders_packaging'); + applyOrderActionState('packaging'); + _syncOrderFilterIcons('packaging'); + restoreColumnWidths('packagingOrdersTable', 'packagingColWidths'); + applyPackagingFrozenPositions(); + enableColumnResize('packagingOrdersTable', 'packagingColWidths'); + enableTableCellSelection('packagingOrdersTable'); + 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; +} + +// ─── 表格自由复制:拖拽选择任意连续单元格,Ctrl+C 复制到剪贴板 ─── +var copyTableConfigs = { + domesticOrdersTable: {tbodyId: 'domesticOrdersTbody', minCellIndex: 2, statusId: 'domesticCopyStatus'}, + overseasOrdersTable: {tbodyId: 'overseasOrdersTbody', minCellIndex: 2, statusId: 'overseasCopyStatus'}, + packagingOrdersTable: {tbodyId: 'packagingOrdersTbody', minCellIndex: 2, statusId: 'packagingCopyStatus'}, + orderTable: {tbodyId: 'orderTbody', minCellIndex: 1, statusId: 'orderCopyStatus'}, + backupTable: {tbodyId: 'backupTbody', minCellIndex: 1, statusId: 'backupCopyStatus'}, + finishedOrdersTable: {tbodyId: 'finishedOrdersTbody', minCellIndex: 1, statusId: 'finishedCopyStatus'} +}; +var copyTableSelections = {}; +var activeCopyTableId = null; + +function _copyTableConfig(tableId) { + return copyTableConfigs[tableId] || null; +} +function _copyTableState(tableId) { + if (!copyTableSelections[tableId]) { + copyTableSelections[tableId] = {anchor: null, focus: null, dragging: false}; + } + return copyTableSelections[tableId]; +} +function _copyTableSelectableCell(tableId, td) { + var config = _copyTableConfig(tableId); + if (!config || !td || td.tagName !== 'TD') return false; + var tr = td.parentElement; + return !!(tr && tr.parentElement && tr.parentElement.id === config.tbodyId && + td.cellIndex >= config.minCellIndex && + !td.classList.contains('order-action-cell') && + !td.classList.contains('backup-status-cell')); +} +function _copyTableCellPosition(tableId, td) { + var config = _copyTableConfig(tableId); + var table = document.getElementById(tableId); + if (!config || !table || !_copyTableSelectableCell(tableId, td)) return null; + var tbody = document.getElementById(config.tbodyId); + return {row: Array.prototype.indexOf.call(tbody.rows, td.parentElement), col: td.cellIndex}; +} +function _copyTableClearSelection(tableId, keepActive) { + var config = _copyTableConfig(tableId); + var table = document.getElementById(tableId); + var state = _copyTableState(tableId); + if (table) table.querySelectorAll('tbody td.copy-table-selected, tbody td.finished-copy-selected').forEach(function(td) { + td.classList.remove('copy-table-selected', 'finished-copy-selected'); + }); + state.anchor = null; + state.focus = null; + state.dragging = false; + if (!keepActive && activeCopyTableId === tableId) activeCopyTableId = null; + _copyTableUpdateStatus(tableId); +} +function clearCopyTableSelection(tableId) { + if (tableId) { + _copyTableClearSelection(tableId); + return; + } + Object.keys(copyTableConfigs).forEach(function(id) { _copyTableClearSelection(id, true); }); + activeCopyTableId = null; +} +function _copyTableSelectedMatrix(tableId) { + var config = _copyTableConfig(tableId); + var state = _copyTableState(tableId); + var tbody = document.getElementById(config ? config.tbodyId : ''); + if (!config || !tbody || !state.anchor || !state.focus) return []; + var rowStart = Math.min(state.anchor.row, state.focus.row); + var rowEnd = Math.max(state.anchor.row, state.focus.row); + var colStart = Math.min(state.anchor.col, state.focus.col); + var colEnd = Math.max(state.anchor.col, state.focus.col); + var matrix = []; + for (var r = rowStart; r <= rowEnd; r++) { + var tr = tbody.rows[r]; + if (!tr) continue; + var row = []; + for (var c = colStart; c <= colEnd; c++) { + var td = tr.cells[c]; + if (!_copyTableSelectableCell(tableId, td) || getComputedStyle(td).display === 'none') continue; + row.push((td.innerText || td.textContent || '').replace(/[\t\r\n]+/g, ' ').trim()); + } + if (row.length) matrix.push(row); + } + return matrix; +} +function _copyTableUpdateStatus(tableId) { + var config = _copyTableConfig(tableId); + var status = config && document.getElementById(config.statusId); + if (!status) return; + var matrix = _copyTableSelectedMatrix(tableId); + if (!matrix.length) { + status.textContent = '拖拽选择单元格,按 Ctrl+C 复制'; + return; + } + var cols = matrix.reduce(function(max, row) { return Math.max(max, row.length); }, 0); + status.textContent = '已选择 ' + matrix.length + ' 行 × ' + cols + ' 列'; +} +function _copyTableRenderSelection(tableId) { + var config = _copyTableConfig(tableId); + var table = document.getElementById(tableId); + var state = _copyTableState(tableId); + if (!config || !table || !state.anchor || !state.focus) return; + table.querySelectorAll('tbody td.copy-table-selected, tbody td.finished-copy-selected').forEach(function(td) { + td.classList.remove('copy-table-selected', 'finished-copy-selected'); + }); + var rowStart = Math.min(state.anchor.row, state.focus.row); + var rowEnd = Math.max(state.anchor.row, state.focus.row); + var colStart = Math.min(state.anchor.col, state.focus.col); + var colEnd = Math.max(state.anchor.col, state.focus.col); + for (var r = rowStart; r <= rowEnd; r++) { + var tr = document.getElementById(config.tbodyId).rows[r]; + if (!tr) continue; + for (var c = colStart; c <= colEnd; c++) { + var td = tr.cells[c]; + if (_copyTableSelectableCell(tableId, td) && getComputedStyle(td).display !== 'none') { + td.classList.add('copy-table-selected'); + } + } + } + _copyTableUpdateStatus(tableId); +} +function _copyTableText(tableId) { + return _copyTableSelectedMatrix(tableId).map(function(row) { return row.join('\t'); }).join('\n'); +} +function _copyTableFallback(text, done) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + try { document.execCommand('copy'); done(); } + catch (e) { showCenteredAlert('复制失败', '请按 Ctrl+C 重试'); } + document.body.removeChild(ta); +} +function copyTableCellSelection(tableId) { + tableId = tableId || activeCopyTableId; + var text = tableId ? _copyTableText(tableId) : ''; + if (!text) { + showCenteredAlert('提示', '请先在表格中拖拽选择要复制的单元格'); + return; + } + var done = function() { showCenteredAlert('已复制', '选中内容已复制,可直接粘贴到 Excel'); }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(done).catch(function() { _copyTableFallback(text, done); }); + } else { + _copyTableFallback(text, done); + } +} +window.copyTableCellSelection = copyTableCellSelection; +window.clearCopyTableSelection = clearCopyTableSelection; + +function enableTableCellSelection(tableId) { + var table = document.getElementById(tableId); + if (!table || table.dataset.copySelectionReady === '1') return; + if (!_copyTableConfig(tableId)) return; + table.classList.add('copy-selectable-table'); + if (!table.hasAttribute('tabindex')) table.setAttribute('tabindex', '-1'); + table.dataset.copySelectionReady = '1'; + table.addEventListener('mousedown', function(e) { + if (e.button !== 0 || e.target.closest('button,input,select,textarea,a,.fn-filt-icon,.order-filt-icon,code')) return; + var td = e.target.closest('td'); + var pos = _copyTableCellPosition(tableId, td); + if (!pos) return; + clearCopyTableSelection(); + var state = _copyTableState(tableId); + state.anchor = pos; + state.focus = pos; + state.dragging = true; + activeCopyTableId = tableId; + e.preventDefault(); + try { table.focus({preventScroll: true}); } catch (_) { table.focus(); } + _copyTableRenderSelection(tableId); + }); + table.addEventListener('mouseover', function(e) { + var state = _copyTableState(tableId); + if (!state.dragging || activeCopyTableId !== tableId) return; + var pos = _copyTableCellPosition(tableId, e.target.closest('td')); + if (!pos) return; + state.focus = pos; + _copyTableRenderSelection(tableId); + }); +} +function _installGlobalCopyTableHandlers() { + if (window._copyTableGlobalHandlersInstalled) return; + window._copyTableGlobalHandlersInstalled = true; + document.addEventListener('mouseup', function() { + if (activeCopyTableId) _copyTableState(activeCopyTableId).dragging = false; + }); + document.addEventListener('copy', function(e) { + if (!activeCopyTableId || !e.clipboardData) return; + var active = document.activeElement; + if (active && (active.matches('input,select,textarea') || active.isContentEditable)) return; + var text = _copyTableText(activeCopyTableId); + if (!text) return; + e.preventDefault(); + e.clipboardData.setData('text/plain', text); + _copyTableUpdateStatus(activeCopyTableId); + }); + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && activeCopyTableId) clearCopyTableSelection(); + }); +} +_installGlobalCopyTableHandlers(); + +// 兼容成品提货页原有按钮和调用名。 +function copyFinishedCellSelection() { copyTableCellSelection('finishedOrdersTable'); } +window.copyFinishedCellSelection = copyFinishedCellSelection; +function enableFinishedCellSelection() { enableTableCellSelection('finishedOrdersTable'); } + +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 = ` + +
+
+

📋 成品提货明细

拖拽选择单元格,按 Ctrl+C 复制
+
+
+
序号' + col.label + filterControl + '
加载中...
暂无数据
' + rowNum + '' + escHtml(window._formatPlanOrderTime(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['是否已做入库'] || '') + '' + 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] || '') + '
'; + if (window._hasPagePerm('orders_packaging', 'detail')) html += ''; + if (window._hasPagePerm('orders_packaging', 'contract')) 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'); + enableFinishedCellSelection(); + 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 += '
'; + if (window._hasPagePerm('orders_finished', 'detail')) html += ' '; + if (window._hasPagePerm('orders_finished', 'delete')) html += ''; + html += '
'; + }); + tbody.innerHTML = html; + } + clearCopyTableSelection('finishedOrdersTable'); + 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' && field === '供应商') || + (type === 'packaging' && (field === '供应商' || 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}, + {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 = formatPrice4(totalMatCost, true); + 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 = formatPrice4(r.material_cost_price, true); + 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 = formatPriceInput4(data.material_cost !== undefined ? data.material_cost : data.material_cost_price); + 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 = formatPriceInput4(data.cost_price); + 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 formatPrice4(val, true); + 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 permissionField = POPUP_FIELD_LABELS[field] || field; + if (!window._canViewField('planning', permissionField)) continue; + 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 permissionField = POPUP_FIELD_LABELS[field] || field; + if (!window._canViewField('planning', permissionField)) continue; + 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; + var bomCanSupplier = window._canViewField('bom', '供应商'); + var bomCanCost = window._canViewField('bom', '成本价'); + bomHtml = bomMaterials.map(function(m) { + return '' + + '' + escHtml(m.material_code) + '' + + '' + escHtml(m.material_name) + '' + + '' + (bomCanSupplier ? escHtml(m.material_supplier || '-') : '-') + '' + + '' + m.available_qty.toLocaleString() + '' + + '' + m.production_in_transit.toLocaleString() + '' + + '' + m.purchase_in_transit.toLocaleString() + '' + + '' + (bomCanCost ? 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 permissionField = POPUP_FIELD_LABELS[field] || field; + if (!window._canViewField('planning', permissionField)) continue; + 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) { + var bomCanSupplier = window._canViewField('bom', '供应商'); + var bomCanCost = window._canViewField('bom', '成本价'); + bomHtml = bomMaterials.map(function(m) { + return '' + + '' + escHtml(m.material_code) + '' + + '' + escHtml(m.material_name) + '' + + '' + (bomCanSupplier ? escHtml(m.material_supplier || '-') : '-') + '' + + '' + m.available_qty.toLocaleString() + '' + + '' + m.production_in_transit.toLocaleString() + '' + + '' + m.purchase_in_transit.toLocaleString() + '' + + '' + (bomCanCost ? 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:formatPrice4(row.cost_price, true)}, + {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; }); + fields = fields.filter(function(f) { return window._canViewField('products', f.label); }); + + 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:formatPriceInput4(row.cost_price), type:'number', step:'0.0001'}, + {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; }); + fields = fields.filter(function(f) { + var label = f.key === 'supplier_code' ? '供应商编码' : (f.key === 'supplier_name_match' ? '供应商名称' : f.label); + return window._canViewField('products', label); + }); + 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 { + var stepAttr = f.step ? ' step="' + f.step + '"' : ''; + 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') { + var numberVal = f.field === 'cost_price' ? formatPriceInput4(val) : (val || 0); + 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; + var fullBomCanCost = window._canViewField('bom', '成本价'); + 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() + '' + + '' + (fullBomCanCost ? '¥' + m.material_cost.toFixed(4) : '-') + '' + + '' + (fullBomCanCost ? '¥' + 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() + '' + + '' + + '' + (fullBomCanCost ? '¥' + 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('❌ 网络错误'); }); +}; diff --git a/tests/test_planning_dual_filter.py b/tests/test_planning_dual_filter.py new file mode 100644 index 0000000..8e538b4 --- /dev/null +++ b/tests/test_planning_dual_filter.py @@ -0,0 +1,175 @@ +"""计划预测“产品类型 + 跟单小组”组合筛选回归测试。""" + +import json +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +APP_JS = (ROOT / "static" / "js" / "app.js").read_text(encoding="utf-8") + + +class PlanningDualFilterFrontendTest(unittest.TestCase): + def test_product_type_options_come_from_linked_backend_query(self): + self.assertIn("if (field === 'product_type')", APP_JS) + self.assertIn( + "fetch('/api/planning/column-values?field=product_type'", + APP_JS, + ) + self.assertIn("var linkedProductTypeFilters = _getPlanLinkedFilters(field);", APP_JS) + + def test_follower_dropdown_comes_from_all_linked_rows(self): + self.assertIn( + "if (field === '采购跟单' || field === '采购计划')", + APP_JS, + ) + self.assertIn( + "fetch('/api/planning/column-values?field=' + encodeURIComponent(field)", + APP_JS, + ) + + def test_top_filters_are_included_in_linked_filter_state(self): + self.assertIn("function _getPlanLinkedFilters(field)", APP_JS) + self.assertIn("linked['采购跟单'] = {type: 'list', values: [follower]};", APP_JS) + + +class PlanningDualFilterApiTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # test_wsgi supplies the isolated local-test database credentials. + import test_wsgi # noqa: F401 + from app import app + from routes.planning import _personnel_group_for_value, _personnel_group_members + from services.db import query + + cls.query = staticmethod(query) + cls.group_for_value = staticmethod(_personnel_group_for_value) + cls.groups = _personnel_group_members() + cls.app = app + cls.app.config.update(TESTING=True) + cls.user_id = query( + "SELECT id FROM dashboard_users WHERE status=1 ORDER BY id LIMIT 1" + )[0]["id"] + + intersections = {} + rows = query( + """ + SELECT product_type, procurement_follower + FROM products + WHERE deleted_at IS NULL + AND product_type NOT IN ('包材', '其他') + AND product_code NOT IN (SELECT product_code FROM plan_hidden) + AND procurement_follower IS NOT NULL + AND procurement_follower != '' + """ + ) + for row in rows: + group = cls.group_for_value(row["procurement_follower"], cls.groups) + if group: + key = (row["product_type"], group) + intersections[key] = intersections.get(key, 0) + 1 + if not intersections: + raise unittest.SkipTest("测试库没有产品类型与跟单小组的交集数据") + cls.product_type, cls.group = max(intersections, key=intersections.get) + + def setUp(self): + self.client = self.app.test_client() + with self.client.session_transaction() as session: + session.update( + user_id=self.user_id, + username="planning-filter-test", + real_name="planning-filter-test", + role="super_admin", + must_change_password=False, + ) + + def _planning_list(self, filters=None, follower=""): + query_string = {"page": 1, "size": 100} + if filters: + query_string.update( + custom_field="__multi__", + custom_value="__multi__:" + json.dumps(filters, ensure_ascii=False), + ) + if follower: + query_string["procurement_follower"] = follower + response = self.client.get("/api/planning/list", query_string=query_string) + self.assertEqual(response.status_code, 200) + body = response.get_json() + self.assertEqual(body["code"], 0) + return body["data"] + + def test_header_filters_return_exact_intersection(self): + type_filter = { + "field": "product_type", + "type": "list", + "values": [self.product_type], + } + group_filter = { + "field": "采购跟单", + "type": "list", + "values": [self.group], + } + by_type = self._planning_list([type_filter]) + by_group = self._planning_list([group_filter]) + combined = self._planning_list([type_filter, group_filter]) + + expected = set(by_type["all_codes"]) & set(by_group["all_codes"]) + self.assertEqual(set(combined["all_codes"]), expected) + self.assertEqual(combined["total"], len(expected)) + + def test_top_follower_and_header_product_type_can_be_combined(self): + type_filter = { + "field": "product_type", + "type": "list", + "values": [self.product_type], + } + header_combined = self._planning_list( + [ + type_filter, + {"field": "采购跟单", "type": "list", "values": [self.group]}, + ] + ) + top_combined = self._planning_list([type_filter], follower=self.group) + self.assertEqual( + set(top_combined["all_codes"]), + set(header_combined["all_codes"]), + ) + + def test_linked_options_keep_the_real_intersection(self): + group_response = self.client.get( + "/api/planning/column-values", + query_string={ + "field": "采购跟单", + "plan_filter": json.dumps( + { + "product_type": { + "type": "list", + "values": [self.product_type], + } + }, + ensure_ascii=False, + ), + }, + ).get_json() + type_response = self.client.get( + "/api/planning/column-values", + query_string={ + "field": "product_type", + "plan_filter": json.dumps( + {"采购跟单": {"type": "list", "values": [self.group]}}, + ensure_ascii=False, + ), + }, + ).get_json() + self.assertEqual(group_response["code"], 0) + self.assertEqual(type_response["code"], 0) + self.assertIn(self.product_type, type_response["data"]) + normalized_groups = { + self.group_for_value(value, self.groups) + for value in group_response["data"] + } + self.assertIn(self.group, normalized_groups) + + +if __name__ == "__main__": + unittest.main()