// 蝶雅采购智能体平台 - 前端逻辑
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 = '' +
'' +
'
' +
'
';
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 += '
';
e.actions.forEach(function(a) {
recentHtml += '
• ' + escHtml(a) + '
';
});
recentHtml += '
';
});
recentHtml += '
';
// 分页控件
if (totalPages > 1) {
recentHtml += '
';
recentHtml += '首页 ';
recentHtml += '‹ ';
for (var p = 1; p <= totalPages; p++) {
var active = p === page;
recentHtml += '' + p + ' ';
}
recentHtml += '= totalPages ? ' disabled' : '') + '>› ';
recentHtml += '= totalPages ? ' disabled' : '') + '>末页 ';
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 += '
';
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 += '';
// 分页
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 += '
当前关注 ' + escHtml(atoms.注视 || '-') + ' ';
html += '技能 ' + (Array.isArray(atoms.技能) ? atoms.技能.join('、') : '-') + ' ';
html += '偏好 ' + (atoms.偏好 ? '语言:' + (atoms.偏好.语言||'中文') : '-') + ' ';
var goals = atoms.目标 || [];
html += '目标 ' + (goals.length > 0 ? goals.map(function(g) { return g.名称 + '(' + g.状态 + ')'; }).join(';') : '-') + ' ';
html += '
';
}
// 画像 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 = `
`;
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 += '' + i + ' ';
}
pgHtml += '= totalPages ? 'disabled' : '') + '>› ';
pgHtml += '= totalPages ? 'disabled' : '') + '>末页 ';
}
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 = '';
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 += '' + escHtml(t.name) + ' (' + (typeMap[t.order_type] || t.order_type) + ') ';
});
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 = '';
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 += '
';
row1 += '
';
// 第二行:店铺排行 + 品牌排行(并列纵向柱状图)
var row2 = '';
row2 += '
';
row2 += '
';
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 = `
序号 日期 货品编号 货品名称 品牌 总出库量 总成本
加载中...
`;
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 += '' + cn + ' ';
});
}
var whSel = document.getElementById('dataWarehouse');
if (whSel && d.warehouses) {
d.warehouses.forEach(function(w) {
whSel.innerHTML += '' + escHtml(w) + ' ';
});
}
var shSel = document.getElementById('dataShop');
if (shSel && d.shops) {
d.shops.forEach(function(s) {
shSel.innerHTML += '' + escHtml(s) + ' ';
});
}
var brSel = document.getElementById('dataBrand');
if (brSel && d.brands) {
d.brands.forEach(function(b) {
brSel.innerHTML += '' + escHtml(b) + ' ';
});
}
});
}
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 += '📋 BOM ';
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 = [
'',
'',
'
',
'
',
'# 货品编号 货品名称 货品类型 供应商编码 供应商名称 用量 ',
'' + window._bomBindRowHtml(1) + ' ',
'
',
'
+ 添加一行
',
'
',
'',
'
'
].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 = [
'',
'',
'
',
'
',
'# 物料编号 物料名称 用量 供应商 ',
'' + bodyHtml + ' ',
'
',
'
',
'',
'
'
].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 += '' +
' ' + f.label +
' ';
});
overlay.innerHTML = [
'',
'',
'
',
'
',
' 全选 ',
'
',
'
' + checkboxesHtml + '
',
'
',
'',
'
'
].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 = '';
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 = [
'',
'',
'
',
'
请先下载模板填写数据,再上传CSV文件导入
',
'
📥 下载导入模板 ',
'
',
'
',
'
',
'
',
'',
'
'
].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 = `
`;
// 并行加载:筛选选项 + 表格数据,互不等待
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 += ' 全选 ';
html += '
';
INV_EXPORT_FIELDS.forEach(function(f) {
var checked = window._invExportSelectedFields.indexOf(f.key) >= 0;
html += '';
html += ' ';
html += f.label + ' ';
});
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 += '取消 ';
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 = '';
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 = `
`;
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 = `
`;
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 += '
全部岗位 ';
posList.forEach(function(p) {
var pn = positionNames[p] || p;
filterHtml += '' + escHtml(pn) + ' ';
});
filterHtml += '';
filterHtml += '
全部级别 ';
levelList.forEach(function(l) {
var ln = levelNames[l] || l;
filterHtml += '' + escHtml(ln) + ' ';
});
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 = '';
html += '
';
html += '';
html += '
';
_permMenuTree.forEach(function(group) {
html += '
';
html += '
' + group.icon + ' ' + escHtml(group.name) + '
';
group.items.forEach(function(item) {
var pageId = item.page;
var hasRolePerm = _rolePagePerms[pageId] == 1;
var feats = item.features || [];
html += '
';
// 菜单项名称
html += '' + escHtml(item.name) + ' ';
// 功能按钮列表
feats.forEach(function(f) {
var pagePerms = userPerms[pageId] || {};
var checked;
if (pagePerms[f.id] !== undefined) {
checked = pagePerms[f.id] == 1;
} else if (isFullAccess) {
checked = true;
} else {
checked = hasRolePerm;
}
html += '';
html += ' ';
html += ' ' + escHtml(f.name) + ' ';
});
html += '
';
});
html += '
';
});
html += '
';
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 = '';
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 += '';
html += '
';
html += '
';
labels.forEach(function(l) { html += '' + l + ' '; });
html += ' ';
data.forEach(function(r) {
html += '';
fields.forEach(function(f) {
var val = r[f];
if (val === null || val === undefined) val = '-';
html += '' + escHtml(String(val)) + ' ';
});
html += ' ';
});
html += '
';
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 = `
`;
// 冻结列按钮 - 事件绑定
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 '📋 BOM ';
}
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 = '';
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 += '';
values.forEach(function(v) {
var key = escHtml(v).replace(/'/g, "\\'");
var checked = _planFilterSelected[field][v] !== false ? 'checked' : '';
html += '';
html += ' ' + escHtml(v);
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 += ' ';
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 = '全部' + label + ' ' +
names.map(function(n) { return '' + escHtml(n) + ' '; }).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 += '' + left[0] + ' ';
html += '' + escHtml(String(left[1] || '-')) + ' ';
html += '' + right[0] + ' ';
html += '' + escHtml(String(right[1] || '-')) + ' ';
html += ' ';
});
html += '
';
// ─── 包材信息 ───
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 += '' + escHtml(b.code) + ' ';
html += '' + escHtml(b.name) + ' ';
html += '' + (b.suggested_qty || 0).toLocaleString() + ' ';
html += '' + (b.actual_qty || 0).toLocaleString() + ' ';
html += '' + escHtml(b.supplier) + ' ';
html += '' + escHtml(b.price) + ' ';
html += ' ';
});
html += '
';
} 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 += '
=0&&ti>=0){var el=items[fi];if(fi' +
'⠿ ' +
' ' + 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 = '' +
'
' +
'
⚙ 字段设置(下单备份) ' +
'✕ ' +
'' +
'
' +
'
' +
'取消 ' +
'💾 保存 ' +
'
';
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 += '' +
' ' + label +
' ';
});
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 = '';
// 弹窗展示
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 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 = '';
html += '
';
// 成品信息卡片
html += '
';
html += '';
html += '
';
html += '
';
html += '
加载中...
';
html += '
';
// 包材信息卡片
html += '
';
html += '';
html += '
';
html += '
加载中...
';
html += '
';
html += '
';
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 = '';
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 += '' + f[0] + ' ';
html += '' + escHtml(String(f[1] || '-')) + ' ';
html += ' ';
});
html += '
';
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 = `
加载中... ';
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 += '' + rowNum + ' ';
// 日期
html += '' + escHtml(r['日期'] || '') + ' 📅 ';
// 主要字段
html += '' + escHtml(r['计划下单时间'] || '') + ' ';
html += '' + escHtml(r['新旧品'] || '') + ' ';
html += '' + escHtml(r['编码'] || '') + ' ';
html += '' + escHtml(r['名称'] || '') + ' ';
html += '' + escHtml(r['下单备注'] || '-') + ' ';
html += ' ';
html += '' + escHtml(r['是否审核价格'] || '') + ' ';
html += '' + (parseInt(r['数量']) || 0).toLocaleString() + ' ';
html += '' + (r['金额'] ? '¥' + parseFloat(r['金额']).toLocaleString() : '') + ' ';
html += '' + escHtml(r['供货商'] || '') + ' ';
html += '' + escHtml(r['仓库'] || '') + ' ';
html += '' + escHtml(r['金蝶采购单'] || '') + ' ';
html += '' + renderNameBadges(r['采购员']) + ' ';
html += '' + renderNameBadges(r['下单人']) + ' ';
html += '' + escHtml(r['进度'] || '') + ' ';
html += '' + escHtml(r['旺店通生产单号'] || '') + ' ';
html += '' + escHtml(r['是否已录入旺店通'] || '') + ' ';
html += '' + renderNameBadges(r['采购开发']) + ' ';
html += '' + escHtml(r['实际到货日期'] || '') + ' ';
html += '' + (parseInt(r['实际到货数量辅助']) || '').toLocaleString() + ' ';
html += '' + escHtml(r['辅助1'] || '') + ' ';
html += '' + (parseInt(r['实际到货数量']) || '').toLocaleString() + ' ';
html += '' + (parseInt(r['未到货数']) || '').toLocaleString() + ' ';
html += '' + escHtml(r['预计到货日期'] || '') + ' ';
html += '' + escHtml(r['金蝶入库'] || '') + ' ';
html += '' + escHtml(r['状态'] || '') + ' ';
// 做货周期 = 实际到货日期 - 日期
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 += '' + manuDays + ' ';
// 1-31列
for (var d = 1; d <= 31; d++) {
var v = parseInt(r[String(d)]) || 0;
html += '' + (v > 0 ? v.toLocaleString() : '') + ' ';
}
html += '' + escHtml(r['父记录'] || '') + ' ';
html += '';
html += '✏️ ';
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 = `
加载中... ';
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 += '' + rowNum + ' ';
html += '' + escHtml(r['日期'] || '') + ' 📅 ';
html += '' + escHtml(r['计划下单时间'] || '') + ' ';
html += '' + escHtml(r['产品编码'] || '') + ' ';
html += ' ';
html += '' + escHtml(r['是否审核价格'] || '') + ' ';
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 += '' + renderNameBadges(r['下单人']) + ' ';
html += '' + escHtml(r['进度'] || '') + ' ';
html += '' + escHtml(r['下单备注'] || '-') + ' ';
html += '' + renderNameBadges(r['采购开发']) + ' ';
html += '' + escHtml(r['旺店通生产单号'] || '') + ' ';
html += '' + escHtml(r['是否已录入到旺店通'] || '') + ' ';
html += '' + escHtml(r['实际到货日期'] || '') + ' ';
html += '' + (parseInt(r['实际到货数量辅助']) || '').toLocaleString() + ' ';
html += '' + escHtml(r['辅助'] || '') + ' ';
html += '' + (parseInt(r['实际到货数量']) || '').toLocaleString() + ' ';
html += '' + escHtml(r['预计到货日期'] || '') + ' ';
html += '' + (parseInt(r['未到货数']) || '').toLocaleString() + ' ';
html += '' + escHtml(r['状态'] || '') + ' ';
// 做货周期 = 实际到货日期 - 日期
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 += '' + manuDaysOv + ' ';
for (var d = 1; d <= 31; d++) {
var v = parseInt(r[String(d)]) || 0;
html += '' + (v > 0 ? v.toLocaleString() : '') + ' ';
}
html += '';
html += '✏️ ';
html += '生成合同 ';
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 = `
#
日期
编码
名称
单价
数量
合计
供应商
下单备注
仓库到货
进度
已做采购单
已入库
采购下单人
旺店通编号
录入旺店通
到货日期
状态
做货天数
包材开发
操作
加载中...
`;
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 = '';
html += '';
html += '
';
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 = ' ';
}
}
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 = ' ';
} 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 += '' + 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 || '-') + '天 ';
});
html += '
';
}
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 = [
'',
'',
'
',
'
',
'# 物料编号 物料名称 用量 成本价 供应商 ',
'' + bodyHtml + ' ',
'
',
'
+ 添加一行
',
'
',
'',
'
'
].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 = [
'',
'',
'
',
'',
'
',
'',
'
',
'
',
'
成品编号 * ',
'
',
'
成品名称 ',
'
',
'
',
'
',
'
',
'
',
'',
'
',
'',
'
',
'
',
'# 物料编号 * 物料名称 用量 * 供应商 成本价 货品类型 ',
'',
window._bomAddMatRowHtml(1),
' ',
'
',
'
',
'
',
'
',
'',
'
'
].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 = [
'',
'',
'
',
'
上传CSV文件导入BOM数据(全量替换)
',
'
CSV格式:货品编号,货品名称,物料编号,物料名称,用量
',
'
',
'
',
'
',
'
',
'',
'
'
].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 = '';
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 += '' + item[0] + ' ';
confirmHtml += '' + escHtml(String(item[1] || '-')) + ' ';
} else {
confirmHtml += ' ';
confirmHtml += ' ';
}
}
confirmHtml += ' ';
});
confirmHtml += '
';
// 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 += '' + escHtml(b.material_code || '') + ' ';
confirmHtml += '' + escHtml(b.material_name || '') + ' ';
confirmHtml += '' + (b.bom_qty || 0) + ' ';
confirmHtml += '' + orderQty.toLocaleString() + ' ';
confirmHtml += '' + (savedActual > 0 ? savedActual.toLocaleString() : '0') + ' ';
confirmHtml += '' + (b.available_qty || 0).toLocaleString() + ' ';
confirmHtml += '' + (b.purchase_in_transit || 0).toLocaleString() + ' ';
confirmHtml += '' + (b.production_in_transit || 0).toLocaleString() + ' ';
var manuscriptVal = savedManuscriptMap[b.material_code] || '';
var manuscriptDisplay = manuscriptVal === '是' ? '是 ' : '否 ';
confirmHtml += '' + manuscriptDisplay + ' ';
confirmHtml += '' + escHtml(savedRemarkMap[b.material_code] || '') + ' ';
confirmHtml += ' ';
});
confirmHtml += '
';
}
confirmHtml += '
';
confirmHtml += '';
confirmHtml += '取消 ';
confirmHtml += '✅ 确认加入
';
modal.innerHTML = '' +
confirmHtml + '
';
document.getElementById('planOrderCancelBtn').onclick = function() { modal.remove(); };
document.getElementById('planOrderConfirmBtn').onclick = function() {
modal.remove();
if (window._pageCache) delete window._pageCache['purchase'];
var orderData = JSON.parse(JSON.stringify(data));
if (window._pendingBomData && window._pendingBomData[data.product_code]) {
orderData.bom_items = JSON.stringify(window._pendingBomData[data.product_code]);
}
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 = `
`;
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 = `
${bomMaterials.length > 0
? '
\u7269\u6599\u7f16\u53f7 \u7269\u6599\u540d\u79f0 \u53ef\u7528\u5e93\u5b58 \u751f\u4ea7\u5728\u9014 \u91c7\u8d2d\u5728\u9014 \u5355\u4ef7 ' + bomHtml + '
'
: '
\u6682\u65e0BOM\u7269\u6599\u6e05\u5355
'
}
${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 = '' +
'\u8ba2\u5355\u7f16\u53f7 \u72b6\u6001 \u4e0b\u5355\u65e5\u671f \u4e0b\u5355\u91cf \u5355\u4ef7 \u91d1\u989d \u8d27\u671f(\u5929) ' +
' ';
items.forEach(function(item) {
var st = statusMap[item.status] || item.status;
html += '' +
'' + 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 += '
';
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 = `
`;
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 = `
${bomMaterials.length > 0
? '
\u7269\u6599\u7f16\u53f7 \u7269\u6599\u540d\u79f0 \u53ef\u7528\u5e93\u5b58 \u751f\u4ea7\u5728\u9014 \u91c7\u8d2d\u5728\u9014 \u5355\u4ef7 ' + bomHtml + '
'
: '
\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 = [
'',
'',
'
',
'
确认删除产品 ' + escHtml(productCode) + ' 吗?
',
'
删除后将移入回收站,可在回收站中恢复
',
'
',
'',
'
'
].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 += '' + left.label + ' ';
html += '' + lv + ' ';
if (right) {
var rv = right.val != null && right.val !== '' ? escHtml(String(right.val)) : '- ';
html += '' + right.label + ' ';
html += '' + rv + ' ';
} else {
html += ' ';
}
html += ' ';
}
html += '
';
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 = '';
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 += '
' + o + ' ';
});
html += '
' + f.label + ' ' + opts + '
';
} else if (f.supSearch) {
html += '
';
} else {
html += '
' + f.label + '
';
}
});
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 += '
';
}
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 = `
`;
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用量 需用量 ' +
'可用库存 生产在途 采购在途 ' +
'操作 ' +
'单价 总成本 缺口 ' +
' ' +
'' + bomHtml + ' ' +
summaryHtml +
'
' +
'
' +
'
';
} else {
body.innerHTML += '';
}
// 订单在途
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 += '' +
'' + 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 += '
';
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 = '';
return;
}
let 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 += '
' + f.label + ' ';
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('❌ 网络错误'); });
};