// 蝶雅采购智能体平台 - 前端逻辑 let charts = {}; // 页面缓存:切换菜单时不销毁页面状态 var CACHE_VERSION = 59; 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; } 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, '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']; var FROZEN_WIDTHS = { 'product_type': 75, 'product_code': 130, 'product_name': 210 }; 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]) { 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', '/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 缓存 function initNavigation() { 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(); } // ─── 反馈列表加载 ─── function loadFbList() { 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?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' : '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 attachHtml = ''; 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; if (isImage) { var imgSrc = a.path.startsWith('/') ? '/static' + a.path : '/static/' + a.path; attachHtml += ''; } else { var sizeStr = a.size > 1024*1024 ? (a.size/1024/1024).toFixed(1)+'MB' : (a.size/1024).toFixed(1)+'KB'; attachHtml += '📎 ' + escHtml(a.name) + ' (' + sizeStr + ')'; } }); attachHtml += '
'; } var actionsHtml = ''; var isAdmin = window._currentUser && (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin'); var isOwner = window._currentUser && window._currentUser.user_id === item.user_id; var canEdit = isAdmin || isOwner; if (isAdmin) { if (item.status === 'pending') { actionsHtml += ''; } else { actionsHtml += ''; } } if (canEdit) { actionsHtml += ''; actionsHtml += ''; } var resolvedInfo = ''; if (item.status === 'resolved' && item.resolved_at) { resolvedInfo = '
✅ 已于 ' + escHtml(item.resolved_at) + ' 修改完成
'; } 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 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; 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':'全部类型'}; html += '
'; html += '
' + escHtml(t.name) + '
'; 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); } 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 fileInput = document.getElementById('ctFormFile'); if (!name) { 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('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 || '操作失败'); }); } // ─── 合同生成功能 ─── var _contractGenDialogOpen = false; var _contractGenerating = false; function generateContractForOrders(orderType, orderIds) { // 读取模板列表 fetch('/api/contract/templates') .then(function(r) { if (!r.ok) throw new Error('请求失败: ' + r.status); return r.json(); }) .then(function(res) { if (res.code !== 0 || !res.data || res.data.length === 0) { alert('请先在 系统设置→合同模板 中上传合同模板'); return; } var templates = res.data; // 弹窗选择模板 if (_contractGenDialogOpen) return; _contractGenDialogOpen = true; var overlay = document.createElement('div'); overlay.className = 'ct-form-overlay'; overlay.id = 'contractGenOverlay'; var typeMap = {'domestic':'国内订单','overseas':'海外订单','packaging':'包材订单','all':'全部类型'}; var templateOptions = ''; templates.forEach(function(t) { templateOptions += ''; }); overlay.innerHTML = '

📄 生成合同

'; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); }) .catch(function(err) { _contractGenDialogOpen = false; alert('获取模板列表失败: ' + err.message); }); } function closeContractGenDialog() { _contractGenDialogOpen = false; var el = document.getElementById('contractGenOverlay'); if (el) el.remove(); } function doGenerateContract(orderType, orderIds) { if (_contractGenerating) return; var templateId = document.getElementById('contractGenTemplate').value; _contractGenerating = true; var btn = document.querySelector('#contractGenOverlay .btn-primary'); btn.disabled = true; btn.textContent = '生成中...'; fetch('/api/contract/generate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order_ids: orderIds, order_type: orderType, template_id: parseInt(templateId)}) }) .then(function(r) { return r.json(); }) .then(function(res) { _contractGenerating = false; if (res.code !== 0) { alert(res.msg || '生成失败'); return; } closeContractGenDialog(); // 显示生成结果 var resultHtml = '
'; resultHtml += '
✅ ' + res.msg + '
'; res.data.forEach(function(item) { resultHtml += '
'; resultHtml += '
' + escHtml(item.supplier) + ' - 共 ' + item.product_count + ' 个产品
'; resultHtml += '
合同编号: ' + escHtml(item.contract_no) + '
'; resultHtml += '
📥 下载合同
'; resultHtml += '
'; }); resultHtml += '
'; var overlay = document.createElement('div'); overlay.className = 'ct-form-overlay'; overlay.innerHTML = '

📄 合同已生成

