// 蝶雅采购智能体平台 - 前端逻辑 let charts = {}; // 页面缓存:切换菜单时不销毁页面状态 var CACHE_VERSION = 73; 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, '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 }; // ─── 知识库页面 ─── // ─── 知识库页面 ─── function loadKnowledgePage(container) { renderKnowledgePage(container); } 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; if (typeof marked !== 'undefined') { contentDiv.innerHTML = '
' + marked.parse(data.content) + '
'; } else { contentDiv.innerHTML = '
' + escHtml(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 += '
'; if (typeof marked !== 'undefined') { html += marked.parse(content); } else { html += '
' + escHtml(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 += '
'; if (typeof marked !== 'undefined') { html += marked.parse(data.profile); } else { html += '
' + escHtml(data.profile.substring(0, 500)) + '
'; } 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') { ['prodCode','prodName','prodSupplier','prodTypeFilter'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); if (typeof loadProducts === 'function') loadProducts(1); } if (page === 'suppliers') { ['supCode','supName'].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); }); } } 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', '/products': 'products', '/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', '/users': 'users', '/login-records': 'login_records', '/logs': 'logs', '/config': 'config', '/permissions': 'permissions', '/knowledge': 'knowledge', '/feedback': 'feedback', '/contract-templates': 'contract_templates' }; // 拦截导航点击,启用 SPA 缓存 // ─── 权限工具函数 ─── // 检查当前用户是否有页面查看权限 window._canViewPage = function(pageId) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || {}; if (pagePerms['view'] !== undefined) return pagePerms['view'] == 1; return true; }; // 检查当前用户是否有某个字段的查看权限 window._canViewField = function(pageId, fieldName) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; if (!window._canViewPage(pageId)) return false; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || {}; var key = 'field:' + fieldName + ':view'; if (pagePerms[key] !== undefined) return pagePerms[key] == 1; return pagePerms['view'] !== 0; }; // 检查当前用户是否有某个字段的编辑权限 window._canEditField = function(pageId, fieldName) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; if (!window._canViewPage(pageId)) return false; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || {}; var key = 'field:' + fieldName + ':edit'; if (pagePerms[key] !== undefined) return pagePerms[key] == 1; return pagePerms['edit'] !== 0; }; // 检查当前用户是否有页面功能权限 window._hasPagePerm = function(pageId, op) { if (!window._currentUser) return true; if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true; if (!window._canViewPage(pageId)) return false; var perms = window._currentUser.permissions || {}; var pagePerms = perms[pageId] || {}; if (pagePerms[op] !== undefined) return pagePerms[op] == 1; return false; // 没设时无权限(由角色级权限兜底) }; // 隐藏无权限的菜单 window._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:'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-sub-item[href="/planning"]', 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:'feedback', sel:'a.nav-item[href="/feedback"]', 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-item[href="/contract-templates"]', sub:false}, {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}, {page:'config_calibrate', sel:'a.nav-sub-item[href="/config-calibrate"]', 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); document.addEventListener('click', function(e) { var link = e.target.closest('.nav-item, .nav-sub-item'); if (!link) return; var page = link.getAttribute('data-page'); if (!page) return; e.preventDefault(); var href = link.getAttribute('href') || '/'; history.pushState({page: page}, '', href); loadPage(page); }); // 浏览器后退/前进 window.addEventListener('popstate', function(e) { var state = e.state; if (state && state.page) { loadPage(state.page); } else { // 首次加载或状态丢失,从当前 URL 推断 var path = window.location.pathname; var p = PAGE_MAP[path]; if (p) loadPage(p); } }); } // 延迟初始化,确保 DOM 加载完成后再绑定事件 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initNavigation); } else { initNavigation(); } // ─── 工具函数 ─── function escJs(str) { return String(str).replace(/'/g, "\\'").replace(/\\/g, '\\\\').replace(/"/g, '"'); } function escHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function 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) { if (!confirm('确认将此反馈标记为已完成?')) 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) { if (!confirm('确认重新打开此反馈?')) 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) { if (!confirm('确认删除此反馈?删除后不可恢复。')) 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); }); } // ─── 合同模板 ─── function loadContractTemplatesPage(container) { container.innerHTML = `
`; loadCtTemplates(); } function loadCtTemplates() { fetch('/api/contract/templates') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var list = document.getElementById('ctList'); if (!res.data || res.data.length === 0) { list.innerHTML = '
暂无模板,请点击右上角上传
'; return; } var html = ''; res.data.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); // 供应商搜索绑定 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) { if (!confirm('确认删除此模板?')) 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) { // 在新标签页中打开预览页面 window.open('/api/contract/template/preview_page?id=' + id, '_blank'); } 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(); }); } function loadDashboard(container) { container.innerHTML = `
`; fetch('/api/dashboard/stats') .then(r => r.json()) .then(res => { if (res.code !== 0) return; // 缓存数据供图表重建 if (!window._pageDataCache) window._pageDataCache = {}; if (!window._pageReloaders) window._pageReloaders = {}; window._pageDataCache['dashboard'] = res.data; window._pageReloaders['dashboard'] = function(data) { renderDashboardCharts(data); }; renderDashboardCharts(res.data); }); } function renderDashboardCharts(d) { const el = document.getElementById('dashContent'); if (!el) return; // 顶部统计卡片 var isAdmin = window._currentRole === 'admin' || window._currentRole === 'super_admin'; var statItems = [ {label:'SKU总数', value:(d.total_products || 0).toLocaleString(), sub:'', icon:'📦', color:'#1890ff'}, {label:'SPU总数', value:(d.spu_total || 0).toLocaleString(), sub:'', icon:'📦', color:'#2f54eb'}, {label:'库存成本', value:'¥' + Math.round(d.inventory_cost || 0).toLocaleString(), sub:'', icon:'💎', color:'#eb2f96'}, {label:'总供应商', value:d.total_suppliers || 0, sub:'家', icon:'🤝', color:'#13c2c2'}, {label:'低库存预警', value:d.low_stock_count || 0, sub:'', icon:'⚠️', color:'#f5222d'} ]; var cols = Math.min(statItems.length, 6); let cards = '
'; statItems.forEach(function(s) { cards += '
'; cards += '
' + s.icon + '
'; cards += '
'; cards += '
' + s.value + '
'; cards += '
' + s.label + (s.sub ? ' ' + s.sub + '' : '') + '
'; cards += '
'; }); cards += '
'; // 第一行图表:日出库趋势 + 品牌占比 var row1 = '
'; row1 += '

📈 日出库趋势

'; row1 += '

🥧 品牌出库占比 TOP8

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

🏪 店铺出库排行

'; row2 += '

🏷️ 品牌出库排行 TOP10

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

📈 日出库趋势

📊 数据汇总

序号日期货品编号货品名称品牌总出库量总成本
加载中...
`; loadSummaryData(1); } let sumCurrentPage = 1; let SUM_PAGE_SIZE = 20; function loadSummaryData(page) { sumCurrentPage = page; var search = document.getElementById('sumSearch')?.value || ''; var start = document.getElementById('sumDateStart')?.value || ''; var end = document.getElementById('sumDateEnd')?.value || ''; // 加载指标卡和趋势图 loadSummaryStats(search); // 加载明细表格 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) .then(r => r.json()) .then(res => { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('sumTbody'); 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(''); } document.getElementById('sumCount').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; // 当前钻取的月份 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; 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 (details.length === 0) { tbody.innerHTML = '暂无明细'; } else { var SOURCE_MAP = {'wdt':'旺店通','jdbz':'京东泊紫','jdgj':'京东国际','jdjwt':'京东洁维塔','tmgj':'天猫国际'}; 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() { document.getElementById('sumSearch').value = ''; document.getElementById('sumDateStart').value = ''; document.getElementById('sumDateEnd').value = ''; loadSummaryData(1); }; function loadDataPage(container) { container.innerHTML = `

出库列表

序号日期货品编号货品名称品牌货品数量成本价金额仓库店铺数据来源
加载中...
`; loadDataPageData(1); // 加载筛选选项 fetch('/api/data/dates') .then(r => r.json()) .then(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 += ''; }); } if (d.warehouses) { window.dataWarehouseOptions = d.warehouses; } if (d.shops) { window.dataShopOptions = d.shops; } if (d.brands) { window.dataBrandOptions = d.brands; } }); } const SOURCE_MAP_CN = { 'wdt': '旺店通', 'jdbz': '京东泊紫', 'jdgj': '京东国际', 'jdjwt': '京东洁维塔', 'tmgj': '天猫国际' }; 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 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 (!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(''); } document.getElementById('dataCount').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); }; // ─── 产品基础表 ─── // 所有产品列定义 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}, ]; // 字段显隐状态 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(); // 清除筛选框搜索记忆 ['prodCode','prodName','prodSupplier','prodTypeFilter'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); // 加载 prodActiveTab = '成品'; updateProdTypeFilterOptions('成品'); applyProdTabColumns('成品'); loadProducts(1); } function applyProdTabColumns(tab) { // 包材基础信息表:跳过不需要的列 // 成品基础信息表:恢复所有列显示 var hideFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer']; hideFields.forEach(function(f) { 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 === '成品' ? '成品列表' : '包材列表'; // 切页清空筛选框,互不影响 ['prodCode','prodName','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 { opts = ''; } sel.innerHTML = opts; } function buildProdThead() { var thead = document.getElementById('prodThead'); if (!thead) return; var theadHtml = '序号'; PROD_COLUMNS.forEach(function(c) { var vis = window.prodColVisible[c.field] ? '' : ' style="display:none"'; var sortAttr = c.sortable ? ' data-sort="' + c.field + '" class="sortable"' : ''; theadHtml += '' + c.label + ''; }); theadHtml += '操作'; theadHtml += ''; thead.innerHTML = theadHtml; } function loadProducts(page) { prodCurrentPage = page; var code = (document.getElementById('prodCode')?.value || '').trim(); var name = (document.getElementById('prodName')?.value || '').trim(); var supplier = (document.getElementById('prodSupplier')?.value || '').trim(); var typeFilter = document.getElementById('prodTypeFilter')?.value || ''; var url = '/api/products/list?page=' + page + '&size=' + PROD_PAGE_SIZE; if (code) url += '&prod_code=' + encodeURIComponent(code); if (name) url += '&prod_name=' + encodeURIComponent(name); 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'); // 缓存当前页数据供快速编辑 if (!window._prodRowCache) window._prodRowCache = {}; fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('prodTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无数据'; } else { var startIdx = (page - 1) * PROD_PAGE_SIZE; // 缓存行数据 data.rows.forEach(function(r) { if (r.product_code) window._prodRowCache[r.product_code] = r; }); tbody.innerHTML = data.rows.map(function(r, i) { var html = '' + (startIdx + i + 1) + ''; PROD_COLUMNS.forEach(function(c) { if (!window.prodColVisible[c.field]) return; var raw = r[c.field]; var val = escHtml(raw || '-'); if (c.field === 'product_code') { val = '' + val + ''; } if (c.field === 'product_type') { var typeColors = {'成品':'tag-blue','包材':'tag-orange','赠品':'tag-green','小样':'tag-purple','物料':'tag-orange'}; var cls = typeColors[raw] || 'tag-grey'; val = '' + val + ''; } if (c.field === 'cost_price') { var p = parseFloat(raw); val = isNaN(p) ? '-' : '¥' + p.toFixed(2); } if (c.field === 'min_order_qty') { var q = parseInt(raw); val = isNaN(q) ? '-' : q.toLocaleString(); } if (c.field === 'created_at') { val = escHtml((raw || '').toString().slice(0, 10) || '-'); } html += '' + val + ''; }); var pcode = r.product_code || ''; html += ''; html += ' '; html += ' '; if (prodActiveTab === '成品') 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.searchProducts = function() { loadProducts(1); }; window.resetProductFilters = function() { document.getElementById('prodCode').value = ''; document.getElementById('prodName').value = ''; document.getElementById('prodSupplier').value = ''; var tf = document.getElementById('prodTypeFilter'); if (tf) tf.value = ''; loadProducts(1); }; // ─── 新增产品 ─── 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(); } else { modal.style.display = 'flex'; // 清空表单 modal.querySelectorAll('input, select').forEach(function(el) { el.value = ''; }); updateAddTypeOptions(); updateAddFieldsVisibility(); } } 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'; } 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 }; 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; } if (!data.procurement_follower) { alert('采购跟单不能为空'); return; } 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) { if (!confirm('确认将 [' + productCode + '] 的包材数据同步到物料清单管理吗?')) 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 && !confirm('有未保存的物料信息,确定跳过吗?')) return; 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(',')); window.open(url, '_blank'); }; // ─── 产品导入 ─── 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 dp = w.disabled || '否'; // 仓库属性:选中项显示为标签 var wpLabels = wp ? wp.split(',').map(function(p) { return '' + escHtml(p.trim()) + ''; }).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 = '⚠️ 网络错误'; }); } // 仓库属性复选框变更 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(','); 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) { // 视觉反馈 } else { alert('保存失败: ' + (res.msg || '')); } }).catch(function() {}); }; // 停用下拉变更 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 dpSelect = row.querySelector('select'); var disabledVal = dpSelect ? dpSelect.value : '否'; var remark = cells[7] ? cells[7].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); } else { msgEl.innerHTML = '⚠️ ' + (res.msg || '保存失败') + ''; } }) .catch(function() { msgEl.innerHTML = '⚠️ 网络错误'; }); }; window.deleteWarehouse = function(code) { if (!confirm('确认删除仓库 ' + code + ' 吗?')) 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}明细

${_invProductType === '成品' ? '' : ''}
序号货品编号货品名称品牌仓库可用库存生产在途采购在途采购跟单采购计划供应商库存日期操作
`; // 并行加载:筛选选项 + 表格数据,互不等待 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); } 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 || ''; let url = '/api/inventory/list?page=' + page + '&size=' + INV_PAGE_SIZE; if (date) url += '&date=' + encodeURIComponent(date); if (code) url += '&prod_code=' + encodeURIComponent(code); if (name) url += '&prod_name=' + encodeURIComponent(name); 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'); if (!data.rows || data.rows.length === 0) { var cols = _invProductType === '成品' ? 14 : 13; 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 || '-') + '' : ''; 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) + '' + renderNameBadges(r['采购跟单']) + '' + renderNameBadges(r['采购计划']) + '' + escHtml(r['供应商'] || '-') + '' + (r.data_date || '-') + ''; }).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; }); 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 = []; document.querySelectorAll('.inv-row-cb:checked').forEach(function(cb) { checked.push(cb.value); }); if (checked.length === 0) { alert('请先勾选要导出的数据'); return; } var fmt = document.getElementById('invExportFormat')?.value || 'xlsx'; // 先弹出字段选择 var curFields = window._invExportSelectedFields || INV_EXPORT_FIELDS.filter(function(f) { return f.def; }).map(function(f) { return f.key; }); var html = '
'; html += '

选择导出字段

'; html += '

已选 ' + checked.length + ' 条数据,格式:' + fmt.toUpperCase() + '

'; 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); }); if (selFields.length === 0) { alert('请至少选择一个字段'); return; } window._invExportSelectedFields = selFields; document.getElementById('invExportModal').remove(); // 执行导出 var date = document.getElementById('invDate')?.value || ''; var type = window._invProductType || ''; fetch('/api/inventory/export', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: checked, format: fmt, date: date, 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 = `

供应商列表

序号供应商编码供应商名称做货类型简称合同简称供应商分组审核人审核日期数据状态禁用状态操作
`; // 清除筛选框搜索记忆 ['supCode','supName'].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 code = (document.getElementById('supCode')?.value || '').trim(); const name = (document.getElementById('supName')?.value || '').trim(); let url = '/api/suppliers/list?page=' + page + '&size=' + SUP_PAGE_SIZE; if (code) url += '&code=' + encodeURIComponent(code); if (name) url += '&name=' + encodeURIComponent(name); 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.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('supCode').value = ''; document.getElementById('supName').value = ''; loadSuppliers(1); }; // ─── 供应商导出 ─── function exportSuppliers() { const code = (document.getElementById('supCode')?.value || '').trim(); const name = (document.getElementById('supName')?.value || '').trim(); let url = '/api/suppliers/export'; if (code) url += '?code=' + encodeURIComponent(code); if (name) url += (code ? '&' : '?') + 'name=' + encodeURIComponent(name); window.open(url, '_blank'); } // ─── 供应商导入 ─── 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) { // ─── 采购跟单分组数据(从全局映射读取) ─── var followGroups = window._personnelGroups.filter(function(g) { return g.name !== '计划小组'; }); // ─── 采购计划分组数据 ─── var planGroups = window._personnelGroups.filter(function(g) { return g.name === '计划小组'; }); var cardsHtml = [ '
', '
', '👥', '

采购跟单

', '' + (function(){ var total = 0; followGroups.forEach(function(g){ total += g.members.length; }); return '共 ' + total + ' 人'; })() + '', '
', '
', renderPersonnelCards(followGroups), '
', '
', '
', '
', '📋', '

采购计划

', '' + (function(){ var total = 0; planGroups.forEach(function(g){ total += g.members.length; }); return '共 ' + total + ' 人'; })() + '', '
', '
', renderPersonnelCards(planGroups), '
', '
' ].join(''); container.innerHTML = [ '', '
', '', cardsHtml, '
' ].join(''); } function renderPersonnelCards(groups) { var html = ''; groups.forEach(function(g) { html += '
'; html += '
'; html += '' + (g.icon || '👤') + ''; html += '' + escHtml(g.name) + ''; html += '' + g.members.length + '人'; html += '
'; html += '
'; g.members.forEach(function(m) { var isLeader = m === g.leader && g.leader !== ''; var cls = 'personnel-tag' + (isLeader ? ' leader' : ''); html += ''; if (isLeader) html += '⭐ '; html += escHtml(m); if (isLeader) html += '组长'; html += ''; }); html += '
'; html += '
'; }); return html; } // ─── 用户管理 ─── function downloadUserTemplate() { fetch('/api/users/template', {credentials: 'same-origin'}) .then(r => { if (!r.ok) throw new Error('下载失败'); return r.blob(); }) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '用户导入模板.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); }) .catch(e => alert(e.message)); } function handleImportUsers() { const input = document.getElementById('userImportFile'); const file = input.files[0]; if (!file) { alert('请先选择CSV文件'); return; } const btn = document.getElementById('importBtn'); if (btn) { btn.disabled = true; btn.textContent = '导入中...'; } const reader = new FileReader(); reader.readAsArrayBuffer(file); reader.onload = function(e) { const bytes = new Uint8Array(e.target.result); let binary = ''; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); const base64 = btoa(binary); fetch('/api/users/import', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({csv_base64: base64}) }) .then(r => { if (!r.ok) throw new Error('请求失败(' + r.status + ')'); return r.json(); }) .then(res => { if (res.code !== 0) { alert(res.msg); return; } let msg = res.msg; if (res.data.errors && res.data.errors.length > 0) { msg += '\n\n错误详情:\n' + res.data.errors.join('\n'); } alert(msg); loadUsers(); }) .catch(err => alert('导入失败:' + err.message)) .finally(() => { if (btn) { btn.disabled = false; btn.textContent = '导入用户'; } input.value = ''; }); }; reader.readAsText(file, 'UTF-8'); } function loadUsersPage(container) { container.innerHTML = `
🔐 权限角色: 超级管理员 全部权限 管理员 管理权限 查看者 只读权限

用户列表

序号用户名姓名角色 ↕级别岗位 手机邮箱状态最后登录操作
`; 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('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':'采购管培生' }; const LEVEL_MAP = {'employee':'员工','leader':'组长','director':'主管'}; 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 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 statusVal = (document.getElementById('userStatusFilter') || {}).value || ''; var isFull = !searchVal && !roleVal && !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 = ''; 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'; }; window.editUser = function(id) { 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) { if (!confirm(`确定删除用户 "${username}"?`)) 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 += ''; fieldKeys.forEach(function(f) { var fv = fields[f]; var viewChecked = fv['view'] === 1 ? 'checked' : ''; var editChecked = fv['edit'] === 1 ? 'checked' : ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
字段查看编辑
' + escHtml(f) + '
'; } html += '
'; }); html += '
'; }); wrap.innerHTML = html; window._fieldPermChanged = false; }).catch(function(e) { wrap.innerHTML = '
加载权限数据失败: ' + (e.message || '') + '
'; }); } catch(e) { var w = document.getElementById('permFieldWrap'); if(w) w.innerHTML = '
JS错误: ' + escHtml(e.message) + '
'; } }; window.markFieldPermChanged = function() { window._fieldPermChanged = true; }; window.saveUserFieldPerms = function() { var userId = document.getElementById('permFieldUser').value; if (!userId) return alert('请先选择用户'); // 先获取该用户当前的所有权限(包含page级权限),避免被覆盖 fetch('/api/permissions/users') .then(function(r) { return r.json(); }) .then(function(usersRes) { var users = usersRes.data || []; var curUser = users.find(function(u) { return u.id == userId; }); var pagePerms = (curUser && curUser.permissions) || {}; var items = []; // 页面级操作权限(从user-op-cb checkbox读取) var processedOps = {}; document.querySelectorAll('#permFieldWrap .user-op-cb').forEach(function(cb) { var page = cb.getAttribute('data-page'); var op = cb.getAttribute('data-op'); if (page && op) { // 勾选=用户增加权限,取消=移除用户级覆盖(由后端角色权限兜底) if (cb.checked) { items.push({page: page, operation: op, permission: 1}); } processedOps[page + '_' + op] = true; } }); // 保留已有用户级页面权限(本次没操作的) Object.keys(pagePerms).forEach(function(p) { Object.keys(pagePerms[p]).forEach(function(op) { if (!op.startsWith('field:') && !processedOps[p + '_' + op]) { items.push({page: p, operation: op, permission: pagePerms[p][op]}); } }); }); // 字段级权限(从checkbox读取) document.querySelectorAll('#permFieldWrap input[type="checkbox"]').forEach(function(cb) { var page = cb.getAttribute('data-page'); var field = cb.getAttribute('data-field'); var op = cb.getAttribute('data-op'); if (page && field && op) { items.push({ page: page, operation: 'field:' + field + ':' + op, permission: cb.checked ? 1 : 0 }); } }); return fetch('/api/permissions/user/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user_id: parseInt(userId), items: items}) }); }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 保存成功'); window._fieldPermChanged = false; // 刷新字段权限显示 loadUserFieldPerms(); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function() { alert('保存失败'); }); }; // ─── 字段级权限应用到表格(隐藏无查看权限的列) ─── window._applyTableFieldPerms = function(tableId, pageId, fieldOrder) { var table = document.getElementById(tableId); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; var ths = headerRow.querySelectorAll('th'); ths.forEach(function(th, idx) { // 优先用 data-field,否则用 th 文本内容匹配 var fieldName = th.getAttribute('data-field') || (th.textContent || '').trim().replace(/[^\u4e00-\u9fa5a-zA-Z0-9\-\/]/g, ''); if (fieldName && !window._canViewField(pageId, fieldName)) { th.style.display = 'none'; table.querySelectorAll('tbody tr').forEach(function(tr) { var td = tr.children[idx]; if (td) td.style.display = 'none'; }); } }); }; // ─── 操作日志 ─── function loadLogsPage(container) { container.innerHTML = `

日志列表

时间操作人操作类型详情IP
`; loadLogs(1); } let logCurrentPage = 1; let LOG_PAGE_SIZE = 30; function loadLogs(page) { logCurrentPage = page; const user = document.getElementById('logUser')?.value || ''; const action = document.getElementById('logAction')?.value || ''; let url = '/api/logs/list?page=' + page + '&size=' + LOG_PAGE_SIZE; if (user) url += '&user=' + encodeURIComponent(user); if (action) url += '&action=' + encodeURIComponent(action); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; const data = res.data; const tbody = document.getElementById('logTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无日志'; } else { tbody.innerHTML = data.rows.map(r => { return '' + (r.created_at || '-') + '' + escHtml(r.real_name || '-') + '' + escHtml(r.action || '-') + '' + escHtml(r.detail || '-') + '' + escHtml(r.ip_address || '-') + ''; }).join(''); } document.getElementById('logCount').textContent = '共 ' + (data.total || 0) + ' 条'; const pel = document.getElementById('logPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, LOG_PAGE_SIZE, 'loadLogs', 'LOG_PAGE_SIZE'); }); } window.resetLogFilters = function() { document.getElementById('logUser').value = ''; document.getElementById('logAction').value = ''; loadLogs(1); }; // ─── 访问记录(登录日志) ─── function loadLoginRecordsPage(container) { container.innerHTML = `

登录记录

时间用户名姓名角色IP地址
`; loadLoginRecords(1); } var loginLogCurrentPage = 1; var LOGIN_LOG_PAGE_SIZE = 30; function loadLoginRecords(page) { loginLogCurrentPage = page; var user = document.getElementById('loginLogUser')?.value || ''; var url = '/api/logs/login_list?page=' + page + '&size=' + LOGIN_LOG_PAGE_SIZE; if (user) url += '&user=' + encodeURIComponent(user); fetch(url) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; var data = res.data; var tbody = document.getElementById('loginLogTbody'); if (!data.rows || data.rows.length === 0) { tbody.innerHTML = '暂无登录记录'; } else { tbody.innerHTML = data.rows.map(function(r) { var roleLabel = r.role || '-'; return '' + escHtml(r.created_at || '-') + '' + escHtml(r.username || '-') + '' + escHtml(r.real_name || '-') + '' + escHtml(roleLabel) + '' + escHtml(r.ip_address || '-') + ''; }).join(''); } document.getElementById('loginLogCount').textContent = '共 ' + (data.total || 0) + ' 条'; var pel = document.getElementById('loginLogPagination'); if (pel) renderPaginationWithSize(pel, page, data.total || 0, LOGIN_LOG_PAGE_SIZE, 'loadLoginRecords', 'LOGIN_LOG_PAGE_SIZE'); }) .finally(function() { // 延迟2秒恢复按钮状态,让用户感知刷新完成 var notice = document.getElementById('loginLogRefreshNotice'); if (notice) { notice.innerHTML = '✅ 刷新成功 ' + new Date().toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit',second:'2-digit'}); notice.style.color = '#52c41a'; setTimeout(function() { notice.style.display = 'none'; notice.style.color = ''; }, 2000); } setTimeout(function() { var btn = document.getElementById('refreshLoginLogBtn'); if (btn) { btn.textContent = '🔄 刷新'; btn.disabled = false; } }, 2000); }); } window.refreshLoginLog = function() { var btn = document.getElementById('refreshLoginLogBtn'); if (btn) { btn.textContent = '⟳ 刷新中...'; btn.disabled = true; } // 显示刷新提示 var notice = document.getElementById('loginLogRefreshNotice'); if (notice) { notice.style.display = 'block'; notice.innerHTML = '⟳ 正在刷新数据...'; } loadLoginRecords(loginLogCurrentPage || 1); }; window.resetLoginLogFilters = function() { document.getElementById('loginLogUser').value = ''; loadLoginRecords(1); }; // ─── 参数配置 ─── // ─── 权限配置页面 ─── // ─── 权限配置 ─── function loadPermissionsPage(container) { container.innerHTML = `
⏳
加载中...
`; loadPermissionsUsers(); } // 全局缓存 var _permUsers = []; var _permPages = []; var _permOps = []; var _permRoles = []; var _roleOpPerms = []; 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(); }) ]) .then(function(results) { var usersRes = results[0]; var pagesRes = results[1]; var opsRes = results[2]; var rolesRes = results[3]; var roleOpRes = results[4]; if (usersRes.code !== 0) { el.innerHTML = '
❌ 加载失败
'; return; } _permUsers = usersRes.data || []; _permPages = pagesRes.data || []; _permOps = opsRes.data || []; _permRoles = rolesRes.data || []; _roleOpPerms = roleOpRes.data || []; // 收集去重的岗位和级别列表用于筛选下拉 var posSet = {}; var levelSet = {}; _permUsers.forEach(function(u) { if (u.position) posSet[u.position] = true; if (u.user_level) levelSet[u.user_level] = true; }); var posList = Object.keys(posSet).sort(); var levelList = Object.keys(levelSet).sort(); var positionNames = { 'bp': 'BP', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'procurement_trainee': '采购管培生', '采购开发': '采购开发' }; var levelNames = {'employee': '员工', 'leader': '主管', 'director': '总监'}; // 筛选栏 var filterHtml = '
'; filterHtml += '
'; filterHtml += ''; filterHtml += ''; filterHtml += ''; filterHtml += '
'; filterHtml += '
'; // 角色操作权限卡片 el.innerHTML = filterHtml; renderPermUserGrid(); }) .catch(function() { el.innerHTML = '
❌ 加载失败
'; }); } function resetPermFilter() { var input = document.getElementById('permFilterName'); if (input) input.value = ''; var posEl = document.getElementById('permFilterPos'); if (posEl) posEl.value = ''; var levelEl = document.getElementById('permFilterLevel'); if (levelEl) levelEl.value = ''; renderPermUserGrid(); } // ─── 角色操作权限 ─── var _roleOpEditingRole = ''; var _roleOpData = {}; // {role_page_operation: 1|0} // 按功能ID给出偏安全的默认值(查看者以外的角色) var featureDefaults = { 'view': 1, 'query': 1, 'export': 1, 'submit': 1, 'edit': 1, 'edit_product': 1, 'edit_article': 1, 'edit_warehouse': 1, 'add': 1, 'create': 1, 'delete': 1, 'delete_article': 1, 'delete_warehouse': 1, 'batch_confirm': 1, 'rollback': 1, 'field_setting': 1, 'frozen': 1, 'import': 1, 'lint': 1, 'graph': 1, 'reset': 1 }; window.openRoleOpPermModal = function(roleId) { _roleOpEditingRole = roleId; // 构建权限索引,未保存的按角色默认值填充 _roleOpData = {}; _roleOpPerms.forEach(function(p) { _roleOpData[p.role + '_' + p.page + '_' + p.operation] = p.permission; }); // 预填默认值 var isFullAdmin = (roleId === 'super_admin' || roleId === 'admin'); var isViewer = (roleId === 'viewer'); _permMenuTree.forEach(function(group) { group.items.forEach(function(item) { (item.features || []).forEach(function(feat) { var key = roleId + '_' + item.page + '_' + feat.id; if (!(key in _roleOpData)) { if (isFullAdmin) { _roleOpData[key] = 1; } else if (featureDefaults[feat.id] !== undefined) { _roleOpData[key] = featureDefaults[feat.id]; } else { _roleOpData[key] = (feat.id === 'view') ? 1 : 0; } } }); }); }); // 渲染弹窗 var roleNames = { 'super_admin': '超级管理员', 'admin': '管理员', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'product_developer': '产品开发', 'operator': '运营专员', 'viewer': '查看者' }; var modalId = 'roleOpPermModal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; modal.innerHTML = '
' + '
' + '

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

' + '' + '
' + '
' + '
' + '' + '' + '' + '
' + '
' + '' + '' + '
'; modal.onclick = function(e) { if (e.target === modal) {} }; document.body.appendChild(modal); renderRoleOpPermTable(roleId); }; function renderRoleOpPermTable(roleId) { var tbody = document.getElementById('roleOpModalTbody'); if (!tbody) return; var bodyHtml = ''; _permMenuTree.forEach(function(group) { var items = group.items || []; if (items.length === 0) return; // 每组一个卡片 bodyHtml += '
'; bodyHtml += '
' + escHtml(group.name) + '
'; items.forEach(function(item) { var feats = item.features || []; bodyHtml += '
'; bodyHtml += '' + escHtml(item.name) + ''; feats.forEach(function(f) { var key = roleId + '_' + item.page + '_' + f.id; var checked = _roleOpData[key] == 1 ? 'checked' : ''; bodyHtml += ''; }); bodyHtml += '
'; }); bodyHtml += '
'; }); tbody.innerHTML = ''; // 把卡片放到tbody的容器里(实际放在其父容器中) var wrap = document.getElementById('roleOpModalTable'); if (wrap) { var parent = wrap.parentNode; // 隐藏table,用卡片替代 wrap.style.display = 'none'; var cardWrap = document.getElementById('roleOpModalCardWrap'); if (!cardWrap) { cardWrap = document.createElement('div'); cardWrap.id = 'roleOpModalCardWrap'; parent.appendChild(cardWrap); } cardWrap.innerHTML = bodyHtml; } } function saveRoleOpPerms(modalId) { var items = []; document.querySelectorAll('.role-op-cb').forEach(function(cb) { items.push({ role: cb.dataset.role, page: cb.dataset.page, operation: cb.dataset.op, permission: cb.checked ? 1 : 0 }); }); fetch('/api/permissions/role-operations/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({items: items}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { // 更新缓存,下次打开弹窗显示最新状态 _roleOpPerms.length = 0; items.forEach(function(item) { _roleOpPerms.push({ role: item.role, page: item.page, operation: item.operation, permission: item.permission }); }); alert('✅ 角色操作权限已保存'); if (modalId) document.getElementById(modalId)?.remove(); } else { alert(res.msg || '保存失败'); } }) .catch(function(e) { console.error(e); alert('保存失败: ' + (e.message || '网络错误')); }); } function renderPermUserGrid() { var el = document.getElementById('permUserGrid'); if (!el) return; var kw = (document.getElementById('permFilterName')?.value || '').trim().toLowerCase(); var posFilter = document.getElementById('permFilterPos')?.value || ''; var levelFilter = document.getElementById('permFilterLevel')?.value || ''; var filtered = _permUsers.filter(function(u) { if (kw) { if (!(u.real_name || '').toLowerCase().includes(kw) && !(u.username || '').toLowerCase().includes(kw)) return false; } if (posFilter && u.position !== posFilter) return false; if (levelFilter && u.user_level !== levelFilter) return false; return true; }); var countEl = document.getElementById('permFilterCount'); if (countEl) countEl.textContent = filtered.length + ' / ' + _permUsers.length + ' 人'; var roleNames = { 'super_admin': '超级管理员', 'admin': '管理员', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'product_developer': '产品开发', 'operator': '运营专员', 'viewer': '查看者', '查看者': '查看者' }; var roleColors = { 'super_admin': '#f5222d', 'admin': '#1890ff', 'procurement_planner': '#722ed1', 'procurement_follower': '#13c2c2', 'product_developer': '#eb2f96', 'operator': '#fa8c16', 'viewer': '#8c8c8c', '查看者': '#8c8c8c' }; var roleBadgeColors = { 'super_admin': '#fff1f0', 'admin': '#e6f7ff', 'procurement_planner': '#f9f0ff', 'procurement_follower': '#e6fffb', 'product_developer': '#fff0f6', 'operator': '#fff7e6', 'viewer': '#f5f5f5', '查看者': '#f5f5f5' }; var roleTextColors = { 'super_admin': '#cf1322', 'admin': '#096dd9', 'procurement_planner': '#531dab', 'procurement_follower': '#08979c', 'product_developer': '#c41d7f', 'operator': '#d46b08', 'viewer': '#595959', '查看者': '#595959' }; // 按角色分组:超级管理员 → 管理员 → 查看者 → 其他 var groupSpecs = [ {role: 'super_admin', icon: '⭐', label: '超级管理员'}, {role: 'admin', icon: '🔐', label: '管理员'}, {role: 'viewer', icon: '👁️', label: '查看者'} ]; var roleGroups = {}; var otherUsers = []; filtered.forEach(function(u) { var found = false; groupSpecs.forEach(function(gs) { if (u.role === gs.role) { if (!roleGroups[gs.role]) roleGroups[gs.role] = []; roleGroups[gs.role].push(u); found = true; } }); if (!found) otherUsers.push(u); }); function renderUserSection(users, sectionInfo) { var h = ''; if (!users || users.length === 0) return ''; var icon = sectionInfo.icon || '👤'; var label = sectionInfo.label || '成员'; var roleId = sectionInfo.role || ''; h += '
'; h += '' + icon + ' ' + label + ''; h += '
'; h += '
'; users.forEach(function(u) { var initial = u.real_name ? u.real_name.charAt(0) : '?'; var roleName = roleNames[u.role] || u.role; var badgeBg = roleBadgeColors[u.role] || '#f5f5f5'; var badgeText = roleTextColors[u.role] || '#595959'; var avatarBg = roleColors[u.role] || '#8c8c8c'; var permCount = Object.keys(u.permissions || {}).length; var positionNames = { 'bp': 'BP', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单', 'procurement_trainee': '采购管培生', '采购开发': '采购开发' }; var levelNames = {'employee': '员工', 'leader': '主管', 'director': '总监'}; var posName = positionNames[u.position] || u.position || ''; var levelName = levelNames[u.user_level] || u.user_level || ''; h += '
'; // 头像 h += '
' + escHtml(initial) + '
'; // 信息 h += '
'; h += '
'; h += escHtml(u.real_name || u.username); h += '' + escHtml(roleName) + ''; if (levelName) { h += '' + levelName + ''; } h += '
'; var infoParts = ['账号: ' + escHtml(u.username)]; if (posName) infoParts.push('岗位: ' + escHtml(posName)); if (levelName) infoParts.push('级别: ' + levelName); if (permCount > 0) infoParts.push('已配置 ' + permCount + ' 个页面'); h += '
' + infoParts.join(' · ') + '
'; h += '
'; h += '›'; h += '
'; }); h += '
'; return h; } var html = ''; var first = true; groupSpecs.forEach(function(gs) { var users = roleGroups[gs.role] || []; if (users.length > 0) { if (!first) html += '
'; html += renderUserSection(users, gs); first = false; } }); if (otherUsers.length > 0) { if (!first) html += '
'; html += renderUserSection(otherUsers, {icon:'👥', label:'其他成员', role:''}); } html += ''; if (filtered.length === 0) { html = '
无匹配用户
'; } el.innerHTML = html; } // 打开用户权限面板 - 使用新API展示用户有效权限 window.openUserPermPanel = function(userId) { // 关闭已有弹窗,防止重复堆积 var existing = document.querySelectorAll('.modal-overlay'); existing.forEach(function(el) { if (el.id !== 'permModal' && el.id !== 'userModal') el.remove(); }); // 从缓存中获取用户信息,避免额外API调用 var user = (_permUsers || []).find(function(u) { return u.id == userId; }); if (!user) { 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 rolePerms = data.role_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 = []; document.querySelectorAll('.user-edit-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}); } } }); var saveBtn = document.getElementById('userPermSaveBtn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '保存中...'; } var body = JSON.stringify({user_id: parseInt(userId), items: items}); fetch('/api/permissions/user/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin', body: body }) .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status + ' ' + r.statusText); return r.json(); }) .then(function(res) { alert(res.msg || (res.code === 0 ? '✅ 保存成功' : '❌ 保存失败')); if (res.code === 0) { document.querySelector('.modal-overlay')?.remove(); } if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; } }) .catch(function(e) { alert('保存失败: ' + e.message + (body ? ' (已发送' + items.length + '项)' : '')); if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; } }); }; var _editingUserId = null; var _rolePagePerms = {}; // {page: 1/0} - 角色默认的页面权限 // 菜单树结构(与侧边栏菜单一致) // 菜单树结构:每个菜单项列出实际的功能按钮(不是通用的增删改查) var _permMenuTree = [ { name: '数据概览', icon: '📊', items: [ {page: 'dashboard', name: '数据概览', features: [{id:'view',name:'查看'}]} ] }, { name: '出库数据', icon: '📦', items: [ {page: 'data_summary', name: '出库汇总', features: [{id:'view',name:'查看'},{id:'query',name:'查询'},{id:'export',name:'导出'}]}, {page: 'data_detail', name: '出库明细', features: [{id:'view',name:'查看'},{id:'query',name:'查询'},{id:'export',name:'导出'}]} ] }, { name: '库存信息', icon: '📋', items: [ {page: 'inventory_finished', name: '成品库存', features: [{id:'view',name:'查看'},{id:'edit_product',name:'编辑产品'},{id:'export',name:'导出'}]}, {page: 'inventory_packaging', name: '包材库存', features: [{id:'view',name:'查看'},{id:'edit_product',name:'编辑产品'},{id:'export',name:'导出'}]} ] }, { name: '计划预测', icon: '📈', items: [ {page: 'planning', name: '计划预测', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑产品'},{id:'delete',name:'删除'},{id:'field_setting',name:'字段设置'},{id:'frozen',name:'冻结列'}]} ] }, { name: '采购订单', icon: '📄', items: [ {page: 'purchase', name: '待下单管理', features: [{id:'view',name:'查看'},{id:'batch_confirm',name:'批量确认下单'},{id:'rollback',name:'回退'},{id:'field_setting',name:'字段设置'},{id:'frozen',name:'冻结列'}]}, {page: 'purchase_ready', name: '下单备份管理', features: [{id:'view',name:'查看'},{id:'rollback',name:'回退'},{id:'field_setting',name:'字段设置'}]}, {page: 'orders_domestic', name: '国内订单管理', features: [{id:'view',name:'查看'}]}, {page: 'orders_overseas', name: '海外订单管理', features: [{id:'view',name:'查看'}]}, {page: 'orders_packaging', name: '包材订单管理', features: [{id:'view',name:'查看'}]}, {page: 'orders_finished', name: '成品提货表', features: [{id:'view',name:'查看'}]} ] }, { name: '测试问题', icon: '🐛', items: [ {page: 'feedback', name: '测试问题', features: [{id:'view',name:'查看'},{id:'submit',name:'提交反馈'}]} ] }, { name: '基础信息', icon: '📚', items: [ {page: 'products', name: '产品信息管理', features: [{id:'view',name:'查看'},{id:'add',name:'新增产品'},{id:'edit',name:'编辑产品'},{id:'field_setting',name:'字段设置'}]}, {page: 'bom', name: '物料清单管理', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'}]}, {page: 'warehouse', name: '仓库信息管理', features: [{id:'view',name:'查看'},{id:'create',name:'新增仓库'},{id:'edit',name:'编辑仓库'},{id:'delete',name:'删除仓库'}]}, {page: 'suppliers', name: '供应商管理', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'},{id:'create',name:'新增供应商'}]}, {page: 'personnel', name: '人员信息管理', features: [{id:'view',name:'查看'}]} ] }, { name: '知识库', icon: '📖', items: [ {page: 'knowledge', name: '知识库', features: [{id:'view',name:'查看'},{id:'create',name:'新建文章'},{id:'edit',name:'编辑文章'},{id:'delete',name:'删除文章'},{id:'import',name:'导入'},{id:'export',name:'导出'},{id:'lint',name:'运行检查'},{id:'graph',name:'关系图谱'}]} ] }, { name: '系统设置', icon: '⚙️', items: [ {page: 'users', name: '用户管理', features: [{id:'view',name:'查看'},{id:'create',name:'新增'},{id:'edit',name:'编辑'},{id:'delete',name:'删除'},{id:'import',name:'导入'}]}, {page: 'permissions', name: '权限配置', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'}]}, {page: 'login_records', name: '访问记录', features: [{id:'view',name:'查看'}]}, {page: 'logs', name: '操作日志', features: [{id:'view',name:'查看'}]}, {page: 'config', name: '参数配置', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'},{id:'reset',name:'恢复默认'}]}, {page: 'config_calibrate', name: '参数校准', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'}]}, {page: 'contract_templates', name: '合同模板', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑上传'},{id:'delete',name:'删除'}]}, {page: 'knowledge', name: '知识库', features: [{id:'view',name:'查看'},{id:'create',name:'新建文章'},{id:'edit',name:'编辑文章'},{id:'delete',name:'删除文章'},{id:'import',name:'导入'},{id:'export',name:'导出'}]} ] } ]; // ─── 每个页面的字段/卡片定义 ─── 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': ['日期','编码','名称','单价','数量','合计','供应商','下单备注','仓库到货','进度','采购下单人','旺店通编号','到货日期','做货天数','包材开发'], // 基础信息 'products': ['货品编号','货品名称','货品类型','品牌','供应商编码','供应商名称','货期','物流时间','采购跟单','采购计划','平台','运营','开发专员','创建时间','导入日期'], 'bom': ['成品编号','成品名称','物料编号','物料名称','用量','供应商','成本价','创建时间'], 'warehouse': ['仓库编号','仓库名称','仓库类型','地址','联系人','联系电话','创建时间'], 'suppliers': ['供应商编码','供应商名称','联系人','联系电话','地址','合作状态','创建时间'], // 知识库 'knowledge': ['标题','主题分类','创建人','创建时间','更新时间','状态'], // 系统设置 'users': ['用户名','姓名','角色','岗位','用户级别','手机号','邮箱','状态','最后登录'], 'permissions': ['用户名','姓名','角色','岗位'], 'login_records': ['用户名','登录时间','IP地址','操作'], 'logs': ['操作人','操作类型','操作详情','操作时间','IP地址'], 'config': ['配置项','配置值','说明','更新时间'], 'config_calibrate': ['参数名称','当前值','校准值','校准时间','操作人'], '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 = renderConfigTable('日销量预测基础配置参数', ['3天平均销量', '7天平均销量', '14天平均销量', '调节系数', '趋势类型'], ['avg_3d', 'avg_7d', 'avg_14d', 'adjust_factor', 'trend_type'], d.sales_predict_config); 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 + globalHtml; }); } 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, '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, '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: '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: 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: 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}, ]; // 筛选字段预定义选项(下拉选择类型) 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, '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) => { const visibleClass = window.planColVisible[c.field] ? '' : ' style="display:none"'; var sortArrow = (planSortField === c.field) ? (planSortAsc ? ' ▲' : ' ▼') : ''; var isSortable = (c.field === '产品分级') || SORTABLE_FIELDS[c.field]; var planTips = { '日销量预测': '日销量预测 = (3天平均×w3 + 7天平均×w7 + 14天平均×w14) × 调整系数', '库存可支撑天数': '库存可支撑天数 = 库存 / 日销量预测', '加工厂库存支撑天数': '加工厂库存支撑 = 加工厂库存 / 日销量预测', '库存工厂可支撑天数': '现货采购在途 = 采购在途 / 日销量预测', '库存工厂待生产可支撑天数': '定制品在途 = 生产在途 / 日销量预测', '总支撑天数': '总支撑天数 = (库存+加工厂库存+生产在途+采购在途) / 日销量预测', '库存健康指数': '日销量预测>0:库存可支撑天数÷(货期+物流);=0:库存可支撑天数÷(货期+物流)', '建议下单数量': '日销量预测=0且库存>0则0,否则(安全库存天数+货期+物流)×日销量预测', '实际下单数量': '建议下单数量向上取整到1000', '正式下单数量': '正式下单数量初始等于实际下单数量,可人工修改', '建议回货数量': 'ROUNDUP((日销量预测×回货天数-库存)×10)/10,上限在途总数', '是否新品': '创建时间5个月以内为新品,否则返单品', '成本价': '从产品信息匹配的成本价格', '销售趋势': '基于3/7/14天日均销量判断的9种趋势类型' }; var tip = planTips[c.label] || ''; var thPos = 'position:sticky;top:0;z-index:10'; if (planFrozen && frozenLefts[c.field] !== undefined) { thPos = 'position:sticky;top:0;left:' + frozenLefts[c.field] + 'px;z-index:999;background:var(--bg-card);transform:translateZ(0);min-width:' + (FROZEN_WIDTHS[c.field] || 100) + 'px'; } var canFilter = c.filterable !== false; var isActive = window._planFilters && window._planFilters[c.field]; var iconColor = isActive ? '#1890ff' : 'var(--text-light)'; var iconWeight = isActive ? 'bold' : 'normal'; var iconChar2 = isActive ? '▼' : '▾'; var filterIcon = canFilter ? '' + iconChar2 + '' : ''; var clickHandler = canFilter ? 'openPlanFilterMenu(\'' + escJs(c.field) + '\', event)' : ''; theadHtml += `${c.label}${sortArrow}${filterIcon}`; }); theadHtml += '操作'; 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) { 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); } } // 如果有计算字段筛选条件,需要拿全量数据做客户端过滤 var hasExcludedFilter = false; if (window._planFilters) { Object.keys(window._planFilters).forEach(function(ff) { if (PLAN_FILTER_EXCLUDE_FIELDS[ff]) hasExcludedFilter = true; }); } var dataSize = hasExcludedFilter ? 10000 : PLAN_PAGE_SIZE; let url = '/api/planning/list?page=' + page + '&size=' + dataSize; 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; }); } // 客户端过滤计算字段(后端无法过滤的字段) if (data.rows && window._planFilters && Object.keys(window._planFilters).length > 0) { var excludedSet = {}; Object.keys(PLAN_FILTER_EXCLUDE_FIELDS).forEach(function(f) { excludedSet[f] = 1; }); var clientFiltered = data.rows.filter(function(rr) { var pass = true; Object.keys(window._planFilters).forEach(function(ff) { if (!excludedSet[ff]) return; // DB字段后端已处理 var fp = window._planFilters[ff]; if (!fp || !fp.type) return; var rrVal = rr[ff]; if (rrVal === undefined || rrVal === null) rrVal = ''; rrVal = String(rrVal).trim(); if (fp.type === 'list' && fp.values) { if (fp.values.indexOf(rrVal) < 0) { pass = false; } } else if (fp.type === 'search' && fp.value) { if (rrVal.indexOf(fp.value) < 0) { pass = false; } } else if (fp.type === 'num') { var numVal = parseFloat(rrVal) || 0; var condVal = parseFloat(fp.val) || 0; if (fp.cond === 'eq' && numVal !== condVal) pass = false; else if (fp.cond === 'neq' && numVal === condVal) pass = false; else if (fp.cond === 'gt' && numVal <= condVal) pass = false; else if (fp.cond === 'gte' && numVal < condVal) pass = false; else if (fp.cond === 'lt' && numVal >= condVal) pass = false; else if (fp.cond === 'lte' && numVal > condVal) pass = false; else if (fp.cond === 'range') { var parts = (fp.val || '').split(',').map(function(x){return parseFloat(x.trim())||0;}); if (parts.length === 2 && (numVal < parts[0] || numVal > parts[1])) pass = false; } } }); return pass; }); var hadExcludedFilterApplied = false; Object.keys(window._planFilters).forEach(function(cf) { if (excludedSet[cf]) hadExcludedFilterApplied = true; }); if (hadExcludedFilterApplied) { data.rows = clientFiltered; data.total = clientFiltered.length; planTotal = clientFiltered.length; } } 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'); // 按权限过滤列(不按显隐过滤,thead用display:none控制显隐) var permFilteredCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField('planning', c.label); }); 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(truncate(raw || '-', 25)); } if (c.field === 'bom') { var pcode = (r.product_code || ''); var pname = escHtml(r.product_name || ''); var suggQty = r['建议下单数量'] || 0; var actQty = r['实际下单数量'] || 0; var remark = escHtml(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 === '采购在途') { return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw); } if (c.field === 'cost_price') { var val = parseFloat(raw); return isNaN(val) ? '-' : '¥' + val.toFixed(2); } if (c.field === '最低销量偏差率' || c.field === '最高销量偏差率' || c.field === '14天波动率') { return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw); } if (c.field === '极值判断') { var cls = 'tag-grey'; if (raw === '极大/极小') cls = 'tag-red'; else if (raw === '极小') cls = 'tag-orange'; else if (raw === '极大') cls = 'tag-orange'; else if (raw === '正常') cls = 'tag-green'; return '' + (raw || '-') + ''; } if (c.field === '波动风险') { var cls = 'tag-blue'; if (raw === 'X') cls = 'tag-green'; else if (raw === 'Y') cls = 'tag-orange'; else if (raw === 'Z') cls = 'tag-red'; return '' + (raw || '-') + ''; } if (c.field === '产品分级') { var gradeColors = {'S级':'tag-red', 'A级':'tag-orange', 'B级':'tag-blue', 'C级':'tag-grey', '不参与评级':'tag-grey'}; var cls = gradeColors[raw] || 'tag-grey'; return '' + (raw || '-') + ''; } if (c.field === '销售趋势') { var trendColors = { '开始上升': 'tag-blue', '开始下跌': 'tag-purple', '平稳趋势': 'tag-green', '上升趋稳': 'tag-blue', '下跌趋稳': 'tag-purple', '持续上升': 'tag-blue', '急跌': 'tag-red', '持续下跌': 'tag-red', '急增': 'tag-orange', '近期无销售': 'tag-grey' }; var cls = trendColors[raw] || 'tag-grey'; return '' + (raw || '-') + ''; } if (c.field === '是否新品') { return (raw === '新品') ? '新品' : '返单品'; } if (c.field === '日销量预测') { var val = parseFloat(raw); if (isNaN(val) || val === 0) return '0'; return '' + Math.round(val) + ''; } if (c.field === '库存可支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '库存工厂可支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '加工厂库存支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '库存工厂待生产可支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '总支撑天数') { var val = parseFloat(raw); if (isNaN(val)) return '-'; if (val === 999) return '999'; return val.toFixed(1); } if (c.field === '库存健康指数') { 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 = escHtml(r.product_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 === '采购跟单' || c.field === '采购计划' || c.field === '开发专员' || 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 += PLAN_COLUMNS.map((c, i) => { const disp = window.planColVisible[c.field] ? '' : ' style="display:none"'; 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[i]}`; }).join(''); var code = r.product_code || ''; // 序列化下单所需数据 var formalQty = parseInt(r['实际下单数量']) || 0; var suggestedQty = parseInt(r['建议下单数量']) || 0; var orderData = JSON.stringify({ product_code: r.product_code || '', product_name: r.product_name || '', product_type: r.product_type || '', product_grade: r['产品分级'] || '', supplier_name: r['供应商名称'] || '', lead_time: r['生产货期'] || 0, logistics_time: r['物流时间'] || 0, total_support_days: r['总支撑天数'] || 0, procurement_follower: r['采购跟单'] || '', procurement_planner: r['采购计划'] || '', platform: r['归属平台'] || '', operator: r['运营专员'] || '', formal_order_qty: formalQty, suggested_qty: suggestedQty, remark: r['下单备注'] || '' }); colsHtml += ''; colsHtml += ' '; if (window._hasPagePerm('planning', 'edit')) colsHtml += ''; if (!window._planOrderDataCache) window._planOrderDataCache = {}; window._planOrderDataCache[code] = orderData; 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 有偏差时修正) 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; }); // 判断是否为数字列 // 产品分级虽然可排序,但只有 S/A/B/C 四种值,用列表筛选 var productGradeFields = ['产品分级']; var isNumericCol = SORTABLE_FIELDS[field] && !productGradeFields.includes(field); var menu = null; if (isNumericCol) { menu = document.createElement('div'); menu.id = 'planFilterMenu'; var isSorted = planSortField === field; var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : ''; _buildNumericFilterMenu(field, label, rect, isSorted, currentOrder, menu); } else { // 获取该列所有唯一值 var values = []; var predefined = PLAN_FILTER_OPTIONS[field]; var hasPlanFilter = window._planFilters && Object.keys(window._planFilters).length > 0; var isExcludedField = PLAN_FILTER_EXCLUDE_FIELDS[field] ? true : false; // 联动筛选:有筛选时从表格提取唯一值(基于已筛选行);但当前字段已有筛选时用预定义列表保持全量选项 if (predefined && hasPlanFilter && window._planFilters[field]) { // 当前字段本身已有筛选 → 用完整预定义列表,勾选状态由 _populateFilterMenuList 回显 values = predefined.slice(); } else if (predefined && hasPlanFilter) { // 其他字段有筛选 → 从表格提取当前已筛选行的唯一值(联动筛选) values = []; var tbody = document.getElementById('planTbody'); if (tbody) { var permCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField && window._canViewField('planning', c.label); }); var colIdx = permCols.findIndex(function(c) { return c.field === field; }); if (colIdx >= 0) { colIdx = colIdx + 1; // +1 for 序号列 var seen = {}; tbody.querySelectorAll('tr').forEach(function(tr) { var td = tr.cells[colIdx]; if (td) { var txt = td.textContent.trim(); if (txt && !seen[txt]) { seen[txt] = 1; values.push(txt); } } }); } } if (values.length === 0) values = predefined.slice(); } else if (predefined && !hasPlanFilter) { values = predefined.slice(); } else if (field === '产品分级' && hasPlanFilter && isExcludedField) { // 从表格提取 values = []; var tbody2 = document.getElementById('planTbody'); if (tbody2) { var permCols2 = PLAN_COLUMNS.filter(function(c) { return window._canViewField && window._canViewField('planning', c.label); }); var colIdx2 = permCols2.findIndex(function(c) { return c.field === field; }); if (colIdx2 >= 0) { colIdx2 = colIdx2 + 1; // +1 for 序号列 var seen2 = {}; tbody2.querySelectorAll('tr').forEach(function(tr) { var td = tr.cells[colIdx2]; if (td) { var txt = td.textContent.trim(); if (txt && !seen2[txt]) { seen2[txt] = 1; values.push(txt); } } }); } } if (values.length === 0) values = ['S', 'A', 'B', 'C']; } else if (field === '产品分级' && (!hasPlanFilter || !isExcludedField)) { values = ['S', 'A', 'B', 'C']; } else if (field === 'brand') { // 品牌列:优先使用缓存 var planFilterParam = ''; var hasPF = window._planFilters && Object.keys(window._planFilters).length > 0; if (hasPF) { planFilterParam = (planActiveView !== 'default' ? '&' : '?') + 'plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters)); } var cacheKey = 'plan:brand:' + (planActiveView || 'default') + (hasPF ? ':filtered' : ''); var cached = !hasPF && window._planColumnCache && window._planColumnCache[cacheKey]; if (cached) { // 命中缓存:直接构建完整菜单 if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; var isSorted = planSortField === field; var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : ''; var menuBrand = document.createElement('div'); menuBrand.id = 'planFilterMenu'; _setupPlanFilterMenuHTML(menuBrand, field, label, rect, currentOrder, isSorted); document.body.appendChild(menuBrand); _populateFilterMenuList(field, cached, false); setupFilterMenuClose(menuBrand, th); } else { var menu = _createFilterMenuShell(field, label, rect); document.body.appendChild(menu); var viewParam = planActiveView !== 'default' ? '?view=' + encodeURIComponent(planActiveView) : ''; fetch('/api/planning/brands' + viewParam + planFilterParam) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { if (!hasPF) { window._planColumnCache = window._planColumnCache || {}; window._planColumnCache[cacheKey] = res.data; } _populateFilterMenuList(field, res.data, false); } }); setupFilterMenuClose(menu, th); } return; } else { // 非预定义非品牌的文本列 var isExcl = PLAN_FILTER_EXCLUDE_FIELDS[field] ? true : false; var hasPF = window._planFilters && Object.keys(window._planFilters).length > 0; // 计算字段+联动筛选:从表格提取 if (hasPF && isExcl) { values = []; if (window._planAllRows) { var seen3 = {}; Object.keys(window._planAllRows).forEach(function(pc) { var r = window._planAllRows[pc]; var rawVal = r[field]; if (rawVal !== undefined && rawVal !== null && rawVal !== '') { var txt = String(rawVal).trim(); if (txt && txt !== '-' && !seen3[txt]) { seen3[txt] = 1; values.push(txt); } } }); values.sort(); } if (values.length === 0) values = ['暂无']; } else { // 非预定义文本列:优先使用缓存 var planFilterParam2 = ''; if (hasPF) { planFilterParam2 = '&plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters)); } var cacheKey2 = 'plan:field:' + field + ':' + (planActiveView || 'default') + (hasPF ? ':filtered' : ''); var cached2 = !hasPF && window._planColumnCache && window._planColumnCache[cacheKey2]; if (cached2) { // 命中缓存:直接构建完整菜单 if (!_planFilterSelected[field]) _planFilterSelected[field] = {}; var isSorted2 = planSortField === field; var currentOrder2 = isSorted2 ? (planSortAsc ? 'asc' : 'desc') : ''; var menuField = document.createElement('div'); menuField.id = 'planFilterMenu'; _setupPlanFilterMenuHTML(menuField, field, label, rect, currentOrder2, isSorted2); document.body.appendChild(menuField); // 采购跟单/采购计划:人员姓名转组别名 var valsCached = cached2; if (field === 'procurement_follower' || field === 'procurement_planner') { var groupNames = {}; valsCached.forEach(function(v) { var names = String(v).split(','); names.forEach(function(n) { n = n.trim(); if (!n) return; var g = window._nameToGroup && window._nameToGroup[n]; if (g && g.name) groupNames[g.name] = 1; else groupNames[n] = 1; }); }); valsCached = Object.keys(groupNames).sort(); } _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 === 'procurement_follower' || field === 'procurement_planner') { var groupNames = {}; vals.forEach(function(v) { var names = String(v).split(','); names.forEach(function(n) { n = n.trim(); if (!n) return; var g = window._nameToGroup && window._nameToGroup[n]; if (g && g.name) groupNames[g.name] = 1; else groupNames[n] = 1; }); }); vals = Object.keys(groupNames).sort(); } if (!hasPF) { window._planColumnCache = window._planColumnCache || {}; window._planColumnCache[cacheKey2] = res.data; } _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)); } 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; } // 回显已保存的数字筛选状态 var savedNumFilter = window._planFilters && window._planFilters[field]; 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 || ''; // 切换到条件Tab planNumSwitchTab('cond'); } else if (savedNumFilter && savedNumFilter.type === 'list' && savedNumFilter.values) { var cbs = document.querySelectorAll('.plan-num-cb'); cbs.forEach(function(cb) { cb.checked = savedNumFilter.values.indexOf(cb.value) >= 0; }); } setTimeout(function() { var inp = document.getElementById('planNumVal'); if (inp) inp.focus(); }, 100); } function planNumFilterApply() { var field = _planFilterMenuField; var condPanel = document.getElementById('planNumCondPanel'); var isCondTab = condPanel && condPanel.style.display !== 'none'; if (isCondTab) { // 条件筛选 var cond = document.getElementById('planNumCond')?.value || ''; var val = document.getElementById('planNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } if (!window._planFilters) window._planFilters = {}; window._planFilters[field] = {type:'num', cond: cond, val: val}; } else { // 内容筛选:复选框值创建列表筛选 var cbs = document.querySelectorAll('.plan-num-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (!window._planFilters) window._planFilters = {}; if (checked.length === 0 || checked.length === document.querySelectorAll('.plan-num-cb').length) { delete window._planFilters[field]; } else { window._planFilters[field] = {type:'list', values: checked}; } } _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); } function planFilterApply() { var field = _planFilterMenuField; // 先检查搜索框是否有内容 var searchInput = document.getElementById('planFilterSearch'); var searchText = searchInput ? searchInput.value.trim() : ''; if (searchText) { // 搜索框有内容 → 文本模糊匹配 if (!window._planFilters) window._planFilters = {}; window._planFilters[field] = {type: 'search', value: searchText}; } else { // 搜索框为空 → 用复选框选择 var cbs = document.querySelectorAll('.plan-filter-cb:checked'); var checked = []; cbs.forEach(function(cb) { checked.push(cb.value); }); if (!window._planFilters) window._planFilters = {}; if (checked.length === 0 || checked.length === document.querySelectorAll('.plan-filter-cb').length) { // 全选或没选 → 清除该列筛选 delete window._planFilters[field]; } else { window._planFilters[field] = {type:'list', values: checked}; } } _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); } function _syncFiltersToUI() { // 仅用于更新表头筛选图标状态 var activeFilters = []; Object.keys(window._planFilters || {}).forEach(function(f) { var filter = window._planFilters[f]; if (filter) { filter.field = f; activeFilters.push(filter); } }); _updateFilterIcons(activeFilters); } function _updateFilterIcons(activeFilters) { var activeFields = {}; activeFilters.forEach(function(f) { activeFields[f.field] = true; }); PLAN_COLUMNS.forEach(function(c, i) { var th = document.querySelector('.plan-th-' + i); if (th) { var isActive = activeFields[c.field]; var icon = th.querySelector('span[title="筛选"]'); if (icon) { icon.style.color = isActive ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = isActive ? 'bold' : 'normal'; icon.textContent = isActive ? '▼' : '▾'; } } }); } function _restoreFilterIcons() { var activeFilters = []; Object.keys(window._planFilters || {}).forEach(function(f) { var fp = window._planFilters[f]; if (fp) { fp.field = f; activeFilters.push(fp); } }); _updateFilterIcons(activeFilters); } function planFilterSearchList(q) { var items = document.querySelectorAll('#planFilterList label'); q = (q || '').toLowerCase(); items.forEach(function(label) { var txt = label.textContent.toLowerCase(); label.style.display = (!q || txt.indexOf(q) >= 0) ? '' : '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) ? '' : '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); } // 根据视图调整列显隐和名称(仅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 = 'default'; function switchPlanView(view) { // 保存当前视图的字段显隐状态 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 = getDistinct(field); sel.innerHTML = '' + names.map(function(n) { return ''; }).join(''); sel.value = currentVal; }); } 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) { // 按权限过滤列 var permFilteredCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField('planning', c.label); }); var newThead = '
序号
'; permFilteredCols.forEach(function(c, i) { var disp = window.planColVisible[c.field] ? '' : ' style="display:none"'; var isNumericSortable = (c.field === '产品分级') || SORTABLE_FIELDS[c.field]; var sortArrowHtml = ''; if (isNumericSortable) { var isSorted = planSortField === c.field; var upActive = isSorted && planSortAsc; var downActive = isSorted && !planSortAsc; var upStyle = upActive ? 'color:#cf1322;font-weight:700;font-size:12px' : 'color:#d9d9d9;font-size:11px'; var downStyle = downActive ? 'color:#cf1322;font-weight:700;font-size:12px' : 'color:#d9d9d9;font-size:11px'; sortArrowHtml = ' ▲▼'; } var planTips = {'日销量预测':'日销量预测 = (3天平均×w3 + 7天平均×w7 + 14天平均×w14) × 调整系数','库存可支撑天数':'库存可支撑天数 = 库存 / 日销量预测,为0时显示999','加工厂库存支撑天数':'加工厂库存支撑 = 加工厂库存 / 日销量预测,为0时显示999','库存工厂可支撑天数':'现货采购在途 = 采购在途 / 日销量预测','库存工厂待生产可支撑天数':'定制品在途 = 生产在途 / 日销量预测','总支撑天数':'总支撑天数 = (库存 + 加工厂库存 + 生产在途 + 采购在途) / 日销量预测','库存健康指数':'日销量预测>0:库存可支撑天数÷(货期+物流);=0:库存可支撑天数÷(货期+物流)','建议下单数量':'日销量预测=0且库存>0则0,否则(安全库存天数+货期+物流)×日销量预测','实际下单数量':'建议下单数量向上取整到1000','正式下单数量':'正式下单数量初始等于实际下单数量,可人工修改','建议回货数量':'ROUNDUP((日销量预测×回货天数-库存)×10)/10,上限在途总数','是否新品':'创建时间5个月以内为新品,否则返单品','成本价':'从产品信息匹配的成本价格','销售趋势':'基于3/7/14天日均销量判断的9种趋势类型'}; var tip = planTips[c.label] || ''; var canFilter = c.filterable !== false; var isActive = window._planFilters && window._planFilters[c.field]; var iconColor = isActive ? '#1890ff' : 'var(--text-light)'; var iconWeight = isActive ? 'bold' : 'normal'; var iconChar = isActive ? '▼' : '▾'; var filterIcon = canFilter ? ' ' + iconChar + '' : ''; var clickHandler = canFilter ? 'openPlanFilterMenu(\'' + escJs(c.field) + '\', event)' : ''; newThead += '' + c.label + sortArrowHtml + filterIcon + ''; }); newThead += '操作'; newThead += ''; planTable.querySelector('thead').innerHTML = newThead; } // 删除方案 window.deletePlanScheme = function(schemeId) { if (!confirm('确定要删除此方案吗?')) 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 _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', '采购跟单': 'procurement_follower', '采购计划': 'procurement_planner', }; 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:'text'}, {field:'采购计划', label:'采购计划', type:'text'}, ]; function loadPurchasePage(container) { container.innerHTML = `