' + resultHtml + '
'; overlay.onclick = function(e) { if (e.target === overlay) {} }; document.body.appendChild(overlay); }) .catch(function(err) { _contractGenerating = false; alert('生成失败: ' + err); }); } // ─── 仪表盘 ─── 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']; function drawTrend() { var trendCanvas = document.getElementById('dashTrendChart'); if (trendCanvas && d.recent_trend && d.recent_trend.length > 0) { new Chart(trendCanvas, { type: 'bar', data: { labels: d.recent_trend.map(function(t) { return (t.date_str || t.date || '').slice(5); }), datasets: [{ label: '出库量', data: d.recent_trend.map(function(t) { return parseInt(t.qty) || 0; }), backgroundColor: 'rgba(24,144,255,0.7)', borderColor: '#1890ff', borderWidth: 1, borderRadius: 4 }] }, 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 } } } } }); } } 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 += ''; }); } var whSel = document.getElementById('dataWarehouse'); if (whSel && d.warehouses) { d.warehouses.forEach(function(w) { whSel.innerHTML += ''; }); } var shSel = document.getElementById('dataShop'); if (shSel && d.shops) { d.shops.forEach(function(s) { shSel.innerHTML += ''; }); } var brSel = document.getElementById('dataBrand'); if (brSel && d.brands) { d.brands.forEach(function(b) { brSel.innerHTML += ''; }); } }); } const SOURCE_MAP_CN = { 'wdt': '旺店通', 'jdbz': '京东泊紫', 'jdgj': '京东国际', 'jdjwt': '京东洁维塔', 'tmgj': '天猫国际' }; let dataCurrentPage = 1; let DATA_PAGE_SIZE = 20; 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 = ''; 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: 'lead_time', label: '货期(天)', defaultShow: true, sortable: false}, {field: 'logistics_time', 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 = `
成品基础信息表
包材基础信息表

成品信息列表

`; // 构建列头 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) { // 包材基础信息表:跳过不需要的列(buildProdThead+loadProducts根据状态跳过渲染) // 成品基础信息表:恢复所有列显示 var hideFields = ['brand','lead_time','logistics_time','procurement_follower','procurement_planner','platform','operator','developer','created_at']; hideFields.forEach(function(f) { window.prodColVisible[f] = tab !== '非成品'; }); // 字段设置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 === '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 modal = document.getElementById('prodImportModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'prodImportModal'; modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); } else { modal.style.display = 'flex'; document.getElementById('prodImportPreview').textContent = ''; document.getElementById('prodImportBtn').disabled = true; document.getElementById('prodImportFile').value = ''; } } 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','lead_time','logistics_time','procurement_follower','procurement_planner','platform','operator','developer','created_at']; 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 || '-'); 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(); 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}) }) .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.editWarehouse = function(btn) { // 从按钮所在行获取仓库数据 var row = btn.parentElement.parentElement; var cells = row.cells; if (cells.length < 5) return; var code = cells[1].textContent.trim(); var name = cells[2].textContent.trim(); var category = cells[3].textContent.trim(); var remark = cells[4].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 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, 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.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 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 userRoleSortOrder = ''; // '' 'asc' 'desc' 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; 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'; tbody.innerHTML += ` DY${String(start + idx + 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); }); }; // ─── 角色权限配置 ─── window.openPermissionConfig = function() { document.getElementById('permModal').style.display = 'flex'; renderPermTable(); }; window.closePermModal = function() { document.getElementById('permModal').style.display = 'none'; }; 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('✅ 权限配置已保存'); closePermModal(); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败'); }); }; // ─── 操作日志 ─── 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 = []; 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(); }) ]) .then(function(results) { var usersRes = results[0]; var pagesRes = results[1]; var opsRes = results[2]; if (usersRes.code !== 0) { el.innerHTML = '
❌ 加载失败
'; return; } _permUsers = usersRes.data || []; _permPages = pagesRes.data || []; _permOps = opsRes.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 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 adminUsers = []; var otherUsers = []; filtered.forEach(function(u) { if (u.role === 'super_admin' || u.role === 'admin') { adminUsers.push(u); } else { otherUsers.push(u); } }); function renderUserSection(users, showTitle) { var h = ''; if (showTitle && users.length > 0) { h += '
👤 管理员
'; } else if (users.length > 0) { 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 = ''; if (adminUsers.length > 0) { html += renderUserSection(adminUsers, true); html += '
'; } if (otherUsers.length > 0) { html += renderUserSection(otherUsers, false); } html += ''; if (filtered.length === 0) { html = '
无匹配用户
'; } el.innerHTML = html; } // 打开用户权限弹窗 var _editingUserId = null; var _rolePagePerms = {}; // {page: 1/0} - 角色默认的页面权限 window.openPermUserModal = function(userId) { _editingUserId = userId; var user = null; _permUsers.forEach(function(u) { if (u.id == userId) user = u; }); if (!user) return; // 先获取该用户角色的默认权限 fetch('/api/permissions/list') .then(function(r) { return r.json(); }) .then(function(res) { _rolePagePerms = {}; if (res.code === 0 && Array.isArray(res.data)) { res.data.forEach(function(p) { if (p.role === user.role) { _rolePagePerms[p.page] = p.permission; } }); } // super/admin: 后端直接放行,表里可能没条目,但前端确保显示为有权限 if (user.role === 'super_admin' || user.role === 'admin') { var hasAny = false; for (var k in _rolePagePerms) { hasAny = true; break; } if (!hasAny) { _permMenuTree.forEach(function(g) { g.items.forEach(function(item) { _rolePagePerms[item.page] = 1; }); }); } } _renderPermUserModal(user); }) .catch(function() { _rolePagePerms = {}; _renderPermUserModal(user); }); }; // 菜单树结构(与侧边栏菜单一致) // 菜单树结构:每个菜单项列出实际的功能按钮(不是通用的增删改查) 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:'batch_order',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:'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:'编辑'}]} ] }, { 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:'编辑'}]} ] } ]; window._renderPermUserModal = function(user) { var userPerms = user.permissions || {}; var isFullAccess = (user.role === 'super_admin' || user.role === 'admin'); if (isFullAccess) { _permMenuTree.forEach(function(group) { group.items.forEach(function(item) { _rolePagePerms[item.page] = 1; }); }); } var html = ''; document.body.insertAdjacentHTML('beforeend', html); }; window.closeUserPermModal = function() { var m = document.getElementById('userPermModal'); if (m) m.remove(); _editingUserId = null; }; window.selectAllPermCbs = function(checked) { var m = document.getElementById('userPermModal'); if (!m) return; m.querySelectorAll('input[type=checkbox]').forEach(function(cb) { cb.checked = checked; }); }; window.saveUserPerm = function() { if (!_editingUserId) return; var m = document.getElementById('userPermModal'); if (!m) return; var items = []; m.querySelectorAll('input[type=checkbox]').forEach(function(cb) { var page = cb.getAttribute('data-page'); var op = cb.getAttribute('data-op'); if (page && op) { items.push({ page: page, operation: op, permission: cb.checked ? 1 : 0 }); } }); fetch('/api/permissions/user/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user_id: _editingUserId, items: items}) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.code === 0) { closeUserPermModal(); loadPermissionsUsers(); } else { alert(res.msg || '保存失败'); } }) .catch(function() { alert('保存失败,请检查网络'); }); }; 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, 'cost_price':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, '库存工厂待生产可支撑天数':1, '总支撑天数':1, '最低销量偏差率':1, '最高销量偏差率':1, '14天波动率':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: 'bom', 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: '正式下单数量', 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: '7天平均销量', label: '7天平均销量', 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, '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; }); } // 弹窗中的临时状态(确定后才生效) 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; 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) { window.planColVisible[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) => { 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 = 86; // 勾选列36px + 序号列50px frozenLefts['_cb'] = 0; frozenLefts['_index'] = 36; 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 filterIcon = '▾'; 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(); loadPlanPage(1); // 加载默认方案 loadDefaultPlanScheme(); } // 采购跟单/采购计划下拉框:选中值时悬浮显示×清除按钮 function setupFilterClearButtons() { var items = [ { el: document.getElementById('planFollowerFilter'), name: '采购跟单' }, { el: document.getElementById('planPlannerFilter'), name: '采购计划' } ]; items.forEach(function(item) { var sel = item.el; if (!sel) return; var wrap = sel.parentNode; if (!wrap.classList.contains('filter-select-wrap')) return; // 防重复 if (wrap.querySelector('.filter-clear-btn')) return; var clearBtn = document.createElement('span'); clearBtn.className = 'filter-clear-btn'; clearBtn.innerHTML = '✕'; clearBtn.style.cssText = 'display:none;position:absolute;right:4px;top:50%;transform:translateY(-50%);width:16px;height:16px;line-height:16px;text-align:center;font-size:12px;cursor:pointer;color:#999;background:#fff;border-radius:50%;z-index:5;user-select:none'; clearBtn.title = '清除筛选'; wrap.style.position = 'relative'; wrap.appendChild(clearBtn); // 更新清除按钮显隐 function updateClearBtn() { clearBtn.style.display = sel.value ? 'block' : 'none'; } updateClearBtn(); sel.addEventListener('change', function() { updateClearBtn(); // 选中值变化时自动查询 searchPlanning(); }); sel.addEventListener('mouseenter', function() { if (sel.value) clearBtn.style.display = 'block'; }); sel.addEventListener('mouseleave', function() { clearBtn.style.display = 'none'; }); clearBtn.addEventListener('mouseenter', function() { clearBtn.style.display = 'block'; }); clearBtn.addEventListener('mouseleave', function() { clearBtn.style.display = 'none'; }); clearBtn.addEventListener('click', function(e) { e.stopPropagation(); sel.value = ''; clearBtn.style.display = 'none'; searchPlanning(); }); }); } let planCurrentPage = 1; let planTotal = 0; let PLAN_PAGE_SIZE = 20; let planSortField = ''; let planSortAsc = false; const GRADE_ORDER = {'S级':0, 'A级':1, 'B级':2, 'C级':3, '不参与评级':4}; function planSortRows(rows) { if (!planSortField) return rows; return rows.sort(function(a, b) { var va = a[planSortField], vb = b[planSortField]; if (planSortField === '产品分级') { va = GRADE_ORDER[va] !== undefined ? GRADE_ORDER[va] : 999; vb = GRADE_ORDER[vb] !== undefined ? GRADE_ORDER[vb] : 999; } else { if (va === null || va === undefined || va === '') va = ''; if (vb === null || vb === undefined || vb === '') vb = ''; va = isNaN(Number(va)) ? String(va).toLowerCase() : Number(va); vb = isNaN(Number(vb)) ? String(vb).toLowerCase() : Number(vb); } if (va < vb) return planSortAsc ? -1 : 1; if (va > vb) return planSortAsc ? 1 : -1; return 0; }); } function loadPlanPage(page) { planCurrentPage = page; const search = document.getElementById('planSearch')?.value || ''; const dateCond = document.getElementById('planDateCond')?.value || ''; const dateVal = document.getElementById('planDateVal')?.value || ''; const supplierFilter = document.getElementById('planSupplierFilter')?.value || ''; const followerFilter = document.getElementById('planFollowerFilter')?.value || ''; const plannerFilter = document.getElementById('planPlannerFilter')?.value || ''; // 从全局筛选状态读取 var filterValue = ''; if (window._planFilters) { var activeCount = 0; var filters = []; Object.keys(window._planFilters).forEach(function(f) { var fp = window._planFilters[f]; if (fp) { fp.field = f; filters.push(fp); activeCount++; } }); if (activeCount > 0) { filterValue = '__multi__:' + JSON.stringify(filters); } } let url = '/api/planning/list?page=' + page + '&size=' + PLAN_PAGE_SIZE; if (search) url += '&search=' + encodeURIComponent(search); if (filterValue) url += '&custom_field=__multi__&custom_value=' + encodeURIComponent(filterValue); if (dateCond && dateVal) url += '&date_cond=' + encodeURIComponent(dateCond) + '&date_val=' + encodeURIComponent(dateVal); if (supplierFilter) url += '&supplier=' + encodeURIComponent(supplierFilter); if (followerFilter) url += '&procurement_follower=' + encodeURIComponent(followerFilter); if (plannerFilter) url += '&procurement_planner=' + encodeURIComponent(plannerFilter); if (planSortField) url += '&sort_by=' + encodeURIComponent(planSortField) + '&sort_order=' + (planSortAsc ? 'asc' : 'desc'); if (planActiveView !== 'default') url += '&view=' + encodeURIComponent(planActiveView); fetch(url) .then(r => r.json()) .then(res => { if (res.code !== 0) return; const data = res.data; planTotal = data.total; window._planCurrentAllCodes = data.all_codes || []; renderPlanTable(planSortRows(data.rows)); document.getElementById('planCount').textContent = '共 ' + data.total + ' 条'; // 填充采购跟单/采购计划下拉选项(仅首次加载或当页面为第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'); if (!rows || rows.length === 0) { tbody.innerHTML = '暂无数据'; return; } const startIdx = (planCurrentPage - 1) * PLAN_PAGE_SIZE; const typeMap = {'成品':'blue','包材':'orange','赠品':'green','小样':'purple','物料':'orange','其他':'grey'}; tbody.innerHTML = rows.map((r, rowIdx) => { const vals = PLAN_COLUMNS.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 || ''); 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 === '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 === '采购在途') { 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 || ''); return '' + '' + currentVal + '' + '✏️' + ''; } 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 = 86; colLefts['_cb'] = 0; colLefts['_index'] = 36; planFrozenFields.forEach(function(f) { if (window.planColVisible[f]) { colLefts[f] = acc; acc += (FROZEN_WIDTHS[f] || 100); } }); } var cbSticky = planFrozen ? 'position:sticky;left:0;z-index:9;background:var(--bg-card);transform:translateZ(0);min-width:36px;text-align:center' : 'text-align:center'; var idxSticky = planFrozen ? 'position:sticky;left:36px;z-index:9;background:var(--bg-card);transform:translateZ(0);min-width:50px' : ''; var code2 = r.product_code || ''; var isChecked = window._planSelectedProductCodes && window._planSelectedProductCodes[code2] ? ' checked' : ''; let colsHtml = ``; 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 }); colsHtml += ''; colsHtml += ' '; colsHtml += ''; if (!window._planOrderDataCache) window._planOrderDataCache = {}; window._planOrderDataCache[code] = orderData; colsHtml += ''; 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(); // 更新选中 UI if (typeof planSyncSelectUI === 'function') planSyncSelectUI(); // 更新筛选图标状态 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 cbTh = headerRow.querySelector('.plan-col-cb'); if (cbTh) actualWidths['_cb'] = cbTh.offsetWidth; 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 (cbTh && actualWidths['_cb']) { cbTh.style.position = 'sticky'; cbTh.style.top = '0'; cbTh.style.left = '0px'; cbTh.style.zIndex = '999'; cbTh.style.background = 'var(--bg-card)'; acc += actualWidths['_cb']; } else { acc += 36; } // 序号列 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 cbTh2 = headerRow.querySelector('.plan-col-cb'); if (cbTh2) widths2['_cb'] = cbTh2.offsetWidth; 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 = widths2['_cb'] || 36; if (cbTh2) { cbTh2.style.left = '0px'; table.querySelectorAll('tbody .plan-col-cb').forEach(function(el) { el.style.position = 'sticky'; el.style.left = '0px'; el.style.zIndex = '9'; el.style.background = 'var(--bg-card)'; }); } acc2 += widths2['_index'] || 50; if (idxTh2) { idxTh2.style.left = (widths2['_cb'] || 36) + 'px'; table.querySelectorAll('tbody .plan-col-index').forEach(function(el) { el.style.left = (widths2['_cb'] || 36) + 'px'; }); } 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() { if (window._planFrozenPendingFields === undefined) { window._planFrozenPendingFields = planFrozenFields.slice(); } var html = '
'; html += '
'; var candidates = [ {field:'_index', label:'序号'}, {field:'product_type', label:'产品类型'}, {field:'product_code', label:'货品编号'}, {field:'product_name', label:'货品名称'}, {field:'产品分级', 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; 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; }); // 判断是否为数字列 var isNumericCol = SORTABLE_FIELDS[field] || 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]; if (predefined) { values = predefined.slice(); } else { var seen = {}; var rows = document.querySelectorAll('#planTbody tr'); rows.forEach(function(tr) { var idx = -1; PLAN_COLUMNS.forEach(function(c, i) { if (c.field === field) idx = i; }); if (idx >= 0) { var td = tr.querySelector('.plan-col-' + idx); if (td) { var txt = td.textContent.trim(); if (txt && !seen[txt]) { seen[txt] = true; values.push(txt); } } } }); values.sort(); } if (values.length > 100) values = values.slice(0, 100); 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 += '
'; html += '
'; html += ''; html += '清除筛选
'; html += '
'; values.forEach(function(v) { var key = escHtml(v).replace(/'/g, "\\'"); var checked = _planFilterSelected[field][v] !== false ? 'checked' : ''; html += ''; }); html += '
'; html += '
'; html += '
'; menu.innerHTML = html; document.body.appendChild(menu); } // 搜索自动聚焦 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) { var html = ''; html += '
' + escHtml(label) + '
'; // 排序 html += '
'; html += '' + ''; html += '
'; // 条件选择 html += '
'; html += ''; html += '
'; // 数值输入 html += '
'; html += ''; html += '
'; // 底部按钮 html += '
'; html += ''; html += ''; html += ''; html += '
'; menu.innerHTML = html; menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:200px;font-size:13px'; document.body.appendChild(menu); setTimeout(function() { var inp = document.getElementById('planNumVal'); if (inp) inp.focus(); }, 100); } function planNumFilterApply() { var field = _planFilterMenuField; 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}; _syncFiltersToUI(); closePlanFilterMenu(); loadPlanPage(1); } function planFilterApply() { var field = _planFilterMenuField; 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 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); } var planActiveView = 'default'; function switchPlanView(view) { 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 = {}; // 更新清除按钮状态 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 newThead = '
序号
'; PLAN_COLUMNS.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] || ''; newThead += '' + c.label + sortArrowHtml + ' ▾'; }); 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 = []; 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 || []; var countEl = document.getElementById('pendingCount'); if (countEl) countEl.textContent = '共 ' + _pendingItems.length + ' 条'; if (_pendingItems.length === 0) { tbody.innerHTML = '暂无待下单商品 ✅'; return; } tbody.innerHTML = _pendingItems.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(''); pendingUpdateCount(); }); } 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.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) { 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 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; fetch('/api/purchase/confirm_order', { 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) { showCenteredAlert('操作成功', '✅ 批量下单成功,共 ' + checkedCodes.length + ' 个'); 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; fetch('/api/purchase/confirm_order', { 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) { showCenteredAlert('操作成功', '✅ 下单成功'); 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','product_grade','supplier_name','remark', 'lead_time','logistics_time','total_support_days', 'procurement_follower','procurement_planner','platform','operator','created_by', 'audit_price','suggested_qty','formal_qty','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:'审核价格',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'; } 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 html = ''; nameList.forEach(function(name) { var bg = colorForName(name) || '#e6f7ff'; var fc = textColorForBG(bg); html += '' + escHtml(name) + ''; }); 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 > 0) ? 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 = []; inputs.forEach(function(inp) { var qty = parseInt(inp.value) || 0; if (qty > 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 }); } }); if (materials.length === 0) { alert('没有实际下单数量>0的物料'); 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(); } showCenteredAlert('保存成功', '✅ BOM已保存,确认下单时将自动提交包材数据'); }; // ─── 计划预测 BOM 物料清单(完整编辑功能) ─── function planToggleBom(productCode) { 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 bomContent = '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; // 读取之前保存的包材数据 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) { var matCode = escHtml(b.material_code || ''); var qty = b.bom_qty || 0; var orderQty = Math.ceil(formalQty * qty); // 如果有保存的数据,用保存的实际下单数量 var savedActual = savedMap[b.material_code] || 0; var actualQty = savedActual > 0 ? 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]; } }); // 实际下单数量输入框:点击自动全选 + 回车跳到下一行 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(); } // 没有下一行:不做处理,保存完成 } }); }); }) .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 : []; 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) 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:center;white-space:nowrap'; var tableHtml = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; var tdBase = 'padding:7px 8px;' + cellBorder + ';text-align:left'; bom.forEach(function(b) { var matCode = escHtml(b.material_code || ''); var qty = b.bom_qty || 0; var orderQty = Math.ceil(formalQty * qty); // 使用已保存的实际下单数量(如有) var actualQty = savedBomActuals[b.material_code] || orderQty; tableHtml += '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }); tableHtml += '
物料编号物料名称用量建议下单数量实际下单数量下单前包材下单后包材下单备注更换稿件操作
' + matCode + '' + escHtml(b.material_name || '') + '' + orderQty + '
'; container.innerHTML = tableHtml; // 恢复已保存的更换稿件状态 container.querySelectorAll('.bom-manuscript').forEach(function(sel) { var mc = sel.getAttribute('data-material'); if (savedBomManuscript[mc]) { sel.value = savedBomManuscript[mc]; } }); // 可编辑输入框:回车保存当前项并跳转到下一项 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]) { seen[matCode] = {material_code: matCode}; 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; }); // 只保存实际下单数量>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(); alert('✅ 包材数据已保存'); } 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) 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 = []; 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 loadBackupList() { 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; _backupItems = res.data || []; var countEl = document.getElementById('backupCount'); if (countEl) countEl.textContent = '共 ' + _backupItems.length + ' 条'; if (_backupItems.length === 0) { tbody.innerHTML = '暂无已下单记录 ✅'; return; } tbody.innerHTML = _backupItems.map(function(item, idx) { var code = escHtml(item.product_code); return '' + '' + (idx + 1) + '' + '' + escHtml(item.product_type || '-') + '' + '' + code + '' + '' + escHtml(item.product_name || '-') + '' + '' + ((item.ordered_at || '').toString().slice(0, 19) || '-') + '' + '' + 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) + '' + '' + escHtml(item.created_by || '-') + '' + '' + (item.audit_price || 0) + '' + '' + (item.suggested_qty || 0) + '' + '' + (item.formal_order_qty || 0) + '' + '' + ((item.created_at || '').toString().slice(0, 19) || '-') + '' + '' + ' ' + ' ' + '' + '' + ''; }).join(''); }); } // ─── 下单备份管理:详情+删除 ─── 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 fields = [ ['产品类型', item.product_type], ['货品编号', item.product_code], ['货品名称', item.product_name], ['产品分级', item.product_grade], ['供应商名称', item.supplier_name], ['货期(天)', item.lead_time], ['物流(天)', item.logistics_time], ['总支撑天数', item.total_support_days], ['采购跟单', item.procurement_follower], ['采购计划', item.procurement_planner], ['归属平台', item.platform], ['运营专员', item.operator], ['审核价格', item.audit_price], ['正式下单数量', item.formal_order_qty], ['建议下单数量', item.suggested_qty], ['下单备注', item.remark], ['下单时间', (item.ordered_at || '').toString().slice(0, 19)], ['加入时间', (item.created_at || '').toString().slice(0, 19)] ]; var html = '
'; html += '

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

'; html += '
'; html += ''; fields.forEach(function(f, i) { var bg = i % 2 === 0 ? '#fafafa' : '#fff'; html += ''; html += ''; html += ''; html += ''; }); html += '
' + f[0] + '' + escHtml(String(f[1] || '-')) + '
'; html += '
'; html += '
'; var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = '
' + html + '
'; document.body.appendChild(overlay); }; // 下单回退:确认弹窗 + 调用后端 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 domesticFrozen = localStorage.getItem('domesticFrozen') !== 'false'; function loadDomesticOrdersPage(container) { container.innerHTML = `

📦 国内采购下单明细

`; // 追加1-31列 for (var i = 1; i <= 31; i++) { container.querySelector('#domesticOrdersHeader').innerHTML += ''; } container.querySelector('#domesticOrdersHeader').innerHTML += ''; container.querySelector('#domesticOrdersTable').innerHTML += ''; container.querySelector('#domesticContent').innerHTML += ''; domesticLoadFilters(); domesticLoadOrders(1); } function domesticLoadFilters() { fetch('/api/domestic/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var sl = document.getElementById('domesticSupplierFilter'); var ss = document.getElementById('domesticStatusFilter'); var so = document.getElementById('domesticOrdererFilter'); (res.data.suppliers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; sl.appendChild(opt); }); (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 domesticDoSearch() { domesticSearch = document.getElementById('domesticSearchInput').value.trim(); domesticLoadOrders(1); } function domesticApplyFilters() { domesticFilterSupplier = document.getElementById('domesticSupplierFilter').value; 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 = ''; domesticLoadOrders(1); } function domesticLoadOrders(page) { domesticOrderPage = page; // 初始化表头冻结状态 (function(){ var h = document.getElementById('domesticOrdersHeader'); if (!h) return; var s = h.querySelectorAll('th'); var f = domesticFrozen; if (s[2]) s[2].style.cssText = f ? 'min-width:140px;position:sticky;left:150px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:140px'; if (s[3]) s[3].style.cssText = f ? 'min-width:60px;position:sticky;left:290px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:60px'; if (s[4]) s[4].style.cssText = f ? 'min-width:120px;position:sticky;left:350px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:120px'; if (s[5]) s[5].style.cssText = f ? 'min-width:200px;position:sticky;left:470px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:200px'; })(); var params = '?page=' + page + '&size=' + DOMESTIC_PAGE_SIZE; if (domesticSearch) params += '&search=' + encodeURIComponent(domesticSearch); if (domesticFilterStatus) params += '&status=' + encodeURIComponent(domesticFilterStatus); if (domesticFilterSupplier) params += '&supplier=' + encodeURIComponent(domesticFilterSupplier); if (domesticFilterOrderer) params += '&orderer=' + encodeURIComponent(domesticFilterOrderer); fetch('/api/domestic/orders/list' + params) .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 = ''; data.rows.forEach(function(r, i) { var rowNum = startIdx + i + 1; var fields = [ '日期','计划下单时间','新旧品','编码','名称','下单备注','单价','是否审核价格','数量','金额', '供货商','仓库','金蝶采购单','采购员','下单人','进度','旺店通生产单号','是否已录入旺店通', '采购开发','实际到货日期','实际到货数量辅助','辅助1','实际到货数量','未到货数','预计到货日期', '金蝶入库','状态' ]; html += ''; html += ''; // 日期 html += ''; // 主要字段 html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; 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 += ''; // 1-31列 for (var d = 1; d <= 31; d++) { var v = parseInt(r[String(d)]) || 0; html += ''; } html += ''; html += ''; html += ''; }); tbody.innerHTML = html; } 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 + '
'; } } }); } 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) { domesticLoadOrders(domesticOrderPage); } else { alert(res.msg || '删除失败'); } }); }; 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) { domesticLoadOrders(1); } else { alert(res.msg || '添加失败'); } }); }; window.domesticExport = function() { // 简单 CSV 导出 var tbody = document.getElementById('domesticOrdersTbody'); if (!tbody) return; var lines = []; var headers = ['#','日期','计划下单时间','新旧品','编码','名称','下单备注','单价','审核价格','数量','金额','供货商','仓库','金蝶采购单','采购员','下单人','进度','旺店通单号','录入旺店通','采购开发','实际到货日期','到货数量辅','辅助1','实到数量','未到货数','预计到货日期','金蝶入库','状态']; for (var i = 1; i <= 31; i++) headers.push(i); headers.push('父记录'); 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 overseasFrozen = localStorage.getItem('overseasFrozen') !== 'false'; function loadOverseasOrdersPage(container) { container.innerHTML = `