📋 待下单商品

序号 产品类型 ▾ 货品编号 ▾ 货品名称 ▾ 确认下单 产品分级 ▾ 供应商名称 ▾ 建议下单数量 ▾ 正式下单数量 ▾ 下单备注 ▾ 货期 ▾ 物流 ▾ 总支撑天数 ▾ 平台 ▾ 运营 ▾ 审核价格 ▾ 下单人员 ▾ 下单时间 采购跟单 ▾ 采购计划 ▾ 加入时间 操作
`; loadPendingList(); // 设置缓存重载器:从缓存恢复时重新拉取最新数据 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 || []; // 清筛选缓存(数据已刷新) invalidateOrderColumnCache('pending'); _syncPendingFilterIcons(); _renderFilteredPending(); }); } // 提取待下单行渲染为独立函数(供 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 || '-') + '' + '' + '' + 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) + '' + '' + ((item.created_at || '').toString().slice(0, 19) || '-') + '' + '' + ' ' + '' + '' + ''; }).join(''); } // ─── 待下单表头筛选 ─── function _pendingGetColType(field) { for (var i = 0; i < PENDING_FILTER_COLS.length; i++) { if (PENDING_FILTER_COLS[i].field === field) return PENDING_FILTER_COLS[i].type; } return 'text'; } function 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 filterActive = pendingOrderFilter[field]; if (colType === 'number') { _buildPendingNumericFilter(field, label, rect); } 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 = String(item[_pendingFieldMap[field] || field] || '').trim(); 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) { 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 sel = pendingOrderFilter[field] || {}; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var checked = (sel[v] !== false) ? 'checked' : ''; html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); pendingFilterMenu = menu; } function _populatePendingFilterList(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[v] !== false) ? 'checked' : ''; html += ''; }); listEl.innerHTML = html; } function _buildPendingNumericFilter(field, label, rect) { var menu = document.createElement('div'); menu.id = 'pendingFilterMenu'; var current = pendingOrderFilter[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var html = '
' + escHtml(label) + '
'; 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:200px;font-size:13px'; document.body.appendChild(menu); pendingFilterMenu = menu; } 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 ? '' : '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}; } } if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } _syncPendingFilterIcons(); _renderFilteredPending(); } function applyPendingNumFilter() { var field = pendingFilterField; 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}; if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; } _syncPendingFilterIcons(); _renderFilteredPending(); } function clearPendingFilter() { delete pendingOrderFilter[pendingFilterField]; 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 = String(item[_pendingFieldMap[f] || f] || ''); if (fp.type === 'search') { return v.toLowerCase().indexOf((fp.value || '').toLowerCase()) >= 0; } if (fp.type === 'list') { 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 _renderFilteredPending() { var filtered = _applyPendingFilters(_pendingItems); var countEl = document.getElementById('pendingCount'); if (countEl) countEl.textContent = '共 ' + filtered.length + ' / ' + _pendingItems.length + ' 条'; var tbody = document.getElementById('pendingTbody'); if (!tbody) return; if (filtered.length === 0) { tbody.innerHTML = '暂无匹配数据'; return; } tbody.innerHTML = _pendingRowsHtml(filtered); pendingUpdateCount(); } 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 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) { // 过滤掉内料 bomMaterials = bomMaterials.filter(function(b) { return !(b.material_name && b.material_name.indexOf('内料') !== -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 += ''; html += ''; html += ''; bomRows.forEach(function(b, i) { var bg = i % 2 === 0 ? '#fff' : '#f5f5f5'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; 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 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:30000;display:flex;align-items:center;justify-content:center;'; var html = '
' + '
✅
' + '

下单完成

' + '

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

'; links.forEach(function(l) { html += '
' + l.label + ' →
'; }); html += '
'; overlay.innerHTML = html; overlay.id = 'confirmRedirectModal'; document.body.appendChild(overlay); } function pendingBatchConfirm() { 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; // 收集这些商品的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) { showCenteredAlert('操作成功', '✅ 批量下单成功,共 ' + checkedCodes.length + ' 个', function() { showConfirmRedirect(res.has_product_order, res.has_packaging_order); }); loadPendingList(); } else { showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败')); } }) .catch(function(err) { showCenteredAlert('网络错误', '❌ 网络错误: ' + (err.message || '请求失败')); }); }); } function pendingConfirmOrder(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; // 携带前端已保存的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) { showCenteredAlert('操作成功', '✅ 下单成功', function() { showConfirmRedirect(res.has_product_order, res.has_packaging_order); }); loadPendingList(); } else { showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败')); } }) .catch(function(err) { showCenteredAlert('网络错误', '❌ 网络错误: ' + (err.message || '请求失败')); }); }); } // ─── 下单备注保存 ─── window.remarkEnterSave = function(e) { if (e.key === 'Enter') { e.preventDefault(); e.target.blur(); } }; window.savePendingRemark = function(input) { 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', '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:'下单时间', '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', '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); }; 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(); }; 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', '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; } var BACKUP_COL_ORDER = ['index','product_type','product_code','product_name', 'ordered_at','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:'操作' }; window.openBackupColModal = function() { if (!window._backupColOrder) { window._backupColOrder = BACKUP_COL_ORDER.slice(); } var order = window._backupColOrder.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') return; var label = BACKUP_COL_LABELS[field] || field; var checked = window._backupColVisible[field] !== false ? ' checked' : ''; bodyHtml += ''; }); bodyHtml += '
' + '
' + '' + '' + '
'; modal.innerHTML = bodyHtml; document.body.appendChild(modal); }; window.saveBackupColSettings = function(modalId) { document.querySelectorAll('#' + modalId + '-body .backup-col-cb').forEach(function(cb) { window._backupColVisible[cb.dataset.field] = cb.checked; }); document.getElementById(modalId).remove(); 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 = [ { name: '跟单一组', leader: '方园霞', members: ['方园霞', '邝碧君', '余瑞霞', '卢浩然'], color: '#e6f4ff', tagColor: '#1677ff' }, { name: '跟单二组', leader: '王加飞', members: ['许晓珊', '刘伟', '王加飞', '邓文婷'], color: '#f0f5ff', tagColor: '#722ed1' }, { name: '跟单三组', leader: '蔡少铃', members: ['闫伟华', '蔡少铃', '颜于凯', '张文菁'], color: '#e6fffb', tagColor: '#13c2c2' }, { name: '现货小组', leader: '', members: ['莫碧霞', '范亚欢'], color: '#fff7e6', tagColor: '#fa8c16' }, { name: '计划小组', leader: '汤定毅', members: ['汤定毅', '李媛', '黄静', '胡孟怡', '张莹纯', '范世澄', '陈瑜'], color: '#f6ffed', tagColor: '#52c41a' } ]; // 构建名字→分组 快速查找 window._nameToGroup = {}; window._personnelGroups.forEach(function(g) { g.members.forEach(function(m) { _nameToGroup[m] = g; }); }); function renderNameBadges(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 group = null; var allInSameGroup = true; nameList.forEach(function(name) { var g = _nameToGroup[name]; if (!g || g.name === '计划小组') { 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) { var current = (row['下单人'] || '').trim(); if (domesticFollowers.length === 0) { return '-'; } var initBg = '#fff', initFc = '#333'; if (current && domesticFollowers.indexOf(current) >= 0) { initBg = colorForName(current) || '#e6f7ff'; initFc = textColorForBG(initBg); } var html = ''; return html; } function toggleBom(btn, productCode) { var tr = btn.closest('tr'); var codeSafe = productCode.replace(/[^a-zA-Z0-9]/g, '_'); var bomRowId = 'bom-row-' + codeSafe; var existing = document.getElementById(bomRowId); if (existing) { existing.remove(); btn.textContent = '📋 BOM'; return; } // 查找BOM数据 var item = null; (_pendingItems || []).forEach(function(it) { if (it.product_code === productCode) item = it; }); var bom = item ? item.bom_items : null; var bomList = []; if (bom) { try { bomList = typeof bom === 'string' ? JSON.parse(bom) : bom; } catch(e) {} } if (!bomList || bomList.length === 0) { alert('该商品暂无物料清单'); return; } var formalQty = item ? (item.formal_order_qty || 0) : 0; // BOM内容 var bomContent = '
'; bomContent += ''; bomList.forEach(function(b) { var matCode = escHtml(b.material_code || ''); var qty = b.quantity || 0; var orderQty = Math.ceil(formalQty * qty); // 使用已保存的实际下单数量(如有) var actualQty = (b.actual_quantity !== undefined && b.actual_quantity !== null) ? b.actual_quantity : orderQty; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; bomContent += ''; }); bomContent += '
物料编号物料名称用量建议下单数量实际下单数量下单前包材下单后包材下单备注更换稿件操作
' + matCode + '' + escHtml(b.material_name || '') + '' + qty + '
'; // 弹窗展示 var modalId = 'bom-modal-' + Date.now(); var modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; modal.innerHTML = '
' + '
' + '

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

' + '' + '
' + '
' + bomContent + '
' + '
' + '' + '' + '
'; modal.onclick = function(e) { if (e.target === modal) modal.remove(); }; document.body.appendChild(modal); } window.saveBomData = function(productCode) { var inputs = document.querySelectorAll('.bom-actual-qty[data-product="' + productCode + '"]'); var materials = []; // 读取成品下单信息 var productActualInput = document.querySelector('.bom-product-actual-qty[data-product="' + productCode + '"]'); var productRemarkInput = document.querySelector('.bom-product-remark[data-product="' + productCode + '"]'); var productActualQty = productActualInput ? (parseInt(productActualInput.value) || 0) : 0; var productRemark = productRemarkInput ? productRemarkInput.value.trim() : ''; // 保存成品数据(单独存,允许为0) materials.push({ material_code: '成品_' + productCode, actual_quantity: productActualQty, remark: productRemark }); inputs.forEach(function(inp) { var qty = parseInt(inp.value) || 0; { var matCode = inp.getAttribute('data-material'); // 读取同行建议下单数量 var orderInput = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); var bomQty = orderInput ? (parseInt(orderInput.value) || 0) : 0; // 读取物料名称(从BOM弹窗内的表格行中获取) var matName = ''; if (orderInput) { var row = orderInput.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) matName = nameCell.textContent.trim(); } } // 读取同行备注 var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); var remarkVal = remarkInput ? remarkInput.value.trim() : ''; // 读取是否修改稿件 var manuscriptSelect = document.querySelector('.bom-manuscript[data-material="' + matCode + '"]'); var manuscriptVal = manuscriptSelect ? manuscriptSelect.value : ''; materials.push({ material_code: matCode, material_name: matName, quantity: bomQty, actual_quantity: qty, remark: remarkVal, change_manuscript: manuscriptVal }); } }); // 过滤完所有物料后,检查是否至少有一条物料记录 var nonProductMaterials = materials.filter(function(m) { return m.material_code.indexOf('成品_') !== 0; }); if (materials.length === 0 || nonProductMaterials.length === 0) { alert('没有需要保存的物料'); return; } // 存到前端变量,下单时一起提交 if (!window._pendingBomData) window._pendingBomData = {}; window._pendingBomData[productCode] = materials; // 关闭BOM弹窗 var bomInput = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"]'); if (bomInput) { var modal = bomInput.closest('[style*="z-index: 10000"]') || bomInput.closest('[id^="bom-modal-"]'); if (modal) modal.remove(); } // 持久化到后端:BOM数据和备注同步到 pending_orders var updatePayload = {product_code: productCode, bom_items: materials}; if (productRemark) updatePayload.remark = productRemark; fetch('/api/purchase/update_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(updatePayload) }).then(function(r) { return r.json(); }).then(function(res) { if (res.code !== 0) console.warn('同步BOM数据失败:', res.msg); }).catch(function(e) { console.warn('同步BOM数据出错:', e); }); showCenteredAlert('保存成功', '✅ BOM已保存并同步到数据库,确认下单将使用最新包材数据'); }; // ─── 计划预测 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)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data) { alert('❌ 获取物料清单失败'); return; } var bom = res.data.bom_materials || []; if (bom.length === 0) { alert('该商品暂无物料清单'); return; } // 获取正式下单数量(从计划预测页面元素获取) var formalQtySpan = document.getElementById('fqv-' + productCode); var formalQty = formalQtySpan ? (parseInt(formalQtySpan.dataset.val) || 0) : 0; var modalId = 'bom-modal-' + Date.now(); var productSummaryHtml = '
' + '
📦 成品下单信息
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
货品编号货品名称建议下单数量实际下单数量下单备注
' + escHtml(productCode) + '' + escHtml(productName) + '' + suggestedQty + '
' + '
'; var bomContent = productSummaryHtml + '
' + '
📦 包材下单信息
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; // 读取之前保存的包材数据 var savedBom = window._pendingBomData && window._pendingBomData[productCode] ? window._pendingBomData[productCode] : []; var savedMap = {}; var savedRemarkMap = {}; var savedManuscriptMap = {}; savedBom.forEach(function(s) { savedMap[s.material_code] = s.actual_quantity; savedRemarkMap[s.material_code] = s.remark || ''; savedManuscriptMap[s.material_code] = s.change_manuscript || ''; }); bom.forEach(function(b) { // 过滤掉含有内料的包材 if (b.material_name && b.material_name.indexOf('内料') !== -1) return; 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; bomContent += '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }); bomContent += '
物料编号物料名称用量建议下单数量实际下单数量下单前包材下单后包材下单备注更换稿件
' + matCode + '' + escHtml(b.material_name || '') + '' + qty + '
'; 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); // 恢复已保存的更换稿件状态 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-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); if (orderInput) { m.quantity = parseInt(orderInput.value) || 0; var row = orderInput.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) m.material_name = nameCell.textContent.trim(); } } var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); if (remarkInput) m.remark = remarkInput.value.trim(); found = true; } }); if (!found && qty > 0) { // 读取同行建议下单数量、物料名称和备注 var orderInput = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]'); var bomQty = orderInput ? (parseInt(orderInput.value) || 0) : 0; var matName = ''; var remarkVal = ''; if (orderInput) { var row = orderInput.closest('tr'); if (row) { var nameCell = row.querySelector('td:nth-child(2)'); if (nameCell) matName = nameCell.textContent.trim(); } } // 读取备注 var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]'); if (remarkInput) remarkVal = remarkInput.value.trim(); window._pendingBomData[productCode].push({ material_code: matCode, material_name: matName, quantity: bomQty, actual_quantity: qty, remark: remarkVal }); } inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 800); // 跳到下一行 var nextInput = actualInputs[idx + 1]; if (nextInput) { nextInput.focus(); nextInput.select(); } // 没有下一行:不做处理,保存完成 } }); }); // 成品下单信息 - 实际下单数量:点击全选 + 回车保存 var productActualInputs = modal.querySelectorAll('.bom-product-actual-qty'); productActualInputs.forEach(function(inp) { inp.addEventListener('focus', function() { this.select(); }); inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); var newQty = parseInt(inp.value) || 0; showCenteredConfirm('确认保存', '确认保存成品实际下单数量为 ' + newQty + ' 吗?保存后将同步更新下方包材的实际下单数量。').then(function(confirmed) { if (!confirmed) return; // 保存成品数量 if (!window._pendingBomData) window._pendingBomData = {}; if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = []; var found = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === '成品_' + productCode) { m.actual_quantity = newQty; found = true; } }); if (!found) { window._pendingBomData[productCode].push({ material_code: '成品_' + productCode, material_name: '', quantity: 0, actual_quantity: newQty, remark: '' }); } // 退出编辑状态 inp.blur(); inp.style.borderColor = '#52c41a'; setTimeout(function() { inp.style.borderColor = ''; }, 800); // 同步更新下方包材实际下单数量 if (bom && bom.length > 0) { var materialActualInputs = modal.querySelectorAll('.bom-actual-qty'); materialActualInputs.forEach(function(mInp) { var matCode = mInp.getAttribute('data-material'); // 查找该物料的BOM用量 var bomQty = 0; bom.forEach(function(b) { if (b.material_code === matCode) bomQty = b.bom_qty || 0; }); if (bomQty > 0) { var newMatQty = Math.ceil(newQty * bomQty); mInp.value = newMatQty; // 更新存储 var fMat = false; window._pendingBomData[productCode].forEach(function(m) { if (m.material_code === matCode) { m.actual_quantity = newMatQty; var orderInput = document.querySelector('.bom-order-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 && newMatQty > 0) { var oInp = document.querySelector('.bom-order-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 }); } } }); 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('❌ 网络错误'); }); } // 计划预测 BOM - 生成物料订单(不依赖 _pendingItems) function planGenerateMatOrder(productCode, materialCode, materialName) { var input = document.querySelector('.bom-order-qty[data-material="' + materialCode + '"][data-product="' + productCode + '"]'); var qty = input ? parseInt(input.value) || 0 : 0; if (qty <= 0) { alert('请先填写有效的下单数量'); return; } if (!confirm('确认生成物料 [' + materialCode + '] 的采购订单,数量:' + qty + '?')) 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-order-qty[data-material="' + materialCode + '"][data-product="' + productCode + '"]'); var qty = input ? parseInt(input.value) || 0 : 0; if (qty <= 0) { alert('请先填写有效的下单数量'); return; } if (!confirm('确认生成物料 [' + materialCode + '] 的采购订单,数量:' + qty + '?')) 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; }); // 调用后端生成物料采购订单 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) { 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) { 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 : []; // 过滤掉内料,只显示非内料包材 bom = bom.filter(function(b) { return !(b.material_name && b.material_name.indexOf('内料') !== -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 + '
'; container.innerHTML = tableHtml; // 恢复已保存的更换稿件状态 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 = parseInt(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 tableRemarkInput = document.querySelector('input[data-pcode="' + productCode + '"]'); var latestRemark = tableRemarkInput ? tableRemarkInput.value : (curItem ? (curItem.remark || '') : ''); // 从表格行输入框读取最新正式下单数量 // 在pendingTable中,正式下单数量的input没有data属性,只能从缓存获取 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: latestRemark, bom_items: JSON.stringify(toSave) }; 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 (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) { 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) { 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}) }); } // ─── 采购订单-下单备份管理 ─── var _backupItems = []; var _backupAllItems = []; // 原始全量数据(未受搜索过滤) var backupOrderFilter = {}; // 表头筛选状态 var backupFilterMenu = null; // 当前筛选弹窗 var backupFilterField = ''; // 当前筛选字段 // 备份列中文名→数据库字段名映射 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'}, ]; var BACKUP_PAGE_SIZE = 50; var backupCurrentPage = 1; function loadPurchaseReadyPage(container) { container.innerHTML = `