🌍 海外采购下单明细

# 日期 计划下单时间 新旧品 编码 名称 下单备注 单价 审核价格 数量 金额 供货商 仓库 金蝶采购单 采购员 下单人 进度 旺店通生产单号 录入旺店通 采购开发 实际到货日期 到货数量辅 辅助1 实到数量 未到货数 预计到货日期 金蝶入库 状态 做货周期' + i + '父记录操作
加载中...
暂无数据
' + rowNum + '' + escHtml(r['日期'] || '') + ' 📅' + escHtml(r['计划下单时间'] || '') + '' + escHtml(r['新旧品'] || '') + '' + escHtml(r['编码'] || '') + '' + escHtml(r['名称'] || '') + '' + escHtml(r['下单备注'] || '-') + '' + escHtml(r['是否审核价格'] || '') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (r['金额'] ? '¥' + parseFloat(r['金额']).toLocaleString() : '') + '' + escHtml(r['供货商'] || '') + '' + escHtml(r['仓库'] || '') + '' + escHtml(r['金蝶采购单'] || '') + '' + renderNameBadges(r['采购员']) + '' + renderNameBadges(r['下单人']) + '' + escHtml(r['进度'] || '') + '' + escHtml(r['旺店通生产单号'] || '') + '' + escHtml(r['是否已录入旺店通'] || '') + '' + renderNameBadges(r['采购开发']) + '' + escHtml(r['实际到货日期'] || '') + '' + (parseInt(r['实际到货数量辅助']) || '').toLocaleString() + '' + escHtml(r['辅助1'] || '') + '' + (parseInt(r['实际到货数量']) || '').toLocaleString() + '' + (parseInt(r['未到货数']) || '').toLocaleString() + '' + escHtml(r['预计到货日期'] || '') + '' + escHtml(r['金蝶入库'] || '') + '' + escHtml(r['状态'] || '') + '' + manuDays + '' + (v > 0 ? v.toLocaleString() : '') + '' + escHtml(r['父记录'] || '') + ''; html += ' '; html += ' '; html += ''; html += '
`; for (var i = 1; i <= 31; i++) { container.querySelector('#overseasOrdersHeader').innerHTML += ''; } container.querySelector('#overseasOrdersHeader').innerHTML += ''; container.querySelector('#overseasOrdersTable').innerHTML += ''; container.querySelector('#overseasContent').innerHTML += ''; overseasLoadFilters(); overseasLoadOrders(1); } function overseasLoadFilters() { fetch('/api/overseas/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var sl = document.getElementById('overseasSupplierFilter'); var ss = document.getElementById('overseasStatusFilter'); var so = document.getElementById('overseasOrdererFilter'); (res.data.suppliers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; sl.appendChild(opt); }); (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() { overseasSearch = document.getElementById('overseasSearchInput').value.trim(); overseasLoadOrders(1); } function overseasApplyFilters() { overseasFilterSupplier = document.getElementById('overseasSupplierFilter').value; 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 = ''; overseasLoadOrders(1); } function overseasLoadOrders(page) { overseasOrderPage = page; // 初始化表头冻结状态 (function(){ var h = document.getElementById('overseasOrdersHeader'); if (!h) return; var s = h.querySelectorAll('th'); var f = overseasFrozen; if (s[2]) s[2].style.cssText = f ? 'min-width:140px;position:sticky;left:150px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:140px'; if (s[3]) s[3].style.cssText = f ? 'min-width:120px;position:sticky;left:290px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:120px'; if (s[4]) s[4].style.cssText = f ? 'min-width:200px;position:sticky;left:410px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:200px'; })(); var params = '?page=' + page + '&size=' + OVERSEAS_PAGE_SIZE; if (overseasSearch) params += '&search=' + encodeURIComponent(overseasSearch); if (overseasFilterStatus) params += '&status=' + encodeURIComponent(overseasFilterStatus); if (overseasFilterSupplier) params += '&supplier=' + encodeURIComponent(overseasFilterSupplier); if (overseasFilterOrderer) params += '&orderer=' + encodeURIComponent(overseasFilterOrderer); fetch('/api/overseas/orders/list' + params) .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 += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; // 做货周期 = 实际到货日期 - 日期 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 += ''; for (var d = 1; d <= 31; d++) { var v = parseInt(r[String(d)]) || 0; html += ''; } html += ''; }); tbody.innerHTML = html; } var pel = document.getElementById('overseasOrdersPagination'); if (pel) { var totalPages = Math.ceil((data.total || 0) / OVERSEAS_PAGE_SIZE); pel.innerHTML = '
'; if (page > 1) pel.innerHTML += ''; pel.innerHTML += '第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) pel.innerHTML += ''; pel.innerHTML += '
'; } }); } 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) { 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) { overseasLoadOrders(1); } else { alert(res.msg || '添加失败'); } }); }; window.overseasExport = function() { var tbody = document.getElementById('overseasOrdersTbody'); if (!tbody) return; var lines = []; var headers = ['#','日期','计划下单时间','产品编码','单价','审核价格','数量','金额','供应商','仓库','货品名称','新旧品','已做采购单','已入库','采购员','下单人','进度','下单备注','采购开发','旺店通单号','录入旺店通','实际到货日期','到货数量辅','辅助','实到数量','预计到货日期','未到货数','状态']; for (var i = 1; i <= 31; i++) headers.push(i); 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 PACKAGING_PAGE_SIZE = 100; var packagingOrderPage = 1; var packagingSearch = ''; var packagingFilterStatus = ''; var packagingFilterSupplier = ''; var packagingFilterOrderer = ''; // ─── 冻结列切换函数 ─── window.domesticToggleFrozen = function() { domesticFrozen = !domesticFrozen; localStorage.setItem('domesticFrozen', domesticFrozen); // 更新表头冻结样式 var thead = document.getElementById('domesticOrdersHeader'); if (thead) { var scope = thead.querySelectorAll('th'); var frozen = domesticFrozen; if (scope[2]) scope[2].style.cssText = frozen ? 'min-width:140px;position:sticky;left:150px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:140px'; if (scope[3]) scope[3].style.cssText = frozen ? 'min-width:60px;position:sticky;left:290px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:60px'; if (scope[4]) scope[4].style.cssText = frozen ? 'min-width:120px;position:sticky;left:350px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:120px'; if (scope[5]) scope[5].style.cssText = frozen ? 'min-width:200px;position:sticky;left:470px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:200px'; } domesticLoadOrders(1); }; window.overseasToggleFrozen = function() { overseasFrozen = !overseasFrozen; localStorage.setItem('overseasFrozen', overseasFrozen); var thead = document.getElementById('overseasOrdersHeader'); if (thead) { var scope = thead.querySelectorAll('th'); var frozen = overseasFrozen; if (scope[2]) scope[2].style.cssText = frozen ? 'min-width:140px;position:sticky;left:150px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:140px'; if (scope[3]) scope[3].style.cssText = frozen ? 'min-width:120px;position:sticky;left:290px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:120px'; if (scope[4]) scope[4].style.cssText = frozen ? 'min-width:200px;position:sticky;left:410px;top:0;z-index:11;background:var(--bg,#f5f5f5)' : 'min-width:200px'; } overseasLoadOrders(1); }; function loadPackagingOrdersPage(container) { container.innerHTML = `