📋 已下单记录

序号 产品类型 ▾ 货品编号 ▾ 货品名称 ▾ 下单时间 下单人 ▾ 生产单号 ▾ 建议下单数量 ▾ 正式下单数量 ▾ 产品分级 ▾ 供应商名称 ▾ 下单备注 ▾ 货期 ▾ 物流 ▾ 总支撑天数 ▾ 采购跟单 ▾ 采购计划 ▾ 平台 ▾ 运营 ▾ 是否审核价格 ▾ 操作
`; loadBackupList(); if (!window._pageReloaders) window._pageReloaders = {}; if (!window._pageDataCache) window._pageDataCache = {}; window._pageReloaders['purchase_ready'] = function() { loadBackupList(); }; window._pageDataCache['purchase_ready'] = { _ts: Date.now() }; } function clearBackupFilter() { ['backupSearch','backupOrderer','backupPlanner'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); backupOrderFilter = {}; _syncBackupFilterIcons(); _renderFilteredBackup(); } function loadBackupList(resetPage) { if (resetPage !== false) backupCurrentPage = 1; var tbody = document.getElementById('backupTbody'); if (!tbody) return; tbody.innerHTML = '
'; fetch('/api/purchase/pending_list?status=ordered') .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) return; _backupAllItems = res.data || []; // 保存原始全量数据 _backupItems = _backupAllItems.slice(); // 搜索过滤前先复制 invalidateOrderColumnCache('backup'); // 填充下拉选项 ['backupOrderer','backupPlanner'].forEach(function(selId) { var sel = document.getElementById(selId); if (!sel) return; var currentVal = sel.value; var field = selId === 'backupOrderer' ? 'created_by' : 'procurement_planner'; var values = {}; _backupAllItems.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) { return items.map(function(item, idx) { var code = escHtml(item.product_code); return '' + '' + (startIdx + idx + 1) + '' + '' + escHtml(item.product_type || '-') + '' + '' + code + '' + '' + escHtml(item.product_name || '-') + '' + '' + ((item.ordered_at || '').toString().slice(0, 19) || '-') + '' + '' + escHtml(item.created_by || '-') + '' + '' + escHtml(item.production_order_no || '-') + '' + '' + (item.suggested_qty || 0) + '' + '' + (item.formal_order_qty || 0) + '' + '' + escHtml(item.product_grade || '-') + '' + '' + escHtml(item.supplier_name || '-') + '' + '' + escHtml(item.remark || '-') + '' + '' + (item.lead_time || 0) + '' + '' + (item.logistics_time || 0) + '' + '' + (item.total_support_days || 0) + '' + '' + renderNameBadges(item.procurement_follower) + '' + '' + renderNameBadges(item.procurement_planner) + '' + '' + escHtml(item.platform || '-') + '' + '' + renderNameBadges(item.operator) + '' + '' + (item.audit_price || 0) + '' + '' + ' ' + ' ' + '' + '' + ''; }).join(''); } function _applyBackupSearch(items) { var kw = (document.getElementById('backupSearch')?.value || '').trim().toLowerCase(); var orderer = (document.getElementById('backupOrderer')?.value || '').trim().toLowerCase(); var planner = (document.getElementById('backupPlanner')?.value || '').trim().toLowerCase(); if (!kw && !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 (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(backupOrderFilter); if (keys.length === 0) return items; return items.filter(function(item) { return keys.every(function(f) { var fp = backupOrderFilter[f]; if (!fp) return true; var v = String(item[_backupFieldMap[f] || f] || ''); if (fp.type === 'search') { return v.toLowerCase().indexOf((fp.value || '').toLowerCase()) >= 0; } if (fp.type === 'list') { 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 items = _backupAllItems.slice(); // 从原始全量数据开始 items = _applyBackupSearch(items); // 先应用搜索栏筛选 items = _applyBackupHeaderFilter(items); // 再应用表头筛选 _backupItems = items; var countEl = document.getElementById('backupCount'); if (countEl) countEl.textContent = '共 ' + items.length + ' / ' + _backupAllItems.length + ' 条'; var tbody = document.getElementById('backupTbody'); if (!tbody) return; if (items.length === 0) { tbody.innerHTML = '暂无匹配数据'; return; } var total = items.length; var totalPages = Math.ceil(total / BACKUP_PAGE_SIZE); if (backupCurrentPage > totalPages) backupCurrentPage = totalPages; if (backupCurrentPage < 1) backupCurrentPage = 1; var startIdx = (backupCurrentPage - 1) * BACKUP_PAGE_SIZE; var pageItems = items.slice(startIdx, startIdx + BACKUP_PAGE_SIZE); tbody.innerHTML = _backupRowsHtml(pageItems, startIdx); var pel = document.getElementById('backupPagination'); if (pel && typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, backupCurrentPage, 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); if (colType === 'number') { _buildBackupNumericFilter(field, label, rect); } 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(_backupAllItems); searchFiltered.forEach(function(item) { var v = String(item[_backupFieldMap[field] || field] || '').trim(); 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) { 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 sel = backupOrderFilter[field] || {}; allChecked = true; var html = '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var checked = (sel[v] !== false) ? 'checked' : ''; html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); backupFilterMenu = menu; } function _buildBackupNumericFilter(field, label, rect) { var menu = document.createElement('div'); menu.id = 'backupFilterMenu'; var current = backupOrderFilter[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var html = '
' + escHtml(label) + '
'; 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:200px;font-size:13px'; document.body.appendChild(menu); backupFilterMenu = menu; } function _populateBackupFilterList(field, values) { var listEl = document.getElementById('backupFilterList'); if (!listEl) return; var sel = backupOrderFilter[field] || {}; var html = '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = (sel[v] !== false) ? 'checked' : ''; 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 ? '' : '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) { backupOrderFilter[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 backupOrderFilter[field]; } else { backupOrderFilter[field] = {type: 'list', values: checked}; } } if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } function applyBackupNumFilter() { var field = backupFilterField; var cond = document.getElementById('backupNumCond')?.value || ''; var val = document.getElementById('backupNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } backupOrderFilter[field] = {type: 'num', cond: cond, val: val}; if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } function clearBackupHeaderFilter() { delete backupOrderFilter[backupFilterField]; if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; } _syncBackupFilterIcons(); _renderFilteredBackup(); } function _syncBackupFilterIcons() { var icons = document.querySelectorAll('#backupTable .bk-filt-icon'); icons.forEach(function(icon) { var field = icon.parentNode.textContent.trim().replace(/▾/, '').trim(); var hasFilter = backupOrderFilter[field]; icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = hasFilter ? 'bold' : 'normal'; }); } function loadBackupListPage(page) { backupCurrentPage = page; _renderFilteredBackup(); } function loadBackupListPage(page) { backupCurrentPage = page; loadBackupList(false); } // ─── 下单备份管理:详情+删除 ─── window._backupItems = []; window.viewBackupDetail = function(code) { var item = null; (_backupItems || []).forEach(function(it) { if (it.product_code === code) item = it; }); if (!item) { alert('未找到数据'); return; } // 解析包材数据 var bomList = []; try { var raw = item.bom_items; if (raw) { bomList = typeof raw === 'string' ? JSON.parse(raw) : (Array.isArray(raw) ? raw : []); } } catch(e) { bomList = []; } 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] ]; // 第二列字段 var col2Fields = [ ['建议下单数量', item.suggested_qty], ['实际下单数量', item.actual_order_qty], ['下单人', item.created_by], ['下单时间', (item.ordered_at || '').toString().slice(0, 19)], ['下单备注', item.remark], ['生产单号', item.production_order_no || ''] ]; html += '
'; html += ''; for (var g = 0; g < 6; g++) { var f1 = col1Fields[g]; var f2 = col2Fields[g]; html += ''; // 第一列 html += ''; html += ''; // 第二列 var isProdNo = f2[0] === '生产单号'; html += ''; if (isProdNo) { html += ''; } else { html += ''; } html += ''; } html += '
' + f1[0] + '' + escHtml(String(f1[1] || '-')) + '' + f2[0] + '' + escHtml(String(f2[1] || '-')) + '
'; // ─── 板块2:包材信息 ─── html += '

📦 包材信息

'; if (bomList.length > 0) { html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; bomList.forEach(function(b) { html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '' html += ''; }); html += '
物料编号物料名称下单数量备注更换稿件采购单号
' + escHtml(b.material_code || '-') + '' + escHtml(b.material_name || '-') + '' + (b.actual_quantity || 0) + '' + escHtml(b.remark || '-') + '' + (b.change_manuscript || '-') + '
'; } else { html += '
暂无包材数据
'; } html += '
'; html += '
'; html += '
修改后点击保存按钮可同时保存生产单号和采购单号
'; html += '
'; html += ''; html += '
'; var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = '
' + html + '
'; document.body.appendChild(overlay); }; // 包材生产单号回车保存 window.backupProdNoKeydown = function(e, input, materialCode) { 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}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) showCenteredAlert('保存失败', res.msg || '保存失败'); }); } }; // 保存成品生产单号 window.saveBackupProdNo = function(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}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { inp.blur(); // 更新缓存数据 (_backupItems || []).forEach(function(it) { if (it.product_code === code) it.production_order_no = val; }); // 同步更新表格中的生产单号列 var tbody = document.getElementById('backupTbody'); 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'); 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}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { input.blur(); // 同步更新前端缓存 (_backupItems || []).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(); } showCenteredAlert('保存成功', '✅ 采购单号已保存'); } else { input.style.borderColor = '#f5222d'; setTimeout(function(){ input.style.borderColor = ''; }, 1500); } }); }; // 保存生产单号+采购单号(统一保存按钮) window.saveBackupAll = function(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) { orderInputs.push({input: inp, productCode: pc, materialCode: mc, val: inp.value.trim()}); } }); } // 3. 依次保存 var promises = []; // 保存生产单号 if (prodNoInp) { var prodVal = prodNoInp.value.trim(); promises.push( fetch('/api/purchase/backup/update_prod_no', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, production_order_no: prodVal}) }).then(function(r){return r.json()}) ); } // 保存采购单号 orderInputs.forEach(function(item) { promises.push( 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}) }).then(function(r){return r.json()}) ); }); // 4. 全部完成后刷新缓存并提示 Promise.all(promises).then(function(results) { 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() : ''; (_backupItems || []).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 = document.getElementById('backupTbody'); 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(); } showCenteredAlert('保存成功', '✅ 生产单号、采购单号已保存'); } }).catch(function() { if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; } showCenteredAlert('保存失败', '❌ 网络错误,请重试'); }); }; // 下单回退:确认弹窗 + 调用后端 window.showBackupRollbackConfirm = function(code) { showCenteredConfirm('回退确认', '确认将 [' + escHtml(code) + '] 回退到待下单列表?
' + '将从国内/海外订单、包材订单中同步删除对应数据。' ).then(function(confirmed) { if (!confirmed) return; fetch('/api/purchase/rollback_order', { 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) { loadBackupList(); } else { alert('回退失败: ' + (res.msg || '')); } }); }); }; window.deleteBackup = function(code) { showCenteredConfirm('删除确认', '确认删除此记录?
编码: ' + escHtml(code) + '').then(function(confirmed) { if (!confirmed) return; fetch('/api/purchase/remove_pending', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_codes: [code]}) }) .then(function(r){return r.json()}) .then(function(res){ if (res.code === 0) { loadBackupList(); } else { showCenteredAlert('删除失败', '删除失败: ' + (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; 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] = 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 = []; // 采购跟单人员列表 // 冻结列索引列表(默认冻结前6列),存JSON数组 // 冻结列默认值 var domesticFrozenFields = [0,1,2]; var domesticFrozen = true; // ─── SKU详情弹窗(点击编码/SKU) ─── window.showSkuInfoModal = function(el, title) { var code = el.textContent.trim(); if (!code) return; // 显示加载中的弹窗 var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center;overflow-y:auto;padding:20px'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = ''; document.body.appendChild(overlay); // 通过API获取商品详情 fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(code)) .then(function(r) { return r.json(); }) .then(function(res) { if (!res.data) { overlay.innerHTML = ''; return; } var d = res.data && res.data.product || {}; var fields = []; if (d.product_code) fields.push(['货品编号', d.product_code]); if (d.product_name) fields.push(['货品名称', d.product_name]); if (d.brand) fields.push(['品牌', d.brand]); if (d.product_type) fields.push(['产品类型', d.product_type]); if (d.product_grade) fields.push(['产品分级', d.product_grade]); if (d.cost_price) fields.push(['成本价', d.cost_price]); if (d.supplier_name) fields.push(['供应商', d.supplier_name]); if (d.procurement_follower) fields.push(['采购跟单', d.procurement_follower]); if (d.procurement_planner) fields.push(['采购计划', d.procurement_planner]); if (d.developer) fields.push(['开发专员', d.developer]); if (d.operator) fields.push(['运营专员', d.operator]); if (d.platform) fields.push(['平台', d.platform]); if (d.lead_time) fields.push(['生产货期', d.lead_time + '天']); if (d.logistics_time) fields.push(['物流时间', d.logistics_time + '天']); if (d.min_order_qty) fields.push(['最低起订量', d.min_order_qty]); if (d.created_at) fields.push(['创建时间', d.created_at]); var bomCount = (res.data.bom_materials || []).length; if (bomCount > 0) fields.push(['关联包材', bomCount + ' 种']); // 调试:记录API返回 if (typeof console !== 'undefined') { var _debugTransit = res.data.orders_in_transit || []; console.log('[SKU] ' + code + ' 在途订单: ' + _debugTransit.length + ' 条'); } // 构建弹窗HTML var html = ''; overlay.innerHTML = html; }) .catch(function() { overlay.innerHTML = ''; }); } // ─── 订单详情弹窗(通用) ─── 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; if (title && title.indexOf('海外') !== -1) { apiUrl = '/api/overseas/orders/update'; refreshFn = 'overseasLoadOrders'; col1 = [ {label:'产品类型', field:'product_type', editable:false}, {label:'货品编号', field:'产品编码', editable:false}, {label:'供应商名称', field:'供应商', editable:false}, {label:'计划下单时间', field:'计划下单时间', editable:false}, {label:'单价', field:'单价', editable:true}, {label:'采购跟单', field:'采购员', editable:true}, {label:'订单所属平台', field:'platform', editable:true}, {label:'金蝶是否开单', field:'是否已做采购单', editable:true, type:'select', options:['是','否']}, {label:'旺店通是否已录入', field:'是否已录入到旺店通', editable:true, type:'select', options:['是','否']}, {label:'预计到货时间', field:'预计到货日期', editable:true, type:'date'}, {label:'订单完结状态', field:'状态', editable:true, type:'select', options:['已完结','未完结','已取消']} ]; col2 = [ {label:'产品种类', field:'product_grade', editable:false}, {label:'货品名称', field:'货品名称', editable:false}, {label:'生产货期', field:'lead_time', editable:false}, {label:'订单下达时间', field:'订单下达供应商时间', editable:true, type:'date'}, {label:'数量', field:'数量', editable:true}, {label:'采购计划', field:'procurement_planner', editable:false}, {label:'产品开发', field:'采购开发', editable:false}, {label:'金蝶是否入库', field:'是否做入库', editable:true, type:'select', options:['是','否']}, {label:'旺店通单号', field:'旺店通生产单号', editable:true}, {label:'待提数量', field:'待提数量', editable:true}, {label:'云仓库存支撑天数', field:'total_support_days', editable:true} ]; } else if (title && title.indexOf('包材') !== -1) { apiUrl = '/api/packaging/orders/update'; refreshFn = 'packagingLoadOrders'; col1 = [ {label:'货品编号', field:'编码', editable:false}, {label:'供应商名称', field:'供应商', editable:true}, {label:'采购计划', field:'采购计划', editable:false}, {label:'单价', field:'单价', editable:true}, {label:'计划下单时间', field:'下单时间', editable:false}, {label:'产品种类', field:'产品种类', editable:true, type:'select', options:['新品','返单品','迭代品']}, {label:'金蝶是否开单', field:'是否已做采购单', editable:true, type:'select', options:['是','否']}, {label:'旺店通是否录入', field:'是否已录入旺店通', editable:true, type:'select', options:['是','否']}, {label:'状态', field:'状态', editable:true, type:'select', options:['已完成','分批送货']} ]; col2 = [ {label:'货品名称', field:'名称', editable:false}, {label:'到货仓库', field:'仓库到货', editable:true}, {label:'包材开发', field:'包材开发', editable:false}, {label:'数量', field:'数量', editable:true}, {label:'订单下达时间', field:'订单下达时间', editable:true, type:'date'}, {label:'有无新稿件', field:'有无新稿件', editable:true, type:'select', options:['有','无']}, {label:'旺店通编号', field:'旺店通编号', editable:false}, {label:'金蝶云是否录入', field:'是否已做入库', editable:true, type:'select', options:['是','否']}, {label:'包材异常状态', field:'包材异常状态', editable:true} ]; } else { apiUrl = '/api/domestic/orders/update'; refreshFn = 'domesticLoadOrders'; col1 = [ {label:'产品类型', field:'product_type', editable:false}, {label:'货品编号', field:'编码', editable:false}, {label:'供应商名称', field:'供货商', editable:true}, {label:'计划下单时间', field:'计划下单时间', editable:false}, {label:'单价', field:'单价', editable:true}, {label:'采购跟单', field:'采购员', editable:true}, {label:'订单所属平台', field:'platform', editable:true}, {label:'金蝶是否开单', field:'金蝶采购单', editable:true, type:'select', options:['是','否']}, {label:'旺店通是否已录入', field:'是否已录入旺店通', editable:true, type:'select', options:['是','否']}, {label:'预计到货时间', field:'预计到货日期', editable:true, type:'date'}, {label:'订单完结状态', field:'状态', editable:true, type:'select', options:['已完结','未完结','已取消']} ]; col2 = [ {label:'产品种类', field:'product_grade', editable:false}, {label:'货品名称', field:'名称', editable:false}, {label:'生产货期', field:'lead_time', editable:false}, {label:'订单下达时间', field:'订单下达供应商时间', editable:true, type:'date'}, {label:'实际下单数量', field:"数量", editable:true}, {label:'采购计划', field:'procurement_planner', editable:false}, {label:'产品开发', field:'采购开发', editable:false}, {label:'金蝶是否入库', field:'金蝶入库', editable:true, type:'select', options:['是','否']}, {label:'旺店通单号', field:'旺店通生产单号', editable:true}, {label:'待提数量', field:'待提数量', editable:true}, {label:'云仓库存支撑天数', field:'total_support_days', editable:true} ]; } var renderValue = function(cfg, val) { if (val === null || val === undefined) val = ''; var sv = String(val); if (!cfg.editable) { return '' + escHtml(sv || '-') + ''; } if (cfg.type === 'select' && cfg.options) { var opts = cfg.options.map(function(o) { var sel = (o === sv) ? ' selected' : ''; return ''; }).join(''); return ''; } if (cfg.type === 'date') { // 去掉时间部分,只取日期 var dateVal = window._padDateStr(sv.split(' ')[0]); return ''; } return ''; }; var html = '
'; html += '
'; col1.forEach(function(cfg) { var val = row[cfg.field]; html += '
' + escHtml(cfg.label) + '
' + renderValue(cfg, val) + '
'; }); html += '
'; html += '
'; col2.forEach(function(cfg) { var val = row[cfg.field]; html += '
' + escHtml(cfg.label) + '
' + renderValue(cfg, val) + '
'; }); html += '
'; html += '
'; // 进度字段(全宽、多行文本框) var progressVal = row['进度']; if (progressVal === null || progressVal === undefined) progressVal = ''; html += '
进度
'; // 到货批次及数量列表(动态加载) html += '
'; html += '
'; html += '

📋 到货批次及数量

'; html += '
'; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
批次编号到货日期到货数量订单未完结数量备注
合计00
'; html += '
'; // 包材信息(动态加载) 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['编码'] || ''; fetch('/api/domestic/orders/detail_extras?order_id=' + encodeURIComponent(orderId) + '&product_code=' + encodeURIComponent(productCode)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code !== 0) { var abBody = overlay.querySelector('#arrivalBatchBody'); if (abBody) abBody.innerHTML = '暂无数据'; var pmBody = overlay.querySelector('#packagingMaterialBody'); if (pmBody) pmBody.innerHTML = '暂无数据'; return; } var data = res.data; // 到货批次 var abBody = overlay.querySelector('#arrivalBatchBody'); var totalQtyEl = overlay.querySelector('#arrivalTotalQty'); if (abBody) { var abHtml = ''; var totalQty = 0; var batchCount = data.arrival_batches ? data.arrival_batches.length : 0; // 数据行 data.arrival_batches.forEach(function(b, idx) { var qty = parseInt(b.arrival_qty) || 0; totalQty += qty; var dateVal = (b.arrival_date || '').substring(0, 10); var isManual = (b.from_type === 'add'); var batchId = b.id || ''; abHtml += ''; abHtml += '' + (idx + 1) + ''; abHtml += '' + (isManual ? '' : '') + '' + '0' + '
' + '' + (isManual ? '' : '') + '
'; abHtml += ''; }); // 补齐最少3行空白占位(可编辑) var placeholderRows = Math.max(0, 3 - batchCount); for (var i = 0; i < placeholderRows; i++) { abHtml += ''; abHtml += '' + (batchCount + i + 1) + ''; abHtml += ''; abHtml += ''; abHtml += '-'; abHtml += '
'; abHtml += ''; } abBody.innerHTML = abHtml; if (totalQtyEl) totalQtyEl.textContent = totalQty; recalcArrivalUnfinished(); } // 包材信息 var pmBody = overlay.querySelector('#packagingMaterialBody'); if (pmBody && data.packaging_materials) { if (data.packaging_materials.length === 0) { pmBody.innerHTML = '无关联包材'; } else { var pmHtml = ''; data.packaging_materials.forEach(function(m) { pmHtml += ''; pmHtml += '' + escHtml(m.order_time || '-') + ''; pmHtml += '' + escHtml(m.material_code || '-') + ''; pmHtml += '' + escHtml(m.material_name || '-') + ''; pmHtml += '' + m.order_qty + ''; pmHtml += '' + m.arrival_qty + ''; pmHtml += '' + escHtml(m.arrival_date || '-') + ''; pmHtml += '📊 ' + escHtml(m.progress || '-') + ''; pmHtml += ''; }); pmBody.innerHTML = pmHtml; } } }) .catch(function(err) { console.error('加载详情额外数据失败:', err); var abBody = overlay.querySelector('#arrivalBatchBody'); if (abBody) abBody.innerHTML = '加载失败'; var pmBody = overlay.querySelector('#packagingMaterialBody'); if (pmBody) pmBody.innerHTML = '加载失败'; }); })(); } function addArrivalBatchRow(btn) { btn.disabled = true; btn.textContent = '⋯'; var overlay = btn.closest('.modal-overlay'); if (!overlay) { var tbl = btn.closest('table'); if (tbl) overlay = tbl.closest('.modal-overlay'); } var orderId = btn.getAttribute('data-order-id'); var tbody = overlay.querySelector('#arrivalBatchBody'); if (!tbody) { btn.disabled = false; btn.textContent = '➕ 添加一行'; return; } if (tbody.querySelector('.batch-editing')) { btn.disabled = false; btn.textContent = '➕ 添加一行'; return; } // 计算下一个批次编号 var lastNum = 0; var rows = tbody.querySelectorAll('tr:not(.batch-placeholder)'); rows.forEach(function(r) { var firstTd = r.querySelector('td'); if (firstTd) { var num = parseInt(firstTd.textContent); if (!isNaN(num) && num > lastNum) lastNum = num; } }); var nextNum = Math.max(lastNum + 1, 4); // 插入编辑行(在最后一个数据行之后,占位行之前) var tr = document.createElement('tr'); tr.className = 'batch-editing'; tr.innerHTML = '' + '' + nextNum + '' + '' + '' + '' + '
' + '' + '' + '' + '
'; // 追加到表格末尾(合计行上方) tbody.appendChild(tr); recalcArrivalUnfinished(); var inp = tr.querySelector('input[type="date"]'); if (inp) inp.focus(); } /** * 取消新增行(删除未保存的行) */ function cancelArrivalBatchRow(btn) { if (!confirm('确认取消本行?未保存的内容将丢失。')) 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 saveNewArrivalBatch(btn, orderId) { var tr = btn.closest('tr'); if (!tr) return; var date = tr.querySelector('.batch-date')?.value || ''; var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0; var remark = tr.querySelector('.batch-remark')?.value || ''; if (!date && !qty) { alert('请输入到货日期或数量'); return; } btn.disabled = true; btn.textContent = '⋯'; fetch('/api/domestic/orders/add_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_id: orderId, arrival_date: date, arrival_qty: qty, remark: remark}) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { var batchNum = ''; var firstTd = tr.querySelector('td'); if (firstTd) batchNum = firstTd.textContent || ''; var batchId = res.data && res.data.id || ''; tr.className = ''; tr.innerHTML = '' + '' + batchNum + '' + '' + '' + '0' + '
'; // 更新合计 var totalEl = tr.closest('.modal-overlay').querySelector('#arrivalTotalQty'); if (totalEl) { var cur = parseInt(totalEl.textContent) || 0; totalEl.textContent = cur + qty; } recalcArrivalUnfinished(); 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) { var tr = btn.closest('tr'); if (!tr) return; var date = tr.querySelector('.batch-date')?.value || ''; var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0; var remark = tr.querySelector('.batch-remark')?.value || ''; btn.disabled = true; btn.textContent = '⋯'; fetch('/api/domestic/orders/update_batch', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: batchId, arrival_date: date, arrival_qty: qty, remark: remark}) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { btn.textContent = '✅'; recalcArrivalUnfinished(); setTimeout(function() { btn.textContent = '✅'; }, 500); } else { alert('保存失败: ' + (res.msg || '未知错误')); } btn.disabled = false; }).catch(function(){ alert('网络错误'); btn.disabled = false; btn.textContent = '✅'; }); } function deleteArrivalBatch(btn, batchId) { if (!confirm('确认删除本行数据?删除后不可恢复。')) 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}) }).then(function(r){return r.json()}).then(function(res){ if (res.code === 0) { tr.remove(); recalcArrivalUnfinished(); if (overlay) { var totalEl = overlay.querySelector('#arrivalTotalQty'); if (totalEl) { var cur = parseInt(totalEl.textContent) || 0; totalEl.textContent = Math.max(0, cur - qty); } } } else { alert('删除失败: ' + (res.msg || '未知错误')); btn.disabled = false; btn.textContent = '✕'; } }).catch(function(){ alert('网络错误'); btn.disabled = false; }); } /** * 到货数量输入框回车保存 */ function batchQtyKeydown(event) { if (event.key === 'Enter') { event.preventDefault(); var tr = event.target.closest('tr'); if (!tr) return; var saveBtn = tr.querySelector('button[onclick*="saveArrivalBatch"], button[onclick*="saveNewArrivalBatch"]'); if (saveBtn) { saveBtn.click(); setTimeout(function() { event.target.blur(); }, 100); } } } /** * 滚动计算订单未完结数量 * 实际下单数量 - 累计到货数量 = 当前行未完结数量 */ function recalcArrivalUnfinished() { var table = document.getElementById('arrivalBatchTable'); if (!table) return; // 从详情表单中的实际下单数量输入框读取值 var overlay = table.closest('.modal-overlay'); var orderInput = overlay ? overlay.querySelector('.detail-input[data-field="数量"]') : null; var orderQty = orderInput ? (parseInt(orderInput.value) || 0) : (parseInt(table.getAttribute('data-order-qty')) || 0); var body = table.querySelector('#arrivalBatchBody'); if (!body) return; var rows = body.querySelectorAll('tr'); var cumulative = 0; var lastUnfinished = 0; var hasUnfinishedValue = false; rows.forEach(function(tr) { var qtyInput = tr.querySelector('.batch-qty'); var unFinishedTd = tr.querySelector('.batch-unfinished'); if (qtyInput && unFinishedTd) { var arrivalQty = parseInt(qtyInput.value) || 0; cumulative += arrivalQty; var remaining = orderQty - cumulative; if (qtyInput.getAttribute('placeholder') === '数量' && !qtyInput.value) { unFinishedTd.textContent = '-'; unFinishedTd.style.color = '#ccc'; } else { unFinishedTd.textContent = remaining; unFinishedTd.style.color = remaining < 0 ? '#f5222d' : ''; lastUnfinished = remaining; hasUnfinishedValue = true; } } }); // 合计行显示最后一个有数据行的未完结数量(总剩余) var footEl = table.querySelector('#arrivalUnfinishedTotal'); if (footEl) { // 没有填写任何到货批次时,未完结数量 = 实际下单数量(即全部未到货) var totalVal = hasUnfinishedValue ? lastUnfinished : orderQty; footEl.textContent = totalVal; footEl.style.color = totalVal < 0 ? '#f5222d' : ''; } // 更新到货数量合计(遍历所有行的 batch-qty 累加) var totalQtyEl = table.querySelector('#arrivalTotalQty'); if (totalQtyEl) { var sumQty = 0; body.querySelectorAll('tr .batch-qty').forEach(function(inp) { sumQty += parseInt(inp.value) || 0; }); totalQtyEl.textContent = sumQty; } } function saveDetailChanges(btn) { var overlay = btn.closest('.modal-overlay'); var orderId = btn.getAttribute('data-order-id'); var apiUrl = btn.getAttribute('data-api') || '/api/domestic/orders/update'; var refreshFn = btn.getAttribute('data-refresh') || 'domesticLoadOrders'; var inputs = overlay.querySelectorAll('.detail-input, .detail-textarea'); var changed = 0; var total = inputs.length; var hasError = false; var promises = []; inputs.forEach(function(inp) { var field = inp.getAttribute('data-field'); var value = inp.value; promises.push( fetch(apiUrl, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: orderId, field: field, value: value}) }).then(function(r){return r.json()}).then(function(res){ if (res.code !== 0) hasError = true; }).catch(function(){ hasError = true; }) ); }); Promise.all(promises).then(function() { btn.disabled = false; btn.textContent = '💾 保存'; if (hasError) { alert('部分字段保存失败,请重试'); } else { overlay.remove(); var pageVar = refreshFn === 'packagingLoadOrders' ? 'packagingOrderPage' : refreshFn === 'overseasLoadOrders' ? 'overseasOrderPage' : 'domesticOrderPage'; if (typeof window[refreshFn] === 'function') { window[refreshFn](window[pageVar] || 1); } } }); // 如果没有可编辑字段,直接关闭 if (total === 0) { overlay.remove(); } // 禁用按钮防重复点击 btn.disabled = true; btn.textContent = '保存中...'; } window._padDateStr = window._padDateStr || function(str) { if (!str) return str; str = str.replace(/\//g, '-'); var parts = str.split(' '); if (parts.length >= 1) { var dp = parts[0].split('-'); if (dp.length >= 2) dp[1] = ('0' + dp[1]).slice(-2); if (dp.length >= 3) dp[2] = ('0' + dp[2]).slice(-2); parts[0] = dp.join('-'); } if (parts.length >= 2) { var tp = parts[1].split(':'); if (tp.length >= 1) tp[0] = ('0' + tp[0]).slice(-2); parts[1] = tp.join(':'); } return parts.join(' '); }; function loadDomesticOrdersPage(container) { container.innerHTML = `

📦 国内采购下单明细

`; // 动态生成表头列 (function() { var cols = DOMESTIC_COLUMNS; var order = window._domesticDragOrder || cols.map(function(c) { return c.field; }); order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; if (!window.domesticColVisible[field]) return; var label = col.label; var fieldAttr = col.field; var thStyle = ''; if (fieldAttr === '货品编码') thStyle = 'min-width:120px;position:sticky;left:90px;top:0;z-index:11;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '货品名称') thStyle = 'min-width:200px;position:sticky;left:210px;top:0;z-index:11;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '日期') thStyle = 'position:sticky;left:410px;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 = ''; } container.querySelector('#domesticOrdersHeader').innerHTML += ''; }); })(); container.querySelector('#domesticOrdersHeader').innerHTML += ''; container.querySelector('#domesticOrdersHeader').innerHTML += ''; container.querySelector('#domesticOrdersTable').innerHTML += ''; // 添加供应商 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'); } // 加载采购跟单人员列表 fetch('/api/users/followers').then(function(r){return r.json()}).then(function(res){ if (res.code === 0) domesticFollowers = res.data || []; }); domesticLoadFilters(); 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 ss = document.getElementById('domesticStatusFilter'); var so = document.getElementById('domesticOrdererFilter'); (res.data.statuses || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; ss.appendChild(opt); }); (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); var domesticFilterStr = ''; if (Object.keys(domesticOrderFilter).length > 0) { domesticFilterStr = encodeURIComponent(JSON.stringify(domesticOrderFilter)); params += '&order_filter=' + domesticFilterStr; } fetch('/api/domestic/orders/list' + params) .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) { var t = window._padDateStr(r['计划下单时间'] || ''); if (t.length > 16) t = t.slice(0, 16); return ''; }, '新旧品': function(r) { return ''; }, '下单备注': function(r) { return ''; }, '单价': function(r) { return ''; }, '是否审核价格': function(r) { return ''; }, '数量': function(r) { return ''; }, '金额': function(r) { var price = parseFloat(r['单价']) || 0; var qty = parseInt(r['数量']) || 0; var amt = price * qty; return ''; }, '供货商': function(r) { return ''; }, '仓库': function(r) { return ''; }, '金蝶采购单': function(r) { return ''; }, '采购员': function(r) { return ''; }, '下单人': function(r) { return ''; }, '进度': function(r) { return ''; }, '旺店通生产单号': function(r) { return ''; }, '是否已录入旺店通': function(r) { return ''; }, '采购开发': function(r) { return ''; }, '实际到货日期': function(r) { return ''; }, '未到货数': function(r) { return ''; }, '预计到货日期': function(r) { return ''; }, '金蝶入库': function(r) { return ''; }, '状态': function(r) { return ''; }, '实际到货数量': function(r) { return ''; } }; data.rows.forEach(function(r, i) { var rowNum = startIdx + i + 1; html += ''; html += ''; html += ''; // 按列顺序和可见性渲染单元格 var order = window._domesticDragOrder || DOMESTIC_COLUMNS.map(function(c) { return c.field; }); order.forEach(function(field) { if (!window.domesticColVisible[field]) return; var renderer = domesticCellRenderers[field]; if (renderer) { html += renderer(r); } else { // 未定义的字段默认渲染 html += ''; } }); // 做货周期 = 实际到货日期 - 跟单下单日期 var manuDays = ''; if (r['实际到货日期'] && r['日期']) { var d1 = new Date(r['实际到货日期'].replace(/-/g, '/')); var d2 = new Date(r['日期'].replace(/-/g, '/')); if (!isNaN(d1) && !isNaN(d2)) { var diff = Math.round((d1 - d2) / 86400000); if (diff >= 0) manuDays = diff + '天'; } } html += ''; html += ''; html += ''; }); tbody.innerHTML = html; // 下单人多选自动保存 tbody.querySelectorAll('.orderer-select').forEach(function(sel) { // 初始化显示选中项的颜色 updateOrdererSelectColor(sel); sel.addEventListener('change', function() { var id = this.getAttribute('data-id'); var value = this.value; updateOrdererSelectColor(this); fetch('/api/domestic/orders/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id, field: '下单人', value: value}) }).then(function(r){return r.json()}); }); }); function updateOrdererSelectColor(selEl) { var selectedOpt = selEl.options[selEl.selectedIndex]; if (selectedOpt && selectedOpt.value) { var bg = selectedOpt.style.background || '#fff'; var fc = selectedOpt.style.color || '#333'; selEl.style.background = bg; selEl.style.color = fc; } else { selEl.style.background = '#fff'; selEl.style.color = '#333'; } } } window._applyTableFieldPerms && window._applyTableFieldPerms('domesticOrdersTable', 'orders_domestic'); // 先恢复保存的列宽,再计算冻结列位置(基于实际宽度) restoreColumnWidths('domesticOrdersTable', 'domesticColWidths'); applyDomesticFrozenPositions(); enableColumnResize('domesticOrdersTable', 'domesticColWidths'); var pel = document.getElementById('domesticOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, DOMESTIC_PAGE_SIZE, 'domesticLoadOrders', 'DOMESTIC_PAGE_SIZE'); } else { // 简单分页 var totalPages = Math.ceil((data.total || 0) / DOMESTIC_PAGE_SIZE); pel.innerHTML = '
'; var phtml = ''; if (page > 1) phtml += ''; phtml += '第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) phtml += ''; pel.innerHTML += phtml + '
'; } } }); } // ─── 状态筛选框同步:根据列表内容更新状态选项 ─── function 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 restoreColumnWidths(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 lastRestore = ths.length - 1; try { var raw = localStorage.getItem(storageKey); var saved = null; if (raw) { saved = JSON.parse(raw); if (!saved._v || saved._v !== ths.length || !saved._c) { saved = null; localStorage.removeItem(storageKey); } } // 没有保存的宽度时,只锁住非自适应列(跳过操作列) if (!saved) { saved = {_v: ths.length, _c: 1}; for (var i = 0; i < ths.length; i++) { if (i === lastRestore || ths[i].classList.contains('ac')) continue; saved[i] = ths[i].offsetWidth + 'px'; } try { localStorage.setItem(storageKey, JSON.stringify(saved)); } catch(e){} } // 恢复宽度(跳过操作列;自适应列只在被拖拽过(有保存值)时才恢复) for (var i = 0; i < ths.length; i++) { if (i === lastRestore) continue; if (saved[i] && (!ths[i].classList.contains('ac') || saved._acSet)) { ths[i].style.width = saved[i]; } } } catch(e){} } // ─── 列宽拖拽调整 ─── var _resizeData = null; 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'); // 为每个 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 thEl = ths[idx]; // 保存所有列的当前宽度,拖拽时锁定防止重排 var allW = {}; for (var ai = 0; ai < ths.length; ai++) { if (ai === ths.length - 1) continue; allW[ai] = ths[ai].offsetWidth; } _resizeData = { tableId: tableId, storageKey: storageKey, table: table, th: thEl, idx: idx, startX: ev.clientX, startW: thEl.offsetWidth, allWidths: allW }; }; }(i)); // 已有 sticky 的不覆盖(冻结列需要保持 sticky) if (th.style.position !== 'sticky') { th.style.position = 'relative'; } th.appendChild(handle); } } // 全局 mousemove/mouseup 处理列拖拽 if (!window._colResizeInit) { window._colResizeInit = true; document.addEventListener('mousemove', function(e) { if (!_resizeData) return; var diff = e.clientX - _resizeData.startX; var newW = Math.max(30, _resizeData.startW + diff); _resizeData.th.style.width = newW + 'px'; _resizeData.th.style.minWidth = newW + 'px'; // 拖拽时锁定其他所有列的宽度,防止浏览器自动重排 if (_resizeData.allWidths && _resizeData.table) { try { var allThs = _resizeData.table.querySelectorAll('thead tr:first-child th'); var lIdx = allThs.length - 1; for (var ti = 0; ti < allThs.length; ti++) { if (ti === lIdx || ti === _resizeData.idx) continue; if (_resizeData.allWidths[ti]) { allThs[ti].style.width = _resizeData.allWidths[ti] + 'px'; } } } catch(e){} } }); document.addEventListener('mouseup', function(e) { if (!_resizeData) return; // 保存所有列当前宽度,防止其他列被自动重排影响 var table = document.getElementById(_resizeData.tableId); if (table) { try { var allThs = table.querySelectorAll('thead tr:first-child th'); var allW = {_v: allThs.length, _c: 1}; var lastSaveIdx = allThs.length - 1; for (var ti = 0; ti < allThs.length; ti++) { if (ti === lastSaveIdx) continue; if (allThs[ti].classList.contains('ac')) allW._acSet = true; allW[ti] = allThs[ti].offsetWidth + 'px'; } localStorage.setItem(_resizeData.storageKey, JSON.stringify(allW)); } catch(e){} } // 重新应用冻结列位置(因为列宽变了) if (_resizeData.tableId === 'domesticOrdersTable') { applyDomesticFrozenPositions(); } else if (_resizeData.tableId === 'overseasOrdersTable') { applyOverseasFrozenPositions(); } else if (_resizeData.tableId === 'packagingOrdersTable') { applyPackagingFrozenPositions(); } _resizeData = null; }); } // ─── 动态计算国内订单冻结列位置(基于实际渲染宽度) ─── function applyDomesticFrozenPositions() { if (!domesticFrozen || !domesticFrozenFields || domesticFrozenFields.length === 0) return; 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 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() { if (!overseasFrozen || !overseasFrozenFields || overseasFrozenFields.length === 0) return; 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 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) + ')'); var newVal = prompt('输入新值(格式: 字段名=值),例如: 状态=已完结'); 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 (!confirm('确认删除此记录?')) 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() { var code = prompt('请输入编码(必填):'); if (!code) return; var name = prompt('请输入名称:'); 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 || '添加失败'); } }); }; window.domesticExport = function() { // 简单 CSV 导出 var tbody = document.getElementById('domesticOrdersTbody'); if (!tbody) return; var lines = []; var headers = ['#','货品编码','货品名称','跟单下单日期','计划下单时间','新旧品','下单备注','单价','审核价格','数量','金额','供货商','仓库','金蝶采购单','采购组别','下单人','进度','旺店通单号','录入旺店通','采购开发','实际到货日期','未到货数','预计到货日期','金蝶入库','状态']; lines.push(headers.join(',')); var rows = tbody.querySelectorAll('tr'); rows.forEach(function(tr) { var cols = tr.querySelectorAll('td'); var row = []; cols.forEach(function(td) { var txt = td.textContent.trim().replace(/,/g, ',').replace(/"/g, '""'); row.push('"' + txt + '"'); }); lines.push(row.join(',')); }); if (lines.length <= 1) return; var blob = new Blob([lines.join('\n')], {type: 'text/csv;charset=utf-8'}); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = '国内订单_' + new Date().toISOString().slice(0,10) + '.csv'; a.click(); }; // ──────────── 海外订单管理 ──────────── var OVERSEAS_PAGE_SIZE = 100; var overseasOrderPage = 1; var overseasSearch = ''; var overseasFilterStatus = ''; var overseasFilterSupplier = ''; var overseasFilterOrderer = ''; var overseasOrderFilter = {}; var overseasOrderFilterActive = {}; var overseasOrderFilterMenu = null; var overseasOrderFilterField = ''; var overseasFrozen = localStorage.getItem('overseasFrozen') !== 'false'; var overseasFrozenFields = (function(){ var arr; try { arr = JSON.parse(localStorage.getItem('overseasFrozenFields')) || [0,1,2]; } catch(e){ arr = [0,1,2]; } return arr.filter(function(v) { return v < 3; }); })(); function loadOverseasOrdersPage(container) { container.innerHTML = `