📦 包材采购下单明细

# 日期 计划下单时间 产品编码 单价 审核价格 数量 金额 供应商 仓库 货品名称 新旧品 已做采购单 已入库 采购员 下单人 进度 下单备注 采购开发 旺店通单号 录入旺店通 实际到货日期 到货数量辅 辅助 实到数量 预计到货日期 未到货数 状态 做货周期' + i + '操作
加载中...
暂无数据
' + rowNum + '' + escHtml(r['日期'] || '') + ' 📅' + escHtml(r['计划下单时间'] || '') + '' + escHtml(r['产品编码'] || '') + '' + escHtml(r['是否审核价格'] || '') + '' + (parseInt(r['数量']) || 0).toLocaleString() + '' + (r['金额'] ? '¥' + parseFloat(r['金额']).toLocaleString() : '') + '' + escHtml(r['供应商'] || '') + '' + escHtml(r['仓库'] || '') + '' + escHtml(r['货品名称'] || '') + '' + escHtml(r['新旧品'] || '') + '' + escHtml(r['是否已做采购单'] || '') + '' + escHtml(r['是否做入库'] || '') + '' + renderNameBadges(r['采购员']) + '' + renderNameBadges(r['下单人']) + '' + escHtml(r['进度'] || '') + '' + escHtml(r['下单备注'] || '-') + '' + renderNameBadges(r['采购开发']) + '' + escHtml(r['旺店通生产单号'] || '') + '' + escHtml(r['是否已录入到旺店通'] || '') + '' + escHtml(r['实际到货日期'] || '') + '' + (parseInt(r['实际到货数量辅助']) || '').toLocaleString() + '' + escHtml(r['辅助'] || '') + '' + (parseInt(r['实际到货数量']) || '').toLocaleString() + '' + escHtml(r['预计到货日期'] || '') + '' + (parseInt(r['未到货数']) || '').toLocaleString() + '' + escHtml(r['状态'] || '') + '' + manuDaysOv + '' + (v > 0 ? v.toLocaleString() : '') + ''; html += ' '; html += ' '; html += ''; html += '
# 日期 编码 名称 单价 数量 合计 供应商 下单备注 仓库到货 进度 已做采购单 已入库 采购下单人 旺店通编号 录入旺店通 到货日期 状态 做货天数 包材开发 操作
加载中...
`; packagingLoadFilters(); packagingLoadOrders(1); } function packagingLoadFilters() { fetch('/api/packaging/orders/filters') .then(function(r){return r.json()}) .then(function(res){ if (res.code !== 0) return; var sl = document.getElementById('packagingSupplierFilter'); var ss = document.getElementById('packagingStatusFilter'); var so = document.getElementById('packagingOrdererFilter'); (res.data.suppliers || []).forEach(function(s) { var opt = document.createElement('option'); opt.value = s; opt.textContent = s; sl.appendChild(opt); }); (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() { packagingSearch = document.getElementById('packagingSearchInput').value.trim(); packagingLoadOrders(1); } function packagingApplyFilters() { packagingFilterSupplier = document.getElementById('packagingSupplierFilter').value; 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 = ''; 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); 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 += '' + rowNum + ''; html += '' + escHtml(r['日期'] || '') + ''; html += '' + escHtml(r['编码'] || '') + ''; html += '' + escHtml(r['名称'] || '') + ''; html += '' + (r['单价'] ? '¥' + parseFloat(r['单价']).toFixed(2) : '') + ''; html += '' + (parseInt(r['数量']) || 0).toLocaleString() + ''; html += '' + (r['合计'] ? '¥' + parseFloat(r['合计']).toLocaleString() : '') + ''; html += '' + escHtml(r['供应商'] || '') + ''; html += '' + escHtml(r['下单备注'] || '') + ''; html += '' + escHtml(r['仓库到货'] || '') + ''; html += '' + escHtml(r['进度'] || '') + ''; html += '' + escHtml(r['是否已做采购单'] || '') + ''; html += '' + escHtml(r['是否已做入库'] || '') + ''; html += '' + renderNameBadges(r['采购下单人']) + ''; html += '' + escHtml(r['旺店通编号'] || '') + ''; html += '' + escHtml(r['是否已录入旺店通'] || '') + ''; html += '' + escHtml(r['到货日期'] || '') + ''; html += '' + escHtml(r['状态'] || '') + ''; html += '' + escHtml(r['做货天数'] || '') + ''; html += '' + renderNameBadges(r['包材开发']) + ''; html += ''; html += ' '; html += ' '; html += ''; html += ''; }); tbody.innerHTML = html; } var pel = document.getElementById('packagingOrdersPagination'); if (pel) { var totalPages = Math.ceil((data.total || 0) / PACKAGING_PAGE_SIZE); pel.innerHTML = '
'; if (page > 1) pel.innerHTML += ''; pel.innerHTML += '第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) pel.innerHTML += ''; pel.innerHTML += '
'; } }); } 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) { 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) { 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(); }; // ──────────── 成品提货表 ──────────── var FINISHED_PAGE_SIZE = 100; var finishedPage = 1; var finishedSearch = ''; var finishedCategory = ''; var finishedSupplier = ''; function loadFinishedProductsPage(container) { container.innerHTML = `