🌍 海外采购下单明细

序号' + label + ' ▾做货周期操作
加载中...
暂无数据
' + escHtml(r['编码'] || '') + '' + escHtml(r['名称'] || '') + '' + escHtml(window._padDateStr(r['日期'] || '')) + ' 📅' + escHtml(t) + '' + escHtml(r['新旧品'] || '') + '' + escHtml(r['下单备注'] || '-') + '' + escHtml(r['是否审核价格'] || '') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (amt ? '¥' + amt.toLocaleString() : '') + '' + escHtml(r['金蝶采购单'] || '') + '' + renderNameBadges(r['采购员']) + '' + renderOrdererSelect(r) + '' + escHtml(r['进度'] || '') + '' + escHtml(r['旺店通生产单号'] || '') + '' + escHtml(r['是否已录入旺店通'] || '') + '' + renderNameBadges(r['采购开发']) + '' + escHtml(r['实际到货日期'] || '') + '' + (parseInt(r['未到货数']) || 0).toLocaleString() + '' + escHtml(r['预计到货日期'] || '') + '' + escHtml(r['金蝶入库'] || '') + '' + escHtml(r['状态'] || '') + '' + ((r['实际到货数量'] || 0) > 0 ? parseInt(r['实际到货数量']).toLocaleString() : '0') + '
' + rowNum + '' + escHtml(r[field] || '') + '' + manuDays + ''; html += ' '; html += ' '; html += ''; html += '
`; // 动态生成海外表头列 (function() { var cols = OVERSEAS_COLUMNS; var order = window._overseasDragOrder || cols.map(function(c) { return c.field; }); order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; if (!window.overseasColVisible[field]) return; var fieldAttr = col.field; var thStyle = ''; if (fieldAttr === '产品编码') thStyle = 'min-width:120px;position:sticky;left:90px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '货品名称') thStyle = 'min-width:200px;position:sticky;left:210px;z-index:3;background:var(--bg,#f5f5f5)'; else if (fieldAttr === '日期') thStyle = 'width:85px;min-width:85px;position:sticky;left:410px;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:90px'; 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:90px'; else if (fieldAttr === '实际到货日期' || fieldAttr === '预计到货日期') thStyle = 'min-width:100px'; else if (fieldAttr === '未到货数') thStyle = 'min-width:80px'; else if (fieldAttr === '状态') thStyle = 'min-width:70px'; else { thStyle = ''; } container.querySelector('#overseasOrdersHeader').innerHTML += ''; }); })(); container.querySelector('#overseasOrdersHeader').innerHTML += ''; container.querySelector('#overseasOrdersHeader').innerHTML += ''; container.querySelector('#overseasOrdersTable').innerHTML += ''; 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(); 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 ss = document.getElementById('overseasStatusFilter'); var so = document.getElementById('overseasOrdererFilter'); (res.data.statuses || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; ss.appendChild(opt); }); (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); var overseasFilterStr = ''; if (Object.keys(overseasOrderFilter).length > 0) { overseasFilterStr = encodeURIComponent(JSON.stringify(overseasOrderFilter)); params += '&order_filter=' + overseasFilterStr; } fetch('/api/overseas/orders/list' + params) .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 = window._overseasDragOrder || OVERSEAS_COLUMNS.map(function(c) { return c.field; }); order.forEach(function(field) { if (!window.overseasColVisible[field]) return; var cellHtml = ''; if (field === '产品编码') { cellHtml = ''; } else if (field === '货品名称') { cellHtml = ''; } else if (field === '日期') { cellHtml = ''; } else if (field === '计划下单时间') { var planTime = window._padDateStr(r['计划下单时间'] || ''); if (planTime.length > 16) planTime = planTime.slice(0, 16); cellHtml = ''; } else if (field === '单价') { cellHtml = ''; } else if (field === '是否审核价格') { cellHtml = ''; } else if (field === '数量') { cellHtml = ''; } else if (field === '金额') { var priceVal = parseFloat(r['单价']) || 0; var qtyVal = parseInt(r['数量']) || 0; var amtVal = priceVal * qtyVal; cellHtml = ''; } else if (field === '供应商') { cellHtml = ''; } else if (field === '仓库') { var orderId = r.id || ''; var whVal = r['仓库'] || ''; cellHtml = ''; } else if (field === '新旧品') { cellHtml = ''; } else if (field === '是否已做采购单') { cellHtml = ''; } else if (field === '是否做入库') { cellHtml = ''; } else if (field === '采购员') { cellHtml = ''; } else if (field === '下单人') { cellHtml = ''; } else if (field === '进度') { cellHtml = ''; } else if (field === '下单备注') { cellHtml = ''; } else if (field === '采购开发') { cellHtml = ''; } else if (field === '旺店通生产单号') { cellHtml = ''; } else if (field === '是否已录入到旺店通') { cellHtml = ''; } else if (field === '实际到货日期') { cellHtml = ''; } else if (field === '预计到货日期') { cellHtml = ''; } else if (field === '实际到货数量') { cellHtml = ''; } else if (field === '未到货数') { cellHtml = ''; } else if (field === '状态') { cellHtml = ''; } else { cellHtml = ''; } html += cellHtml; }); // 做货周期 = 实际到货日期 - 跟单下单日期 var manuDaysOv = ''; if (r['实际到货日期'] && r['日期']) { var d1 = new Date(r['实际到货日期'].replace(/-/g, '/')); var d2 = new Date(r['日期'].replace(/-/g, '/')); if (!isNaN(d1) && !isNaN(d2)) { var diff = Math.round((d1 - d2) / 86400000); if (diff >= 0) manuDaysOv = diff + '天'; } } html += ''; html += ''; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('overseasOrdersTable', 'orders_overseas'); _syncOrderFilterIcons('overseas'); restoreColumnWidths('overseasOrdersTable', 'overseasColWidths'); applyOverseasFrozenPositions(); enableColumnResize('overseasOrdersTable', 'overseasColWidths'); var pel = document.getElementById('overseasOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, OVERSEAS_PAGE_SIZE, 'overseasLoadOrders', 'OVERSEAS_PAGE_SIZE'); } } }); } window.overseasEditRow = function(id) { var newVal = prompt('输入新值(格式: 字段名=值),例如: 状态=已完结'); 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 (!confirm('确认删除此记录?')) 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() { var code = prompt('请输入产品编码(必填):'); if (!code) return; var name = prompt('请输入货品名称:'); 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() { var tbody = document.getElementById('overseasOrdersTbody'); if (!tbody) return; var lines = []; var headers = ['#','货品编码','货品名称','跟单下单日期','计划下单时间','单价','审核价格','数量','金额','供应商','仓库','新旧品','已做采购单','已入库','采购组别','下单人','进度','下单备注','采购开发','旺店通单号','录入旺店通','实际到货日期','预计到货日期','未到货数','状态']; lines.push(headers.join(',')); var rows = tbody.querySelectorAll('tr'); rows.forEach(function(tr) { var cols = tr.querySelectorAll('td'); var row = []; cols.forEach(function(td) { var txt = td.textContent.trim().replace(/,/g, ',').replace(/"/g, '""'); row.push('"' + txt + '"'); }); lines.push(row.join(',')); }); if (lines.length <= 1) return; var blob = new Blob([lines.join('\n')], {type: 'text/csv;charset=utf-8'}); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = '海外订单_' + new Date().toISOString().slice(0,10) + '.csv'; a.click(); }; 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'; var packagingFrozenFields = (function(){ var arr; try { arr = JSON.parse(localStorage.getItem('packagingFrozenFields')) || [0,1,2]; } catch(e){ arr = [0,1,2]; } return arr.filter(function(v) { return v < 3; }); })(); // ─── 冻结列切换函数 ─── window.openDomesticFrozenModal = function() { // 从 localStorage 加载当前冻结列设置 try { domesticFrozenFields = JSON.parse(localStorage.getItem('domesticFrozenFields')) || [0,1,2]; } catch(e){ domesticFrozenFields = [0,1,2]; } var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结 domesticFrozenFields = domesticFrozenFields.filter(function(v) { return v < labels.length; }); var html = '
'; html += '
'; labels.forEach(function(label, idx) { var checked = domesticFrozenFields.indexOf(idx) >= 0; html += ''; }); html += '
'; document.getElementById('domesticFrozenModalBody').innerHTML = html; document.getElementById('domesticFrozenModal').style.display = 'flex'; }; window.domesticFrozenCbChange = function() { var checked = []; document.querySelectorAll('.domestic-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); }); domesticFrozenFields = checked.sort(function(a,b){return a-b;}); var allCb = document.getElementById('domesticFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.domestic-frozen-cb').length; }; window.domesticFrozenToggleAll = function(checked) { document.querySelectorAll('.domestic-frozen-cb').forEach(function(cb) { cb.checked = checked; }); domesticFrozenCbChange(); }; function closeDomesticFrozenModal() { document.getElementById('domesticFrozenModal').style.display = 'none'; } function confirmDomesticFrozen() { domesticFrozen = domesticFrozenFields.length > 0; localStorage.setItem('domesticFrozen', domesticFrozen); localStorage.setItem('domesticFrozenFields', JSON.stringify(domesticFrozenFields)); closeDomesticFrozenModal(); domesticLoadOrders(1); } // ─── 海外冻结列弹窗 ─── window.openOverseasFrozenModal = function() { try { overseasFrozenFields = JSON.parse(localStorage.getItem('overseasFrozenFields')) || [0,1,2]; } catch(e){ overseasFrozenFields = [0,1,2]; } var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结 overseasFrozenFields = overseasFrozenFields.filter(function(v) { return v < labels.length; }); var html = '
'; html += '
'; labels.forEach(function(label, idx) { var checked = overseasFrozenFields.indexOf(idx) >= 0; html += ''; }); html += '
'; document.getElementById('overseasFrozenModalBody').innerHTML = html; document.getElementById('overseasFrozenModal').style.display = 'flex'; }; window.overseasFrozenCbChange = function() { var checked = []; document.querySelectorAll('.overseas-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); }); overseasFrozenFields = checked.sort(function(a,b){return a-b;}); var allCb = document.getElementById('overseasFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.overseas-frozen-cb').length; }; window.overseasFrozenToggleAll = function(checked) { document.querySelectorAll('.overseas-frozen-cb').forEach(function(cb) { cb.checked = checked; }); overseasFrozenCbChange(); }; function closeOverseasFrozenModal() { document.getElementById('overseasFrozenModal').style.display = 'none'; } function confirmOverseasFrozen() { overseasFrozen = overseasFrozenFields.length > 0; localStorage.setItem('overseasFrozen', overseasFrozen); localStorage.setItem('overseasFrozenFields', JSON.stringify(overseasFrozenFields)); closeOverseasFrozenModal(); overseasLoadOrders(1); } // ─── 包材冻结列弹窗 ─── window.openPackagingFrozenModal = function() { try { packagingFrozenFields = JSON.parse(localStorage.getItem('packagingFrozenFields')) || [0,1,2]; } catch(e){ packagingFrozenFields = [0,1,2]; } var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结 packagingFrozenFields = packagingFrozenFields.filter(function(v) { return v < labels.length; }); var html = '
'; html += '
'; labels.forEach(function(label, idx) { var checked = packagingFrozenFields.indexOf(idx) >= 0; html += ''; }); html += '
'; document.getElementById('packagingFrozenModalBody').innerHTML = html; document.getElementById('packagingFrozenModal').style.display = 'flex'; }; window.packagingFrozenCbChange = function() { var checked = []; document.querySelectorAll('.packaging-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); }); packagingFrozenFields = checked.sort(function(a,b){return a-b;}); var allCb = document.getElementById('packagingFrozenCbAll'); if (allCb) allCb.checked = checked.length === document.querySelectorAll('.packaging-frozen-cb').length; }; window.packagingFrozenToggleAll = function(checked) { document.querySelectorAll('.packaging-frozen-cb').forEach(function(cb) { cb.checked = checked; }); packagingFrozenCbChange(); }; function closePackagingFrozenModal() { document.getElementById('packagingFrozenModal').style.display = 'none'; } function confirmPackagingFrozen() { packagingFrozen = packagingFrozenFields.length > 0; localStorage.setItem('packagingFrozen', packagingFrozen); localStorage.setItem('packagingFrozenFields', JSON.stringify(packagingFrozenFields)); closePackagingFrozenModal(); packagingLoadOrders(1); } // ─── 包材冻结列位置 ─── function applyPackagingFrozenPositions() { // 包装表格: [checkbox(0), 序号(1), 货品编码(2), 货品名称(3), 日期(4), ...] var frozenColMap = function(idx) { return idx + 1; }; var table = document.getElementById('packagingOrdersTable'); if (!table) return; var headerRow = table.querySelector('thead tr'); if (!headerRow) return; var scope = headerRow.querySelectorAll('th'); var tbody = table.querySelector('tbody'); // checkbox(0) 和 序号(1) 始终固定 var frozenSet = {0: true, 1: true}; if (packagingFrozen && packagingFrozenFields) { for (var fi = 0; fi < packagingFrozenFields.length; fi++) { frozenSet[frozenColMap(packagingFrozenFields[fi])] = true; } } // 处理 th:清除非冻结列的 sticky,设置冻结列 for (var i = 0; i < scope.length; i++) { // 操作列保留 sticky:right if (scope[i].style.position === 'sticky' && scope[i].style.right !== '') continue; if (frozenSet[i]) { scope[i].style.position = 'sticky'; scope[i].style.top = '0'; scope[i].style.zIndex = '3'; } else { scope[i].style.position = ''; scope[i].style.left = ''; scope[i].style.zIndex = ''; } } scope[0].style.left = '0px'; // checkbox 始终 left:0 // 处理 td if (tbody) { for (var ri = 0; ri < tbody.rows.length; ri++) { var tds = tbody.rows[ri].cells; for (var i = 0; i < tds.length; i++) { if (tds[i].style.position === 'sticky' && tds[i].style.right !== '') continue; if (frozenSet[i]) { tds[i].style.position = 'sticky'; tds[i].style.top = '0'; tds[i].style.zIndex = '2'; } else { tds[i].style.position = ''; tds[i].style.left = ''; tds[i].style.zIndex = ''; } } tds[0].style.left = '0px'; tds[0].style.background = 'var(--card-bg,#fff)'; } } // 测量冻结列宽度并设置 left 偏移 var widths = []; var acc = 0; // 获取所有冻结列的宽度(按 col index 顺序) var frozenIdxList = []; for (var i = 0; i <= 10; i++) { if (frozenSet[i]) frozenIdxList.push(i); } frozenIdxList.forEach(function(idx) { var w = idx < scope.length ? scope[idx].offsetWidth : 0; widths.push(w); }); // 为每个冻结的 th 设置 left for (var fi = 1; fi < frozenIdxList.length; fi++) { var idx = frozenIdxList[fi]; if (idx >= scope.length) break; acc += widths[fi - 1]; scope[idx].style.left = acc + 'px'; } // 为 td 设置 left if (tbody && frozenIdxList.length > 1) { for (var ri = 0; ri < tbody.rows.length; ri++) { var tds = tbody.rows[ri].cells; var acc2 = widths[0]; // checkbox width for (var fi = 1; fi < frozenIdxList.length; fi++) { var idx = frozenIdxList[fi]; if (idx >= tds.length) break; tds[idx].style.left = acc2 + 'px'; tds[idx].style.background = 'var(--card-bg,#fff)'; acc2 += widths[fi]; } } } // 一次性测量所有冻结列宽度(勾选框排首位) var widths = []; var acc = 0; var cbW = scope[0].offsetWidth; widths.push(cbW); acc += cbW; for (var fi = 0; fi < packagingFrozenFields.length; fi++) { var idx = frozenColMap(packagingFrozenFields[fi]); if (idx >= scope.length) break; var th = scope[idx]; var w = th.offsetWidth; widths.push(w); var leftVal = acc + 'px'; th.style.position = 'sticky'; th.style.top = '0'; th.style.left = leftVal; th.style.zIndex = fi < 2 ? '3' : '11'; th.style.background = 'var(--bg,#f5f5f5)'; acc += w; } // 更新 td if (tbody && widths.length > 0) { var rows = tbody.rows; for (var ri = 0; ri < rows.length; ri++) { var tr = rows[ri]; var tds = tr.cells; var acc2 = 0; tds[0].style.position = 'sticky'; tds[0].style.left = acc2 + 'px'; tds[0].style.zIndex = '2'; tds[0].style.background = 'var(--card-bg,#fff)'; acc2 += widths[0]; for (var fi = 0; fi < packagingFrozenFields.length; fi++) { var idx = frozenColMap(packagingFrozenFields[fi]); if (idx >= tds.length) break; tds[idx].style.position = 'sticky'; tds[idx].style.left = acc2 + 'px'; tds[idx].style.zIndex = '2'; tds[idx].style.background = 'var(--card-bg,#fff)'; acc2 += widths[fi + 1]; } } } } function loadPackagingOrdersPage(container) { container.innerHTML = `

📦 包材采购下单明细

序号' + col.label + ' ▾做货周期操作
加载中...
暂无数据
' + rowNum + '' + escHtml(r['产品编码'] || '') + '' + escHtml(r['货品名称'] || '') + '' + escHtml(window._padDateStr(r['日期'] || '')) + ' 📅' + escHtml(planTime) + '' + escHtml(r['是否审核价格'] || '') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (amtVal ? '¥' + amtVal.toLocaleString() : '') + '' + escHtml(r['供应商'] || '') + '' + escHtml(r['新旧品'] || '') + '' + escHtml(r['是否已做采购单'] || '') + '' + escHtml(r['是否做入库'] || '') + '' + renderNameBadges(r['采购员']) + '' + renderPersonBadges(r['下单人']) + '' + escHtml(r['进度'] || '') + '' + escHtml(r['下单备注'] || '-') + '' + renderNameBadges(r['采购开发']) + '' + escHtml(r['旺店通生产单号'] || '') + '' + escHtml(r['是否已录入到旺店通'] || '') + '' + escHtml(r['实际到货日期'] || '') + '' + escHtml(r['预计到货日期'] || '') + '' + ((r['实际到货数量'] || 0) > 0 ? parseInt(r['实际到货数量']).toLocaleString() : '0') + '' + (parseInt(r['未到货数']) || 0).toLocaleString() + '' + escHtml(r['状态'] || '') + '' + escHtml(r[field] || '') + '' + manuDaysOv + ''; html += ' '; html += ' '; html += ''; html += '
`; // 动态生成包材表头列 (function() { var cols = PACKAGING_COLUMNS; var order = window._packagingDragOrder || cols.map(function(c) { return c.field; }); order.forEach(function(field) { var col = cols.find(function(c) { return c.field === field; }); if (!col) return; if (!window.packagingColVisible[field]) return; var fieldAttr = col.field; var thStyle = ''; if (fieldAttr === '日期') thStyle = 'position:sticky;left:90px;z-index:3;background:var(--bg,#f5f5f5);min-width:100px'; else if (fieldAttr === '编码') thStyle = 'min-width:140px'; else if (fieldAttr === '名称') thStyle = 'min-width:250px'; else if (fieldAttr === '单价') thStyle = 'min-width:70px'; else if (fieldAttr === '数量') thStyle = 'min-width:60px'; else if (fieldAttr === '合计') thStyle = 'min-width:80px'; else if (fieldAttr === '供应商') thStyle = 'min-width:100px'; else if (fieldAttr === '下单备注') thStyle = 'min-width:120px'; else if (fieldAttr === '仓库到货') thStyle = 'min-width:100px'; else if (fieldAttr === '进度') thStyle = 'min-width:160px'; else if (fieldAttr === '是否已做采购单') thStyle = 'min-width:90px'; 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:90px'; 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 = ''; } container.querySelector('#packagingOrdersTable thead tr').innerHTML += ''; }); })(); container.querySelector('#packagingOrdersTable thead tr').innerHTML += ''; container.querySelector('#packagingOrdersTable').innerHTML += ''; 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(); packagingLoadOrders(1); loadDefaultOrderScheme('packaging'); // 后台预拉取所有列的筛选值 prefetchOrderColumnValues('packaging'); } function packagingLoadFilters() { fetch('/api/packaging/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var ss = document.getElementById('packagingStatusFilter'); var so = document.getElementById('packagingOrdererFilter'); (res.data.statuses || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; ss.appendChild(opt); }); (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; 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 = {}; _syncOrderFilterIcons('packaging'); packagingLoadOrders(1); } 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); var packagingFilterStr = ''; if (Object.keys(packagingOrderFilter).length > 0) { packagingFilterStr = encodeURIComponent(JSON.stringify(packagingOrderFilter)); params += '&order_filter=' + packagingFilterStr; } fetch('/api/packaging/orders/list' + params) .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 = window._packagingDragOrder || PACKAGING_COLUMNS.map(function(c) { return c.field; }); order.forEach(function(field) { if (!window.packagingColVisible[field]) return; var cellHtml = ''; 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 === '仓库到货') { var orderId2 = r.id || ''; var whArrVal = r['仓库到货'] || ''; cellHtml = ''; } else if (field === '进度') { cellHtml = ''; } else if (field === '是否已做采购单') { cellHtml = ''; } else if (field === '是否已做入库') { cellHtml = ''; } else if (field === '采购下单人') { cellHtml = ''; } else if (field === '旺店通编号') { cellHtml = ''; } else if (field === '是否已录入旺店通') { cellHtml = ''; } else if (field === '到货日期') { cellHtml = ''; } else if (field === '状态') { cellHtml = ''; } else if (field === '做货天数') { cellHtml = ''; } else if (field === '实际到货数量') { cellHtml = ''; } else if (field === '未到货数') { cellHtml = ''; } else if (field === '包材开发') { cellHtml = ''; } else { cellHtml = ''; } html += cellHtml; }); html += ''; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('packagingOrdersTable', 'orders_packaging'); _syncOrderFilterIcons('packaging'); restoreColumnWidths('packagingOrdersTable', 'packagingColWidths'); applyPackagingFrozenPositions(); enableColumnResize('packagingOrdersTable', 'packagingColWidths'); var pel = document.getElementById('packagingOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, PACKAGING_PAGE_SIZE, 'packagingLoadOrders', 'PACKAGING_PAGE_SIZE'); } } }); } window.packagingEditRow = function(id) { var newVal = prompt('输入新值(格式: 字段名=值),例如: 状态=已完结'); 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.packagingDeleteRow = function(id) { if (!confirm('确认删除?')) 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() { var code = prompt('请输入编码(必填):'); if (!code) return; var name = prompt('请输入名称:'); 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() { var tbody = document.getElementById('packagingOrdersTbody'); if (!tbody) return; var lines = []; var headers = ['#','日期','编码','名称','单价','数量','金额','供应商','下单备注','仓库到货','进度','已做采购单','已入库','采购下单人','旺店通编号','录入旺店通','到货日期','状态','做货天数','包材开发']; lines.push(headers.join(',')); tbody.querySelectorAll('tr').forEach(function(tr) { var row = []; tr.querySelectorAll('td').forEach(function(td) { var txt = td.textContent.trim().replace(/,/g, ',').replace(/"/g, '""'); row.push('"' + txt + '"'); }); lines.push(row.join(',')); }); if (lines.length <= 1) return; var blob = new Blob([lines.join('\n')], {type: 'text/csv;charset=utf-8'}); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = '包材订单_' + new Date().toISOString().slice(0,10) + '.csv'; a.click(); }; 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 FINISHED_FILTER_COLS = [ {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:'text'}, {field:'海外下单表进度', label:'海外下单表进度', type:'text'}, {field:'产品进度', label:'产品进度', type:'text'}, {field:'包材库存', label:'包材库存', type:'number'}, {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 = ''; function loadFinishedProductsPage(container) { container.innerHTML = `

📋 成品提货明细