📋 成品提货明细

# 序号 品类 货品编号 货品名称 供应商 可用库存 昨日销量 3天均销 7天均销 15天均销 支撑天数 采购总数量 生产在途 采购在途 国内下单表进度 海外下单表进度 产品进度 包材库存 采购跟单 采购计划 备注 操作
加载中...
`; finishedLoadFilters(); finishedLoadOrders(1); } 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 = ''; 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); 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 += ''; }); tbody.innerHTML = html; } var pel = document.getElementById('finishedOrdersPagination'); if (pel) { var totalPages = Math.ceil((data.total || 0) / FINISHED_PAGE_SIZE); pel.innerHTML = '
'; if (page > 1) pel.innerHTML += ''; pel.innerHTML += '第 ' + page + '/' + totalPages + ' 页'; if (page < totalPages) pel.innerHTML += ''; pel.innerHTML += '
'; } }); } 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) { finishedLoadOrders(finishedPage); } else { alert(res.msg || '删除失败'); } }); }; // ─── 下单备注可编辑通用函数 ─── // ─── 字段设置功能 ─── const DOMESTIC_COLUMNS = [ {field:'日期',label:'日期',defShow:true}, {field:'计划下单时间',label:'计划下单时间',defShow:true}, {field:'新旧品',label:'新旧品',defShow:true}, {field:'编码',label:'编码',defShow:true}, {field:'名称',label:'名称',defShow:true}, {field:'下单备注',label:'下单备注',defShow:true}, {field:'单价',label:'单价',defShow:true}, {field:'是否审核价格',label:'审核价格',defShow:true}, {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:'辅助1',label:'辅助1',defShow:true}, {field:'实际到货数量',label:'实到数量',defShow:true}, {field:'未到货数',label:'未到货数',defShow:true}, {field:'预计到货日期',label:'预计到货日期',defShow:true}, {field:'金蝶入库',label:'金蝶入库',defShow:true}, {field:'状态',label:'状态',defShow:true}, {field:'1',label:'1',defShow:false}, {field:'2',label:'2',defShow:false}, {field:'3',label:'3',defShow:false}, {field:'4',label:'4',defShow:false}, {field:'5',label:'5',defShow:false}, {field:'6',label:'6',defShow:false}, {field:'7',label:'7',defShow:false}, {field:'8',label:'8',defShow:false}, {field:'9',label:'9',defShow:false}, {field:'10',label:'10',defShow:false}, {field:'11',label:'11',defShow:false}, {field:'12',label:'12',defShow:false}, {field:'13',label:'13',defShow:false}, {field:'14',label:'14',defShow:false}, {field:'15',label:'15',defShow:false}, {field:'16',label:'16',defShow:false}, {field:'17',label:'17',defShow:false}, {field:'18',label:'18',defShow:false}, {field:'19',label:'19',defShow:false}, {field:'20',label:'20',defShow:false}, {field:'21',label:'21',defShow:false}, {field:'22',label:'22',defShow:false}, {field:'23',label:'23',defShow:false}, {field:'24',label:'24',defShow:false}, {field:'25',label:'25',defShow:false}, {field:'26',label:'26',defShow:false}, {field:'27',label:'27',defShow:false}, {field:'28',label:'28',defShow:false}, {field:'29',label:'29',defShow:false}, {field:'30',label:'30',defShow:false}, {field:'31',label:'31',defShow:false}, {field:'父记录',label:'父记录',defShow:false} ]; const OVERSEAS_COLUMNS = [ {field:'日期',label:'日期',defShow:true}, {field:'计划下单时间',label:'计划下单时间',defShow:true}, {field:'产品编码',label:'产品编码',defShow:true}, {field:'单价',label:'单价',defShow:true}, {field:'是否审核价格',label:'审核价格',defShow:true}, {field:'数量',label:'数量',defShow:true}, {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}, {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:'1',label:'1',defShow:false}, {field:'2',label:'2',defShow:false}, {field:'3',label:'3',defShow:false}, {field:'4',label:'4',defShow:false}, {field:'5',label:'5',defShow:false}, {field:'6',label:'6',defShow:false}, {field:'7',label:'7',defShow:false}, {field:'8',label:'8',defShow:false}, {field:'9',label:'9',defShow:false}, {field:'10',label:'10',defShow:false}, {field:'11',label:'11',defShow:false}, {field:'12',label:'12',defShow:false}, {field:'13',label:'13',defShow:false}, {field:'14',label:'14',defShow:false}, {field:'15',label:'15',defShow:false}, {field:'16',label:'16',defShow:false}, {field:'17',label:'17',defShow:false}, {field:'18',label:'18',defShow:false}, {field:'19',label:'19',defShow:false}, {field:'20',label:'20',defShow:false}, {field:'21',label:'21',defShow:false}, {field:'22',label:'22',defShow:false}, {field:'23',label:'23',defShow:false}, {field:'24',label:'24',defShow:false}, {field:'25',label:'25',defShow:false}, {field:'26',label:'26',defShow:false}, {field:'27',label:'27',defShow:false}, {field:'28',label:'28',defShow:false}, {field:'29',label:'29',defShow:false}, {field:'30',label:'30',defShow:false}, {field:'31',label:'31',defShow:false} ]; function getDomesticVisible() { var stored = localStorage.getItem('domesticColVisible'); if (stored) { try { var m = JSON.parse(stored); return m; } catch(e) {} } var m = {}; DOMESTIC_COLUMNS.forEach(function(c) { m[c.field] = c.defShow; }); return m; } function getOverseasVisible() { var stored = localStorage.getItem('overseasColVisible'); if (stored) { try { var m = JSON.parse(stored); return m; } catch(e) {} } var m = {}; OVERSEAS_COLUMNS.forEach(function(c) { m[c.field] = c.defShow; }); return m; } function saveDomesticVisible(m) { localStorage.setItem('domesticColVisible', JSON.stringify(m)); } function saveOverseasVisible(m) { localStorage.setItem('overseasColVisible', JSON.stringify(m)); } function openDomesticColModal() { openColSettingsModal('domestic', DOMESTIC_COLUMNS, getDomesticVisible(), saveDomesticVisible); } function openOverseasColModal() { openColSettingsModal('overseas', OVERSEAS_COLUMNS, getOverseasVisible(), saveOverseasVisible); } function openColSettingsModal(type, columns, visibleMap, saveFn) { var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = 'display:flex;z-index:9999'; var html = ''; overlay.innerHTML = html; document.body.appendChild(overlay); } window.toggleColAll = function(checked) { document.querySelectorAll('.col-checkbox').forEach(function(cb) { cb.checked = checked; }); }; window.updateColSelectAll = function() { var all = document.querySelectorAll('.col-checkbox'); var checked = document.querySelectorAll('.col-checkbox:checked'); var selectAll = document.getElementById('colSelectAll'); if (selectAll) selectAll.checked = (all.length === checked.length); }; window.resetColDefaults = function(type) { var cols = type === 'domestic' ? DOMESTIC_COLUMNS : OVERSEAS_COLUMNS; document.querySelectorAll('.col-checkbox').forEach(function(cb) { var col = cols.find(function(c) { return c.field === cb.value; }); cb.checked = col ? col.defShow : false; }); updateColSelectAll(); }; window.applyColSettings = function(type) { var cols = type === 'domestic' ? DOMESTIC_COLUMNS : OVERSEAS_COLUMNS; var visible = {}; document.querySelectorAll('.col-checkbox').forEach(function(cb) { visible[cb.value] = cb.checked; }); if (type === 'domestic') { saveDomesticVisible(visible); domesticLoadOrders(1); } else { saveOverseasVisible(visible); overseasLoadOrders(1); } document.querySelector('.modal-overlay').remove(); }; 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.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; if (window.domesticLoadOrders) domesticLoadOrders(domesticOrderPage || 1); else if (window.overseasLoadOrders) overseasLoadOrders(overseasOrderPage || 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); } }); } // ─── 日期编辑函数 ─── 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 productCost = g.product_cost_price ? '¥' + parseFloat(g.product_cost_price).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; 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('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 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) { 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); } // 占位:下单功能(待完善) // ─── 全选/批量下单 ─── // 全局选中状态(跨页持久化) if (!window._planSelectedProductCodes) window._planSelectedProductCodes = {}; // ─── 全选/批量下单 ─── if (!window._planSelectedProductCodes) window._planSelectedProductCodes = {}; /** 选择当前页所有行(表头勾选按钮) */ function planToggleCurrentPage(checked) { document.querySelectorAll('.plan-row-cb').forEach(function(cb) { cb.checked = checked; if (checked) { window._planSelectedProductCodes[cb.value] = true; } else { delete window._planSelectedProductCodes[cb.value]; } }); planSyncSelectUI(); } /** 全选所有数据(页面头部全选按钮) */ function planToggleAllRows(checked) { var currentAll = window._planCurrentAllCodes || []; currentAll.forEach(function(code) { if (checked) { window._planSelectedProductCodes[code] = true; } else { delete window._planSelectedProductCodes[code]; } }); // 同步当前页勾选框的视觉状态 document.querySelectorAll('.plan-row-cb').forEach(function(cb) { cb.checked = window._planSelectedProductCodes[cb.value] || false; }); planSyncSelectUI(); } /** 单个勾选变化时同步到全局 */ function planUpdateSelectAll() { document.querySelectorAll('.plan-row-cb').forEach(function(cb) { if (cb.checked) { window._planSelectedProductCodes[cb.value] = true; } else { delete window._planSelectedProductCodes[cb.value]; } }); planSyncSelectUI(); } /** 统一更新全选框 UI + 选中计数 */ function planSyncSelectUI() { var all = document.querySelectorAll('.plan-row-cb'); var checked = document.querySelectorAll('.plan-row-cb:checked'); var allPageChecked = all.length > 0 && all.length === checked.length; document.getElementById('planSelectAllCb').checked = allPageChecked; // 全选(所有数据)的状态:是否所有 product_code 都被选中 var totalAll = window._planCurrentAllCodes || []; var allChecked = totalAll.length > 0 && totalAll.every(function(code) { return window._planSelectedProductCodes[code]; }); document.getElementById('planSelectAllCb2').checked = allChecked; // 选中计数 var selCount = Object.keys(window._planSelectedProductCodes || {}).length; var scEl = document.getElementById('planSelectedCount'); if (scEl) scEl.textContent = selCount > 0 ? '已选 ' + selCount + ' 个' : ''; } // ─── 正式下单数量编辑 ─── function editFormalQty(productCode) { 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') || '操作完成'); } // 通用居中确认弹窗(返回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) { modal.remove(); } }); }; 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) { modal.remove(); resolve(false); } }); }); }; // 从计划预测中移除产品 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; var hasBom = bomMaterials.length > 0; // 商品基础信息:2列布局 var confirmHtml = '
'; confirmHtml += '

确认加入待下单列表?

'; confirmHtml += '
'; confirmHtml += ''; var fieldTriples = [ [['货品编号', data.product_code], ['供应商', data.supplier_name], ['归属平台', data.platform]], [['货品名称', data.product_name], ['货期(天)', data.lead_time], ['采购跟单', data.procurement_follower]], [['产品类型', data.product_type], ['物流(天)', data.logistics_time], ['采购计划', data.procurement_planner]], [['产品分级', data.product_grade], ['总支撑天数', data.total_support_days], ['实际下单数量', data.formal_order_qty || 0]] ]; 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 = 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]); } 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
\u67f1\u72b6\u56fe / \u6298\u7ebf\u56fe
${sales.length === 0 ? '
\u6682\u65e0\u9500\u91cf\u6570\u636e
' : ''}
`; // \u5f02\u6b65\u52a0\u8f7d\u8ba2\u5355\u6570\u636e fetch('/api/purchase/orders/by_product?code=' + encodeURIComponent(productCode)) .then(function(r) { return r.json(); }) .then(function(res) { document.getElementById('orderInTransitLoading').textContent = ''; var container = document.getElementById('orderInTransitBody'); if (!container) return; if (res.code !== 0 || !res.data || !res.data.items || res.data.items.length === 0) { container.innerHTML = '
\u6682\u65e0\u5728\u9014\u8ba2\u5355
'; return; } var items = res.data.items; var summary = res.data.summary; var statusMap = {'pending':'\u23f3 \u5f85\u5904\u7406','confirmed':'\u2705 \u5df2\u786e\u8ba4','shipped':'\u{1f69a} \u5df2\u53d1\u8d27','received':'\u{1f4e6} \u5df2\u6536\u8d27','cancelled':'\u274c \u5df2\u53d6\u6d88'}; var html = '' + '' + ''; items.forEach(function(item) { var st = statusMap[item.status] || item.status; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
\u8ba2\u5355\u7f16\u53f7\u72b6\u6001\u4e0b\u5355\u65e5\u671f\u4e0b\u5355\u91cf\u5355\u4ef7\u91d1\u989d\u8d27\u671f(\u5929)
' + escHtml(item.order_no) + '' + st + '' + (item.order_time || '-') + '' + (item.order_qty || 0).toLocaleString() + '\u00a5' + parseFloat(item.unit_price || 0).toFixed(2) + '\u00a5' + parseFloat(item.amount || 0).toLocaleString() + '' + (item.lead_time || '-') + '
'; html += '
' + '\u5171 ' + summary.total_orders + ' \u7b14\u5728\u9014\u8ba2\u5355\uff0c' + '\u5408\u8ba1\u6570\u91cf: ' + summary.total_qty.toLocaleString() + '\uff0c' + '\u5408\u8ba1\u91d1\u989d: \u00a5' + summary.total_amount.toLocaleString() + '' + '
'; container.innerHTML = html; }) .catch(function() { var el = document.getElementById('orderInTransitLoading'); if (el) el.textContent = ''; var c = document.getElementById('orderInTransitBody'); if (c) c.innerHTML = '
\u67e5\u8be2\u5931\u8d25
'; }); // \u9500\u91cf\u56fe\u8868 if (sales.length > 0) { window._salesData = sales; window._currentSalesChartType = 'bar'; setTimeout(function() { renderSalesChart(sales, '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) {} }; 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 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.lead_time}, {label:'物流时间(天)', val:row.logistics_time}, {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}, ]; 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','lead_time','logistics_time','procurement_follower','procurement_planner','platform','operator','developer','created_at']; var fields = [ {key:'product_code', label:'货品编号', val:row.product_code, type:'text', ro:true}, {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:'lead_time', label:'货期(天)', val:row.lead_time, type:'number'}, {key:'logistics_time', label:'物流时间(天)', val:row.logistics_time, type:'number', max:999}, {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','lead_time','logistics_time','procurement_follower','procurement_planner','platform','operator','developer','created_at']; var fields = {}; var fieldNames = ['product_name','brand','product_type','supplier_code','supplier_name_match', 'lead_time','logistics_time','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') val = parseInt(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(); 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 += '

货品编号: ' + escHtml(productCode) + ' | 货品名称: ' + escHtml(p.product_name || '-') + '

'; 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; // 查找所有以 edit_ 开头的 input 元素 document.querySelectorAll('#productEditModal input[id^="edit_"]').forEach(function(el) { var fieldName = el.id.replace('edit_', ''); 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; } 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 += '
' + '' + '' + '' + '订单在途' + '加载中...' + '
' + '
' + '
查询中...
' + '
'; // 异步加载订单数据 fetch('/api/purchase/orders/by_product?code=' + encodeURIComponent(productCode)) .then(function(r) { return r.json(); }) .then(function(res) { document.getElementById('orderInTransitLoading').textContent = ''; var container = document.getElementById('orderInTransitBody'); if (res.code !== 0 || !res.data || !res.data.items || res.data.items.length === 0) { container.innerHTML = '
暂无在途订单
'; return; } var items = res.data.items; var summary = res.data.summary; var statusMap = {'pending':'⏳ 待处理','confirmed':'✅ 已确认','shipped':'🚚 已发货','received':'📦 已收货','cancelled':'❌ 已取消'}; var html = '' + '' + '' + ''; items.forEach(function(item) { var st = statusMap[item.status] || item.status; html += '' + '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
订单编号状态下单日期下单量单价金额货期(天)
' + escHtml(item.order_no) + '' + st + '' + (item.order_time || '-') + '' + (item.order_qty || 0).toLocaleString() + '¥' + parseFloat(item.unit_price || 0).toFixed(2) + '¥' + parseFloat(item.amount || 0).toLocaleString() + '' + (item.lead_time || '-') + '
'; html += '
' + '共 ' + summary.total_orders + ' 笔在途订单,' + '合计数量: ' + summary.total_qty.toLocaleString() + ',' + '合计金额: ¥' + summary.total_amount.toLocaleString() + '' + '
'; container.innerHTML = html; }) .catch(function() { document.getElementById('orderInTransitLoading').textContent = ''; document.getElementById('orderInTransitBody').innerHTML = '
查询失败
'; }); // 销量趋势 body.innerHTML += '
' + '
' + '' + '' + '' + '销量趋势' + '
' + '
' + '
' + '' + '
' + (sales.length === 0 ? '
暂无销量数据
' : '') + '
' + '
'; // 底部信息 body.innerHTML += ''; if (sales.length > 0) { setTimeout(function() { renderSalesChart(sales); }, 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'; } } renderSalesChart(window._salesData, type); }; 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' }); } 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 } } } } } }); } 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_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:'text'}, {label:'禁用状态', id:'es_bstatus', val:s.disabled_status, type:'text'} ]; fields.forEach(function(f) { html += '
'; 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) { 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() || '', 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() || '' }; if (!confirm('确认保存供应商修改?')) return; fetch('/api/supplier/update', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({supplier_code: code, 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.deleteSupplier = function(code) { if (!code) return; if (!confirm('确认删除供应商 [' + code + '] 吗?此操作不可恢复!')) return; 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('❌ 网络错误'); }); };