序号' + col.label + ' ▾操作
加载中...
暂无数据
' + rowNum + '' + escHtml(r['日期'] || '') + '' + escHtml(r['编码'] || '') + '' + escHtml(r['名称'] || '') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (amtVal ? '¥' + amtVal.toLocaleString() : '') + '' + escHtml(r['供应商'] || '') + '' + escHtml(r['下单备注'] || '') + '' + escHtml(r['进度'] || '') + '' + escHtml(r['是否已做采购单'] || '') + '' + escHtml(r['是否已做入库'] || '') + '' + renderPersonBadges(r['采购下单人']) + '' + escHtml(r['旺店通编号'] || '') + '' + escHtml(r['是否已录入旺店通'] || '') + '' + escHtml(r['到货日期'] || '') + '' + escHtml(r['状态'] || '未完结') + '' + escHtml(r['做货天数'] || '') + '' + ((r['实际到货数量'] || 0) > 0 ? parseInt(r['实际到货数量']).toLocaleString() : '0') + '' + (parseInt(r['未到货数']) || 0).toLocaleString() + '' + renderPersonBadges(r['包材开发']) + '' + escHtml(r[field] || '') + ''; html += ' '; html += ' '; html += ''; html += '
# 序号 品类 ▾ 货品编号 ▾ 货品名称 ▾ 供应商 ▾ 可用库存 ▾ 昨日销量 ▾ 3天均销 ▾ 7天均销 ▾ 15天均销 ▾ 支撑天数 ▾ 采购总数量 ▾ 生产在途 ▾ 采购在途 ▾ 国内下单表进度 ▾ 海外下单表进度 ▾ 产品进度 ▾ 包材库存 ▾ 采购跟单 ▾ 采购计划 ▾ 备注 ▾ 操作
加载中...
`; finishedLoadFilters(); finishedLoadOrders(1); // 后台预拉取成品提货表所有列的筛选值 prefetchOrderColumnValues('finished'); } 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); var finishedFilterStr = ''; if (Object.keys(finishedOrderFilter).length > 0) { finishedFilterStr = encodeURIComponent(JSON.stringify(finishedOrderFilter)); params += '&order_filter=' + finishedFilterStr; } fetch('/api/finished/list' + params) .then(function(r){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 + ''; html += '' + escHtml(r['序号'] || '') + ''; html += '' + escHtml(r['品类'] || '') + ''; html += '' + escHtml(r['货品编号'] || '') + ''; html += '' + escHtml(r['货品名称'] || '') + ''; html += '' + escHtml(r['供应商'] || '') + ''; html += '' + (parseInt(r['可用库存']) || 0).toLocaleString() + ''; html += '' + escHtml(r['昨日销量'] || '') + ''; html += '' + escHtml(r['3天均销量'] || '') + ''; html += '' + escHtml(r['7天均销量'] || '') + ''; html += '' + escHtml(r['15天均销量'] || '') + ''; html += '' + escHtml(r['支撑天数'] || '') + ''; html += '' + (parseInt(r['采购总数量']) || 0).toLocaleString() + ''; html += '' + (parseInt(r['生产在途']) || 0).toLocaleString() + ''; html += '' + (parseInt(r['采购在途']) || 0).toLocaleString() + ''; html += '' + escHtml(r['国内下单表进度'] || '') + ''; html += '' + escHtml(r['海外下单表进度'] || '') + ''; html += '' + escHtml(r['产品进度'] || '') + ''; html += '' + (parseInt(r['包材库存']) || 0).toLocaleString() + ''; html += '' + renderNameBadges(r['采购跟单']) + ''; html += '' + renderNameBadges(r['采购计划']) + ''; html += '' + escHtml(r['备注'] || '') + ''; html += ''; html += ' '; html += ''; html += ''; }); tbody.innerHTML = html; } window._applyTableFieldPerms && window._applyTableFieldPerms('finishedOrdersTable', 'orders_finished'); var pel = document.getElementById('finishedOrdersPagination'); if (pel) { if (typeof renderPaginationWithSize === 'function') { renderPaginationWithSize(pel, page, data.total || 0, FINISHED_PAGE_SIZE, 'finishedLoadOrders', 'FINISHED_PAGE_SIZE'); } } }); } window.finishedDeleteRow = function(id) { if (!confirm('确认删除此记录?')) 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 _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 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); if (colType === 'number') { _buildFinishedNumericFilter(field, label, rect); } else { var filterActive = finishedOrderFilter && Object.keys(finishedOrderFilter).length > 0; var hasOtherFilters = filterActive && !finishedOrderFilter[field]; var hasSearchCond = !!(document.getElementById('finishedSearchInput')?.value.trim() || document.getElementById('finishedCategoryFilter')?.value || document.getElementById('finishedSupplierFilter')?.value); var cacheKey = 'finished:' + field; var cached = !hasOtherFilters && !hasSearchCond && window._orderColumnCache && window._orderColumnCache[cacheKey]; if (cached) { _buildFinishedListFilter(field, label, rect, cached); } else if (hasOtherFilters || hasSearchCond) { // 有筛选条件:从当前表格DOM提取列值(联动筛选) _buildFinishedFilterShell(field, label, rect); var vals = []; var seen = {}; var tbody = document.getElementById('finishedOrdersTbody'); if (tbody) { var fCols = FINISHED_FILTER_COLS; var colIdx = fCols.findIndex(function(c) { return c.field === field; }); if (colIdx >= 0) { var tdIdx = colIdx + 2; // +checkbox +序号 tbody.querySelectorAll('tr').forEach(function(tr) { var td = tr.cells[tdIdx]; if (td) { var txt = td.textContent.trim(); if (txt && txt !== '-' && !seen[txt]) { seen[txt] = 1; vals.push(txt); } } }); } } vals.sort(); if (vals.length === 0) { var fp = ''; if (finishedOrderFilter && Object.keys(finishedOrderFilter).length > 0) { fp = '&order_filter=' + encodeURIComponent(JSON.stringify(finishedOrderFilter)); } fetch('/api/finished/column-values?field=' + encodeURIComponent(field) + fp) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { _populateFinishedFilterList(field, res.data); } }); } else { _populateFinishedFilterList(field, vals); } } else { _buildFinishedFilterShell(field, label, rect); var filterParam = ''; if (finishedOrderFilter && Object.keys(finishedOrderFilter).length > 0) { filterParam = '&order_filter=' + encodeURIComponent(JSON.stringify(finishedOrderFilter)); } fetch('/api/finished/column-values?field=' + encodeURIComponent(field) + filterParam) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { if (!hasOtherFilters) { window._orderColumnCache = window._orderColumnCache || {}; window._orderColumnCache[cacheKey] = res.data; } _populateFinishedFilterList(field, res.data); } }); } } 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'; 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 html = '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var checked = (sel[v] !== false) ? 'checked' : ''; html += ''; }); html += '
'; html += '
'; html += ''; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); finishedFilterMenu = menu; } function _buildFinishedNumericFilter(field, label, rect) { var menu = document.createElement('div'); menu.id = 'finishedFilterMenu'; var current = finishedOrderFilter[field] || {}; var cond = current.cond || ''; var val = current.val || ''; var html = '
' + escHtml(label) + '
'; 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:200px;font-size:13px'; document.body.appendChild(menu); finishedFilterMenu = menu; } function _populateFinishedFilterList(field, values) { var listEl = document.getElementById('finishedFilterList'); if (!listEl) return; var sel = finishedOrderFilter[field] || {}; var html = '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = (sel[v] !== false) ? 'checked' : ''; 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 ? '' : '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) { 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 cond = document.getElementById('finishedNumCond')?.value || ''; var val = document.getElementById('finishedNumVal')?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } finishedOrderFilter[field] = {type: 'num', cond: cond, val: val}; if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; } _syncFinishedFilterIcons(); finishedLoadOrders(1); } 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.parentNode.textContent.trim().replace(/▾/, '').trim(); var hasFilter = finishedOrderFilter[field]; icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)'; icon.style.fontWeight = hasFilter ? 'bold' : 'normal'; }); } // ─── 下单备注可编辑通用函数 ─── // ─── 订单表头列筛选功能 ─── // 根据类型获取筛选状态对象 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) { var 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 _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); if (colType === 'number') { _buildOrderNumericFilter(type, field, label, rect); } else { // 检查缓存:优先从当前表格DOM提取(联动搜索条件),否则用API/缓存 var filterActive = _orderFilterActive(type); var hasOtherFilters = filterActive && Object.keys(filterActive).length > 0; var hasSearchCond = _orderHasSearch(type); var cacheKey = type + ':' + field; var cached = !hasOtherFilters && !hasSearchCond && window._orderColumnCache && window._orderColumnCache[cacheKey]; if (cached) { // 命中缓存(无任何筛选条件),直接显示 _buildOrderFilterList(type, field, label, rect, cached); } else if (hasOtherFilters || hasSearchCond) { // 有筛选条件:从当前表格DOM提取列值(联动筛选) _buildOrderFilterShell(type, field, label, rect); var vals = _extractColumnValuesFromDOM(type, field); if (vals.length === 0) { // DOM提取失败则fallback到API var apiMap2 = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'}; var fp2 = ''; if (hasOtherFilters) { fp2 = '&order_filter=' + encodeURIComponent(JSON.stringify(filterActive)); } fetch(apiMap2[type] + '?field=' + encodeURIComponent(field) + fp2) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { _populateOrderFilterList(type, field, res.data); } }); } else { _populateOrderFilterList(type, field, vals); } } 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'}; var filterParam = ''; if (hasOtherFilters) { filterParam = '&order_filter=' + encodeURIComponent(JSON.stringify(filterActive)); } fetch(apiMap[type] + '?field=' + encodeURIComponent(field) + filterParam) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0 && res.data) { // 无联动筛选时缓存结果 if (!hasOtherFilters) { window._orderColumnCache = window._orderColumnCache || {}; window._orderColumnCache[cacheKey] = res.data; } _populateOrderFilterList(type, field, res.data); } }); } } // 点击外部关闭 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) { var menu = document.createElement('div'); menu.id = 'orderFilterMenu_' + type; 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 filterState = _orderFilterState(type); // 加锁防止快速多次点击 if (window._orderFilterLock) return; window._orderFilterLock = true; setTimeout(function() { window._orderFilterLock = false; }, 300); if (!filterState[field]) filterState[field] = {}; var sel = filterState[field]; var html = ''; html += '
' + escHtml(label) + '
'; html += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var key = escHtml(v).replace(/'/g, "\\'"); var checked = (sel[v] !== false) ? 'checked' : ''; 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; 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; document.body.appendChild(menu); _setOrderFilterMenu(type, menu); } // 填充订单筛选列表(从API返回的数据) function _populateOrderFilterList(type, field, values) { var listEl = document.getElementById('orderFilterList_' + type); if (!listEl) return; var filterState = _orderFilterState(type); if (!filterState[field]) filterState[field] = {}; var sel = filterState[field]; var html = ''; html += '
'; html += ''; html += '清除筛选
'; values.forEach(function(v) { var checked = (sel[v] !== false) ? 'checked' : ''; 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 html = ''; html += '
' + escHtml(label) + '
'; 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:200px;font-size:13px'; document.body.appendChild(menu); _setOrderFilterMenu(type, menu); } 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) { // 搜索框有内容 → 文本模糊匹配 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 cond = document.getElementById('orderNumCond_' + type)?.value || ''; var val = document.getElementById('orderNumVal_' + type)?.value || ''; if (!cond || !val) { alert('请选择条件并输入数值'); return; } var filterState = _orderFilterState(type); filterState[field] = {type: 'num', cond: cond, val: val}; closeOrderFilterMenu(type); _syncOrderFilterIcons(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) ? '' : '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,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,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,type:'number'}, {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,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; return []; } // 获取可见性映射(兼容localStorage) function getOrderColVisible(type) { var key = type + 'ColVisible'; var stored = localStorage.getItem(key); if (stored) { try { var m = JSON.parse(stored); 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]) { window[dragVar] = cols.map(function(c) { return c.field; }); } } // 确保所有页面已初始化 initOrderColState('domestic'); initOrderColState('overseas'); initOrderColState('packaging'); // ─── 拖拽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'; // 初始化拖拽顺序 window[dragVar] = cols.map(function(c) { return c.field; }); // 把当前生效状态复制到临时状态 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]); document.getElementById(type + 'ColModal').style.display = 'none'; // 全量刷新页面,确保表头与表体列顺序一致(同计划预测 confirmPlanCol 做法) var pageMap = { 'domestic': 'orders_domestic', 'overseas': 'orders_overseas', 'packaging': 'orders_packaging' }; 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) { if (!confirm('确定要删除此方案吗?')) 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; } if (confirm('是否保存修改?')) { 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); }; }; // ─── 单价保存函数 ─── // ─── 供应商模糊搜索联动 ─── // ─── 供应商模糊搜索下拉(按简称匹配)─── 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'); if (confirm('是否保存修改?将供应商修改为:' + newVal)) { 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'); if (confirm('是否保存修改?')) { 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; } // 修改日期 → 弹确认 if (confirm('是否保存修改?')) { 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) { if (!confirm('确认提交此订单?提交后不可撤回。')) 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) { if (!confirm('确定删除此订单?删除后不可恢复。')) 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; var mq = parseFloat(m.quantity) || 0; totalMatCost += mc * mq; }); var productCost = totalMatCost > 0 ? '¥' + totalMatCost.toFixed(2) : '-'; var productSupplier = g.product_supplier || '-'; var foldIcon = isExpanded ? '▼' : '▶'; // 成品库存(从第一行取,所有行同产品数据一致) var prodAvail = g.materials[0] ? (parseInt(g.materials[0].product_available_qty) || 0) : 0; var prodProdTr = g.materials[0] ? (parseInt(g.materials[0].product_production_in_transit) || 0) : 0; var prodPurchTr = g.materials[0] ? (parseInt(g.materials[0].product_purchase_in_transit) || 0) : 0; var prodInvClass = prodAvail > 0 ? '' : ' style="color:#ff4d4f;font-weight:600"'; // 判断是否有实际包材 var hasRealMats = g.materials.some(function(r) { return r.material_code && r.material_code.trim(); }); var maintLabel = hasRealMats ? '有BOM' : '无BOM'; html += '' + '' + foldIcon + '' + '' + escHtml(g.product_code) + '' + '' + escHtml(g.product_name) + '' + '-' + '成品' + '' + productCost + '' + '' + escHtml(productSupplier) + '' + '' + prodAvail.toLocaleString() + '' + '' + prodProdTr.toLocaleString() + '' + '' + prodPurchTr.toLocaleString() + '' + '' + maintLabel + '' + '' + ' ' + ' ' + '' + '' + ''; // Material detail rows (hidden unless expanded) if (isExpanded) { // 过滤掉占位空物料行 var realMats = g.materials.filter(function(r) { return r.material_code && r.material_code.trim(); }); if (realMats.length === 0) { html += '' + '暂无包材数据,请点击编辑维护'; } else { realMats.forEach(function(r) { var qty = parseInt(r.quantity) + ''; var matCost = r.material_cost_price ? '¥' + parseFloat(r.material_cost_price).toFixed(2) : '-'; var matSupplier = r.material_supplier || '-'; var avail = r.mat_available_qty || 0; var prodTr = r.mat_prod_transit || 0; var purchTr = r.mat_purch_transit || 0; html += '' + '' + '' + escHtml(r.material_code) + '' + '' + escHtml(r.material_name) + '' + '' + qty + '' + '包材' + '' + matCost + '' + '' + escHtml(matSupplier) + '' + '' + (avail || 0) + '' + '' + (prodTr || 0) + '' + '' + (purchTr || 0) + '' + '' + ''; }); } } }); tbody.innerHTML = html; } var pel = document.getElementById('bomPagination'); if (pel) { renderPaginationWithSize(pel, page, data.total || 0, BOM_PAGE_SIZE, 'loadBom', 'BOM_PAGE_SIZE'); } }); } window.toggleBomMaterial = function(productCode) { bomExpanded[productCode] = !bomExpanded[productCode]; loadBom(bomCurrentPage); }; // 绑定回车搜索 window.bomSearchKeydown = function(evt) { if (evt.key === 'Enter') bomSearch(); }; window.bomSearch = function() { bomCurrentPage = 1; loadBom(1); }; // 重置所有筛选条件 window.bomReset = function() { var si = document.getElementById('bomSearchInput'); var sf = document.getElementById('bomSupplierFilter'); var mf = document.getElementById('bomMaintFilter'); if (si) si.value = ''; if (sf) sf.value = ''; if (mf) mf.value = ''; bomCurrentPage = 1; loadBom(1); }; window.bomMaintFilterChange = function() { bomCurrentPage = 1; loadBom(1); }; // ─── 物料清单编辑弹窗 ─── window.showBomEdit = function(productCode) { if (!productCode) return; // 缓存弹窗打开时的旧物料数据,用于检测编号变更 window._oldBomMaterials = null; 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) || []; // 缓存旧物料(深拷贝,用于后续检测编号变化) 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')); }); }; window._bomEditRowHtml = function(idx, data) { data = data || {}; var code = escHtml(String(data.material_code || '')); var name = escHtml(String(data.material_name || '')); var qty = data.bom_qty || 1; var cost = data.material_cost || data.material_cost_price || 0; var supName = escHtml(String(data.material_supplier || '')); var html = ''; html += '' + idx + ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '✕'; html += ''; return html; }; window.addBomEditRow = function() { var tbody = document.getElementById('bomEditTbody'); if (!tbody) return; var idx = tbody.querySelectorAll('.bom-bind-row').length + 1; tbody.insertAdjacentHTML('beforeend', window._bomEditRowHtml(idx)); window._bindBomSupEvents(tbody.querySelectorAll('.bom-bind-row:last-child .bom-sup-name')); }; window.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; if (!confirm('确认删除 [' + productCode + '] 的物料清单数据吗?此操作不可恢复!')) return; fetch('/api/bom/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ ' + (res.msg || '删除成功')); loadBom(bomCurrentPage); } else { alert('❌ ' + (res.msg || '删除失败')); } }) .catch(function() { alert('❌ 网络错误'); }); }; // ─── BOM新增弹窗 ─── window.openBomAddModal = function() { var old = document.getElementById('bomAddModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'bomAddModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); // 初始给一行 + 绑定供应商搜索 setTimeout(function() { var tb = document.getElementById('bomNewMatTbody'); if (tb && !tb.querySelectorAll('tr').length) { tb.innerHTML = window._bomAddMatRowHtml(1); } // 绑定供应商自动搜索 window._bindBomSupEvents(document.querySelectorAll('#bomAddModal .bom-sup-name')); }, 50); }; // 新增弹窗 - 物料行HTML window._bomAddMatRowHtml = function(idx, data) { data = data || {}; var code = escHtml(String(data.material_code || '')); var name = escHtml(String(data.material_name || '')); var qty = data.quantity || 1; var sup = escHtml(String(data.supplier_name || '')); var ptype = data.product_type || '包材'; var cost = data.cost_price || 0; return '' + '' + idx + '' + '' + '' + '' + '' + '' + '' + '✕' + ''; }; // 新增弹窗 - 添加行 window.bomAddNewRow = function() { var tb = document.getElementById('bomNewMatTbody'); if (!tb) return; var idx = tb.querySelectorAll('tr').length + 1; tb.insertAdjacentHTML('beforeend', window._bomAddMatRowHtml(idx)); window._bindBomSupEvents(tb.querySelectorAll('tr:last-child .bom-sup-name')); }; // 新增弹窗 - 删除行 window.bomAddRemoveRow = function(el) { var tr = el.closest('tr'); if (!tr) return; var tb = tr.closest('tbody'); tr.remove(); // 重新编号 if (tb) { [].forEach.call(tb.querySelectorAll('tr'), function(r, i) { r.querySelector('td:first-child').textContent = i + 1; }); } }; // 关闭弹窗 window.closeBomAddModal = function() { var m = document.getElementById('bomAddModal'); if (m) m.remove(); }; // 提交新增BOM window.submitBomAdd = function() { var productCode = document.getElementById('bomNewProdCode').value.trim(); if (!productCode) { alert('请输入成品编号'); return; } var tbody = document.getElementById('bomNewMatTbody'); if (!tbody) return; var materials = []; [].forEach.call(tbody.querySelectorAll('.bom-add-row'), function(row) { var code = (row.querySelector('.bom-add-mat-code')?.value || '').trim(); var name = (row.querySelector('.bom-add-mat-name')?.value || '').trim(); var qty = parseFloat(row.querySelector('.bom-add-mat-qty')?.value) || 1; var supName = (row.querySelector('.bom-sup-name')?.value || '').trim(); var cost = parseFloat(row.querySelector('.bom-add-mat-cost')?.value) || 0; var ptype = (row.querySelector('.bom-add-mat-type')?.value || '包材').trim(); if (code) { materials.push({material_code: code, material_name: name, quantity: qty, supplier_name: supName, cost_price: cost, product_type: ptype}); } }); if (materials.length === 0) { alert('请填写至少一行包材信息'); return; } var btn = document.querySelector('.btn-primary[onclick*="submitBomAdd"]'); if (btn) { btn.disabled = true; btn.textContent = '保存中...'; } var prodName = document.getElementById('bomNewProdName')?.value?.trim() || ''; var prodSup = document.getElementById('bomNewProdSup')?.value?.trim() || ''; fetch('/api/bom/create_with_materials', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, product_name: prodName, product_supplier: prodSup, materials: materials}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (btn) { btn.disabled = false; btn.textContent = '💾 保存 BOM'; } if (res.code === 0) { alert('✅ ' + res.msg); closeBomAddModal(); loadBom(1); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function(err) { if (btn) { btn.disabled = false; btn.textContent = '💾 保存 BOM'; } alert('❌ ' + (err.message || '网络错误')); }); }; // ─── BOM导入弹窗 ─── window.openBomImportModal = function() { var old = document.getElementById('bomImportModal'); if (old) old.remove(); var overlay = document.createElement('div'); overlay.id = 'bomImportModal'; overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center'; overlay.onclick = function(e) { if (e.target === overlay) {} }; overlay.innerHTML = [ '' ].join(''); document.body.appendChild(overlay); }; window.previewBomImportFile = function() { var f = document.getElementById('bomImportFile'); var p = document.getElementById('bomImportPreview'); var b = document.getElementById('bomImportBtn'); if (f.files && f.files.length > 0) { p.textContent = '已选择:' + f.files[0].name + '(' + (f.files[0].size / 1024).toFixed(1) + 'KB)'; b.disabled = false; } else { p.textContent = ''; b.disabled = true; } }; window.submitBomImport = function() { var f = document.getElementById('bomImportFile'); if (!f.files || !f.files[0]) return; var btn = document.getElementById('bomImportBtn'); var reader = new FileReader(); reader.onload = function(e) { try { var text = e.target.result; 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 = []; lines.forEach(function(line, i) { if (i === 0 && line.includes('货品编号')) return; var cols = line.split(','); if (cols.length >= 4) { items.push({ product_code: cols[0].trim(), product_name: (cols[1] || '').trim(), material_code: cols[2].trim(), material_name: (cols[3] || '').trim(), quantity: parseFloat(cols[4]) || 1 }); } }); 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; } }; reader.readAsText(f.files[0]); }; // ─── BOM导出 ─── window.exportBom = function() { window.open('/api/bom/export', '_blank'); }; // ─── 统一产品详情弹窗(固定字段,参考弹窗展示内容1方案)─── var POPUP_VISIBLE_FIELDS = [ 'product_type', '产品种类', 'product_code', 'product_name', '产品分级', '昨日销量', '销售趋势', '库存', '加工厂库存', '生产在途', '采购在途', '日销量预测', '供应商名称', '3天平均销量', '7天平均销量', '14天平均销量', '库存可支撑天数', '加工厂库存支撑天数', '总支撑天数', '30天平均销量', '极值判断', '生产货期', '物流时间', '创建时间' ]; /** 字段显示名称映射 */ var POPUP_FIELD_LABELS = { 'product_type': '产品类型', '产品种类': '产品种类', 'product_code': '货品编号', 'product_name': '货品名称', '产品分级': '产品分级', 'brand': '品牌', 'specification': '规格', 'cost_price': '成本价', '昨日销量': '昨日销量', '3天平均销量': '3天平均销量', '7天平均销量': '7天平均销量', '14天平均销量': '14天平均销量', '30天平均销量': '30天平均销量', '60天平均销量': '60天平均销量', '销售趋势': '销售趋势', '库存': '库存', '加工厂库存': '加工厂库存', '生产在途': '生产在途', '采购在途': '采购在途', '日销量预测': '日销量预测', '库存可支撑天数': '库存可支撑天数', '库存工厂可支撑天数': '现货采购在途', '加工厂库存支撑天数': '加工厂库存支撑', '库存工厂待生产可支撑天数': '定制品在途', '总支撑天数': '总支撑天数', '库存健康指数': '库存健康指数', '风险把控': '风险把控', '建议下单数量': '建议下单数量', '实际下单数量': '实际下单数量', '建议回货数量': '建议回货数量', '供应商名称': '供应商名称', 'supplier_code': '供应商编码', '采购跟单': '采购跟单', '采购计划': '采购计划', '运营专员': '运营专员', '开发专员': '开发专员', '归属平台': '归属平台', '生产货期': '生产货期', '物流时间': '物流时间', 'lead_time': '生产货期', 'logistics_time': '物流时间', '最低起订量': '最低起订量', '创建时间': '创建时间', '是否新品': '是否新品', '一级类目': '一级类目', '二级类目': '二级类目', '三级类目': '三级类目', '极值判断': '极值判断', '14天波动率': '14天波动率', '波动风险': '波动风险', '3天最高销量': '3天最高销量', '7天最高销量': '7天最高销量', '14天最高销量': '14天最高销量', '30天最高销量': '30天最高销量', '14天最低销量': '14天最低销量', '14天标准差': '14天标准差', '最低销量偏差率': '最低销量偏差率', '最高销量偏差率': '最高销量偏差率', }; /** 格式化字段值 */ function formatPopupFieldValue(field, val, p) { if (val === null || val === undefined || val === '') return '-'; if (field === 'cost_price') return '¥' + parseFloat(val || 0).toFixed(4); if (field === 'product_type') { return escHtml(val); } if (field === 'product_code' || field === 'product_name') { return escHtml(val); } if (field === '产品分级') { var gcolors = {'S级':'#f50','A级':'#1890ff','B级':'#52c41a','C级':'#faad14','不参与评级':'#999'}; var gc = gcolors[val] || '#999'; return '' + escHtml(val) + ''; } if (field === '库存健康指数') { var v = parseFloat(val); if (isNaN(v) || v === 999) return '999'; if (v >= 1.5) return '' + v.toFixed(1) + ''; if (v >= 0.8) return '' + v.toFixed(1) + ''; return '' + v.toFixed(1) + ''; } if (field === '风险把控') { var rc = {'正常':'tag-green','新品':'tag-blue','呆滞品':'tag-orange','需启动清库存':'tag-red'}; return '' + escHtml(val) + ''; } if (field === '销售趋势') { var tc = { '开始上升':'tag-blue','开始下跌':'tag-orange','平稳趋势':'tag-green', '持续上升':'tag-red','持续下跌':'tag-purple','上升趋稳':'tag-blue', '下跌趋稳':'tag-orange','急增':'tag-red','急跌':'tag-red','近期无销售':'tag-grey' }; var c = tc[val] || 'tag-grey'; return '' + escHtml(val) + ''; } if (field === '日销量预测') { if (val === '近期无销售') return '近期无销售'; return '' + escHtml(val) + ''; } if (field === '建议下单数量' || field === '实际下单数量') { var v = parseInt(val); if (isNaN(v) || v === 0) return '0'; return '' + v.toLocaleString() + ''; } if (field === '建议回货数量') { var v = parseFloat(val); if (isNaN(v) || v === 0) return '0'; return '' + v.toFixed(1) + ''; } if (field === '库存可支撑天数' || field === '总支撑天数' || field === '加工厂库存支撑天数' || field === '库存工厂可支撑天数' || field === '库存工厂待生产可支撑天数') { var v = parseFloat(val); if (isNaN(v)) return '-'; if (v === 999) return '999'; return v.toFixed(1); } if (typeof val === 'number') { return val.toLocaleString(); } return escHtml(val); } // 占位:下单功能(待完善) // ─── 正式下单数量编辑 ─── function editFormalQty(productCode) { if (window._canEditField && !window._canEditField('planning', '正式下单数量')) return; var valSpan = document.getElementById('fqv-' + productCode); if (!valSpan) return; var currentVal = parseInt(valSpan.dataset.val) || 0; var code = productCode; // 替换为输入框 var input = document.createElement('input'); input.type = 'number'; input.className = 'form-input'; input.style.width = '80px'; input.value = currentVal; input.dataset.original = currentVal; valSpan.replaceWith(input); input.focus(); input.select(); // Enter 确认 input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); // 已有确认弹窗打开,忽略重复回车 if (input.dataset.confirming) return; var newVal = parseInt(input.value); if (isNaN(newVal) || newVal < 0) newVal = 0; if (newVal === parseInt(input.dataset.original)) { replaceBack(code, newVal); return; } // 标记正在确认,阻止 blur 恢复原值 input.dataset.confirming = '1'; showCenteredConfirm('修改确认', '确认将正式下单数量修改为 ' + newVal + '?').then(function(confirmed) { delete input.dataset.confirming; if (confirmed) { // 保存到后端 - 只更新数量,不创建待下单 fetch('/api/purchase/update_formal_qty', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code, formal_order_qty: newVal}) }); replaceBack(code, newVal); } else { // 取消 → 恢复原值 replaceBack(code, parseInt(input.dataset.original)); } }); } else if (e.key === 'Escape') { replaceBack(code, parseInt(input.dataset.original)); } }); // 没有回车确认就失焦 → 恢复原值 input.addEventListener('blur', function() { if (input.dataset.confirming) return; setTimeout(function() { replaceBack(code, parseInt(input.dataset.original)); }, 200); }); } function replaceBack(code, val) { var input = document.querySelector('.formal-qty-wrap input'); if (!input) return; var wrap = input.closest('.formal-qty-wrap'); if (!wrap) return; var newSpan = document.createElement('span'); newSpan.className = 'formal-qty-val'; newSpan.id = 'fqv-' + code; newSpan.dataset.code = code; newSpan.dataset.val = val; newSpan.textContent = val; input.replaceWith(newSpan); } function planBatchOrder() { var checkedCodes = Object.keys(window._planSelectedProductCodes); if (checkedCodes.length === 0) { alert('请先勾选要下单的商品'); return; } if (!confirm('确认将选中的 ' + checkedCodes.length + ' 个商品加入待下单列表?')) 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) { 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:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; modal.innerHTML = '
' + '
' + '

' + escHtml(title) + '

' + '

' + message + '

' + '
' + '
' + '' + '
'; document.body.appendChild(modal); document.getElementById('alertOkBtn').onclick = function() { modal.remove(); }; // 点击遮罩层不关闭弹窗(仅通过按钮关闭) modal.addEventListener('click', function(e) { if (e.target === 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:10000;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) {} }); }); }; // 从计划预测中移除产品 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)) .then(function(r) { return r.json(); }) .then(function(res) { var bomMaterials = (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; // 过滤掉内料,只显示非内料之外包材数据 bomMaterials = bomMaterials.filter(function(b) { return !(b.material_name && b.material_name.indexOf('内料') !== -1); }); 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] || '') + '
'; } 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']; var salesFields = ['\u4ea7\u54c1\u5206\u7ea7', '\u6628\u65e5\u9500\u91cf', '\u9500\u552e\u8d8b\u52bf', '\u5e93\u5b58', '\u52a0\u5de5\u5382\u5e93\u5b58', '\u751f\u4ea7\u5728\u9014', '\u91c7\u8d2d\u5728\u9014', '\u65e5\u9500\u91cf\u9884\u6d4b', '3\u5929\u5e73\u5747\u9500\u91cf', '7\u5929\u5e73\u5747\u9500\u91cf', '14\u5929\u5e73\u5747\u9500\u91cf', '\u5e93\u5b58\u53ef\u652f\u6491\u5929\u6570', '\u52a0\u5de5\u5382\u5e93\u5b58\u652f\u6491\u5929\u6570', '\u603b\u652f\u6491\u5929\u6570', '30\u5929\u5e73\u5747\u9500\u91cf', '\u6781\u503c\u5224\u65ad']; // \u751f\u6210\u57fa\u7840\u4fe1\u606f\u5b57\u6bb5HTML\uff082\u5217\u7b49\u8ddd\uff09 var basicHtml = ''; for (var i = 0; i < basicFields.length; i++) { var field = basicFields[i]; var label = POPUP_FIELD_LABELS[field] || field; var val = p[field] !== undefined ? p[field] : '-'; var valHtml = formatPopupFieldValue(field, val, p); basicHtml += '
' + '' + escHtml(label) + '' + '' + valHtml + '' + '
'; } // \u751f\u6210\u9500\u91cf\u4fe1\u606f\u5b57\u6bb5HTML\uff084\u5217\u7b49\u8ddd\uff09 var salesHtml = ''; for (var i = 0; i < salesFields.length; i++) { var field = salesFields[i]; var label = POPUP_FIELD_LABELS[field] || field; var val = p[field] !== undefined ? p[field] : '-'; var valHtml = formatPopupFieldValue(field, val, p); salesHtml += '
' + '' + escHtml(label) + '' + '' + valHtml + '' + '
'; } // BOM\u7269\u6599\u6e05\u5355 var bomHtml = ''; var order = null; var bomMaterials = []; var sales = []; if (bomRes.code === 0) { order = bomRes.data.order; bomMaterials = bomRes.data.bom_materials || []; sales = bomRes.data.sales || []; } if (bomMaterials.length > 0) { var totalMatCost = 0; bomHtml = bomMaterials.map(function(m) { return '' + '' + escHtml(m.material_code) + '' + '' + escHtml(m.material_name) + '' + '' + m.available_qty.toLocaleString() + '' + '' + m.production_in_transit.toLocaleString() + '' + '' + m.purchase_in_transit.toLocaleString() + '' + '' + m.material_cost.toFixed(4) + '' + ''; }).join(''); } body.innerHTML = `
\u57fa\u7840\u4fe1\u606f
${basicHtml}
\u9500\u91cf\u4fe1\u606f
默认数据 云仓数据 外仓数据
${salesHtml}
BOM\u7269\u6599\u6e05\u5355 ${order && order.total_order_qty ? '\u6210\u54c1\u4e0b\u5355\u91cf: ' + (order.total_order_qty || 0).toLocaleString() + ' \u4ef6\uff08\u5efa\u8bae: ' + (order.total_suggested_qty || 0).toLocaleString() + '\uff09' : ''}
${bomMaterials.length > 0 ? '' + bomHtml + '
\u7269\u6599\u7f16\u53f7\u7269\u6599\u540d\u79f0\u53ef\u7528\u5e93\u5b58\u751f\u4ea7\u5728\u9014\u91c7\u8d2d\u5728\u9014\u5355\u4ef7
' : '
\u6682\u65e0BOM\u7269\u6599\u6e05\u5355
' }
\u8ba2\u5355\u5728\u9014 \u52a0\u8f7d\u4e2d...
\u67e5\u8be2\u4e2d...
\u9500\u91cf\u8d8b\u52bf
\u65e5\u671f\u8303\u56f4 \u2014 | \u67f1\u72b6\u56fe / \u6298\u7ebf\u56fe
${sales.length === 0 ? '
\u6682\u65e0\u9500\u91cf\u6570\u636e
' : ''}
`; // 使用 detail_with_bom 返回的 orders_in_transit 数据显示订单在途(国内/海外/包材订单) var transitOrders = bomRes.data && bomRes.data.orders_in_transit || []; document.getElementById('orderInTransitLoading').textContent = ''; var container = document.getElementById('orderInTransitBody'); if (container) { if (transitOrders.length === 0) { container.innerHTML = '
暂无在途订单
'; } else { var totalQty = 0; var html = ''; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; transitOrders.forEach(function(o) { totalQty += parseInt(o['下单数量']) || 0; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
下单时间货品编码货品名称下单数量未到数量下单进度预计到货时间
' + (o['下单时间'] || '-') + '' + (o['编码'] || '') + '' + (o['货品名称'] || '-') + '' + (parseInt(o['下单数量']) || 0).toLocaleString() + '' + (parseInt(o['未到数量']) || 0).toLocaleString() + '' + (o['下单进度'] || '-') + '' + (o['预计到货时间'] || '-') + '
'; html += '
' + '共 ' + transitOrders.length + ' 笔在途订单,' + '合计数量: ' + totalQty.toLocaleString() + '' + '
'; container.innerHTML = html; } } // \u9500\u91cf\u56fe\u8868 if (sales.length > 0) { window._salesData = sales; window._currentSalesChartType = 'bar'; setTimeout(function() { var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); if (startEl) startEl.value = sales[0].date; if (endEl) endEl.value = sales[sales.length - 1].date; renderSalesChartByRange('bar'); }, 100); } }); }; // ─── BOM管理模式下的简化详情弹窗(仅基础信息 + BOM物料清单)─── window.showBomProductDetail = function(productCode) { if (!productCode) return; const old = document.getElementById('productDetailModal'); if (old) old.remove(); const modal = document.createElement('div'); modal.id = 'productDetailModal'; modal.className = 'pd-overlay'; modal.style.display = 'flex'; modal.innerHTML = `
产品详情 \u2014 加载中...
加载中...
`; document.body.appendChild(modal); Promise.all([ fetch('/api/product/planning_detail?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }), fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)).then(function(r) { return r.json(); }) ]).then(function(results) { var body = document.getElementById('pdBody'); if (!body) return; var planRes = results[0]; var bomRes = results[1]; if (planRes.code !== 0) { body.innerHTML = '
' + (planRes.msg || '\u67e5\u8be2\u5931\u8d25') + '
'; return; } var p = planRes.data; var titleEl = modal.querySelector('.pd-title'); if (titleEl) { titleEl.innerHTML = '\u4ea7\u54c1\u8be6\u60c5 \u2014 ' + escHtml(p.product_name || productCode) + ''; } var basicFields = ['product_type', '\u4ea7\u54c1\u79cd\u7c7b', 'product_code', 'product_name', '\u4f9b\u5e94\u5546\u540d\u79f0', '\u751f\u4ea7\u8d27\u671f', '\u7269\u6d41\u65f6\u95f4', '\u521b\u5efa\u65f6\u95f4']; var basicHtml = ''; for (var i = 0; i < basicFields.length; i++) { var field = basicFields[i]; var label = POPUP_FIELD_LABELS[field] || field; var val = p[field] !== undefined ? p[field] : '-'; var valHtml = formatPopupFieldValue(field, val, p); basicHtml += '
' + '' + escHtml(label) + '' + '' + valHtml + '' + '
'; } var bomHtml = ''; var order = null; var bomMaterials = []; if (bomRes.code === 0) { order = bomRes.data.order; bomMaterials = bomRes.data.bom_materials || []; } if (bomMaterials.length > 0) { bomHtml = bomMaterials.map(function(m) { return '' + '' + escHtml(m.material_code) + '' + '' + escHtml(m.material_name) + '' + '' + m.available_qty.toLocaleString() + '' + '' + m.production_in_transit.toLocaleString() + '' + '' + m.purchase_in_transit.toLocaleString() + '' + '' + m.material_cost.toFixed(4) + '' + ''; }).join(''); } body.innerHTML = `
\u57fa\u7840\u4fe1\u606f
${basicHtml}
BOM\u7269\u6599\u6e05\u5355 ${order && order.total_order_qty ? '\u6210\u54c1\u4e0b\u5355\u91cf: ' + (order.total_order_qty || 0).toLocaleString() + ' \u4ef6\uff08\u5efa\u8bae: ' + (order.total_suggested_qty || 0).toLocaleString() + '\uff09' : ''}
${bomMaterials.length > 0 ? '' + bomHtml + '
\u7269\u6599\u7f16\u53f7\u7269\u6599\u540d\u79f0\u53ef\u7528\u5e93\u5b58\u751f\u4ea7\u5728\u9014\u91c7\u8d2d\u5728\u9014\u5355\u4ef7
' : '
\u6682\u65e0BOM\u7269\u6599\u6e05\u5355
' }
`; }); }; // 切换销量信息tab页 window.switchSalesTab = function(tab) { var tabs = ['default', 'cloud', 'external']; tabs.forEach(function(t) { var tabEl = document.querySelector('.sales-tab[data-tab="' + t + '"]'); var contentEl = document.getElementById('salesTab' + t.charAt(0).toUpperCase() + t.slice(1)); if (tab === t) { if (tabEl) { tabEl.style.background = '#e6f7ff'; tabEl.style.color = '#1890ff'; tabEl.style.fontWeight = '600'; } if (contentEl) contentEl.style.display = ''; } else { if (tabEl) { tabEl.style.background = ''; tabEl.style.color = '#888'; tabEl.style.fontWeight = ''; } if (contentEl) contentEl.style.display = 'none'; } }); }; // ─── 编辑弹窗(动态跟随列显隐)─── // 列字段名 → 可编辑字段映射 const PLAN_EDITABLE_FIELDS = { 'cost_price': {label:'成本价', field:'cost_price', type:'number'}, 'product_type': {label:'产品类型', field:'product_type', type:'select', options:['成品','包材','现货','赠品','清库存','其他']}, '产品种类': {label:'产品种类', field:'product_type', type:'text'}, '品牌': {label:'品牌', field:'brand', type:'text'}, 'specification': {label:'规格', field:'specification', type:'text'}, '一级类目': {label:'一级类目', field:'category1', type:'text'}, '二级类目': {label:'二级类目', field:'category2', type:'text'}, '三级类目': {label:'三级类目', field:'category3', type:'text'}, '供应商名称': {label:'供应商名称', field:'supplier_name_match', type:'text'}, '供应商编码': {label:'供应商编码', field:'supplier_code', type:'text'}, '生产货期': {label:'货期', field:'lead_time', type:'number'}, '物流时间': {label:'物流时间', field:'logistics_time', type:'number'}, '采购跟单': {label:'采购跟单', field:'procurement_follower', type:'text'}, '采购计划': {label:'采购计划', field:'procurement_planner', type:'text'}, '开发专员': {label:'开发专员', field:'developer', type:'text'}, '归属平台': {label:'归属平台', field:'platform', type:'text'}, '运营专员': {label:'运营专员', field:'operator', type:'text'} }; // 产品信息管理 - 删除产品 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 (code) url += '&prod_code=' + encodeURIComponent(code); if (name) url += '&prod_name=' + encodeURIComponent(name); 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) { if (!confirm('确认恢复产品 ' + productCode + ' 吗?')) 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) { if (!confirm('确认彻底删除产品 ' + productCode + ' 吗?\n此操作不可恢复!')) 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; } if (!confirm('确认批量恢复 ' + codes.length + ' 个商品吗?')) 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; } if (!confirm('确认彻底删除 ' + codes.length + ' 个商品吗?\n此操作不可恢复!')) return; var done = 0; codes.forEach(function(code) { fetch('/api/product/permanent_delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: code}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) done++; }); }); setTimeout(function() { loadRecycleBin(_recycleCurrentPage); window._recycleChanged = true; }, 500); }; window.closeRecycleBin = function() { var modal = document.getElementById('recycleBinModal'); if (modal) modal.remove(); // 如果回收站有改动(恢复/删除),刷新主列表 if (window._recycleChanged) { window._recycleChanged = false; loadProducts(prodCurrentPage || 1); } }; // 产品信息管理 - 查看详情 window.viewProductDetail = function(productCode) { if (!productCode) return; var row = window._prodRowCache && window._prodRowCache[productCode]; if (!row) { showProductDetailEdit(productCode); return; } var old = document.getElementById('productViewModal'); if (old) old.remove(); var isPackaging = prodActiveTab === '非成品'; var packHide = ['品牌','采购跟单','采购计划','平台','运营','开发专员']; var fields = [ {label:'货品编号', val:row.product_code}, {label:'货品名称', val:row.product_name}, {label:'品牌', val:row.brand}, {label:'货品类型', val:row.product_type}, {label:'供应商编码', val:row.supplier_code}, {label:'供应商名称', val:row.supplier_name_match}, {label:'成本价', val:row.cost_price}, {label:'货期(天)', val:row.lead_time}, {label:'物流时间(天)', val:row.logistics_time}, {label:'最低起订量', val:row.min_order_qty != null ? parseInt(row.min_order_qty) : '-'}, {label:'采购跟单', val:row.procurement_follower}, {label:'采购计划', val:row.procurement_planner}, {label:'平台', val:row.platform}, {label:'运营', val:row.operator}, {label:'开发专员', val:row.developer}, {label:'创建时间', val:(row.created_at||'').toString().slice(0,10)}, {label:'导入日期', val:row.import_date}, ]; if (isPackaging) fields = fields.filter(function(f) { return packHide.indexOf(f.label) < 0; }); var html = ''; for (var i = 0; i < fields.length; i += 2) { var left = fields[i]; var right = fields[i + 1]; var bg = (i % 4 === 0) ? '#f9f9f9' : '#fff'; 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 += '
' + left.label + '' + lv + '' + 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 = []; 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; }); } }); } 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.selectAddSupplier = function(code, name) { document.getElementById('pa_supcode').value = code || ''; document.getElementById('pa_supname').value = name || ''; document.getElementById('pa_supDropdown').style.display = 'none'; }; // 点击其他地方关闭下拉 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'; }); // ─── 采购跟单记忆 ─── window.getFollowerHistory = function() { var raw = localStorage.getItem('_pa_follower_history'); return raw ? JSON.parse(raw) : []; }; window.saveFollowerHistory = function(val) { if (!val) return; var list = window.getFollowerHistory(); list = list.filter(function(v) { return v !== val; }); list.unshift(val); if (list.length > 20) list = list.slice(0, 20); localStorage.setItem('_pa_follower_history', JSON.stringify(list)); }; window.showFollowerHistory = function() { var dd = document.getElementById('pa_folDropdown'); if (!dd) return; var input = document.getElementById('pa_follower'); var val = (input.value || '').toLowerCase(); var list = window.getFollowerHistory(); if (val) list = list.filter(function(v) { return v.toLowerCase().indexOf(val) >= 0; }); if (list.length === 0) { dd.style.display = 'none'; return; } var html = ''; list.forEach(function(v) { html += '
' + escHtml(v) + '
'; }); dd.innerHTML = html; dd.style.display = 'block'; }; window.selectFollower = function(val) { document.getElementById('pa_follower').value = val; document.getElementById('pa_folDropdown').style.display = 'none'; }; // 产品信息管理快捷编辑(使用缓存数据,无需等待) window.quickEditProduct = function(productCode) { if (!productCode) return; var row = window._prodRowCache && window._prodRowCache[productCode]; if (!row) { // 缓存未命中,回退到原始编辑 showProductDetailEdit(productCode); return; } // 关闭已有弹窗 var oldEdit = document.getElementById('productEditModal'); if (oldEdit) oldEdit.remove(); var modal = document.createElement('div'); modal.id = 'productEditModal'; modal.className = 'pd-overlay'; modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center'; modal.innerHTML = '
' + '
✏️ 编辑产品信息' + '
' + '
' + _buildProductEditForm(row) + '
'; document.body.appendChild(modal); }; // 构建产品编辑表单 function _buildProductEditForm(row) { var isNonProd = prodActiveTab === '非成品'; var hideFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer']; var fields = [ {key:'product_code', label:'货品编号', val:row.product_code, type:'text'}, {key:'product_name', label:'货品名称', val:row.product_name, type:'text'}, {key:'brand', label:'品牌', val:row.brand, type:'text'}, {key:'product_type', label:'货品类型', val:row.product_type, type:'select', opts:['成品','现货','清库存','赠品','原材料','包材','其他']}, {key:'supplier_code', label:'供应商编码', val:row.supplier_code, type:'text', ro:true}, {key:'supplier_name_match', label:'供应商名称', val:row.supplier_name_match, type:'text', supSearch:true}, {key:'cost_price', label:'成本价', val:row.cost_price, type:'number'}, {key:'lead_time', label:'货期(天)', val:row.lead_time, type:'number'}, {key:'logistics_time', label:'物流时间(天)', val:row.logistics_time, type:'number', max:999}, {key:'min_order_qty', label:'最低起订量', val:row.min_order_qty != null ? parseInt(row.min_order_qty) : 0, type:'number'}, {key:'procurement_follower', label:'采购跟单', val:row.procurement_follower, type:'text'}, {key:'procurement_planner', label:'采购计划', val:row.procurement_planner, type:'text'}, {key:'platform', label:'平台', val:row.platform, type:'text'}, {key:'operator', label:'运营', val:row.operator, type:'text'}, {key:'developer', label:'开发专员', val:row.developer, type:'text'}, {key:'created_at', label:'创建时间', val:row.created_at ? String(row.created_at).slice(0,10) : '', type:'date'}, ]; if (isNonProd) fields = fields.filter(function(f) { return hideFields.indexOf(f.key) < 0; }); var html = '
'; fields.forEach(function(f) { var val = escHtml(String(f.val != null ? f.val : '')); var roAttr = f.ro ? ' readonly style="background:#f5f5f5;color:#999"' : ''; if (f.type === 'select') { var opts = ''; (f.opts || []).forEach(function(o) { var sel = o === f.val ? ' selected' : ''; opts += ''; }); html += '
'; } else if (f.supSearch) { html += '
'; } else { html += '
'; } }); html += '
'; html += '
' + '' + '
'; return html; } // ─── 编辑弹窗供应商模糊搜索 ─── window.editSupSearch = function(input) { var dd = input.parentElement.querySelector('.pe-sup-dd'); if (!dd) return; var val = input.value.trim(); if (!val || !window._supplierList) { dd.style.display = 'none'; return; } var v = val.toLowerCase(); var matched = window._supplierList.filter(function(s) { return (s.supplier_name || '').toLowerCase().indexOf(v) >= 0; }); if (matched.length === 0 || matched.length > 50) { dd.style.display = 'none'; return; } dd.innerHTML = ''; dd.style.display = 'block'; matched.slice(0, 20).forEach(function(s) { var item = document.createElement('div'); item.style.cssText = 'padding:4px 8px;cursor:pointer;font-size:12px;border-bottom:1px solid #f0f0f0'; item.className = 'pe-sup-dd-item'; item.dataset.code = s.supplier_code || ''; item.dataset.name = s.supplier_name || ''; item.innerHTML = '' + escHtml(s.supplier_name) + '' + (s.supplier_code ? ' ' + escHtml(s.supplier_code) + '' : ''); dd.appendChild(item); }); }; window.saveQuickEditProduct = function(productCode) { var isNonProd = prodActiveTab === '非成品'; var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer','created_at']; var fields = {}; var newCodeEl = document.getElementById('pe_product_code'); var newCode = newCodeEl ? newCodeEl.value.trim() : productCode; if (newCode && newCode !== productCode) { fields['product_code'] = newCode; } var fieldNames = ['product_name','brand','product_type','supplier_code','supplier_name_match', 'cost_price','lead_time','logistics_time','min_order_qty', 'procurement_follower','procurement_planner', 'platform','operator','developer','created_at']; 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; editFields.push({cfg: cfg, colField: colField, val: val}); } }); 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; html += '
'; html += ''; var val = (ef.val !== null && ef.val !== undefined) ? ef.val : ''; if (f.type === 'select' && f.options) { html += ''; } else if (f.type === 'number') { html += ''; } else { html += ''; } html += '
'; }); html += '
'; } html += '
'; html += ''; if (editFields.length > 0) { html += ''; } html += '
'; document.getElementById('pdEditBody').innerHTML = html; }); }; window.confirmProductEdit = function(productCode) { var fields = {}; var changed = false; // 读取货品编号 var newCodeInput = document.getElementById('edit_product_code'); var newCode = newCodeInput ? newCodeInput.value.trim() : productCode; if (newCode !== productCode && newCode) { fields['product_code'] = newCode; changed = true; } // 查找所有以 edit_ 开头的 input 元素(排除 product_code,因为已单独处理) document.querySelectorAll('#productEditModal input[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':'归属平台' }; // 自定义确认弹窗(替代原生 confirm) var oldConfirm = document.getElementById('editConfirmModal'); if (oldConfirm) oldConfirm.remove(); var confirmHtml = ''; confirmHtml += '
'; confirmHtml += '

确认保存以下修改?

'; Object.keys(fields).forEach(function(f) { confirmHtml += '
'; confirmHtml += '' + (fieldLabels[f] || f) + ''; confirmHtml += '' + fields[f] + ''; confirmHtml += '
'; }); confirmHtml += '
'; var confirmModal = document.createElement('div'); confirmModal.id = 'editConfirmModal'; confirmModal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto'; confirmModal.innerHTML = '
' + confirmHtml + '
' + '' + '
'; document.body.appendChild(confirmModal); // 绑定确认按钮 document.getElementById('editConfirmSaveBtn').onclick = function() { confirmModal.remove(); doSaveProductEdit(productCode, fields); }; }; window.doSaveProductEdit = function(productCode, fields) { if (typeof loadPlanPage !== 'function' && typeof planCurrentPage === 'undefined') { planCurrentPage = 1; } // 如果货品编号变更,使用新编号刷新页面 var refreshCode = fields['product_code'] || productCode; fetch('/api/product/batch_update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_code: productCode, fields: fields}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { alert('✅ 保存成功'); var editModal = document.getElementById('productEditModal'); if (editModal) editModal.remove(); if (typeof loadPlanPage === 'function') loadPlanPage(planCurrentPage || 1); } else { alert('❌ ' + (res.msg || '保存失败')); } }) .catch(function() { alert('❌ 网络错误'); }); }; window.showProductDetailFull = function(productCode) { if (!productCode) return; const old = document.getElementById('productDetailFullModal'); if (old) old.remove(); const modal = document.createElement('div'); modal.id = 'productDetailFullModal'; modal.className = 'pd-overlay'; modal.style.display = 'flex'; modal.innerHTML = `
完整产品详情 — ${escHtml(productCode)}
加载中...
`; document.body.appendChild(modal); fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode)) .then(r => r.json()) .then(res => { const body = document.getElementById('pdFullBody'); if (res.code !== 0) { body.innerHTML = '
' + (res.msg || '查询失败') + '
'; return; } const p = res.data.product; const order = res.data.order; const bomMaterials = res.data.bom_materials || []; const sales = res.data.sales || []; // BOM表格 var bomHtml = ''; if (bomMaterials.length > 0) { var totalMatCost = 0; bomHtml = bomMaterials.map(function(m) { totalMatCost += m.total_cost; var shortage = m.required_qty - m.available_qty; var shortageClass = shortage > 0 ? 'pd-warn' : 'pd-ok'; return '' + '' + escHtml(m.material_code) + '' + '' + escHtml(m.material_name) + '' + '' + m.bom_qty + '' + '' + (m.required_qty || 0).toLocaleString() + '' + '' + m.available_qty.toLocaleString() + '' + '' + m.production_in_transit.toLocaleString() + '' + '' + m.purchase_in_transit.toLocaleString() + '' + '¥' + m.material_cost.toFixed(4) + '' + '¥' + m.total_cost.toLocaleString() + '' + '' + (shortage > 0 ? '+' : '') + shortage.toLocaleString() + '' + ''; }).join(''); var summaryHtml = '' + '合计' + '' + bomMaterials.reduce(function(s,m){return s+m.required_qty;},0).toLocaleString() + '' + '' + bomMaterials.reduce(function(s,m){return s+m.available_qty;},0).toLocaleString() + '' + '' + bomMaterials.reduce(function(s,m){return s+m.production_in_transit;},0).toLocaleString() + '' + '' + bomMaterials.reduce(function(s,m){return s+m.purchase_in_transit;},0).toLocaleString() + '' + '' + '¥' + totalMatCost.toLocaleString() + '' + '' + ''; } body.innerHTML = ''; // BOM物料清单 if (bomMaterials.length > 0) { body.innerHTML += '
' + '
' + '' + '' + '' + 'BOM物料清单' + '' + '成品下单量: ' + (order.total_order_qty || 0).toLocaleString() + ' 件' + '(建议: ' + (order.total_suggested_qty || 0).toLocaleString() + ')' + '' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + bomHtml + '' + summaryHtml + '
物料编号物料名称BOM用量需用量可用库存生产在途采购在途操作单价总成本缺口
' + '
' + '
'; } else { body.innerHTML += '
暂无BOM物料清单
'; } // 订单在途 body.innerHTML += '
' + '' + '' + '' + '订单在途' + '加载中...' + '
' + '
' + '
查询中...
' + '
'; // 使用 detail_with_bom 返回的 orders_in_transit 数据显示订单在途(国内/海外/包材订单) var transitOrders = bomRes.data && bomRes.data.orders_in_transit || []; document.getElementById('orderInTransitLoading').textContent = ''; var container = document.getElementById('orderInTransitBody'); if (container) { if (transitOrders.length === 0) { container.innerHTML = '
暂无在途订单
'; } else { var totalQty = 0; var html = ''; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; transitOrders.forEach(function(o) { totalQty += parseInt(o['下单数量']) || 0; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
下单时间货品编码货品名称下单数量未到数量下单进度预计到货时间
' + (o['下单时间'] || '-') + '' + (o['编码'] || '') + '' + (o['货品名称'] || '-') + '' + (parseInt(o['下单数量']) || 0).toLocaleString() + '' + (parseInt(o['未到数量']) || 0).toLocaleString() + '' + (o['下单进度'] || '-') + '' + (o['预计到货时间'] || '-') + '
'; html += '
' + '共 ' + transitOrders.length + ' 笔在途订单,' + '合计数量: ' + totalQty.toLocaleString() + '' + '
'; container.innerHTML = html; container.innerHTML = html; } } // 销量趋势 body.innerHTML += '
' + '
' + '' + '' + '' + '销量趋势' + '
' + '日期范围' + '' + '—' + '' + '' + '|' + '柱状图' + '/' + '折线图' + '
' + '
' + '
' + '
' + '' + '
' + (sales.length === 0 ? '
暂无销量数据
' : '') + '
' + '
'; // 底部信息 body.innerHTML += ''; if (sales.length > 0) { // 设置日期范围输入框的默认值(数据的起止日期) setTimeout(function() { if (sales.length > 0) { var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); if (startEl) startEl.value = sales[0].date; if (endEl) endEl.value = sales[sales.length - 1].date; } renderSalesChartByRange('bar'); }, 100); } }); }; window.closeProductDetailFull = function() { const modal = document.getElementById('productDetailFullModal'); if (modal) modal.remove(); if (window._salesChart) { window._salesChart.destroy(); window._salesChart = null; } }; window.switchSalesChartType = function(type) { if (!window._salesData) return; window._currentSalesChartType = type; var barEl = document.getElementById('chartTypeBar'); var lineEl = document.getElementById('chartTypeLine'); if (barEl && lineEl) { if (type === 'bar') { barEl.style.color = '#1890ff'; barEl.style.fontWeight = '600'; lineEl.style.color = '#8c8c8c'; lineEl.style.fontWeight = 'normal'; } else { barEl.style.color = '#8c8c8c'; barEl.style.fontWeight = 'normal'; lineEl.style.color = '#1890ff'; lineEl.style.fontWeight = '600'; } } renderSalesChartByRange(type); }; function getSalesFilteredData() { var allSales = window._salesData || []; if (allSales.length === 0) return []; var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); var startDate = startEl ? startEl.value : ''; var endDate = endEl ? endEl.value : ''; if (!startDate && !endDate) return allSales; return allSales.filter(function(s) { var d = s.date; if (startDate && d < startDate) return false; if (endDate && d > endDate) return false; return true; }); } function updateSalesChartRange() { renderSalesChartByRange(window._currentSalesChartType || 'bar'); } function resetSalesChartRange() { var startEl = document.getElementById('salesDateStart'); var endEl = document.getElementById('salesDateEnd'); if (startEl) startEl.value = ''; if (endEl) endEl.value = ''; renderSalesChartByRange(window._currentSalesChartType || 'bar'); } function renderSalesChartByRange(chartType) { chartType = chartType || 'bar'; var filtered = getSalesFilteredData(); renderSalesChart(filtered, chartType); } function renderSalesChart(sales, chartType) { chartType = chartType || 'bar'; const canvas = document.getElementById('salesTrendChart'); if (!canvas) return; if (window._salesChart) { window._salesChart.destroy(); } const labels = sales.map(function(s) { return s.date; }); const data = sales.map(function(s) { return s.qty; }); const barColor = 'rgba(24, 144, 255, 0.6)'; const lineColor = '#096dd9'; 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: false } }, 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_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() || '', 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(), 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('❌ 网络错误'); }); };