dy-dashboard/static/js/app.js.bak
2026-10-28 11:14:17 +08:00

19464 lines
1.0 MiB
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 蝶雅采购智能体平台 - 前端逻辑
let charts = {};
// 页面缓存:切换菜单时不销毁页面状态
var CACHE_VERSION = 73;
if (!window._pageCache) window._pageCache = {};
if (!window._pageDataCache) window._pageDataCache = {};
if (!window._pageReloaders) window._pageReloaders = {};
if (window._cacheVersion !== CACHE_VERSION) {
window._pageCache = {};
window._pageDataCache = {};
window._pageReloaders = {};
window._cacheVersion = CACHE_VERSION;
}
// 全局点击关闭自动补全下拉
if (!window._autoCompleteInit) {
window._autoCompleteInit = true;
document.addEventListener('click', function(e) {
var dropdowns = document.querySelectorAll('.autocomplete-dropdown');
dropdowns.forEach(function(dd) {
var wrap = dd.closest('.autocomplete-wrap');
if (wrap && !wrap.contains(e.target)) {
dd.style.display = 'none';
}
});
});
}
const PAGE_LOADERS = {
'dashboard': loadDashboard,
'data_detail': loadDataPage,
'data_summary': loadDataSummaryPage,
'data_outbound': loadDataPage,
'products': loadProductsPage,
'users': loadUsersPage,
'logs': loadLogsPage,
'login_records': loadLoginRecordsPage,
'config': loadConfigPage,
'permissions': loadPermissionsPage,
'warehouse': loadWarehousePage,
'inventory': loadInventoryPage,
'inventory_finished': loadInventoryPage,
'inventory_packaging': loadInventoryPage,
'suppliers': loadSuppliersPage,
'personnel': loadPersonnelPage,
'planning': loadPlanningPage,
'purchase': loadPurchasePage,
'purchase_ready': loadPurchaseReadyPage,
'orders': loadOrdersPage,
'orders_domestic': loadDomesticOrdersPage,
'orders_overseas': loadOverseasOrdersPage,
'orders_packaging': loadPackagingOrdersPage,
'orders_finished': loadFinishedProductsPage,
'bom': loadBomPage,
'knowledge': loadKnowledgePage,
'feedback': loadFeedbackPage,
'contract_templates': loadContractTemplatesPage
};
// ─── 知识库页面 ───
// ─── 知识库页面 ───
function loadKnowledgePage(container) {
renderKnowledgePage(container);
}
function renderKnowledgePage(container) {
// 配置 marked
if (typeof marked !== 'undefined') {
marked.setOptions({
breaks: true,
gfm: true
});
}
container.innerHTML = `
<style>
.kb-tabs { display:flex;gap:4px;margin-bottom:16px;background:var(--bg-card,#f7f8fa);padding:4px;border-radius:10px; }
.kb-tab { padding:8px 20px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text-light,#888);border-radius:8px;transition:all .25s;user-select:none; }
.kb-tab:hover { color:var(--text,#333);background:rgba(0,0,0,0.04); }
.kb-tab:nth-child(1).active { color:#1890ff;background:#e6f7ff;box-shadow:0 1px 4px rgba(24,144,255,0.12); }
.kb-tab:nth-child(2).active { color:#722ed1;background:#f9f0ff;box-shadow:0 1px 4px rgba(114,46,209,0.12); }
.kb-tab:nth-child(3).active { color:#13c2c2;background:#e6fffb;box-shadow:0 1px 4px rgba(19,194,194,0.12); }
.kb-topics { display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px; }
.kb-topic-card { background:var(--bg-card,#fff);border-radius:8px;border:1px solid var(--border,#e8e8e8);overflow:hidden; }
.kb-topic-header { padding:12px 16px;background:#f9f9f9;border-bottom:1px solid var(--border,#e8e8e8);font-weight:600;font-size:15px;color:var(--text,#333);display:flex;align-items:center;gap:8px; }
.kb-topic-body { padding:8px 12px; }
.kb-article-item { display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-radius:6px;cursor:pointer;text-decoration:none;color:var(--text,#333);transition:background .15s; }
.kb-article-item:hover { background:#f0f5ff; }
.kb-article-title { font-weight:500;font-size:14px; }
.kb-article-summary { font-size:12px;color:var(--text-light,#999);margin-left:6px; }
.kb-article-date { font-size:12px;color:var(--text-light,#999);white-space:nowrap; }
.kb-empty { padding:12px;color:var(--text-light,#999);font-size:13px; }
.kb-article-view { line-height:1.9;font-size:15px;color:#333; }
.kb-article-view h1 { font-size:22px;border-bottom:1px solid #eee;padding-bottom:10px;margin:0 0 16px 0; }
.kb-article-view h2 { font-size:18px;margin:20px 0 10px 0;padding-bottom:6px;border-bottom:1px solid #eee; }
.kb-article-view h3 { font-size:16px;margin:16px 0 8px 0; }
.kb-article-view h4 { font-size:15px;margin:12px 0 6px 0; }
.kb-article-view p { margin:0 0 10px 0; }
.kb-article-view blockquote { border-left:3px solid #1890ff;margin:10px 0;padding:6px 14px;background:#f6f9ff;color:#555; }
.kb-article-view code { background:#f0f0f0;padding:2px 6px;border-radius:3px;font-size:13px;color:#d63384; }
.kb-article-view pre { background:#1e1e2e;padding:14px 18px;border-radius:8px;overflow-x:auto;margin:12px 0; }
.kb-article-view pre code { background:none;color:#cdd6f4;padding:0;font-size:13px;line-height:1.6; }
.kb-article-view table { border-collapse:collapse;width:100%;margin:12px 0;font-size:14px; }
.kb-article-view th { background:#f5f5f5;padding:8px 12px;border:1px solid #e0e0e0;font-weight:600;text-align:left; }
.kb-article-view td { padding:8px 12px;border:1px solid #e0e0e0; }
.kb-article-view tr:hover td { background:#fafafa; }
.kb-article-view a { color:#1890ff;text-decoration:none; }
.kb-article-view a:hover { text-decoration:underline; }
.kb-article-view ul, .kb-article-view ol { padding-left:20px;margin:8px 0; }
.kb-article-view li { margin:4px 0; }
.kb-article-view hr { border:none;border-top:1px solid #eee;margin:16px 0; }
.kb-article-view strong { font-weight:600; }
.kb-section { font-size:15px;color:var(--text-light,#999);padding:8px 0; }
.kb-profile-block { background:var(--bg-card,#fff);border-radius:8px;border:1px solid var(--border,#e8e8e8);margin-bottom:12px;overflow:hidden; }
.kb-profile-hd { padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--border,#e8e8e8);display:flex;align-items:center;gap:8px; }
.kb-profile-bd { padding:12px 16px;line-height:1.8;font-size:14px; }
.kb-profile-bd table { border-collapse:collapse;width:100%; }
.kb-profile-bd td,.kb-profile-bd th { padding:6px 10px;border:1px solid #eee;font-size:13px; }
.kb-profile-bd th { background:#fafafa;font-weight:600; }
/* 知识库全高填充 */
.page-container { display:flex;flex-direction:column;flex:1;min-height:0; }
.kb-full { display:flex;flex-direction:column;flex:1;min-height:0; }
.kb-full .kb-tabs { flex-shrink:0; }
.kb-full .kb-pane { flex:1;display:flex;flex-direction:column;min-height:0; }
.kb-full .kb-pane .card { flex:1;display:flex;flex-direction:column;min-height:0; }
.kb-full .kb-pane .card .card-body { flex:1;overflow-y:auto; }
</style>
<div class="page-header" style="display:flex;align-items:center;justify-content:space-between">
<h1 style="display:flex;align-items:center;gap:10px"><span style="font-size:28px">📚</span> 知识库</h1>
<div style="display:flex;gap:8px">
<input type="text" id="kbSearchInput" class="form-input" placeholder="搜索文章..." style="width:220px;padding:6px 12px" onkeydown="if(event.key==='Enter')kbSearch()">
<button class="btn btn-sm btn-primary" onclick="kbSearch()" style="padding:6px 14px">搜索</button>
<button class="btn btn-sm" onclick="kbLoadIndex()" style="padding:6px 14px" title="刷新">🔄</button>
</div>
</div>
<!-- 标签页导航 -->
<div class="kb-full">
<div class="kb-tabs">
<div class="kb-tab active" id="kbTabWiki" onclick="kbSwitchTab('wiki')">📖 知识库</div>
<div class="kb-tab" id="kbTabDatalib" onclick="kbSwitchTab('datalib')">📁 资料库</div>
<div class="kb-tab" id="kbTabProfile" onclick="kbSwitchTab('profile')">🧑 画像</div>
<div class="kb-tab" id="kbTabAnalysis" onclick="kbSwitchTab('analysis')">🔬 分析</div>
<div style="margin-left:auto;display:flex;gap:4px;padding:4px">
<button class="btn btn-sm" onclick="kbShowCreateModal()" title="新建文章">📝 新建</button>
<button class="btn btn-sm" onclick="kbShowImportModal()" title="导入文件">📥 导入</button>
</div>
</div>
<!-- 知识库面板 -->
<div class="kb-pane" id="kbPaneWiki">
<div class="card" id="kbMainCard">
<div id="kbContent">
<div style="text-align:center;padding:60px 20px;color:var(--text-light,#999)">
<div style="font-size:48px;margin-bottom:16px">⏳</div>
<div style="font-size:15px">正在加载知识库...</div>
</div>
</div>
</div>
<div class="card" id="kbArticleCard" style="display:none">
<div class="card-header" style="display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border,#e8e8e8)">
<button class="btn btn-sm" onclick="kbBackToList()" style="padding:4px 12px">← 返回列表</button>
<div style="flex:1">
<h3 id="kbArticleTitle" style="margin:0;font-size:15px;font-weight:600"></h3>
<span id="kbArticleMeta" style="font-size:12px;color:var(--text-light,#999)"></span>
</div>
<div style="display:flex;gap:4px">
<button class="btn btn-sm" id="kbEditBtn" onclick="kbShowEditModal()" title="编辑">✏️ 编辑</button>
<button class="btn btn-sm" id="kbExportBtn" onclick="kbExportArticle()" title="导出">📤 导出</button>
<button class="btn btn-sm" id="kbDeleteBtn" onclick="kbDeleteArticle()" style="color:var(--danger)" title="删除">🗑️ 删除</button>
</div>
</div>
<div class="card-body" id="kbArticleContent" style="padding:20px 24px"></div>
</div>
</div>
<!-- 资料库面板 -->
<div class="kb-pane" id="kbPaneDatalib" style="display:none">
<div style="text-align:center;padding:60px;color:var(--text-light,#999)">
<div style="font-size:48px;margin-bottom:16px">⏳</div>
<div>正在加载资料库...</div>
</div>
</div>
<!-- 画像面板 -->
<div class="kb-pane" id="kbPaneProfile" style="display:none">
<div style="text-align:center;padding:60px;color:var(--text-light,#999)">
<div style="font-size:48px;margin-bottom:16px">⏳</div>
<div>正在加载画像...</div>
</div>
</div>
<!-- 分析面板 -->
<div class="kb-pane" id="kbPaneAnalysis" style="display:none">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
<div class="card">
<div class="card-header" style="display:flex;align-items:center;gap:8px;padding:10px 14px">
<h3 style="margin:0;font-size:14px">🧹 知识库检查</h3>
<button class="btn btn-sm btn-primary" onclick="kbRunLint()" style="margin-left:auto;padding:4px 12px">运行检查</button>
</div>
<div class="card-body" id="kbLintResult" style="padding:12px 14px;font-size:13px;line-height:1.7;min-height:120px;color:var(--text-light,#999)">
点击「运行检查」开始
</div>
</div>
<div class="card">
<div class="card-header" style="padding:10px 14px">
<h3 style="margin:0;font-size:14px">⚠️ 矛盾分析</h3>
</div>
<div class="card-body" id="kbContradictionResult" style="padding:12px 14px;font-size:13px;line-height:1.7;min-height:120px;color:var(--text-light,#999)">
暂无分析结果
</div>
</div>
</div>
<div class="card">
<div class="card-header" style="padding:10px 14px">
<h3 style="margin:0;font-size:14px">🔗 关系图谱</h3>
</div>
<div class="card-body" id="kbGraphContainer" style="padding:12px 14px;min-height:300px;display:flex;align-items:center;justify-content:center;color:var(--text-light,#999)">
<div style="text-align:center">
<div style="font-size:40px;margin-bottom:8px">🗺️</div>
<div>点击「加载图谱」查看文章关联</div>
<button class="btn btn-sm btn-primary" onclick="kbLoadGraph()" style="margin-top:8px">加载图谱</button>
</div>
</div>
</div>
</div>
</div>
<!-- 新建/编辑文章弹窗 -->
<div class="modal-overlay" id="kbEditorModal" style="display:none">
<div class="modal" style="width:720px">
<div class="modal-header">
<h3 id="kbEditorTitle">✏️ 编辑文章</h3>
<span class="modal-close" onclick="kbCloseEditor()">&times;</span>
</div>
<div class="modal-body" style="padding:16px 20px">
<div style="margin-bottom:10px">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">标题</label>
<input type="text" id="kbEditorArticleTitle" class="form-input" style="width:100%;padding:6px 10px" placeholder="文章标题">
</div>
<div style="margin-bottom:10px">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">主题分类</label>
<select id="kbEditorTopic" class="form-input" style="width:100%;padding:6px 10px"></select>
</div>
<div style="margin-bottom:4px">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">内容(Markdown)</label>
<textarea id="kbEditorContent" class="form-input" style="width:100%;min-height:320px;padding:8px 10px;font-family:monospace;font-size:13px" placeholder="在此编写 Markdown 内容..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="kbCloseEditor()">取消</button>
<button class="btn btn-primary" id="kbEditorSaveBtn" onclick="kbSaveArticle()">保存</button>
</div>
</div>
</div>
<!-- 导入弹窗 -->
<div class="modal-overlay" id="kbImportModal" style="display:none">
<div class="modal" style="width:480px">
<div class="modal-header">
<h3>📥 导入文件</h3>
<span class="modal-close" onclick="document.getElementById('kbImportModal').style.display='none'">&times;</span>
</div>
<div class="modal-body" style="padding:16px 20px">
<div style="margin-bottom:10px">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">目标主题</label>
<select id="kbImportTopic" class="form-input" style="width:100%;padding:6px 10px"></select>
</div>
<div>
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">选择 Markdown 文件</label>
<input type="file" id="kbImportFile" accept=".md,.txt,.markdown" class="form-input" style="width:100%;padding:6px">
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="document.getElementById('kbImportModal').style.display='none'">取消</button>
<button class="btn btn-primary" onclick="kbDoImport()">导入</button>
</div>
</div>
</div>
`;
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 = '<div style="text-align:center;padding:60px 20px;color:var(--text-light,#999)"><div style="font-size:48px;margin-bottom:16px">⏳</div><div>正在加载知识库...</div></div>';
// 同时获取索引和操作日志
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 = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败</div>';
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 = '<div class="card" style="margin-bottom:12px;padding:0" id="kbRecentCard">' +
'<div class="card-header" style="padding:8px 14px;font-size:13px;font-weight:600;border-bottom:1px solid var(--border,#e8e8e8);display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="kbToggleRecent()">' +
'<span>🕐 最近更新' + (totalPages > 1 ? ' <span style="font-size:12px;color:#999;font-weight:400">第' + page + '/' + totalPages + '页</span>' : '') + '</span>' +
'<span id="kbRecentToggle" style="font-size:12px;color:var(--text-light,#999);user-select:none">▼ 收起</span>' +
'</div>' +
'<div id="kbRecentBody" style="display:block;max-height:320px;overflow-y:auto">' +
'<div style="padding:6px 14px;font-size:13px;color:var(--text-light,#666);line-height:2.2">';
pageEntries.forEach(function(e, idx) {
var dateId = 'kbDate_' + e.date.replace(/-/g, '_');
recentHtml += '<div style="margin-bottom:4px">';
recentHtml += '<div style="cursor:pointer;display:flex;align-items:center;gap:6px;padding:4px 0" onclick="kbToggleDateActions(\'' + dateId + '\')">';
recentHtml += '<span id="' + dateId + '_arrow" style="font-size:10px;color:#999;transition:transform .2s">▶</span>';
recentHtml += '<strong>📅 ' + escHtml(e.date) + '</strong>';
recentHtml += '<span style="font-size:12px;color:#999">(' + e.actions.length + ' 项操作)</span>';
recentHtml += '</div>';
recentHtml += '<div id="' + dateId + '" style="display:none;padding-left:24px;font-size:12px;color:var(--text-light,#999);line-height:2">';
e.actions.forEach(function(a) {
recentHtml += '<div>• ' + escHtml(a) + '</div>';
});
recentHtml += '</div></div>';
});
recentHtml += '</div>';
// 分页控件
if (totalPages > 1) {
recentHtml += '<div style="display:flex;justify-content:center;gap:3px;padding:6px 8px;border-top:1px solid var(--border,#f0f0f0);flex-wrap:wrap">';
recentHtml += '<button class="kb-page-btn" onclick="kbRecentGoPage(1)" style="font-size:11px;padding:2px 8px;border:1px solid #d9d9d9;border-radius:4px;cursor:pointer;background:#fff' + (page <= 1 ? ';opacity:0.4;cursor:default' : '') + '"' + (page <= 1 ? ' disabled' : '') + '>首页</button>';
recentHtml += '<button class="kb-page-btn" onclick="kbRecentGoPage(' + (page - 1) + ')" style="font-size:11px;padding:2px 8px;border:1px solid #d9d9d9;border-radius:4px;cursor:pointer;background:#fff' + (page <= 1 ? ';opacity:0.4;cursor:default' : '') + '"' + (page <= 1 ? ' disabled' : '') + '>‹</button>';
for (var p = 1; p <= totalPages; p++) {
var active = p === page;
recentHtml += '<button class="kb-page-btn" onclick="kbRecentGoPage(' + p + ')" style="font-size:11px;padding:2px 8px;border:1px solid ' + (active ? '#1890ff' : '#d9d9d9') + ';border-radius:4px;cursor:pointer;background:' + (active ? '#e6f7ff' : '#fff') + ';color:' + (active ? '#1890ff' : '#333') + ';font-weight:' + (active ? '600' : '400') + '">' + p + '</button>';
}
recentHtml += '<button class="kb-page-btn" onclick="kbRecentGoPage(' + (page + 1) + ')" style="font-size:11px;padding:2px 8px;border:1px solid #d9d9d9;border-radius:4px;cursor:pointer;background:#fff' + (page >= totalPages ? ';opacity:0.4;cursor:default' : '') + '"' + (page >= totalPages ? ' disabled' : '') + '>›</button>';
recentHtml += '<button class="kb-page-btn" onclick="kbRecentGoPage(' + totalPages + ')" style="font-size:11px;padding:2px 8px;border:1px solid #d9d9d9;border-radius:4px;cursor:pointer;background:#fff' + (page >= totalPages ? ';opacity:0.4;cursor:default' : '') + '"' + (page >= totalPages ? ' disabled' : '') + '>末页</button>';
recentHtml += '</div>';
}
recentHtml += '</div></div>';
}
var topics = idxRes.data.topics || [];
var html = recentHtml + '<div class="kb-topics">';
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 += '<div class="kb-topic-card">';
html += '<div class="kb-topic-header"><span>📂</span> ' + escHtml(topic.name) + ' <span style="font-size:12px;color:var(--text-light,#999);font-weight:400">(' + totalCount + '篇)</span></div>';
html += '<div class="kb-topic-body">';
// 渲染子主题(作为可点击的文件夹,点击展开)
if (hasSubtopics) {
topic.subtopics.forEach(function(sub) {
if (sub.articles.length > 0) {
html += '<a class="kb-article-item" href="javascript:void(0)" onclick="kbOpenSubTopic(\'' + escJs(topic.name) + '\', \'' + escJs(sub.name) + '\')" style="font-weight:500">';
html += '<div><span>📁</span> ' + escHtml(sub.name) + '</div>';
html += '<span style="font-size:12px;color:var(--text-light,#999)">' + sub.articles.length + '篇 ›</span>';
html += '</a>';
hasAny = true;
}
});
}
// 渲染直属文章(不在子主题内的)
topic.articles.forEach(function(art) {
hasAny = true;
html += '<a class="kb-article-item" href="javascript:void(0)" onclick="kbOpenArticle(\'' + escJs(art.path) + '\')">';
html += '<div><span class="kb-article-title">📄 ' + escHtml(art.title) + '</span>';
if (art.summary && art.summary !== '—') {
html += '<span class="kb-article-summary">' + escHtml(art.summary) + '</span>';
}
html += '</div>';
html += '<span class="kb-article-date">' + (art.updated !== '—' ? art.updated : '') + '</span>';
html += '</a>';
});
if (!hasSubtopics && topic.articles.length === 0) {
html += '<div class="kb-empty">暂无文章</div>';
}
html += '</div></div>';
});
html += '</div>';
if (!hasAny) {
html = recentHtml + '<div style="text-align:center;padding:60px;color:var(--text-light,#999)"><div style="font-size:48px;margin-bottom:12px">📭</div><div style="font-size:15px">知识库暂无内容</div><div style="font-size:13px;margin-top:6px">让 OpenClaw 摄入资料后,文章会自动出现在这里</div></div>';
}
// 缓存索引数据用于子主题展开
window._kbIndexData = idxRes.data;
el.innerHTML = html;
})
.catch(function() {
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败,请检查网络连接</div>';
});
}
// 打开子主题(如训练记录)
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 = '<div style="padding-bottom:12px">';
html += '<a class="kb-article-item" href="javascript:void(0)" onclick="kbLoadIndex()" style="display:inline-flex;gap:4px;color:var(--primary);font-weight:500">← 返回 ' + escHtml(topicName) + '</a>';
html += '<h3 style="margin:8px 0 0;font-size:16px;display:flex;align-items:center;gap:6px"><span>📁</span> ' + escHtml(subTopicName) + ' <span style="font-size:13px;color:var(--text-light,#999);font-weight:400">(' + articles.length + '篇)</span></h3>';
html += '</div>';
html += '<div style="border:1px solid var(--border,#e8e8e8);border-radius:6px;overflow:hidden">';
pageItems.forEach(function(art) {
html += '<a class="kb-article-item" href="javascript:void(0)" onclick="kbOpenArticle(\'' + escJs(art.path) + '\')">';
html += '<div><span class="kb-article-title">📄 ' + escHtml(art.title) + '</span>';
if (art.summary && art.summary !== '—') {
html += '<span class="kb-article-summary">' + escHtml(art.summary) + '</span>';
}
html += '</div>';
html += '<span class="kb-article-date">' + (art.updated !== '—' ? art.updated : '') + '</span>';
html += '</a>';
});
html += '</div>';
// 分页
if (isTraining && totalPages > 1) {
html += '<div style="display:flex;justify-content:center;align-items:center;gap:6px;padding:14px 0 4px">';
if (page > 1) html += '<button class="btn btn-sm" onclick="kbOpenSubTopic(\'' + escJs(topicName) + '\', \'' + escJs(subTopicName) + '\', ' + (page - 1) + ')">上一页</button>';
html += '<span style="font-size:13px;color:var(--text-light,#999)">第 ' + page + '/' + totalPages + ' 页</span>';
if (page < totalPages) html += '<button class="btn btn-sm" onclick="kbOpenSubTopic(\'' + escJs(topicName) + '\', \'' + escJs(subTopicName) + '\', ' + (page + 1) + ')">下一页</button>';
html += '</div>';
}
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 = '<div style="text-align:center;padding:60px 20px;color:var(--text-light,#999)"><div style="font-size:48px;margin-bottom:16px">⏳</div><div>加载文章...</div></div>';
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 = '<div style="padding:40px;color:var(--danger)">❌ ' + (res.msg || '加载失败') + '</div>';
return;
}
var data = res.data;
document.getElementById('kbArticleTitle').textContent = data.path;
document.getElementById('kbArticleMeta').textContent = '更新于 ' + data.updated;
if (typeof marked !== 'undefined') {
contentDiv.innerHTML = '<div class="kb-article-view">' + marked.parse(data.content) + '</div>';
} else {
contentDiv.innerHTML = '<div class="kb-article-view"><pre>' + escHtml(data.content) + '</pre></div>';
}
// 外链在新窗口打开
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 = '<div style="padding:40px;color:var(--danger)">❌ 加载失败</div>';
});
}
// 加载资料库
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 = '<div style="padding:40px;text-align:center;color:var(--text-light,#999)">暂无数据</div>';
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 += '<div class="kb-profile-block">';
html += '<div class="kb-profile-hd">' + s.icon + ' ' + escHtml(s.key) + '</div>';
html += '<div class="kb-profile-bd">';
if (typeof marked !== 'undefined') {
html += marked.parse(content);
} else {
html += '<pre>' + escHtml(content) + '</pre>';
}
html += '</div></div>';
});
el.innerHTML = html;
})
.catch(function() {
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败</div>';
});
}
// 加载画像
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 = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败</div>';
return;
}
var data = res.data || {};
var html = '';
// 画像概览
if (data.atoms) {
var atoms = data.atoms;
html += '<div class="kb-profile-block">';
html += '<div class="kb-profile-hd">🧑 个人画像概览</div>';
html += '<div class="kb-profile-bd">';
html += '<table><tr><th style="width:100px">当前关注</th><td>' + escHtml(atoms.注视 || '-') + '</td></tr>';
html += '<tr><th>技能</th><td>' + (Array.isArray(atoms.技能) ? atoms.技能.join('、') : '-') + '</td></tr>';
html += '<tr><th>偏好</th><td>' + (atoms.偏好 ? '语言:' + (atoms.偏好.语言||'中文') : '-') + '</td></tr>';
var goals = atoms.目标 || [];
html += '<tr><th>目标</th><td>' + (goals.length > 0 ? goals.map(function(g) { return g.名称 + '(' + g.状态 + ')'; }).join(';') : '-') + '</td></tr>';
html += '</table></div></div>';
}
// 画像 Markdown
if (data.profile) {
html += '<div class="kb-profile-block"><div class="kb-profile-hd">📝 画像详情</div>';
html += '<div class="kb-profile-bd">';
if (typeof marked !== 'undefined') {
html += marked.parse(data.profile);
} else {
html += '<pre>' + escHtml(data.profile.substring(0, 500)) + '</pre>';
}
html += '</div></div>';
}
el.innerHTML = html || '<div style="padding:40px;text-align:center;color:var(--text-light,#999)">暂无画像数据</div>';
})
.catch(function() {
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败</div>';
});
}
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 = '<div style="text-align:center;padding:40px;color:var(--text-light,#999)">🔍 搜索中...</div>';
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 = '<div style="text-align:center;padding:60px;color:var(--text-light,#999)"><div style="font-size:48px;margin-bottom:12px">🔍</div><div style="font-size:15px">未找到相关结果</div><div style="font-size:13px;margin-top:6px">试试其他关键词</div><div style="margin-top:16px"><a href="javascript:void(0)" class="btn btn-sm" onclick="kbLoadIndex()">← 返回列表</a></div></div>';
return;
}
var html = '<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px"><span style="color:var(--text-light,#999);font-size:13px">找到 <strong>' + res.count + '</strong> 篇相关文章</span><a href="javascript:void(0)" class="btn btn-sm" onclick="kbLoadIndex()">← 返回列表</a></div>';
res.data.forEach(function(item) {
html += '<a href="javascript:void(0)" onclick="kbOpenArticle(\'' + escJs(item.path) + '\')" class="kb-article-item" style="margin:0 12px 4px 12px">';
html += '<div><span class="kb-article-title">📄 ' + escHtml(item.title) + '</span><span class="kb-article-summary">' + escHtml(item.path) + '</span></div>';
html += '</a>';
});
el.innerHTML = html;
});
}
// ─── 计划预测 - 冻结列配置 ───
var planFrozen = true;
var planFrozenFields = ['product_type', 'product_code', 'product_name'];
// 三个Tab各自独立的冻结列设置
if (!window._planViewFrozenFields) {
window._planViewFrozenFields = {
'default': ['product_type', 'product_code', 'product_name'],
'cloud': ['product_type', 'product_code', 'product_name'],
'external': ['product_type', 'product_code', 'product_name']
};
}
var FROZEN_WIDTHS = {
'product_type': 75,
'product_code': 130,
'product_name': 210,
'云仓库存': 90,
'外仓库存': 90,
'云仓昨日销量': 90,
'外仓昨日销量': 90,
'云仓3天平均销量': 100,
'外仓3天平均销量': 100,
'云仓7天平均销量': 100,
'外仓7天平均销量': 100,
'云仓14天平均销量': 100,
'外仓14天平均销量': 100,
'云仓日销量预测': 100,
'外仓日销量预测': 100
};
function loadPage(page) {
const mainContent = document.getElementById('mainContent');
// 更新导航高亮
document.querySelectorAll('.nav-item, .nav-sub-item').forEach(el => el.classList.remove('active'));
document.querySelector(`.nav-item[data-page="${page}"]`)?.classList.add('active');
document.querySelector(`.nav-sub-item[data-page="${page}"]`)?.classList.add('active');
// 自动展开子菜单
document.querySelectorAll('.nav-submenu').forEach(m => m.classList.remove('open'));
document.querySelectorAll('.nav-arrow').forEach(a => a.classList.remove('rotated'));
// 更新库存页面类型变量(成品/包材)
if (page === 'inventory_finished' || page === 'inventory_packaging') {
_invProductType = page === 'inventory_finished' ? '成品' : '包材';
}
var activeSub = document.querySelector(`.nav-sub-item[data-page="${page}"]`);
if (activeSub) {
var submenu = activeSub.closest('.nav-submenu');
if (submenu) {
submenu.classList.add('open');
var arrow = submenu.closest('.nav-group')?.querySelector('.nav-arrow');
if (arrow) arrow.classList.add('rotated');
}
}
// 从 DOM 中移除所有已缓存的页面容器(保持内存缓存),
// 避免跨页面重复 ID 导致 getElementById 定位到错误的元素
var cachedContainers = mainContent.querySelectorAll('.page-container');
cachedContainers.forEach(function(el) {
el.remove();
});
// 如果已有缓存,重新附加到 DOM 并显示
if (window._pageCache[page]) {
var cached = window._pageCache[page];
mainContent.appendChild(cached);
cached.style.display = '';
// 产品基础表/供应商管理:每次切回时清除筛选框记忆并刷新
if (page === 'products') {
['prodCode','prodName','prodSupplier','prodTypeFilter'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
if (typeof loadProducts === 'function') loadProducts(1);
}
if (page === 'suppliers') {
['supCode','supName'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
if (typeof loadSuppliers === 'function') loadSuppliers(1);
}
// 先销毁原有 Chart 实例(canvas detach 后 chart 可能状态错乱)
if (typeof Chart !== 'undefined') {
const canvases = cached.querySelectorAll('canvas');
canvases.forEach(function(c) {
try {
var chart = Chart.getChart(c);
if (chart) { chart.destroy(); }
} catch(e) {}
});
}
// 等浏览器布局完成后重新绘制
if (window._pageDataCache && window._pageDataCache[page]) {
var reloader = window._pageReloaders[page];
if (reloader) {
requestAnimationFrame(function() {
setTimeout(function() {
reloader(window._pageDataCache[page]);
}, 50);
});
}
}
return;
}
// 首次加载:移除初始loading占位,创建容器
const loading = document.getElementById('pageLoading');
if (loading) loading.style.display = 'none';
const container = document.createElement('div');
container.className = 'page-container';
container.id = 'page-container-' + page;
mainContent.appendChild(container);
// 权限检查:无查看权限则显示无权限提示
if (PAGE_LOADERS[page] && !window._canViewPage(page)) {
container.innerHTML = '<div style="text-align:center;padding:80px 20px;color:var(--text-light,#999)"><div style="font-size:60px;margin-bottom:16px">🔒</div><h2 style="color:#999;margin-bottom:8px">无访问权限</h2><p style="font-size:14px">请联系管理员配置权限</p></div>';
window._pageCache[page] = container;
return;
}
// 调用对应加载函数
if (PAGE_LOADERS[page]) {
PAGE_LOADERS[page](container);
}
window._pageCache[page] = container;
}
// ============ SPA 导航系统 ============
// 菜单页面路径 → page key 映射
var PAGE_MAP = {
'/': 'dashboard',
'/data/summary': 'data_summary',
'/data/detail': 'data_detail',
'/inventory/finished': 'inventory_finished',
'/inventory/packaging': 'inventory_packaging',
'/planning': 'planning',
'/products': 'products',
'/bom': 'bom',
'/warehouse': 'warehouse',
'/suppliers': 'suppliers',
'/personnel': 'personnel',
'/purchase': 'purchase',
'/purchase/ready': 'purchase_ready',
'/orders/domestic': 'orders_domestic',
'/orders/overseas': 'orders_overseas',
'/orders/packaging': 'orders_packaging',
'/users': 'users',
'/login-records': 'login_records',
'/logs': 'logs',
'/config': 'config',
'/permissions': 'permissions',
'/knowledge': 'knowledge',
'/feedback': 'feedback',
'/contract-templates': 'contract_templates'
};
// 拦截导航点击,启用 SPA 缓存
// ─── 权限工具函数 ───
// 检查当前用户是否有页面查看权限
window._canViewPage = function(pageId) {
if (!window._currentUser) return true;
if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true;
var perms = window._currentUser.permissions || {};
var pagePerms = perms[pageId] || {};
if (pagePerms['view'] !== undefined) return pagePerms['view'] == 1;
return true;
};
// 检查当前用户是否有某个字段的查看权限
window._canViewField = function(pageId, fieldName) {
if (!window._currentUser) return true;
if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true;
if (!window._canViewPage(pageId)) return false;
var perms = window._currentUser.permissions || {};
var pagePerms = perms[pageId] || {};
var key = 'field:' + fieldName + ':view';
if (pagePerms[key] !== undefined) return pagePerms[key] == 1;
return pagePerms['view'] !== 0;
};
// 检查当前用户是否有某个字段的编辑权限
window._canEditField = function(pageId, fieldName) {
if (!window._currentUser) return true;
if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true;
if (!window._canViewPage(pageId)) return false;
var perms = window._currentUser.permissions || {};
var pagePerms = perms[pageId] || {};
var key = 'field:' + fieldName + ':edit';
if (pagePerms[key] !== undefined) return pagePerms[key] == 1;
return pagePerms['edit'] !== 0;
};
// 检查当前用户是否有页面功能权限
window._hasPagePerm = function(pageId, op) {
if (!window._currentUser) return true;
if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return true;
if (!window._canViewPage(pageId)) return false;
var perms = window._currentUser.permissions || {};
var pagePerms = perms[pageId] || {};
if (pagePerms[op] !== undefined) return pagePerms[op] == 1;
return false; // 没设时无权限(由角色级权限兜底)
};
// 隐藏无权限的菜单
window._hideNoPermNavItems = function() {
try {
if (!window._currentUser) return;
if (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin') return;
// 页面→导航菜单链接映射(包括子菜单nav-sub-item和顶层菜单nav-item)
var pageNavMap = [
{page:'dashboard', sel:'a.nav-item[href="/dashboard"]', sub:false},
{page:'data_summary', sel:'a.nav-sub-item[href="/data/summary"]', sub:true},
{page:'data_detail', sel:'a.nav-sub-item[href="/data/detail"]', sub:true},
{page:'inventory_finished', sel:'a.nav-sub-item[href="/inventory/finished"]', sub:true},
{page:'inventory_packaging', sel:'a.nav-sub-item[href="/inventory/packaging"]', sub:true},
{page:'planning', sel:'a.nav-sub-item[href="/planning"]', sub:true},
{page:'purchase', sel:'a.nav-sub-item[href="/purchase"]', sub:true},
{page:'purchase_ready', sel:'a.nav-sub-item[href="/purchase/ready"]', sub:true},
{page:'orders_domestic', sel:'a.nav-sub-item[href="/orders/domestic"]', sub:true},
{page:'orders_overseas', sel:'a.nav-sub-item[href="/orders/overseas"]', sub:true},
{page:'orders_packaging', sel:'a.nav-sub-item[href="/orders/packaging"]', sub:true},
{page:'orders_finished', sel:'a.nav-sub-item[href="/orders/finished"]', sub:true},
{page:'feedback', sel:'a.nav-item[href="/feedback"]', sub:false},
{page:'products', sel:'a.nav-sub-item[href="/products"]', sub:true},
{page:'bom', sel:'a.nav-sub-item[href="/bom"]', sub:true},
{page:'warehouse', sel:'a.nav-sub-item[href="/warehouse"]', sub:true},
{page:'suppliers', sel:'a.nav-sub-item[href="/suppliers"]', sub:true},
{page:'personnel', sel:'a.nav-sub-item[href="/personnel"]', sub:true},
{page:'knowledge', sel:'a.nav-item[href="/knowledge"]', sub:false},
{page:'contract_templates', sel:'a.nav-item[href="/contract-templates"]', sub:false},
{page:'users', sel:'a.nav-sub-item[href="/users"]', sub:true},
{page:'permissions', sel:'a.nav-sub-item[href="/permissions"]', sub:true},
{page:'login_records', sel:'a.nav-sub-item[href="/login-records"]', sub:true},
{page:'logs', sel:'a.nav-sub-item[href="/logs"]', sub:true},
{page:'config', sel:'a.nav-sub-item[href="/config"]', sub:true},
{page:'config_calibrate', sel:'a.nav-sub-item[href="/config-calibrate"]', sub:true}
];
// 收集需要检查的菜单组(子菜单用)
var processedSubmenus = {};
pageNavMap.forEach(function(item) {
if (!window._canViewPage(item.page)) {
var el = document.querySelector(item.sel);
if (el) {
if (item.sub) {
var submenu = el.closest('.nav-submenu');
if (submenu) {
processedSubmenus[submenu.id] = (processedSubmenus[submenu.id] || 0) + 1;
el.remove();
}
} else {
el.remove();
}
}
}
});
// 检查并移除空的菜单组(排除已被 nav-hidden 隐藏的项)
Object.keys(processedSubmenus).forEach(function(gid) {
var sm = document.getElementById(gid);
if (!sm) return;
var visible = sm.querySelectorAll('a.nav-sub-item:not(.nav-hidden)');
if (visible.length === 0) {
sm.remove();
var gKey = gid.replace('submenu-', '');
var gh = document.querySelector('.nav-group-header[data-group="' + gKey + '"]');
if (gh) gh.remove();
}
});
} catch(e) {}
};
// 加载当前用户权限配置(权限已由后端模板同步注入,此处仅做兜底)
window._loadUserPerms = function() {
if (!window._currentUser) return;
if (!window._currentUser.permissions) window._currentUser.permissions = {};
};
// ─── 表格列权限过滤工具 ───
// 根据权限过滤列定义,只保留有查看权限的列
window._filterColsByPerm = function(pageId, columns, labelField) {
return columns.filter(function(c) {
var label = labelField ? c[labelField] : c.label || c.field || c;
return window._canViewField(pageId, label);
});
};
// 检查某列是否有查看权限(用于 colspan 等计算)
window._colHasViewPerm = function(pageId, label) {
return window._canViewField(pageId, label);
};
function initNavigation() {
// 加载当前用户权限
window._loadUserPerms();
// 隐藏无权限的导航菜单
try { window._hideNoPermNavItems(); } catch(e) {}
// DOM可能未完全解析,延迟再试一次
setTimeout(function() {
try { window._hideNoPermNavItems(); } catch(e) {}
}, 300);
document.addEventListener('click', function(e) {
var link = e.target.closest('.nav-item, .nav-sub-item');
if (!link) return;
var page = link.getAttribute('data-page');
if (!page) return;
e.preventDefault();
var href = link.getAttribute('href') || '/';
history.pushState({page: page}, '', href);
loadPage(page);
});
// 浏览器后退/前进
window.addEventListener('popstate', function(e) {
var state = e.state;
if (state && state.page) {
loadPage(state.page);
} else {
// 首次加载或状态丢失,从当前 URL 推断
var path = window.location.pathname;
var p = PAGE_MAP[path];
if (p) loadPage(p);
}
});
}
// 延迟初始化,确保 DOM 加载完成后再绑定事件
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNavigation);
} else {
initNavigation();
}
// ─── 工具函数 ───
function escJs(str) { return String(str).replace(/'/g, "\\'").replace(/\\/g, '\\\\').replace(/"/g, '&quot;'); }
function escHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 = `
<style>
.fb-header { display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px; }
.fb-tabs { display:flex;gap:4px;background:var(--bg-card,#f7f8fa);padding:4px;border-radius:10px; }
.fb-tab { padding:8px 20px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text-light,#888);border-radius:8px;transition:all .25s;user-select:none; }
.fb-tab:hover { color:var(--text,#333);background:rgba(0,0,0,0.04); }
.fb-tab.active { color:#1890ff;background:#e6f7ff;box-shadow:0 1px 4px rgba(24,144,255,0.12); }
.fb-grid { display:grid;grid-template-columns:1fr 1fr;gap:16px; }
@media (max-width:1100px) { .fb-grid { grid-template-columns:1fr; } }
.fb-list { background:var(--bg-card,#fff);border:1px solid var(--border,#e8e8e8);border-radius:8px;overflow:hidden; }
.fb-list-header { padding:12px 16px;background:#f9f9f9;border-bottom:1px solid var(--border,#e8e8e8);font-weight:600;font-size:14px; }
.fb-item { padding:14px 16px;border-bottom:1px solid var(--border,#f0f0f0);cursor:pointer;transition:background .12s; }
.fb-item:hover { background:#f5f7fa; }
.fb-item.selected { background:#e6f7ff;border-left:3px solid #1890ff; }
.fb-item-title { font-weight:600;font-size:14px;margin-bottom:4px; }
.fb-item-meta { display:flex;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--text-light,#999); }
.fb-badge { display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600; }
.fb-badge-pending { background:#fff7e6;color:#fa8c16; }
.fb-badge-resolved { background:#f6ffed;color:#52c41a; }
.fb-badge-urgent { background:#fff1f0;color:#f5222d; }
.fb-badge-high { background:#fff7e6;color:#fa8c16; }
.fb-badge-medium { background:#e6f7ff;color:#1890ff; }
.fb-badge-low { background:#f5f5f5;color:#999; }
.fb-detail { background:var(--bg-card,#fff);border:1px solid var(--border,#e8e8e8);border-radius:8px;overflow:hidden; }
.fb-detail-empty { padding:40px 20px;text-align:center;color:var(--text-light,#bbb);font-size:14px; }
.fb-detail-body { padding:20px; }
.fb-detail-title { font-size:18px;font-weight:700;margin-bottom:12px; }
.fb-detail-meta { display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px;font-size:13px;color:var(--text-light,#888); }
.fb-detail-desc { font-size:14px;line-height:1.8;color:#333;margin-bottom:16px;white-space:pre-wrap; }
.fb-attachments { display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px; }
.fb-attach-item { display:flex;align-items:center;gap:6px;padding:6px 12px;background:#f5f5f5;border-radius:6px;font-size:12px;cursor:pointer;text-decoration:none;color:#333; }
.fb-attach-item:hover { background:#e6f7ff; }
.fb-attach-broken { color:#fa8c16 !important; background:#fff7e6 !important; }
.fb-attach-broken:hover { background:#fff1cc !important; }
.fb-attach-img { width:100px;height:100px;object-fit:cover;border-radius:6px;cursor:pointer;border:1px solid #eee;transition:transform .15s; }
.fb-attach-img:hover { transform:scale(1.05);box-shadow:0 2px 8px rgba(0,0,0,0.12); }
.fb-attach-del { position:absolute;top:-6px;right:-6px;background:#f5222d;color:#fff;border-radius:50%;width:20px;height:20px;font-size:12px;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0.8;transition:opacity .2s;z-index:10; }
.fb-attach-del:hover { opacity:1; }
.fb-preview-overlay { position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.75);z-index:20000;display:flex;align-items:center;justify-content:center;cursor:pointer; }
.fb-preview-overlay img { max-width:90vw;max-height:90vh;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,0.3); }
.fb-preview-close { position:fixed;top:20px;right:30px;font-size:28px;color:#fff;background:rgba(0,0,0,0.4);border:none;width:44px;height:44px;border-radius:50%;cursor:pointer;z-index:20001;display:flex;align-items:center;justify-content:center;transition:background .2s; }
.fb-preview-close:hover { background:rgba(0,0,0,0.7); }
.fb-form-overlay { position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:15000;display:flex;align-items:center;justify-content:center; }
.fb-form-box { background:#fff;border-radius:12px;padding:24px;width:540px;max-width:94vw;max-height:90vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.18); }
.fb-form-box h2 { margin:0 0 16px 0;font-size:18px; }
.fb-form-row { margin-bottom:14px; }
.fb-form-row label { display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:#555; }
.fb-form-row input,.fb-form-row select,.fb-form-row textarea { width:100%;box-sizing:border-box;padding:8px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;outline:none;transition:border .2s; }
.fb-form-row input:focus,.fb-form-row select:focus,.fb-form-row textarea:focus { border-color:#1890ff;box-shadow:0 0 0 2px rgba(24,144,255,0.1); }
.fb-form-row textarea { min-height:100px;resize:vertical; }
.fb-form-actions { display:flex;justify-content:flex-end;gap:10px;margin-top:6px; }
.fb-file-list { display:flex;flex-wrap:wrap;gap:8px;margin-top:8px; }
.fb-file-tag { display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:#f5f5f5;border-radius:4px;font-size:12px; }
.fb-file-tag .remove { cursor:pointer;color:#999;font-size:14px; }
.fb-file-tag .remove:hover { color:#f5222d; }
.fb-pagination { display:flex;justify-content:center;gap:4px;padding:12px 0; }
.fb-page-btn { padding:4px 12px;border:1px solid #d9d9d9;border-radius:4px;font-size:12px;cursor:pointer;background:#fff; }
.fb-page-btn:hover { border-color:#1890ff;color:#1890ff; }
.fb-page-btn.active { background:#1890ff;color:#fff;border-color:#1890ff; }
.fb-page-btn:disabled { opacity:0.4;cursor:default; }
.fb-dropzone { border:2px dashed #d9d9d9;border-radius:8px;padding:20px;text-align:center;cursor:pointer;transition:all .2s; }
.fb-dropzone:hover,.fb-dropzone.dragover { border-color:#1890ff;background:#f0f8ff; }
</style>
<div class="page-header">
<h1>🐛 测试问题</h1>
<p class="page-desc">内部问题反馈与优化建议收集</p>
</div>
<div class="fb-header">
<div class="fb-tabs" id="fbTabs">
<div class="fb-tab active" data-status="" onclick="switchFbTab('')">全部</div>
<div class="fb-tab" data-status="pending" onclick="switchFbTab('pending')">待处理</div>
<div class="fb-tab" data-status="resolved" onclick="switchFbTab('resolved')">已完成</div>
</div>
<div style="display:flex;gap:8px;align-items:center">
<select id="fbCategory" class="form-input" style="width:auto;min-width:100px;height:32px;font-size:13px" onchange="window._fbCategory=this.value;window._fbPage=1;loadFbList()">
<option value="">全部分类</option>
<option value="功能问题">功能问题</option>
<option value="数据错误">数据错误</option>
<option value="界面优化">界面优化</option>
<option value="性能问题">性能问题</option>
<option value="新增需求">新增需求</option>
<option value="其他">其他</option>
</select>
<select id="fbSort" class="form-input" style="width:auto;min-width:100px;height:32px;font-size:13px" onchange="window._fbSort=this.value;window._fbPage=1;loadFbList()">
<option value="default">默认排序</option>
<option value="priority">按紧急程度</option>
</select>
<input type="text" id="fbSearch" class="form-input" style="width:200px" placeholder="🔍 搜索标题或描述" onkeydown="if(event.key==='Enter'){window._fbSearch=this.value;window._fbPage=1;loadFbList();}">
<button class="btn btn-outline" onclick="var s=document.getElementById('fbSearch');s.value='';window._fbSearch='';window._fbPage=1;loadFbList()">清除</button>
<button class="btn btn-primary" onclick="showFbForm()">+ 新增反馈</button>
</div>
</div>
<div class="fb-grid">
<div class="fb-list" id="fbList">
<div class="fb-list-header">反馈列表 (<span id="fbTotal">0</span>)</div>
<div id="fbItems" style="min-height:200px"></div>
<div class="fb-pagination" id="fbPagination"></div>
</div>
<div class="fb-detail" id="fbDetail">
<div class="fb-detail-empty">👈 点击左侧反馈查看详情</div>
</div>
</div>
`;
window._fbStatus = '';
window._fbPage = 1;
window._fbSort = 'default';
window._fbCategory = '';
window._fbSelected = null;
loadFbList();
}
// ─── 反馈列表加载(旧版,由 feedback.js 新版覆盖) ───
function loadFbList() {
if (typeof loadFbActiveList === 'function') { loadFbActiveList(); return; }
var status = window._fbStatus || '';
var page = window._fbPage || 1;
var search = (window._fbSearch || '').trim();
var sort = window._fbSort || 'default';
var category = window._fbCategory || '';
var url = '/api/feedback/list?tab=list&page=' + page + '&size=10&status=' + status + '&sort=' + sort;
if (category) url += '&category=' + encodeURIComponent(category);
if (search) url += '&search=' + encodeURIComponent(search);
fetch(url)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var data = res.data;
document.getElementById('fbTotal').textContent = data.total;
var html = '';
if (data.rows.length === 0) {
html = '<div style="padding:40px;text-align:center;color:#bbb;font-size:14px">暂无反馈</div>';
} 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 += '<div class="fb-item' + selected + '" onclick="selectFbItem(' + item.id + ')">';
html += '<div class="fb-item-title">' + escHtml(item.title) + '</div>';
html += '<div class="fb-item-meta">';
html += '<span class="fb-badge ' + statusClass + '">' + statusText + '</span>';
html += '<span class="fb-badge ' + priorityClass + '">' + escHtml(item.priority) + '</span>';
html += '<span>' + escHtml(item.category) + '</span>';
html += '<span>' + escHtml(item.user_name) + '</span>';
html += '<span>' + (item.created_at || '') + '</span>';
html += '</div></div>';
});
}
document.getElementById('fbItems').innerHTML = html;
// 分页
var totalPages = Math.ceil(data.total / data.size);
var pgHtml = '';
if (totalPages > 1) {
pgHtml += '<button class="fb-page-btn" onclick="fbGoPage(1)" ' + (page <= 1 ? 'disabled' : '') + '>首页</button>';
pgHtml += '<button class="fb-page-btn" onclick="fbGoPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>‹</button>';
var start = Math.max(1, page - 2);
var end = Math.min(totalPages, page + 2);
for (var i = start; i <= end; i++) {
pgHtml += '<button class="fb-page-btn' + (i === page ? ' active' : '') + '" onclick="fbGoPage(' + i + ')">' + i + '</button>';
}
pgHtml += '<button class="fb-page-btn" onclick="fbGoPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>›</button>';
pgHtml += '<button class="fb-page-btn" onclick="fbGoPage(' + totalPages + ')" ' + (page >= totalPages ? 'disabled' : '') + '>末页</button>';
}
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 = '<div class="fb-detail-empty">👈 点击左侧反馈查看详情</div>';
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 = '<div class="fb-detail-empty">👈 点击左侧反馈查看详情</div>';
return;
}
var statusClass = item.status === 'resolved' ? 'fb-badge-resolved' : (item.status === 'rejected' ? 'fb-badge-pending' : 'fb-badge-pending');
var statusText = item.status === 'resolved' ? '✅ 已完成' : (item.status === 'rejected' ? '❌ 已驳回' : '⏳ 待处理');
var priorityClass = item.priority === '紧急' ? 'fb-badge-urgent' : (item.priority === '高' ? 'fb-badge-high' : (item.priority === '低' ? 'fb-badge-low' : 'fb-badge-medium'));
var attachHtml = '';
var canDelete = item.user_id === (window._currentUser && window._currentUser.id) ||
(window._currentUser && (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin'));
if (item.attachments && item.attachments.length > 0) {
attachHtml = '<div class="fb-attachments" style="align-items:flex-start">';
item.attachments.forEach(function(a) {
var isImage = ['png','jpg','jpeg','gif','webp','bmp','svg'].indexOf(a.type) >= 0;
var delBtn = canDelete ? '<span class="fb-attach-del" onclick="fbConfirmDelAttach(' + item.id + ',\'' + a.path + '\')" title="删除此附件">🗑️</span>' : '';
if (isImage) {
var imgSrc = a.path.startsWith('/static/') ? a.path : a.path.startsWith('/') ? '/static' + a.path : '/static/' + a.path;
var fname = escHtml(a.name);
attachHtml += '<div style="position:relative;display:inline-block"><img class="fb-attach-img" src="' + imgSrc + '" title="' + fname + '" onclick="previewFbImage(\'' + imgSrc + '\')" onerror="fbImgError(this)" data-fb-img-src="' + imgSrc + '" data-fb-img-name="' + fname + '">' + delBtn + '</div>';
} else {
var sizeStr = a.size > 1024*1024 ? (a.size/1024/1024).toFixed(1)+'MB' : (a.size/1024).toFixed(1)+'KB';
var fileHref = a.path.startsWith('/static/') ? a.path : a.path.startsWith('/') ? '/static' + a.path : '/static/' + a.path;
attachHtml += '<div style="display:inline-flex;align-items:center;gap:4px">' + delBtn + '<a class="fb-attach-item" href="' + fileHref + '" target="_blank">📎 ' + escHtml(a.name) + ' (' + sizeStr + ')</a></div>';
}
});
attachHtml += '</div>';
}
var actionsHtml = '';
var isAdmin = window._currentUser && (window._currentUser.role === 'admin' || window._currentUser.role === 'super_admin');
var isSuperAdmin = window._currentUser && window._currentUser.role === 'super_admin';
// 反馈问题(tab=list):待处理=标记完成+删除,已完成=重新打开+删除
if (window._fbMainTab === 'list') {
if (isAdmin) {
if (item.status === 'active') {
actionsHtml += '<button class="btn btn-success" onclick="fbConfirmResolve(' + item.id + ')">✅ 标记完成</button>';
} else if (item.status === 'resolved') {
actionsHtml += '<button class="btn btn-outline" onclick="fbUnresolve(' + item.id + ')">🔄 重新打开</button>';
}
}
if (isSuperAdmin) {
actionsHtml += '<button class="btn btn-outline" style="color:#f5222d;border-color:#f5222d" onclick="fbConfirmDelete(' + item.id + ')">🗑️ 删除</button>';
}
}
var resolvedInfo = '';
if (item.status === 'resolved' && item.resolved_at) {
resolvedInfo = '<div style="margin-top:12px;padding:10px 14px;background:#f6ffed;border-radius:6px;font-size:13px;color:#52c41a">✅ 已于 ' + escHtml(item.resolved_at) + ' 修改完成</div>';
}
var rejectInfo = '';
if (item.status === 'rejected' && item.reject_reason) {
rejectInfo = '<div style="margin-top:12px;padding:10px 14px;background:#fff2f0;border-radius:6px;font-size:13px;color:#f5222d">❌ 驳回理由:' + escHtml(item.reject_reason) + '</div>';
}
document.getElementById('fbDetail').innerHTML = '<div class="fb-detail-body"><div class="fb-detail-title">' + escHtml(item.title) + '</div><div class="fb-detail-meta"><span class="fb-badge ' + statusClass + '">' + statusText + '</span><span class="fb-badge ' + priorityClass + '">' + escHtml(item.priority) + '</span><span>' + escHtml(item.category) + '</span><span>' + escHtml(item.user_name) + '</span><span>' + (item.created_at || '') + '</span></div><div class="fb-detail-desc">' + escHtml(item.description || '暂无详细描述') + '</div>' + attachHtml + '<div style="display:flex;gap:10px">' + actionsHtml + '</div>' + resolvedInfo + '</div>';
}
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 = '<img src="' + imgSrc + '"><button class="fb-preview-close" onclick="this.parentElement.remove()">✕</button>';
overlay.onclick = function(e) { if (e.target === overlay) {} };
document.body.appendChild(overlay);
}
// 反馈附件图片加载失败时替换为文件链接
function fbImgError(el) {
var src = el.getAttribute('data-fb-img-src') || el.src;
var name = el.getAttribute('data-fb-img-name') || '文件';
var link = document.createElement('a');
link.className = 'fb-attach-item fb-attach-broken';
link.href = src;
link.target = '_blank';
link.title = '图片文件不存在';
link.textContent = '📎 ' + name + ' (文件缺失)';
el.parentNode.replaceChild(link, el);
}
// 附件删除确认弹窗
function fbConfirmDelAttach(id, filePath) {
var overlay = document.createElement('div');
overlay.className = 'fb-form-overlay';
overlay.style.zIndex = '30000';
overlay.innerHTML = '<div class="fb-form-box" style="width:400px;text-align:center">' +
'<h2>🗑️ 确认删除附件</h2>' +
'<p style="color:#666;margin:16px 0 20px;font-size:14px">确认删除此附件?<br>删除后不可恢复。</p>' +
'<div class="fb-form-actions" style="justify-content:center;gap:12px">' +
'<button class="btn btn-outline" onclick="this.closest(\'.fb-form-overlay\').remove()">取消</button>' +
'<button class="btn" style="background:#f5222d;color:#fff;border:none;border-radius:6px;padding:6px 20px;font-size:13px;cursor:pointer" onclick="doFbDelAttach(' + id + ',\'' + filePath + '\')">确认删除</button>' +
'</div></div>';
overlay.onclick = function(e) { if (e.target === overlay) {} };
document.body.appendChild(overlay);
}
function doFbDelAttach(id, filePath) {
var overlays = document.querySelectorAll('.fb-form-overlay');
overlays.forEach(function(o) { o.remove(); });
fetch('/api/feedback/delete_attachment', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, path: filePath})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
// 刷新当前反馈详情
if (window._fbSelected && window._fbSelected.id === id) {
fetch('/api/feedback/detail?id=' + id)
.then(function(r) { return r.json(); })
.then(function(res2) {
if (res2.code === 0 && res2.data) {
window._fbSelected = res2.data;
showFbDetail();
}
});
}
if (window._fbMainTab === 'review') loadFbReviewList();
else loadFbActiveList();
} else {
alert(res.msg || '操作失败');
}
});
}
// ─── 新增/编辑反馈表单 ───
function showFbForm(item) {
var isEdit = item && item.id;
var overlay = document.createElement('div');
overlay.className = 'fb-form-overlay';
overlay.id = 'fbFormOverlay';
var titleText = isEdit ? '✏️ 编辑反馈' : '📝 提交反馈';
var btnText = isEdit ? '保存修改' : '提交反馈';
overlay.innerHTML = '<div class="fb-form-box" onclick="event.stopPropagation()"><span id="fbEditId" style="display:none">' + (item ? item.id : '') + '</span><h2>' + titleText + '</h2><div class="fb-form-row"><label>标题 *</label><input id="fbFormTitle" placeholder="请用一句话概括问题或建议" maxlength="200" value="' + escHtml(item ? item.title : '') + '"></div><div style="display:flex;gap:12px"><div class="fb-form-row" style="flex:1"><label>分类</label><select id="fbFormCategory"><option value="功能问题">功能问题</option><option value="数据错误">数据错误</option><option value="界面优化">界面优化</option><option value="性能问题">性能问题</option><option value="新增需求">新增需求</option><option value="其他">其他</option></select></div><div class="fb-form-row" style="flex:1"><label>紧急程度</label><select id="fbFormPriority"><option value="紧急">紧急</option><option value="高">高</option><option value="中">中</option><option value="低">低</option></select></div></div><div class="fb-form-row"><label>详细描述</label><textarea id="fbFormDesc" placeholder="请详细描述问题现象或建议内容...">' + escHtml(item ? item.description : '') + '</textarea></div><div class="fb-form-row"><label>附件(支持图片、文档、压缩包等,编辑时重新上传会追加)</label><div class="fb-dropzone" id="fbDropzone" onclick="document.getElementById(&#39;fbFileInput&#39;).click()" ondrop="handleFbDrop(event)" ondragover="event.preventDefault();this.classList.add(&#39;dragover&#39;)" ondragleave="this.classList.remove(&#39;dragover&#39;)">📎 点击选择文件,或将文件拖拽到此处</div><input type="file" id="fbFileInput" multiple style="display:none" onchange="handleFbFileSelect(event)"><div class="fb-file-list" id="fbFileList"></div></div><div class="fb-form-actions"><button class="btn btn-outline" onclick="closeFbForm()">取消</button><button class="btn btn-primary" onclick="submitFbForm()">' + btnText + '</button></div></div>';
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 += '<div class="fb-file-tag">📎 ' + escHtml(a.name) + '</div>';
});
document.getElementById('fbFileList').innerHTML = '<div style="font-size:12px;color:#999;margin-bottom:4px">已有附件:</div>' + 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 ? '<img src="' + URL.createObjectURL(f) + '" style="width:32px;height:32px;object-fit:cover;border-radius:4px">' : '📄';
html += '<div class="fb-file-tag">' + preview + ' ' + escHtml(f.name) + ' <span class="remove" onclick="removeFbFile(' + i + ')">✕</span></div>';
}
}
document.getElementById('fbFileList').innerHTML = html;
}
function submitFbForm() {
var title = document.getElementById('fbFormTitle').value.trim();
if (!title) { alert('请输入标题'); return; }
var desc = document.getElementById('fbFormDesc').value.trim();
var category = document.getElementById('fbFormCategory').value;
var priority = document.getElementById('fbFormPriority').value;
var editId = document.getElementById('fbEditId') ? document.getElementById('fbEditId').textContent.trim() : '';
var formData = new FormData();
formData.append('title', title);
formData.append('description', desc);
formData.append('category', category);
formData.append('priority', priority);
if (editId) {
formData.append('id', editId);
}
if (window._fbFiles) {
for (var i = 0; i < window._fbFiles.length; i++) {
formData.append('files', window._fbFiles[i]);
}
}
var btn = document.querySelector('#fbFormOverlay .btn-primary');
btn.disabled = true;
btn.textContent = '提交中...';
var url = editId ? '/api/feedback/update' : '/api/feedback/submit';
fetch(url, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
closeFbForm();
window._fbSelected = null;
if (typeof window._fbOnSave === 'function') {
var cb = window._fbOnSave;
window._fbOnSave = null;
cb();
} else {
loadFbList();
}
} else {
alert(res.msg || '提交失败');
btn.disabled = false;
btn.textContent = editId ? '保存修改' : '提交反馈';
}
})
.catch(function() {
alert('提交失败,请重试');
btn.disabled = false;
btn.textContent = editId ? '保存修改' : '提交反馈';
});
}
// ─── 标记完成/重新打开 ───
function resolveFbItem(id) {
if (!confirm('确认将此反馈标记为已完成?')) return;
fetch('/api/feedback/resolve', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
loadFbList();
} else {
alert(res.msg || '操作失败');
}
});
}
function unresolveFbItem(id) {
if (!confirm('确认重新打开此反馈?')) return;
fetch('/api/feedback/unresolve', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
loadFbList();
} else {
alert(res.msg || '操作失败');
}
});
}
// ─── 删除反馈 ───
function deleteFbItem(id) {
if (!confirm('确认删除此反馈?删除后不可恢复。')) return;
fetch('/api/feedback/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
window._fbSelected = null;
document.getElementById('fbDetail').innerHTML = '<div class="fb-detail-empty">👈 点击左侧反馈查看详情</div>';
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 = `
<style>
.ct-header { display:flex;justify-content:space-between;align-items:center;margin-bottom:16px; }
.ct-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px; }
.ct-card { background:var(--bg-card,#fff);border:1px solid var(--border,#e8e8e8);border-radius:8px;overflow:hidden; }
.ct-card-body { padding:16px; }
.ct-card-title { font-weight:600;font-size:15px;margin-bottom:6px; }
.ct-card-meta { font-size:12px;color:var(--text-light,#999);margin-bottom:10px; }
.ct-card-actions { display:flex;gap:8px; }
.ct-empty { padding:40px;text-align:center;color:var(--text-light,#bbb);font-size:14px; }
.ct-form-overlay { position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:15000;display:flex;align-items:center;justify-content:center; }
.ct-form-box { background:#fff;border-radius:12px;padding:24px;width:500px;max-width:94vw;box-shadow:0 8px 32px rgba(0,0,0,0.18); }
.ct-form-box h2 { margin:0 0 16px 0;font-size:18px; }
.ct-form-row { margin-bottom:14px; }
.ct-form-row label { display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:#555; }
.ct-form-row input,.ct-form-row select { width:100%;box-sizing:border-box;padding:8px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;outline:none; }
.ct-form-row input[type="file"] { padding:6px 12px; }
.ct-supplier-option { padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid #f5f5f5; }
.ct-supplier-option:hover { background:#e6f7ff; }
.ct-form-actions { display:flex;justify-content:flex-end;gap:10px;margin-top:6px; }
</style>
<div class="page-header">
<h1>📄 合同模板</h1>
<p class="page-desc">上传和管理采购合同模板</p>
</div>
<div class="ct-header">
<div></div>
<button class="btn btn-primary" onclick="showCtUploadForm()">+ 上传模板</button>
</div>
<div id="ctList" class="ct-grid"></div>
`;
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 = '<div class="ct-empty">暂无模板,请点击右上角上传</div>';
return;
}
var html = '';
res.data.forEach(function(t) {
var typeMap = {'domestic':'国内订单','overseas':'海外订单','packaging':'包材订单','all':'全部类型'};
var boundTag = '';
if (t.bound_supplier_name) {
boundTag = '<span style="display:inline-block;background:#fff7e6;border:1px solid #ffd591;border-radius:4px;padding:0 8px;color:#d46b08;font-size:12px;margin-left:8px;">🔗 ' + escHtml(t.bound_supplier_name) + '</span>';
} else {
boundTag = '<span style="display:inline-block;background:#f6f6f6;border:1px solid #d9d9d9;border-radius:4px;padding:0 8px;color:#888;font-size:12px;margin-left:8px;">通用</span>';
}
html += '<div class="ct-card"><div class="ct-card-body">';
html += '<div class="ct-card-title">' + escHtml(t.name) + boundTag + '</div>';
html += '<div class="ct-card-meta">' + (typeMap[t.order_type] || t.order_type) + ' | 文件: ' + escHtml(t.file_name || '') + ' | ' + (t.created_at || '') + '</div>';
html += '<div class="ct-card-actions"><button class="btn btn-sm btn-outline" style="color:#722ed1;border-color:#722ed1" onclick="previewCtTemplate(' + t.id + ')">👁️ 预览</button><button class="btn btn-sm btn-outline" style="color:#52c41a;border-color:#52c41a" onclick="downloadContractFile(\'/api/contract/template/download?id=' + t.id + '\',\'' + escHtml(t.name) + '.xlsx\')">📥 下载</button><button class="btn btn-sm btn-outline" style="color:#1890ff;border-color:#1890ff" onclick="editCtTemplate(' + t.id + ')">编辑</button><button class="btn btn-sm btn-outline" style="color:#f5222d;border-color:#f5222d" onclick="deleteCtTemplate(' + t.id + ')">删除</button></div>';
html += '</div></div>';
});
list.innerHTML = html;
});
}
function showCtUploadForm() {
var overlay = document.createElement('div');
overlay.className = 'ct-form-overlay';
overlay.id = 'ctFormOverlay';
overlay.innerHTML = '<div class="ct-form-box" onclick="event.stopPropagation()"><h2>📤 上传合同模板</h2><div class="ct-form-row"><label>模板名称</label><input id="ctFormName" placeholder="如:标准采购合同"></div><div class="ct-form-row"><label>适用类型</label><select id="ctFormType"><option value="all">全部类型</option><option value="domestic">国内订单</option><option value="overseas">海外订单</option><option value="packaging">包材订单</option></select></div><div class="ct-form-row"><label>模板属性</label><select id="ctFormAttr" class="form-input" onchange="toggleCtUploadSupplier(this.value)"><option value="universal" selected>通用模板</option><option value="bound">特定模板</option></select></div>'
+ '<div id="ctUploadSupplierArea" style="display:none;">'
+ '<div class="ct-form-row"><label>绑定供应商</label><div style="display:flex;gap:6px;"><input id="ctSupplierSearch" class="form-input" placeholder="输入供应商名称搜索..." style="flex:1;"><button class="btn btn-sm btn-outline" onclick="clearCtSupplierBind()" style="white-space:nowrap;">清除</button></div>'
+ '<div id="ctSupplierSuggestions" style="max-height:180px;overflow-y:auto;border:1px solid #e8e8e8;border-radius:6px;margin-top:4px;display:none;"></div><div id="ctSupplierTag" style="margin-top:4px;"></div>'
+ '<input type="hidden" id="ctSupplierId" value=""></div></div>'
+ '<div class="ct-form-row"><label>模板文件(.xlsx)</label><input type="file" id="ctFormFile" accept=".xlsx"></div>'
+ '<div class="ct-form-actions"><button class="btn btn-outline" onclick="closeCtForm()">取消</button><button class="btn btn-primary" onclick="submitCtUpload()">上传</button></div></div>';
overlay.onclick = function(e) { if (e.target === overlay) {} };
document.body.appendChild(overlay);
// 供应商搜索绑定
document.getElementById('ctSupplierSearch').addEventListener('input', function() {
var q = this.value.trim();
var suggestions = document.getElementById('ctSupplierSuggestions');
if (q.length < 1) { suggestions.style.display = 'none'; return; }
fetch('/api/suppliers/list?search=' + encodeURIComponent(q) + '&size=20')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0 || !res.data || !res.data.rows || res.data.rows.length === 0) {
suggestions.innerHTML = '<div style="padding:10px;color:#999;">无匹配供应商</div>';
suggestions.style.display = 'block';
return;
}
var h = '';
res.data.rows.forEach(function(s) {
h += '<div class="ct-supplier-option" data-id="' + s.id + '" data-name="' + escHtml(s.supplier_name) + '" onclick="selectCtSupplier(this)">' + escHtml(s.supplier_name) + ' <span style="color:#999;">' + escHtml(s.supplier_code || '') + '</span></div>';
});
suggestions.innerHTML = h;
suggestions.style.display = 'block';
});
});
}
function toggleCtUploadSupplier(val) {
var area = document.getElementById('ctUploadSupplierArea');
if (area) {
area.style.display = (val === 'bound') ? '' : 'none';
}
}
function selectCtSupplier(el) {
document.getElementById('ctSupplierId').value = el.getAttribute('data-id');
document.getElementById('ctSupplierSearch').value = el.getAttribute('data-name');
document.getElementById('ctSupplierSuggestions').style.display = 'none';
document.getElementById('ctSupplierTag').innerHTML = '<span style="display:inline-block;background:#e6f7ff;border:1px solid #91d5ff;border-radius:4px;padding:2px 10px;color:#1890ff;font-size:13px;">📌 绑定:' + escHtml(el.getAttribute('data-name')) + '</span>';
}
function clearCtSupplierBind() {
document.getElementById('ctSupplierId').value = '';
document.getElementById('ctSupplierSearch').value = '';
document.getElementById('ctSupplierSuggestions').style.display = 'none';
document.getElementById('ctSupplierTag').innerHTML = '';
}
function closeCtForm() {
var el = document.getElementById('ctFormOverlay');
if (el) el.remove();
}
function submitCtUpload() {
var name = document.getElementById('ctFormName').value.trim();
var type = document.getElementById('ctFormType').value;
var attr = document.getElementById('ctFormAttr').value;
var supplierId = document.getElementById('ctSupplierId').value;
var fileInput = document.getElementById('ctFormFile');
if (!name) { alert('请输入模板名称'); return; }
if (attr === 'bound' && !supplierId) { alert('请选择要绑定的供应商'); return; }
if (!fileInput.files || !fileInput.files[0]) { alert('请选择文件'); return; }
var formData = new FormData();
formData.append('name', name);
formData.append('order_type', type);
formData.append('supplier_id', attr === 'bound' ? supplierId : '');
formData.append('file', fileInput.files[0]);
var btn = document.querySelector('#ctFormOverlay .btn-primary');
btn.disabled = true; btn.textContent = '上传中...';
fetch('/api/contract/template/upload', { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) { closeCtForm(); loadCtTemplates(); }
else { alert(res.msg || '上传失败'); btn.disabled = false; btn.textContent = '上传'; }
});
}
function deleteCtTemplate(id) {
if (!confirm('确认删除此模板?')) return;
fetch('/api/contract/template/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id: id}) })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) loadCtTemplates();
else alert(res.msg || '操作失败');
});
}
function downloadContractFile(url, fileName) {
fetch(url)
.then(function(r) {
if (!r.ok) { alert('下载失败'); return; }
return r.blob();
})
.then(function(blob) {
if (!blob) return;
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
})
.catch(function() {
// fallback: 直接打开
window.location.href = url;
});
}
function previewCtTemplate(id) {
// 在新标签页中打开预览页面
window.open('/api/contract/template/preview_page?id=' + id, '_blank');
}
function editCtTemplate(id) {
// 获取模板数据
fetch('/api/contract/templates')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var t = null;
for (var i = 0; i < res.data.length; i++) {
if (res.data[i].id === id) { t = res.data[i]; break; }
}
if (!t) { alert('模板不存在'); return; }
showCtEditDialog(t);
});
}
function showCtEditDialog(t) {
var overlay = document.createElement('div');
overlay.className = 'ct-form-overlay';
overlay.id = 'ctEditOverlay';
overlay.onclick = function(e) { if (e.target === overlay) {} };
var typeOptions = ['all','domestic','overseas','packaging'];
var typeLabels = {'all':'全部类型','domestic':'国内订单','overseas':'海外订单','packaging':'包材订单'};
var typeSel = '';
typeOptions.forEach(function(v) {
var sel = (v === t.order_type) ? ' selected' : '';
typeSel += '<option value="' + v + '"' + sel + '>' + typeLabels[v] + '</option>';
});
var isBound = t.supplier_id ? true : false;
var boundSupplierName = t.bound_supplier_name || '';
var boundSupplierId = t.supplier_id || '';
overlay.innerHTML = '<div class="ct-form-box" onclick="event.stopPropagation()"><h2>✏️ 编辑合同模板</h2>'
+ '<div class="ct-form-row"><label>模板名称</label><input id="ctEditName" class="form-input" value="' + escHtml(t.name) + '"></div>'
+ '<div class="ct-form-row"><label>适用类型</label><select id="ctEditType" class="form-input">' + typeSel + '</select></div>'
+ '<div class="ct-form-row"><label>模板属性</label><select id="ctEditAttr" class="form-input" onchange="toggleCtEditSupplier(this.value)"><option value="universal"' + (!isBound ? ' selected' : '') + '>通用模板</option><option value="bound"' + (isBound ? ' selected' : '') + '>特定模板</option></select></div>'
+ '<div id="ctEditSupplierArea" style="' + (isBound ? '' : 'display:none;') + '">'
+ '<div class="ct-form-row"><label>绑定供应商</label>'
+ '<div style="display:flex;gap:6px;"><input id="ctEditSupplierSearch" class="form-input" placeholder="输入供应商名称搜索..." style="flex:1;" value="' + escHtml(boundSupplierName) + '">'
+ '<button class="btn btn-sm btn-outline" onclick="clearCtEditSupplierBind()" style="white-space:nowrap;">清除</button></div>'
+ '<div id="ctEditSupplierSuggestions" style="max-height:180px;overflow-y:auto;border:1px solid #e8e8e8;border-radius:6px;margin-top:4px;display:none;"></div>'
+ '<div id="ctEditSupplierTag" style="margin-top:4px;">'
+ (isBound ? '<span style="display:inline-block;background:#e6f7ff;border:1px solid #91d5ff;border-radius:4px;padding:2px 10px;color:#1890ff;font-size:13px;">📌 绑定:' + escHtml(boundSupplierName) + '</span>' : '')
+ '</div><input type="hidden" id="ctEditSupplierId" value="' + boundSupplierId + '"></div></div>'
+ '<div class="ct-form-row"><label>更换模板文件 <span style="color:#999;font-weight:400;">(不选则不更换)</span></label>'
+ '<div style="font-size:12px;color:#999;margin-bottom:4px;">当前文件:' + escHtml(t.file_name || '') + '</div>'
+ '<input type="file" id="ctEditFile" accept=".xlsx"></div>'
+ '<div class="ct-form-actions"><button class="btn btn-outline" onclick="closeCtEdit()">取消</button><button class="btn btn-primary" onclick="submitCtEdit(' + t.id + ')">保存</button></div></div>';
document.body.appendChild(overlay);
// 供应商搜索绑定
document.getElementById('ctEditSupplierSearch').addEventListener('input', function() {
var q = this.value.trim();
var suggestions = document.getElementById('ctEditSupplierSuggestions');
if (q.length < 1) { suggestions.style.display = 'none'; return; }
fetch('/api/suppliers/list?search=' + encodeURIComponent(q) + '&size=20')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0 || !res.data || !res.data.rows || res.data.rows.length === 0) {
suggestions.innerHTML = '<div style="padding:10px;color:#999;">无匹配供应商</div>';
suggestions.style.display = 'block';
return;
}
var h = '';
res.data.rows.forEach(function(s) {
h += '<div class="ct-supplier-option" data-id="' + s.id + '" data-name="' + escHtml(s.supplier_name) + '" onclick="selectCtEditSupplier(this)">' + escHtml(s.supplier_name) + ' <span style="color:#999;">' + escHtml(s.supplier_code || '') + '</span></div>';
});
suggestions.innerHTML = h;
suggestions.style.display = 'block';
});
});
}
function toggleCtEditSupplier(val) {
var area = document.getElementById('ctEditSupplierArea');
if (area) {
area.style.display = (val === 'bound') ? '' : 'none';
}
}
function selectCtEditSupplier(el) {
document.getElementById('ctEditSupplierId').value = el.getAttribute('data-id');
document.getElementById('ctEditSupplierSearch').value = el.getAttribute('data-name');
document.getElementById('ctEditSupplierSuggestions').style.display = 'none';
document.getElementById('ctEditSupplierTag').innerHTML = '<span style="display:inline-block;background:#e6f7ff;border:1px solid #91d5ff;border-radius:4px;padding:2px 10px;color:#1890ff;font-size:13px;">📌 绑定:' + escHtml(el.getAttribute('data-name')) + '</span>';
}
function clearCtEditSupplierBind() {
document.getElementById('ctEditSupplierId').value = '';
document.getElementById('ctEditSupplierSearch').value = '';
document.getElementById('ctEditSupplierSuggestions').style.display = 'none';
document.getElementById('ctEditSupplierTag').innerHTML = '';
}
function closeCtEdit() {
var el = document.getElementById('ctEditOverlay');
if (el) el.remove();
}
function submitCtEdit(id) {
var name = document.getElementById('ctEditName').value.trim();
var type = document.getElementById('ctEditType').value;
var attr = document.getElementById('ctEditAttr').value;
var supplierId = document.getElementById('ctEditSupplierId').value;
var fileInput = document.getElementById('ctEditFile');
if (!name) { alert('请输入模板名称'); return; }
if (attr === 'bound' && !supplierId) { alert('请选择要绑定的供应商'); return; }
var formData = new FormData();
formData.append('id', id);
formData.append('name', name);
formData.append('order_type', type);
formData.append('supplier_id', attr === 'bound' ? supplierId : '');
if (fileInput.files && fileInput.files[0]) {
formData.append('file', fileInput.files[0]);
}
var btn = document.querySelector('#ctEditOverlay .btn-primary');
btn.disabled = true; btn.textContent = '保存中...';
fetch('/api/contract/template/update', { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) { closeCtEdit(); loadCtTemplates(); }
else { alert(res.msg || '保存失败'); btn.disabled = false; btn.textContent = '保存'; }
});
}
// ─── 合同生成功能 ───
var _contractGenDialogOpen = false;
function generateContractForOrders(orderType, orderIds) {
if (_contractGenDialogOpen) return;
_contractGenDialogOpen = true;
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:15000;display:flex;align-items:center;justify-content:center';
overlay.id = 'contractGenOverlay';
overlay.onclick = function(e) { if (e.target === overlay) {} };
var box = document.createElement('div');
box.style.cssText = 'background:#fff;border-radius:12px;padding:24px;width:760px;max-width:94vw;box-shadow:0 8px 32px rgba(0,0,0,0.18)';
box.onclick = function(e) { e.stopPropagation(); };
var h2 = document.createElement('h2');
h2.textContent = '📄 生成合同';
h2.style.cssText = 'margin:0 0 4px 0;font-size:18px';
box.appendChild(h2);
var typeMap = {'domestic':'国内订单','overseas':'海外订单','packaging':'包材订单'};
var infoLine = document.createElement('p');
infoLine.style.cssText = 'margin:0 0 16px 0;font-size:13px;color:#666';
infoLine.textContent = (typeMap[orderType] || orderType) + ' | 已选 ' + orderIds.length + ' 条订单';
box.appendChild(infoLine);
// 加载中
var statusDiv = document.createElement('div');
statusDiv.id = 'contractGenStatus';
statusDiv.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">⏳ 正在分析供应商 & 模板匹配…</div>';
box.appendChild(statusDiv);
// 已生成合同结果区(初始隐藏)
var resultDiv = document.createElement('div');
resultDiv.id = 'contractGenResult';
resultDiv.style.display = 'none';
box.appendChild(resultDiv);
var ba = document.createElement('div');
ba.id = 'contractGenActions';
ba.style.cssText = 'display:flex;justify-content:flex-end;gap:10px;margin-top:16px';
var cancelBtn = document.createElement('button');
cancelBtn.className = 'btn btn-outline'; cancelBtn.textContent = '取消';
cancelBtn.onclick = function() { _contractGenDialogOpen = false; var el = document.getElementById('contractGenOverlay'); if (el) el.remove(); };
ba.appendChild(cancelBtn);
box.appendChild(ba);
overlay.appendChild(box);
document.body.appendChild(overlay);
// 订单明细容器
var itemsDiv = document.createElement('div');
itemsDiv.id = 'contractGenItems';
itemsDiv.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">⏳ 加载订单明细…</div>';
box.insertBefore(itemsDiv, statusDiv);
// 加载订单明细
fetch('/api/domestic/orders/by_ids', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ids: orderIds, order_type: orderType})
}).then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data && res.data.length > 0) {
var ih = '<div style="margin-bottom:12px;font-size:13px;font-weight:600;color:#333;">📋 合同基础信息</div>';
ih += '<div style="max-height:200px;overflow-y:auto;margin-bottom:12px;">';
ih += '<table style="width:100%;border-collapse:collapse;font-size:12px;">';
ih += '<thead><tr style="background:#fafafa;"><th style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">货品编码</th><th style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">货品名称</th><th style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">数量</th><th style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">单价</th><th style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">金额</th></tr></thead><tbody>';
var totalAmt = 0;
res.data.forEach(function(item) {
var qty = item['数量'] || 0;
var price = item['单价'] || 0;
var amt = qty * price;
totalAmt += amt;
ih += '<tr><td style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">' + escHtml(item['编码'] || '') + '</td>';
ih += '<td style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">' + escHtml(item['名称'] || '') + '</td>';
ih += '<td style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">' + qty + '</td>';
ih += '<td style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">' + (price.toFixed(2)) + '</td>';
ih += '<td style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">¥' + (amt.toFixed(2)) + '</td></tr>';
});
ih += '<tr style="background:#fafafa;font-weight:600;"><td colspan="4" style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">合计</td><td style="padding:6px 8px;border:1px solid #f0f0f0;text-align:center;">¥' + (totalAmt.toFixed(2)) + '</td></tr>';
ih += '</tbody></table></div>';
itemsDiv.innerHTML = ih;
} else {
itemsDiv.innerHTML = '';
}
}).catch(function(err) {
itemsDiv.innerHTML = '<div style="text-align:center;padding:10px;color:#999;font-size:12px;">加载明细失败</div>';
});
// 加载供应商分组 + 模板匹配
fetch('/api/contract/supplier_groups', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({order_ids: orderIds, order_type: orderType})
}).then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0 || !res.data || res.data.length === 0) {
statusDiv.innerHTML = '<div style="text-align:center;padding:20px;color:#f5222d;">❌ 获取分组信息失败</div>';
return;
}
var groups = res.data;
var hasMissing = false;
var h = '<div style="margin-bottom:12px;font-size:13px;font-weight:600;color:#333;">📋 模板匹配</div>';
h += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
h += '<thead><tr style="background:#fafafa;"><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">供应商</th><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">产品数</th><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">使用模板</th></tr></thead><tbody>';
groups.forEach(function(g) {
var templateLabel = '';
if (g.template_type === 'bound') {
templateLabel = '<span style="color:#1890ff;">🔗 ' + escHtml(g.bound_template.name) + '</span>';
} else if (g.template_type === 'universal') {
templateLabel = '<span style="color:#52c41a;">📄 ' + escHtml(g.universal_template_name) + '(通用)</span>';
} else {
templateLabel = '<span style="color:#f5222d;">❌ 无可用模板</span>';
hasMissing = true;
}
h += '<tr><td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + escHtml(g.supplier) + '</td>';
h += '<td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + g.count + '</td>';
h += '<td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + templateLabel + '</td></tr>';
});
h += '</tbody></table>';
if (hasMissing) {
h += '<div style="margin-top:8px;padding:8px 12px;background:#fff2f0;border:1px solid #ffccc7;border-radius:6px;font-size:12px;color:#f5222d;">⚠️ 部分供应商无可用模板,请先上传对应模板</div>';
}
statusDiv.innerHTML = h;
// 生成合同按钮
if (!hasMissing) {
var genBtn = document.createElement('button');
genBtn.id = 'contractGenBtn';
genBtn.className = 'btn btn-primary';
genBtn.textContent = '✅ 生成合同';
genBtn.style.marginLeft = '10px';
genBtn.onclick = function() {
genBtn.disabled = true; genBtn.textContent = '⏳ 生成中…';
doGenerateContract(orderType, orderIds, overlay);
};
ba.insertBefore(genBtn, cancelBtn);
}
}).catch(function(err) {
statusDiv.innerHTML = '<div style="text-align:center;padding:20px;color:#f5222d;">❌ 加载模板匹配失败</div>';
});
}
function doGenerateContract(orderType, orderIds, overlay) {
fetch('/api/contract/generate', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({order_ids: orderIds, order_type: orderType})
}).then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) {
alert(res.msg || '生成失败');
_contractGenDialogOpen = false;
var el = document.getElementById('contractGenOverlay');
if (el) el.remove();
return;
}
// 在当前弹窗下方追加已生成的合同
var resultDiv = document.getElementById('contractGenResult');
if (!resultDiv) return;
resultDiv.style.display = '';
var th = '<div style="margin-top:16px;margin-bottom:8px;font-size:13px;font-weight:600;color:#333;">📋 已生成的合同</div>';
th += '<table style="width:100%;border-collapse:collapse;font-size:12px;">';
th += '<thead><tr style="background:#fafafa;"><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">供应商</th><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">合同编号</th><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">金额</th><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">生成时间</th><th style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">文件</th></tr></thead><tbody>';
(res.data || []).forEach(function(g) {
var totalAmt = g.total_amount || 0;
var amtStr = '¥' + (typeof totalAmt === 'number' ? totalAmt.toFixed(2) : parseFloat(totalAmt).toFixed(2));
var timeStr = g.generated_at || '';
var fileBtn = g.file_path
? '<button class="btn btn-sm btn-primary" style="font-size:11px;padding:2px 10px;" onclick="downloadContractFile(\'' + g.file_path + '\',\'' + escHtml(g.file_name || '合同.xlsx') + '\')">📥 下载</button>'
: '<span style="color:#f5222d;">失败</span>';
th += '<tr><td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + escHtml(g.supplier) + '</td>';
th += '<td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;"><code>' + escHtml(g.contract_no || '') + '</code></td>';
th += '<td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + amtStr + '</td>';
th += '<td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + timeStr + '</td>';
th += '<td style="padding:8px 10px;border:1px solid #f0f0f0;text-align:center;">' + fileBtn + '</td></tr>';
});
th += '</tbody></table>';
resultDiv.innerHTML = th;
// 移除生成合同按钮,取消→关闭
var genBtn = document.getElementById('contractGenBtn');
if (genBtn) genBtn.style.display = 'none';
var actions = document.getElementById('contractGenActions');
if (actions) {
var btns = actions.querySelectorAll('button');
btns.forEach(function(b) {
if (b.textContent === '取消' || b.textContent === '关闭') {
b.textContent = '关闭';
}
});
}
}).catch(function() {
alert('❌ 网络错误,生成合同失败');
_contractGenDialogOpen = false;
var el = document.getElementById('contractGenOverlay');
if (el) el.remove();
});
}
function loadDashboard(container) {
container.innerHTML = `
<div class="page-header">
<h1>数据概览</h1>
<p class="page-desc">供应链运营数据总览</p>
</div>
<div id="dashContent"><div class="page-loading"><div class="loading-spinner"></div></div></div>
`;
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 = '<div class="stats-grid" style="grid-template-columns:repeat(' + cols + ',1fr)">';
statItems.forEach(function(s) {
cards += '<div class="stat-card" style="border-left:4px solid ' + s.color + '">';
cards += '<div class="stat-icon">' + s.icon + '</div>';
cards += '<div class="stat-info">';
cards += '<div class="stat-value" style="color:' + s.color + '">' + s.value + '</div>';
cards += '<div class="stat-label">' + s.label + (s.sub ? ' <small style="color:#999">' + s.sub + '</small>' : '') + '</div>';
cards += '</div></div>';
});
cards += '</div>';
// 第一行图表:日出库趋势 + 品牌占比
var row1 = '<div style="display:flex;gap:15px;margin-top:20px">';
row1 += '<div class="card" style="flex:1.6"><div class="card-header"><h3>📈 日出库趋势</h3></div><div class="card-body" style="padding:12px 16px"><div style="height:280px"><canvas id="dashTrendChart"></canvas></div></div></div>';
row1 += '<div class="card" style="flex:1"><div class="card-header"><h3>🥧 品牌出库占比 TOP8</h3></div><div class="card-body" style="padding:12px 16px"><div style="height:280px"><canvas id="dashBrandChart"></canvas></div></div></div>';
row1 += '</div>';
// 第二行:店铺排行 + 品牌排行(并列纵向柱状图)
var row2 = '<div style="display:flex;gap:15px;margin-top:15px">';
row2 += '<div class="card" style="flex:1"><div class="card-header"><h3>🏪 店铺出库排行</h3></div><div class="card-body" style="padding:12px 16px"><div style="height:320px"><canvas id="dashShopChart"></canvas></div></div></div>';
row2 += '<div class="card" style="flex:1"><div class="card-header"><h3>🏷️ 品牌出库排行 TOP10</h3></div><div class="card-body" style="padding:12px 16px"><div style="height:320px"><canvas id="dashBrandRankChart"></canvas></div></div></div>';
row2 += '</div>';
el.innerHTML = cards + row1 + row2;
// 绘制所有图表
waitChartJS().then(function() {
var COLORS = ['#1890ff','#52c41a','#fa8c16','#722ed1','#13c2c2','#eb2f96','#faad14','#f5222d','#2f54eb','#a0d911'];
// 日出库趋势:按月份展示,点击月份后展示每日数据
var _trendDailyData = (d.recent_trend || []).map(function(t) {
return { date: t.date_str || t.date || '', qty: parseInt(t.qty) || 0 };
});
var _trendChartInstance = null;
var _trendHeaderEl = null;
function _aggregateByMonth(dailyData) {
var months = {};
dailyData.forEach(function(d) {
var monthKey = d.date.slice(0, 7); // YYYY-MM
if (!months[monthKey]) months[monthKey] = { month: monthKey, qty: 0, count: 0 };
months[monthKey].qty += d.qty;
months[monthKey].count++;
});
var result = Object.keys(months).sort().map(function(k) { return months[k]; });
return result;
}
function _buildTrendChart(data, labels, labelText) {
if (_trendChartInstance) {
_trendChartInstance.destroy();
_trendChartInstance = null;
}
var canvas = document.getElementById('dashTrendChart');
if (!canvas) return;
var isMonthView = /^\d{4}-\d{2}$/.test(labels[0]);
_trendChartInstance = new Chart(canvas, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: labelText,
data: data,
backgroundColor: 'rgba(24,144,255,0.6)',
borderColor: '#1890ff',
borderWidth: 1,
borderRadius: 4,
order: 2
},
{
label: '趋势线',
type: 'line',
data: data,
borderColor: '#f5222d',
borderWidth: 2,
pointBackgroundColor: '#f5222d',
pointBorderColor: '#fff',
pointBorderWidth: 1,
pointRadius: 3,
pointHoverRadius: 5,
fill: false,
tension: 0,
order: 1
}
]
},
options: {
responsive: true, maintainAspectRatio: false,
onClick: function(e, elements) {
if (elements && elements.length > 0) {
var idx = elements[0].index;
var label = this.data.labels[idx];
if (isMonthView && /^\d{4}-\d{2}$/.test(label)) {
_showDailyByMonth(label);
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: { boxWidth: 12, padding: 4, font: { size: 11 } }
}
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } },
x: { grid: { display: false } }
}
}
});
}
function _showMonthlyView() {
if (!_trendDailyData.length) return;
var monthly = _aggregateByMonth(_trendDailyData);
var labels = monthly.map(function(m) { return m.month; });
var data = monthly.map(function(m) { return m.qty; });
if (_trendHeaderEl) _trendHeaderEl.innerHTML = '📈 日出库趋势';
_buildTrendChart(data, labels, '月出库量');
}
function _showDailyByMonth(monthKey) {
var filtered = _trendDailyData.filter(function(d) { return d.date.indexOf(monthKey) === 0; });
if (!filtered.length) return;
var labels = filtered.map(function(d) { return d.date.slice(5); }); // MM-DD
var data = filtered.map(function(d) { return d.qty; });
if (_trendHeaderEl) _trendHeaderEl.innerHTML = '📈 ' + monthKey + ' 日出库趋势 <span style="font-size:12px;color:#999;cursor:pointer" onclick="_trendBackToMonth()">← 返回月份</span>';
_buildTrendChart(data, labels, '日出库量');
}
// 注册返回函数到全局
window._trendBackToMonth = _showMonthlyView;
function drawTrend() {
var trendCanvas = document.getElementById('dashTrendChart');
if (!trendCanvas || !_trendDailyData.length) return;
_trendHeaderEl = document.querySelector('.card-header:has(#dashTrendChart) h3') ||
document.querySelector('.card-header h3');
// 找最近一次匹配的标题 - 其实我们直接定位
var cardHeaders = document.querySelectorAll('.card-header h3');
for (var hi = 0; hi < cardHeaders.length; hi++) {
if (cardHeaders[hi].textContent.indexOf('日出库趋势') >= 0) {
_trendHeaderEl = cardHeaders[hi];
break;
}
}
_showMonthlyView();
}
function drawBrand() {
var brandCanvas = document.getElementById('dashBrandChart');
if (brandCanvas && d.brand_stats && d.brand_stats.length > 0) {
var topBrands = d.brand_stats.slice(0, 8);
new Chart(brandCanvas, {
type: 'doughnut',
data: { labels: topBrands.map(function(b) { return b['品牌']; }), datasets: [{ data: topBrands.map(function(b) { return parseInt(b['总数量']) || 0; }), backgroundColor: COLORS.slice(0, topBrands.length), borderWidth: 2, borderColor: '#fff' }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { font: { size: 11 }, boxWidth: 12, padding: 6 } }, tooltip: { callbacks: { label: function(ctx) { var total = ctx.dataset.data.reduce(function(a,b) { return a + b; }, 0); return ctx.label + ': ' + ctx.raw.toLocaleString() + ' (' + ((ctx.raw / total) * 100).toFixed(1) + '%)'; } } } }, cutout: '55%' }
});
}
}
function drawShop() {
var shopCanvas = document.getElementById('dashShopChart');
if (shopCanvas && d.shop_stats && d.shop_stats.length > 0) {
var shopData = d.shop_stats.slice(0, 10);
new Chart(shopCanvas, {
type: 'bar',
data: { labels: shopData.map(function(s) { return (s['店铺'] || '').length > 10 ? (s['店铺'] || '').substring(0, 10) + '..' : (s['店铺'] || ''); }), datasets: [{ label: '出库量', data: shopData.map(function(s) { return parseInt(s['总数量']) || 0; }), backgroundColor: 'rgba(250,140,22,0.7)', borderColor: '#fa8c16', borderWidth: 1, borderRadius: 3 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false }, ticks: { font: { size: 10 }, maxRotation: 30 } } } }
});
}
}
function drawBrandRank() {
var brandRankCanvas = document.getElementById('dashBrandRankChart');
if (brandRankCanvas && d.brand_stats && d.brand_stats.length > 0) {
var brandRankData = d.brand_stats.slice(0, 10);
new Chart(brandRankCanvas, {
type: 'bar',
data: { labels: brandRankData.map(function(b) { return b['品牌']; }), datasets: [{ label: '出库量', data: brandRankData.map(function(b) { return parseInt(b['总数量']) || 0; }), backgroundColor: 'rgba(114,46,209,0.7)', borderColor: '#722ed1', borderWidth: 1, borderRadius: 3 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { callback: function(v) { return v.toLocaleString(); } } }, x: { grid: { display: false }, ticks: { font: { size: 10 }, maxRotation: 30 } } } }
});
}
}
drawTrend();
drawBrand();
drawShop();
drawBrandRank();
});
}
// ─── 出库数据 ───
let _sumChart = null;
function loadDataSummaryPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>出库数据汇总</h1>
<p class="page-desc">按货品编号汇总每日出库量,点击SKU查看仓库/店铺明细</p>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px;align-items:center">
<input type="text" id="sumSearch" class="form-input" style="min-width:140px;flex:1;max-width:260px" placeholder="🔍 货品编号/货品名称" autocomplete="off" onkeydown="if(event.key==='Enter')loadSummaryData(1)">
<input type="date" id="sumDateStart" class="form-input" style="min-width:130px;flex:0 1 170px" onchange="loadSummaryData(1)">
<input type="date" id="sumDateEnd" class="form-input" style="min-width:130px;flex:0 1 170px" onchange="loadSummaryData(1)">
<button class="btn btn-primary" onclick="loadSummaryData(1)">查询</button>
<button class="btn" onclick="resetSummaryFilters()">重置</button>
<span class="data-count" id="sumCount"></span>
</div>
</div>
</div>
<!-- 汇总指标卡 -->
<div class="stats-grid" id="sumStatsGrid" style="grid-template-columns:repeat(4,1fr);margin-top:15px">
<div class="stat-card" style="border-left:4px solid #1890ff">
<div class="stat-icon">📦</div>
<div class="stat-info">
<div class="stat-value" id="sumTotalQty" style="color:#1890ff">-</div>
<div class="stat-label">总出库量 <small style="color:#999">件</small></div>
</div>
</div>
<div class="stat-card" style="border-left:4px solid #52c41a">
<div class="stat-icon">🏷️</div>
<div class="stat-info">
<div class="stat-value" id="sumTotalSku" style="color:#52c41a">-</div>
<div class="stat-label">出库SKU数 <small style="color:#999">个</small></div>
</div>
</div>
<div class="stat-card" style="border-left:4px solid #fa8c16">
<div class="stat-icon">📅</div>
<div class="stat-info">
<div class="stat-value" id="sumTotalDays" style="color:#fa8c16">-</div>
<div class="stat-label">出库天数 <small style="color:#999">天</small></div>
</div>
</div>
<div class="stat-card" style="border-left:4px solid #722ed1">
<div class="stat-icon">📊</div>
<div class="stat-info">
<div class="stat-value" id="sumAvgDaily" style="color:#722ed1">-</div>
<div class="stat-label">日均出库 <small style="color:#999">件</small></div>
</div>
</div>
</div>
<!-- 日出库趋势图 -->
<div class="card" style="margin-top:15px">
<div class="card-header"><h3>📈 日出库趋势</h3></div>
<div class="card-body" style="padding:12px 16px">
<div style="height:280px"><canvas id="sumTrendChart"></canvas></div>
</div>
</div>
<!-- 明细表格 -->
<div class="card" style="margin-top:15px">
<div class="card-header"><h3>📊 数据汇总</h3></div>
<div class="card-body" style="padding:0;overflow-x:auto">
<table class="data-table" id="sumTable">
<thead><tr><th style="text-align:center">序号</th><th style="text-align:center">日期</th><th style="text-align:center">货品编号</th><th style="text-align:center">货品名称</th><th style="text-align:center">品牌</th><th style="text-align:center">总出库量</th><th style="text-align:center">总成本</th></tr></thead>
<tbody id="sumTbody"><tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>
</table>
<div id="sumPagination" class="pagination-controls" style="padding:8px 16px"></div>
</div>
</div>
<!-- SKU明细弹窗 -->
<div class="modal-overlay" id="skuModal" style="display:none">
<div class="modal" style="width:85vw;max-width:900px">
<div class="modal-header">
<h3>📦 出库明细 — <span id="skuModalTitle"></span></h3>
<span class="modal-close" onclick="closeSkuModal()">&times;</span>
</div>
<div class="modal-body" style="padding:16px 20px;max-height:65vh;overflow-y:auto">
<div style="margin-bottom:12px">
<span style="color:var(--text-light);font-size:13px">日期:</span>
<strong id="skuModalDate" style="font-size:14px"></strong>
<span style="color:var(--text-light);font-size:13px;margin-left:20px">货品:</span>
<strong id="skuModalName" style="font-size:14px"></strong>
</div>
<table class="data-table" id="skuDetailTable">
<thead><tr><th style="text-align:center">仓库</th><th style="text-align:center">店铺</th><th style="text-align:center">数据来源</th><th style="text-align:center">出库数量</th><th style="text-align:center">成本</th></tr></thead>
<tbody id="skuDetailTbody"></tbody>
</table>
<div id="skuDetailSummary" style="margin-top:10px;text-align:right;font-size:14px;font-weight:600;color:var(--text-primary)"></div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeSkuModal()">关闭</button>
</div>
</div>
</div>
`;
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 = '<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-light)">暂无数据</td></tr>';
} else {
var startIdx = (page - 1) * SUM_PAGE_SIZE;
tbody.innerHTML = data.rows.map(function(r, i) {
return '<tr><td style="text-align:center">' + (startIdx + i + 1) + '</td><td style="text-align:center">' + escHtml(r.日期 || '-') + '</td><td style="text-align:center"><a href="javascript:;" onclick="showSkuDetail(\'' + escHtml(r.货品编号) + '\', \'' + escHtml(r.日期) + '\', \'' + escJs(r.货品名称) + '\')" class="sku-link"><code>' + escHtml(r.货品编号) + '</code></a></td><td style="text-align:center">' + escHtml(r.货品名称 || '-') + '</td><td style="text-align:center">' + escHtml(r.品牌 || '-') + '</td><td style="text-align:center;font-weight:600;color:#1890ff">' + (r.total_qty || 0) + '</td><td style="text-align:center">' + (r.total_cost || 0).toFixed(2) + '</td></tr>';
}).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 + ' 日出库趋势 <span style="font-size:12px;color:var(--text-light);font-weight:normal;cursor:pointer" onclick="loadSummaryStats(document.getElementById(\'sumSearch\')?.value || \'\')">(点击返回月视图)</span>';
} else {
headerEl.innerHTML = '📈 月度出库趋势 <span style="font-size:12px;color:var(--text-light);font-weight:normal">(点击数据点钻取日数据)</span>';
}
}
}
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 = '<tr><td colspan="5" style="text-align:center;padding:20px;color:var(--text-light)">加载中...</td></tr>';
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 = '<tr><td colspan="5" style="text-align:center;padding:20px;color:var(--text-light)">暂无明细</td></tr>';
} 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 '<tr><td style="text-align:center">' + escHtml(d.仓库 || '-') + '</td><td style="text-align:center">' + escHtml(d.店铺 || '-') + '</td><td style="text-align:center">' + srcCn + '</td><td style="text-align:center;font-weight:600">' + (d.qty || 0) + '</td><td style="text-align:center">' + (d.cost || 0).toFixed(2) + '</td></tr>';
}).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 = `
<div class="page-header">
<h1>出库数据</h1>
<p class="page-desc">每日出库明细数据</p>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px;align-items:center">
<input type="text" id="dataSearch" class="form-input" style="width:200px" placeholder="🔍 货品编号/货品名称" autocomplete="off" onkeydown="if(event.key==='Enter')loadDataPageData(1)">
<input type="date" id="dataDateStart" class="form-input" style="width:135px" onchange="loadDataPageData(1)">
<input type="date" id="dataDateEnd" class="form-input" style="width:135px" onchange="loadDataPageData(1)">
<div class="autocomplete-wrap" style="position:relative;display:inline-block;vertical-align:top">
<input type="text" id="dataWarehouse" class="form-input" style="width:120px" placeholder="全部仓库" autocomplete="off" oninput="dataSearchFilter(this,'dataWarehouseOptions')" onkeydown="if(event.key==='Enter'){this.dataset.selected=this.value;loadDataPageData(1)}" onfocus="dataSearchFilter(this,'dataWarehouseOptions')" data-selected="">
<div id="dataWarehouseDropdown" class="autocomplete-dropdown" style="position:absolute;top:100%;left:0;z-index:1000;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.15);min-width:180px"></div>
</div>
<div class="autocomplete-wrap" style="position:relative;display:inline-block;vertical-align:top">
<input type="text" id="dataShop" class="form-input" style="width:120px" placeholder="全部店铺" autocomplete="off" oninput="dataSearchFilter(this,'dataShopOptions')" onkeydown="if(event.key==='Enter'){this.dataset.selected=this.value;loadDataPageData(1)}" onfocus="dataSearchFilter(this,'dataShopOptions')" data-selected="">
<div id="dataShopDropdown" class="autocomplete-dropdown" style="position:absolute;top:100%;left:0;z-index:1000;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.15);min-width:180px"></div>
</div>
<div class="autocomplete-wrap" style="position:relative;display:inline-block;vertical-align:top">
<input type="text" id="dataBrand" class="form-input" style="width:120px" placeholder="全部品牌" autocomplete="off" oninput="dataSearchFilter(this,'dataBrandOptions')" onkeydown="if(event.key==='Enter'){this.dataset.selected=this.value;loadDataPageData(1)}" onfocus="dataSearchFilter(this,'dataBrandOptions')" data-selected="">
<div id="dataBrandDropdown" class="autocomplete-dropdown" style="position:absolute;top:100%;left:0;z-index:1000;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.15);min-width:180px"></div>
</div>
<select id="dataSource" class="form-input" style="width:110px" onchange="loadDataPageData(1)"><option value="">全部来源</option></select>
<button class="btn btn-primary" onclick="loadDataPageData(1)">查询</button>
<button class="btn" onclick="resetDataFilters()">重置</button>
<span class="data-count" id="dataCount"></span>
</div>
</div>
</div>
<div class="page-flex" style="flex:1;min-height:300px;margin-top:15px">
<div class="card">
<div class="card-header"><h3>出库列表</h3></div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="dataTable">
<thead style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)"><tr><th style="text-align:center">序号</th><th style="text-align:center;white-space:nowrap">日期</th><th style="text-align:center">货品编号</th><th style="text-align:center">货品名称</th><th style="text-align:center">品牌</th><th style="text-align:center">货品数量</th><th style="text-align:center">成本价</th><th style="text-align:center">金额</th><th style="text-align:center">仓库</th><th style="text-align:center">店铺</th><th style="text-align:center">数据来源</th></tr></thead>
<tbody id="dataTbody"><tr><td colspan="11" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>
</table>
</div>
</div>
<div id="dataPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center;z-index:20"></div>
</div>
</div>
`;
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 += '<option value="' + escHtml(w) + '">' + cn + '</option>';
});
}
if (d.warehouses) {
window.dataWarehouseOptions = d.warehouses;
}
if (d.shops) {
window.dataShopOptions = d.shops;
}
if (d.brands) {
window.dataBrandOptions = d.brands;
}
});
}
const SOURCE_MAP_CN = {
'wdt': '旺店通',
'jdbz': '京东泊紫',
'jdgj': '京东国际',
'jdjwt': '京东洁维塔',
'tmgj': '天猫国际'
};
let dataCurrentPage = 1;
let DATA_PAGE_SIZE = 20;
// 模糊搜索自动补全的数据源
window.dataWarehouseOptions = [];
window.dataShopOptions = [];
window.dataBrandOptions = [];
/**
* 模糊搜索自动补全函数
* @param {HTMLInputElement} inputEl - 输入框元素
* @param {string} optionsVar - 全局选项数组变量名
*/
window.dataSearchFilter = function(inputEl, optionsVar) {
var val = inputEl.value.trim().toLowerCase();
var dropdownId = inputEl.id + 'Dropdown';
var dd = document.getElementById(dropdownId);
if (!dd) return;
var options = window[optionsVar];
if (!options || options.length === 0) {
dd.style.display = 'none';
return;
}
// 过滤匹配的选项
var matched = [];
if (!val) {
// 空输入显示全部(最多50个避免卡顿)
matched = options.slice(0, 50);
} else {
options.forEach(function(opt) {
if (opt.toLowerCase().indexOf(val) !== -1) {
matched.push(opt);
}
});
// 始终允许直接输入的值
if (matched.length === 0 && inputEl.value) {
matched = [inputEl.value];
}
}
if (matched.length === 0) {
dd.style.display = 'none';
return;
}
// 渲染下拉列表
var html = '';
// 始终显示「全部」选项
html += '<div class="autocomplete-item" style="padding:6px 12px;cursor:pointer;border-bottom:1px solid #f0f0f0;color:#999" onmouseover="this.style.background=\'#f5f5f5\'" onmouseout="this.style.background=\'\'" onclick="document.getElementById(\'' + inputEl.id + '\').value=\'\';document.getElementById(\'' + inputEl.id + '\').dataset.selected=\'\';document.getElementById(\'' + dropdownId + '\').style.display=\'none\';loadDataPageData(1)">全部</div>';
matched.forEach(function(opt) {
var hOpt = opt.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
html += '<div class="autocomplete-item" style="padding:6px 12px;cursor:pointer" onmouseover="this.style.background=\'#f5f5f5\'" onmouseout="this.style.background=\'\'" onclick="document.getElementById(\'' + inputEl.id + '\').value=\'' + hOpt.replace(/'/g, '\\\'') + '\';document.getElementById(\'' + inputEl.id + '\').dataset.selected=\'' + hOpt.replace(/'/g, '\\\'') + '\';document.getElementById(\'' + dropdownId + '\').style.display=\'none\';loadDataPageData(1)">' + hOpt + '</div>';
});
dd.innerHTML = html;
dd.style.display = 'block';
// 限制列表高度
dd.style.maxHeight = '200px';
};
function loadDataPageData(page) {
dataCurrentPage = page;
var search = document.getElementById('dataSearch')?.value || '';
var start = document.getElementById('dataDateStart')?.value || '';
var end = document.getElementById('dataDateEnd')?.value || '';
var warehouse = document.getElementById('dataWarehouse')?.value || '';
var shop = document.getElementById('dataShop')?.value || '';
var brand = document.getElementById('dataBrand')?.value || '';
var source = document.getElementById('dataSource')?.value || '';
var url = '/api/data/list?page=' + page + '&size=' + DATA_PAGE_SIZE;
if (search) url += '&search=' + encodeURIComponent(search);
if (start) url += '&date_from=' + start;
if (end) url += '&date_to=' + end;
if (warehouse) url += '&warehouse=' + encodeURIComponent(warehouse);
if (shop) url += '&shop=' + encodeURIComponent(shop);
if (brand) url += '&brand=' + encodeURIComponent(brand);
if (source) url += '&source=' + encodeURIComponent(source);
fetch(url)
.then(r => r.json())
.then(res => {
if (res.code !== 0) return;
var data = res.data;
var tbody = document.getElementById('dataTbody');
if (!data.rows || data.rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="11" style="text-align:center;padding:40px;color:var(--text-light)">暂无数据</td></tr>';
} 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 '<tr><td style="text-align:center">' + (startIdx + i + 1) + '</td><td style="text-align:center;white-space:nowrap">' + escHtml(r.日期 || '-') + '</td><td style="text-align:center"><code>' + escHtml(r.货品编号 || '-') + '</code></td><td style="text-align:center">' + escHtml(r.货品名称 || '-') + '</td><td style="text-align:center">' + escHtml(r.品牌 || '-') + '</td><td style="text-align:center">' + qty + '</td><td style="text-align:center">' + unitCostStr + '</td><td style="text-align:center">' + amountStr + '</td><td style="text-align:center">' + escHtml(wh) + '</td><td style="text-align:center">' + escHtml(sh) + '</td><td style="text-align:center">' + srcCn + '</td></tr>';
}).join('');
}
document.getElementById('dataCount').textContent = '共 ' + (data.total || 0) + ' 条';
var pel = document.getElementById('dataPagination');
if (pel) renderPaginationWithSize(pel, page, data.total || 0, DATA_PAGE_SIZE, 'loadDataPageData', 'DATA_PAGE_SIZE');
});
}
window.searchData = function() { loadDataPageData(1); };
window.resetDataFilters = function() {
document.getElementById('dataSearch').value = '';
document.getElementById('dataDateStart').value = '';
document.getElementById('dataDateEnd').value = '';
document.getElementById('dataWarehouse').value = '';
document.getElementById('dataShop').value = '';
document.getElementById('dataBrand').value = '';
document.getElementById('dataSource').value = '';
// 清除模糊搜索的 selected 标记
var wh = document.getElementById('dataWarehouse'); if (wh) wh.dataset.selected = '';
var sh = document.getElementById('dataShop'); if (sh) sh.dataset.selected = '';
var br = document.getElementById('dataBrand'); if (br) br.dataset.selected = '';
loadDataPageData(1);
};
// ─── 产品基础表 ───
// 所有产品列定义
const PROD_COLUMNS = [
{field: 'product_code', label: '货品编号', defaultShow: true, sortable: true},
{field: 'product_name', label: '货品名称', defaultShow: true, sortable: true},
{field: 'product_type', label: '货品类型', defaultShow: true, sortable: false},
{field: 'brand', label: '品牌', defaultShow: true, sortable: true},
{field: 'supplier_code', label: '供应商编码', defaultShow: true, sortable: false},
{field: 'supplier_name_match', label: '供应商名称', defaultShow: true, sortable: false},
{field: 'cost_price', label: '成本价', defaultShow: true, sortable: true},
{field: 'lead_time', label: '货期(天)', defaultShow: true, sortable: false},
{field: 'logistics_time', label: '物流时间(天)', defaultShow: true, sortable: false},
{field: 'min_order_qty', label: '最低起订量', defaultShow: true, sortable: false},
{field: 'procurement_follower', label: '采购跟单', defaultShow: true, sortable: false},
{field: 'procurement_planner', label: '采购计划', defaultShow: true, sortable: false},
{field: 'platform', label: '平台', defaultShow: true, sortable: false},
{field: 'operator', label: '运营', defaultShow: true, sortable: false},
{field: 'developer', label: '开发专员', defaultShow: true, sortable: false},
{field: 'created_at', label: '创建时间', defaultShow: true, sortable: true},
{field: 'import_date', label: '导入日期', defaultShow: true, sortable: false},
];
// 字段显隐状态
if (!window.prodColVisible) {
window.prodColVisible = {};
PROD_COLUMNS.forEach(c => { window.prodColVisible[c.field] = c.defaultShow; });
}
function toggleProdCol(field) {
window.prodColVisible[field] = !window.prodColVisible[field];
document.querySelectorAll(`.col-${field}`).forEach(el => {
el.style.display = window.prodColVisible[field] ? '' : 'none';
});
const cb = document.getElementById('col-cb-' + field);
if (cb) cb.checked = window.prodColVisible[field];
}
// ─── 产品基础表 ───
let prodCurrentPage = 1;
let PROD_PAGE_SIZE = 50;
let prodSortBy = '';
let prodSortAsc = true;
let prodActiveTab = '成品'; // '成品' | '非成品'
function loadProductsPage(container) {
container.innerHTML = `
<style>
.prod-tabs { display:flex;gap:4px;margin-bottom:14px;background:var(--bg-card,#f7f8fa);padding:4px;border-radius:10px; }
.prod-tab { padding:8px 22px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text-light,#888);border-radius:8px;transition:all .25s;user-select:none; }
.prod-tab:hover { color:var(--text,#333);background:rgba(0,0,0,0.04); }
.prod-tab:nth-child(1).active { color:#1890ff;background:#e6f7ff;box-shadow:0 1px 4px rgba(24,144,255,0.12); }
.prod-tab:nth-child(2).active { color:#f5a623;background:#fff7e6;box-shadow:0 1px 4px rgba(245,166,35,0.12); }
</style>
<div class="page-header" style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">
<h1 style="margin:0;flex-shrink:0">产品信息管理</h1>
<div style="display:inline-flex;align-items:center;gap:4px;font-size:13px;color:var(--text-light,#999);padding:2px 8px;background:#f5f5f5;border-radius:4px;white-space:nowrap;cursor:default">
<span style="color:#1890ff;font-weight:500;text-decoration:none;cursor:pointer" onclick="window.open('/suppliers','_self')">供应商管理</span>
<span style="font-size:12px;color:#ccc">→</span>
<span>产品信息与供应商关联数据</span>
</div>
</div>
<div class="prod-tabs" id="prodTabs">
<div class="prod-tab active" data-type="成品" onclick="switchProdTab('成品')">成品基础信息表</div>
<div class="prod-tab" data-type="非成品" onclick="switchProdTab('非成品')">包材基础信息表</div>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px">
<input type="text" id="prodCode" class="form-input" style="width:110px" placeholder="货品编号" autocomplete="off" onkeydown="if(event.key==='Enter')searchProducts()">
<input type="text" id="prodName" class="form-input" style="width:130px" placeholder="货品名称" autocomplete="off" onkeydown="if(event.key==='Enter')searchProducts()">
<input type="text" id="prodSupplier" class="form-input" style="width:120px" placeholder="供应商" autocomplete="off" onkeydown="if(event.key==='Enter')searchProducts()">
<select id="prodTypeFilter" class="form-input" style="width:110px;padding:6px 8px">
<option value="">货品类型</option>
<option value="成品">成品</option>
<option value="现货">现货</option>
<option value="清库存">清库存</option>
<option value="赠品">赠品</option>
<option value="原材料">原材料</option>
</select>
<button class="btn btn-primary" onclick="searchProducts()">查询</button>
<button class="btn" onclick="resetProductFilters()">重置</button>
<span class="data-count" id="prodCount"></span>
</div>
</div>
</div>
<div class="page-flex" style="flex:1;min-height:300px">
<div class="card" style="margin-top:15px">
<div class="card-header" style="display:flex;align-items:center;gap:8px">
<h3 style="flex:0 0 auto" id="prodListTitle">成品信息列表</h3>
${window._hasPagePerm('products', 'add') ? '<button class="btn btn-sm btn-primary" onclick="openProductAddModal()">+ 新增</button>' : ''}
<div style="margin-left:auto;display:flex;gap:6px" id="prodToolbar">
<button class="btn btn-sm" onclick="openRecycleBin()" style="color:#f5222d;border-color:#f5222d">🗑 回收站</button>
<button class="btn btn-sm" onclick="exportProducts()">📤 导出</button>
<button class="btn btn-sm" onclick="openProductImportModal()">📥 导入</button>
<button class="btn btn-sm" onclick="openProdColModal()">⚙ 字段设置</button>
</div>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="prodTable"><thead id="prodThead" style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)"></thead><tbody id="prodTbody"></tbody></table>
</div></div>
<div id="prodPagination" class="card-footer pagination-sticky" style="padding:8px 16px;text-align:center"></div>
</div>
</div>
`;
// 构建列头
buildProdThead();
// 添加字段设置弹窗 HTML
const prodColModalHtml = `
<div class="modal-overlay" id="prodColModal" style="display:none">
<div class="modal" style="width:720px">
<div class="modal-header">
<h3>字段设置</h3>
<span class="modal-close" onclick="closeProdColModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:65vh;overflow-y:auto;padding:16px 20px">
<div class="col-toggle-all" style="padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:8px">
<label class="cb-all"><input type="checkbox" id="prod-cb-all" checked onchange="toggleProdColPendingAll(this.checked)"> <strong>全选</strong></label>
</div>
<input type="text" id="prodColSearchInput" class="form-input" placeholder="搜索字段名称..." oninput="filterProdColCheckboxes()" style="margin-bottom:8px;width:100%;box-sizing:border-box">
<div id="prodColCheckboxes">
<div id="prodColGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px 12px;padding:4px 0"></div>
</div>
</div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center">
<div>
<button class="btn btn-outline" onclick="resetProdCol()" style="font-size:12px;color:#888">↺ 恢复默认</button>
</div>
<div style="display:flex;gap:8px">
<button class="btn" onclick="closeProdColModal()">取消</button>
<button class="btn btn-primary" onclick="confirmProdCol()">确定</button>
</div>
</div>
</div>
</div>
`;
if (!document.getElementById('prodColModal')) {
container.insertAdjacentHTML('afterend', prodColModalHtml);
}
// 初始化拖拽顺序
window._prodColDragOrder = PROD_COLUMNS.map(c => c.field);
window._prodColDefaultOrder = PROD_COLUMNS.map(c => c.field);
if (!window._prodColPendingVisible) {
window._prodColPendingVisible = {};
PROD_COLUMNS.forEach(c => { window._prodColPendingVisible[c.field] = c.defaultShow; });
}
renderProdColCheckboxes();
// 清除筛选框搜索记忆
['prodCode','prodName','prodSupplier','prodTypeFilter'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
// 加载
prodActiveTab = '成品';
updateProdTypeFilterOptions('成品');
applyProdTabColumns('成品');
loadProducts(1);
}
function applyProdTabColumns(tab) {
// 包材基础信息表:跳过不需要的列
// 成品基础信息表:恢复所有列显示
var hideFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer'];
hideFields.forEach(function(f) {
window.prodColVisible[f] = tab !== '非成品';
});
// 包材tab显示成本价、货期、物流时间、最低起订量、创建时间
['cost_price','lead_time','logistics_time','min_order_qty','created_at'].forEach(function(f) {
window.prodColVisible[f] = true;
});
// 字段设置checkbox状态同步
document.querySelectorAll('#prodColModal input[type="checkbox"]').forEach(function(cb) {
var field = cb.id && cb.id.replace('col-cb-', '');
if (field && window.prodColVisible[field] !== undefined) cb.checked = window.prodColVisible[field];
});
}
function switchProdTab(tab) {
prodActiveTab = tab;
document.querySelectorAll('#prodTabs .prod-tab').forEach(function(el) {
el.classList.toggle('active', el.dataset.type === tab);
});
document.getElementById('prodListTitle').textContent = tab === '成品' ? '成品列表' : '包材列表';
// 切页清空筛选框,互不影响
['prodCode','prodName','prodSupplier','prodTypeFilter'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
// 更新货品类型下拉选项
updateProdTypeFilterOptions(tab);
applyProdTabColumns(tab);
// 重建表头使列显隐与当前tab一致
buildProdThead();
loadProducts(1);
}
function updateProdTypeFilterOptions(tab) {
var sel = document.getElementById('prodTypeFilter');
if (!sel) return;
var opts = '';
if (tab === '成品') {
// 成品基础表展示全部产品,下拉包含所有类型供筛选
opts = '<option value="">全部类型</option><option value="成品">成品</option><option value="现货">现货</option><option value="清库存">清库存</option><option value="赠品">赠品</option><option value="原材料">原材料</option>';
} else {
opts = '<option value="">全部类型</option><option value="包材">包材</option><option value="其他">其他</option>';
}
sel.innerHTML = opts;
}
function buildProdThead() {
var thead = document.getElementById('prodThead');
if (!thead) return;
var theadHtml = '<tr><th style="width:50px">序号</th>';
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 += '<th' + sortAttr + ' class="col-' + c.field + '"' + vis + '>' + c.label + '</th>';
});
theadHtml += '<th class="col-action" style="width:120px;min-width:120px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border);text-align:center">操作</th>';
theadHtml += '</tr>';
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 = '<tr><td colspan="' + (PROD_COLUMNS.length + 1) + '" style="text-align:center;padding:40px;color:var(--text-light)">暂无数据</td></tr>';
} 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 = '<td>' + (startIdx + i + 1) + '</td>';
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 = '<code>' + val + '</code>';
}
if (c.field === 'product_type') {
var typeColors = {'成品':'tag-blue','包材':'tag-orange','赠品':'tag-green','小样':'tag-purple','物料':'tag-orange'};
var cls = typeColors[raw] || 'tag-grey';
val = '<span class="tag ' + cls + '">' + val + '</span>';
}
if (c.field === 'cost_price') {
var p = parseFloat(raw);
val = isNaN(p) ? '-' : '¥' + p.toFixed(2);
}
if (c.field === 'min_order_qty') {
var q = parseInt(raw);
val = isNaN(q) ? '-' : q.toLocaleString();
}
if (c.field === 'created_at') {
val = escHtml((raw || '').toString().slice(0, 10) || '-');
}
html += '<td class="col-' + c.field + '">' + val + '</td>';
});
var pcode = r.product_code || '';
html += '<td class="col-action" style="position:sticky;right:0;z-index:2;background:var(--bg,#fff);border-left:1px solid var(--border);white-space:nowrap">';
html += '<button class="btn btn-sm btn-outline-primary" onclick="viewProductDetail(\'' + escHtml(pcode) + '\')" title="详情" style="font-size:11px;color:#1890ff;border-color:#1890ff">详情</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="quickEditProduct(\'' + escHtml(pcode) + '\')" title="编辑" style="font-size:11px;color:#fa8c16;border-color:#fa8c16">编辑</button> ';
if (prodActiveTab === '成品') html += '<button class="btn btn-sm btn-outline" onclick="viewProductBom(\'' + escHtml(pcode) + '\')" title="查看BOM清单" style="font-size:11px;color:#722ed1;border-color:#722ed1">📋 BOM</button> ';
html += '<button class="btn btn-sm btn-outline-danger" onclick="deleteProduct(\'' + escHtml(pcode) + '\')" title="删除" style="font-size:11px;color:#f5222d;border-color:#f5222d">删除</button>';
return '<tr>' + html + '</tr>';
}).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 = `
<div class="modal" style="width:560px">
<div class="modal-header">
<h3>新增产品</h3>
<span class="modal-close" onclick="closeProductAddModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:70vh;overflow-y:auto;padding:16px 20px">
<div id="prodAddForm">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px 16px">
<div><label style="font-size:12px;color:var(--text-secondary)">货品编号 *</label><input id="pa_code" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)">货品名称 *</label><input id="pa_name" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 品牌</label><input id="pa_brand" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" required autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 货品类型</label>
<select id="pa_type" class="form-input" style="width:100%">
<option value="">请选择</option>
<option value="成品">成品</option>
<option value="清库存">清库存</option>
<option value="现货">现货</option>
<option value="赠品">赠品</option>
<option value="原材料">原材料</option>
</select>
</div>
<div><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 供应商编码</label><input id="pa_supcode" class="form-input" style="width:100%;box-sizing:border-box;background:#f5f5f5;color:#999" readonly placeholder="自动匹配"></div>
<div style="position:relative"><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 供应商名称</label><input id="pa_supname" class="form-input" style="width:100%;box-sizing:border-box" placeholder="输入简称自动匹配" required autocomplete="off" oninput="lookupAddSupplierName(this.value)"><div id="pa_supDropdown" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div>
<div><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 货期(天)</label><input id="pa_lead" type="number" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" required autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 物流时间(天)</label><input id="pa_logis" type="number" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" required max="999" autocomplete="off"></div>
<div style="position:relative"><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 采购跟单</label><input id="pa_follower" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" required autocomplete="off" onfocus="showFollowerHistory()" oninput="showFollowerHistory()"><div id="pa_folDropdown" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:150px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div>
<div><label style="font-size:12px;color:var(--text-secondary)"><span style="color:#f5222d">*</span> 采购计划</label><input id="pa_planner" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" required autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)">创建时间</label><input id="pa_created" type="date" class="form-input" style="width:100%;box-sizing:border-box" autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)">平台</label><input id="pa_platform" class="form-input" style="width:100%;box-sizing:border-box" placeholder="选填" autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)">运营</label><input id="pa_operator" class="form-input" style="width:100%;box-sizing:border-box" placeholder="选填" autocomplete="off"></div>
<div><label style="font-size:12px;color:var(--text-secondary)">开发专员</label><input id="pa_developer" class="form-input" style="width:100%;box-sizing:border-box" placeholder="选填" autocomplete="off"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeProductAddModal()">取消</button>
<button class="btn btn-primary" onclick="submitProductAdd()">保存</button>
</div>
</div>
`;
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 = '<option value="">请选择</option><option value="包材">包材</option><option value="其他">其他</option>';
} else {
sel.innerHTML = '<option value="">请选择</option><option value="成品">成品</option><option value="清库存">清库存</option><option value="现货">现货</option><option value="赠品">赠品</option><option value="原材料">原材料</option>';
}
}
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 = '<tr class="bom-bind-row">';
html += '<td>' + idx + '</td>';
html += '<td><input class="form-input bom-mat-code" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" placeholder="必填" autocomplete="off"></td>';
html += '<td><input class="form-input bom-mat-name" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" placeholder="必填" autocomplete="off"></td>';
html += '<td><input class="form-input bom-mat-type" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;background:#f5f5f5;color:#999;text-align:center" value="包材" readonly></td>';
html += '<td><input class="form-input bom-sup-code" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;background:#f5f5f5;color:#999" readonly placeholder="自动匹配"></td>';
html += '<td style="position:relative"><input class="form-input bom-sup-name" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" placeholder="输入关键词" autocomplete="off">';
html += '<div class="bom-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></td>';
html += '<td style="width:65px;text-align:center"><input class="form-input bom-mat-qty" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;text-align:center" type="number" value="1" min="0.01" step="0.01" autocomplete="off"></td>';
html += '<td style="width:30px;text-align:center"><span style="cursor:pointer;color:#999;font-size:14px" onclick="removeBomBindRow(this)">✕</span></td>';
html += '</tr>';
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 = [
'<div class="modal" style="width:95vw;max-width:1100px">',
'<div class="modal-header">',
'<h3>📦 绑定包材 BOM</h3>',
'<span style="font-size:13px;color:#666">为成品 <strong>' + escHtml(productCode) + '</strong> 绑定包材</span>',
'<span class="modal-close" onclick="confirmCloseBomBind(\'' + escJs(productCode) + '\')" style="margin-left:auto;font-size:22px;cursor:pointer;color:#999">&times;</span>',
'</div>',
'<div class="modal-body" style="padding:10px 16px;overflow:auto;max-height:65vh">',
'<table class="data-table" style="width:100%;font-size:12px">',
'<thead><tr><th style="width:30px">#</th><th>货品编号</th><th>货品名称</th><th style="width:60px">货品类型</th><th>供应商编码</th><th>供应商名称</th><th style="width:60px;text-align:center">用量</th><th style="width:30px"></th></tr></thead>',
'<tbody id="bomBindTbody">' + window._bomBindRowHtml(1) + '</tbody>',
'</table>',
'<div style="margin-top:6px"><button class="btn btn-sm" onclick="addBomBindRow()">+ 添加一行</button></div>',
'</div>',
'<div class="modal-footer" style="display:flex;justify-content:space-between">',
'<button class="btn" onclick="confirmCloseBomBind(\'' + escJs(productCode) + '\')">跳过</button>',
'<button class="btn btn-primary" onclick="submitBomBind(\'' + escJs(productCode) + '\')">✅ 确认绑定</button>',
'</div>',
'</div>'
].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 = '<span>' + escHtml(s.supplier_name) + '</span>' + (s.supplier_code ? ' <code style="font-size:11px;color:#999">' + escHtml(s.supplier_code) + '</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 = '<tr><td colspan="5" style="text-align:center;padding:40px;color:#999">暂无绑定包材</td></tr>';
} else {
materials.forEach(function(m, i) {
bodyHtml += '<tr><td>' + (i + 1) + '</td><td><code>' + escHtml(m.material_code || '-') + '</code></td><td>' + escHtml(m.material_name || '-') + '</td><td style="text-align:center">' + m.bom_qty + '</td><td>' + escHtml(m.material_supplier || '-') + '</td></tr>';
});
}
overlay.innerHTML = [
'<div class="modal" style="width:600px">',
'<div class="modal-header"><h3>📋 包材清单</h3><code style="font-size:12px;color:#666">' + escHtml(productCode) + '</code><span class="modal-close" onclick="document.getElementById(\'viewBomModal\').remove()" style="margin-left:auto;font-size:22px;cursor:pointer;color:#999">✕</span></div>',
'<div class="modal-body" style="padding:12px 20px;max-height:60vh;overflow:auto">',
'<table class="data-table" style="width:100%;font-size:13px">',
'<thead><tr><th style="width:40px">#</th><th>物料编号</th><th>物料名称</th><th style="width:60px;text-align:center">用量</th><th>供应商</th></tr></thead>',
'<tbody>' + bodyHtml + '</tbody>',
'</table>',
'</div>',
'<div class="modal-footer" style="display:flex;justify-content:space-between">',
'<button class="btn" onclick="syncProductBom(\'' + escJs(productCode) + '\')" style="color:#722ed1;border-color:#722ed1">🔄 同步</button>',
'<button class="btn" onclick="document.getElementById(\'viewBomModal\').remove()">关闭</button>',
'</div>',
'</div>'
].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 += '<label style="display:flex;align-items:center;gap:6px;padding:4px 0;font-size:13px;cursor:pointer">' +
'<input type="checkbox" class="export-field-cb" value="' + f.field + '" ' + checked + '> ' + f.label +
'</label>';
});
overlay.innerHTML = [
'<div class="modal" style="width:420px">',
'<div class="modal-header"><h3>📤 导出字段选择</h3><span class="modal-close" onclick="document.getElementById(\'prodExportModal\').remove()">&times;</span></div>',
'<div class="modal-body" style="max-height:60vh;overflow-y:auto;padding:12px 20px">',
'<div style="margin-bottom:8px">',
'<label style="font-size:12px;cursor:pointer"><input type="checkbox" id="export-cb-all" checked onchange="toggleExportAll(this.checked)"> <strong>全选</strong></label>',
'</div>',
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:2px 16px">' + checkboxesHtml + '</div>',
'</div>',
'<div class="modal-footer" style="display:flex;justify-content:flex-end;gap:8px;padding:10px 20px">',
'<button class="btn" onclick="document.getElementById(\'prodExportModal\').remove()">取消</button>',
'<button class="btn btn-primary" onclick="confirmExportProducts()">确认导出</button>',
'</div>',
'</div>'
].join('');
document.body.appendChild(overlay);
}
window.toggleExportAll = function(checked) {
document.querySelectorAll('.export-field-cb').forEach(function(cb) { cb.checked = checked; });
};
window.confirmExportProducts = function() {
var type = prodActiveTab === '成品' ? '非包材' : '非成品专用';
var selected = [];
document.querySelectorAll('.export-field-cb:checked').forEach(function(cb) {
selected.push(cb.value);
});
if (selected.length === 0) {
alert('请至少选择一个字段');
return;
}
// 关闭弹窗
var overlay = document.getElementById('prodExportModal');
if (overlay) overlay.remove();
var url = '/api/products/export?product_type=' + encodeURIComponent(type) + '&fields=' + encodeURIComponent(selected.join(','));
window.open(url, '_blank');
};
// ─── 产品导入 ───
function openProductImportModal() {
// 每次打开重建弹窗,避免旧缓存导致元素缺失
var oldModal = document.getElementById('prodImportModal');
if (oldModal) oldModal.remove();
var modal = document.createElement('div');
modal.id = 'prodImportModal';
modal.className = 'modal-overlay';
modal.style.display = 'flex';
// 根据当前tab决定模板下载链接
var isPackaging = prodActiveTab === '非成品';
var templateUrl = isPackaging ? '/api/products/template?type=包材' : '/api/products/template';
var templateLabel = isPackaging ? '📥 下载包材导入模板' : '📥 下载导入模板';
modal.innerHTML = `
<div class="modal" style="width:420px">
<div class="modal-header">
<h3>导入产品</h3>
<span class="modal-close" onclick="closeProductImportModal()">&times;</span>
</div>
<div class="modal-body" style="padding:20px;text-align:center">
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:16px">
请先下载模板填写数据,再上传CSV文件导入
</p>
<a class="btn" href="${templateUrl}" target="_blank" style="display:inline-block;margin-bottom:16px">${templateLabel}</a>
<div style="border:2px dashed var(--border,#e8e8e8);border-radius:8px;padding:20px;cursor:pointer;transition:border-color .2s"
onmouseenter="this.style.borderColor='var(--primary)'" onmouseleave="this.style.borderColor='var(--border,#e8e8e8)'"
onclick="document.getElementById('prodImportFile').click()">
<div style="font-size:36px;margin-bottom:8px">📂</div>
<div style="font-size:14px;color:var(--text)">点击选择 CSV 文件</div>
<div style="font-size:12px;color:var(--text-light);margin-top:4px">或拖拽文件到此区域</div>
</div>
<input type="file" id="prodImportFile" accept=".csv" style="display:none" onchange="previewProductImportFile()">
<div id="prodImportPreview" style="margin-top:12px;font-size:13px;color:var(--text-secondary)"></div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeProductImportModal()">取消</button>
<button class="btn btn-primary" id="prodImportBtn" disabled onclick="submitProductImport()">开始导入</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
function closeProductImportModal() {
var modal = document.getElementById('prodImportModal');
if (modal) modal.style.display = 'none';
}
function previewProductImportFile() {
var fileInput = document.getElementById('prodImportFile');
var preview = document.getElementById('prodImportPreview');
var btn = document.getElementById('prodImportBtn');
if (fileInput.files && fileInput.files.length > 0) {
preview.textContent = '已选择:' + fileInput.files[0].name + '(' + (fileInput.files[0].size / 1024).toFixed(1) + 'KB)';
btn.disabled = false;
} else {
preview.textContent = '';
btn.disabled = true;
}
}
function submitProductImport() {
var fileInput = document.getElementById('prodImportFile');
if (!fileInput.files || !fileInput.files[0]) return;
var formData = new FormData();
formData.append('file', fileInput.files[0]);
document.getElementById('prodImportBtn').disabled = true;
document.getElementById('prodImportBtn').textContent = '导入中...';
fetch('/api/products/import', {
method: 'POST',
body: formData
})
.then(function(r) { return r.json(); })
.then(function(res) {
alert(res.msg || (res.code === 0 ? '✅ 导入成功' : '❌ 导入失败'));
closeProductImportModal();
loadProducts(1);
})
.catch(function() { alert('❌ 网络错误'); })
.finally(function() {
document.getElementById('prodImportBtn').disabled = false;
document.getElementById('prodImportBtn').textContent = '开始导入';
});
}
// ─── 产品基础表 字段设置弹窗 ───
function renderProdColCheckboxes() {
const grid = document.getElementById('prodColGrid');
if (!grid) return;
grid.innerHTML = '';
const order = window._prodColDragOrder || PROD_COLUMNS.map(function(c) { return c.field; });
var tab = prodActiveTab;
var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer'];
order.forEach(function(field) {
const col = PROD_COLUMNS.find(function(c) { return c.field === field; });
if (!col) return;
// 包材tab隐藏不可用字段
if (tab === '非成品' && skipFields.indexOf(field) >= 0) return;
const checked = window._prodColPendingVisible[field] !== false ? 'checked' : '';
const label = document.createElement('label');
label.draggable = true;
label.dataset.field = field;
label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px';
label.innerHTML = '<span style="color:#bbb;font-size:12px;cursor:grab">⠿</span><input type="checkbox" id="prod-cb-' + field + '" ' + checked + ' onchange="toggleProdColPending(\'' + field + '\')"> ' + 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 = '<tr><th style="width:50px">序号</th>';
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 += '<th' + sortAttr + ' class="col-' + c.field + '"' + vis + '>' + c.label + '</th>';
});
theadHtml += '<th class="col-action" style="width:120px;min-width:120px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th>';
theadHtml += '</tr>';
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 = `
<div class="page-header">
<h1>仓库信息管理</h1>
<p class="page-desc">仓库信息与分类</p>
</div>
<div style="display:flex;gap:20px;align-items:stretch">
<div style="flex:1;min-width:0;display:flex;flex-direction:column">
<div class="card" style="display:flex;flex-direction:column;flex:1;min-height:0">
<div class="card-header"><h3>仓库列表</h3><div style="margin-left:auto;display:flex;gap:6px"><button class="btn btn-sm" onclick="openWarehouseImportModal()">📥 导入</button></div></div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="whTable">
<thead><tr><th style="text-align:center">序号</th><th style="text-align:center">仓库编号</th><th style="text-align:center">仓库名称</th><th style="text-align:center">仓库分类</th><th style="text-align:center">仓库属性</th><th style="text-align:center">停用</th><th style="text-align:center">备注</th><th class="col-action" style="width:120px;min-width:120px;text-align:center;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th></tr></thead>
<tbody id="whTbody"></tbody>
</table>
</div></div>
<div id="whPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div>
</div>
</div>
<div style="width:320px;flex-shrink:0">
<div class="card">
<div class="card-header"><h3>➕ 添加/编辑仓库</h3></div>
<div class="card-body">
<div class="form-group">
<label>仓库编号</label>
<input type="text" id="whCode" class="form-input" placeholder="如 25">
</div>
<div class="form-group">
<label>仓库名称</label>
<input type="text" id="whName" class="form-input" placeholder="如 25××仓库">
</div>
<div class="form-group">
<label>仓库分类</label>
<select id="whCategory" class="form-input">
<option value="内部">内部</option>
<option value="平台">平台</option>
<option value="分销委外仓">分销委外仓</option>
</select>
</div>
<div class="form-group">
<label>仓库属性(可多选)</label>
<div style="display:flex;gap:12px;margin-top:4px">
<label style="cursor:pointer;font-size:13px;user-select:none"><input type="checkbox" id="whPropYuncang" value="云仓"> 云仓</label>
<label style="cursor:pointer;font-size:13px;user-select:none"><input type="checkbox" id="whPropWaicang" value="外仓"> 外仓</label>
<label style="cursor:pointer;font-size:13px;user-select:none"><input type="checkbox" id="whPropHaiwaicang" value="海外仓"> 海外仓</label>
</div>
</div>
<div class="form-group">
<label>停用</label>
<select id="whDisabled" class="form-input">
<option value="否">否</option>
<option value="是">是</option>
</select>
</div>
<div class="form-group">
<label>备注</label>
<input type="text" id="whRemark" class="form-input" placeholder="可选">
</div>
<button class="btn btn-primary" onclick="saveWarehouse()" style="width:100%;margin-top:8px">💾 保存仓库</button>
<div id="whSaveMsg" style="margin-top:8px;font-size:13px"></div>
</div>
</div>
</div>
</div>
`;
fetchWarehouses(1);
}
function fetchWarehouses(page) {
whCurrentPage = page || 1;
var url = '/api/warehouse/list?page=' + whCurrentPage + '&size=' + WH_PAGE_SIZE;
fetch(url)
.then(r => r.json())
.then(res => {
if (res.code !== 0) return;
var data = res.data || {};
var rows = data.rows || [];
var total = data.total || 0;
var offset = (whCurrentPage - 1) * WH_PAGE_SIZE;
var tbody = document.getElementById('whTbody');
tbody.innerHTML = rows.map(function(w, i) {
var code = escHtml(w.warehouse_code || '-');
var wp = w.warehouse_property || '';
var dp = w.disabled || '否';
// 仓库属性:选中项显示为标签
var wpLabels = wp ? wp.split(',').map(function(p) { return '<span style="display:inline-block;background:#e6f7ff;color:#1890ff;border:1px solid #91d5ff;border-radius:4px;padding:1px 6px;font-size:12px;margin-right:2px">' + escHtml(p.trim()) + '</span>'; }).join('') : '<span style="color:#999">-</span>';
// 停用:选中状态
var dpLabel = dp === '是' ? '<span style="color:#ff4d4f;font-weight:600">是</span>' : '<span style="color:#999">否</span>';
return '<tr><td style="text-align:center">' + (offset + i + 1) + '</td><td style="text-align:center"><code>' + code + '</code></td><td style="text-align:center">' + escHtml(w.warehouse_name || '-') + '</td><td style="text-align:center">' + escHtml(w.category || '-') + '</td>' +
'<td style="text-align:center;min-width:140px">' +
'<div style="display:inline-flex;gap:6px;align-items:center">' +
'<label style="cursor:pointer;font-size:12px;user-select:none"><input type="checkbox" data-wp="云仓" onchange="updateWhProperty(\'' + escJs(w.warehouse_code) + '\')"' + (wp.indexOf('云仓') >= 0 ? ' checked' : '') + '> 云仓</label>' +
'<label style="cursor:pointer;font-size:12px;user-select:none"><input type="checkbox" data-wp="外仓" onchange="updateWhProperty(\'' + escJs(w.warehouse_code) + '\')"' + (wp.indexOf('外仓') >= 0 ? ' checked' : '') + '> 外仓</label>' +
'<label style="cursor:pointer;font-size:12px;user-select:none"><input type="checkbox" data-wp="海外仓" onchange="updateWhProperty(\'' + escJs(w.warehouse_code) + '\')"' + (wp.indexOf('海外仓') >= 0 ? ' checked' : '') + '> 海外仓</label>' +
'</div>' +
'</td>' +
'<td style="text-align:center"><select style="padding:2px 4px;font-size:12px;border-radius:4px;border:1px solid #d9d9d9" onchange="updateWhDisabled(\'' + escJs(w.warehouse_code) + '\',this.value)">' +
'<option value="否"' + (dp === '否' ? ' selected' : '') + '>否</option>' +
'<option value="是"' + (dp === '是' ? ' selected' : '') + '>是</option>' +
'</select></td>' +
'<td style="text-align:center">' + escHtml(w.remark || '-') + '</td>' +
'<td style="text-align:center;white-space:nowrap"><button class="btn btn-sm btn-outline" onclick="editWarehouse(this)" style="margin-right:4px">编辑</button><button class="btn btn-sm btn-outline-danger" onclick="deleteWarehouse(\'' + escJs(w.warehouse_code) + '\')" style="color:#f5222d;border-color:#f5222d">删除</button></td></tr>';
'<td style="white-space:nowrap"><button class="btn btn-sm btn-outline" onclick="editWarehouse(this)" style="margin-right:4px">编辑</button><button class="btn btn-sm btn-outline-danger" onclick="deleteWarehouse(\'' + escJs(w.warehouse_code) + '\')" style="color:#f5222d;border-color:#f5222d">删除</button></td></tr>';
}).join('') || '<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--text-light)">暂无数据</td></tr>';
var pel = document.getElementById('whPagination');
if (pel) {
var totalPages = Math.ceil(total / WH_PAGE_SIZE);
if (totalPages <= 1) {
pel.innerHTML = '';
} else {
var ph = '<div class="pagination-inner">';
ph += '<button class="btn btn-sm" onclick="fetchWarehouses(1)" ' + (whCurrentPage <= 1 ? 'disabled' : '') + '>&#9664;&#9664;</button>';
ph += '<button class="btn btn-sm" onclick="fetchWarehouses(' + (whCurrentPage - 1) + ')" ' + (whCurrentPage <= 1 ? 'disabled' : '') + '>&#9664;</button>';
var start = Math.max(1, whCurrentPage - 2);
var end = Math.min(totalPages, whCurrentPage + 2);
for (var i = start; i <= end; i++) {
ph += '<button class="btn btn-sm ' + (i === whCurrentPage ? 'btn-primary' : '') + '" onclick="fetchWarehouses(' + i + ')">' + i + '</button>';
}
ph += '<button class="btn btn-sm" onclick="fetchWarehouses(' + (whCurrentPage + 1) + ')" ' + (whCurrentPage >= totalPages ? 'disabled' : '') + '>&#9654;</button>';
ph += '<button class="btn btn-sm" onclick="fetchWarehouses(' + totalPages + ')" ' + (whCurrentPage >= totalPages ? 'disabled' : '') + '>&#9654;&#9654;</button>';
ph += '<span class="page-info" style="margin-left:8px">' + whCurrentPage + '/' + totalPages + '页 共' + total + '条</span>';
ph += '</div>';
pel.innerHTML = ph;
}
}
});
}
window.saveWarehouse = function() {
const code = document.getElementById('whCode').value.trim();
const name = document.getElementById('whName').value.trim();
const category = document.getElementById('whCategory').value;
const remark = (document.getElementById('whRemark')?.value || '').trim();
// 仓库属性
var wpVals = [];
if (document.getElementById('whPropYuncang') && document.getElementById('whPropYuncang').checked) wpVals.push('云仓');
if (document.getElementById('whPropWaicang') && document.getElementById('whPropWaicang').checked) wpVals.push('外仓');
if (document.getElementById('whPropHaiwaicang') && document.getElementById('whPropHaiwaicang').checked) wpVals.push('海外仓');
var warehouse_property = wpVals.join(',');
var disabled = document.getElementById('whDisabled') ? document.getElementById('whDisabled').value : '否';
if (!code || !name) {
document.getElementById('whSaveMsg').innerHTML = '<span style="color:red">⚠️ 编号和名称不能为空</span>';
return;
}
const btn = document.querySelector('.btn-primary[onclick="saveWarehouse()"]');
btn.disabled = true;
btn.textContent = '保存中...';
fetch('/api/warehouse/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({warehouse_code: code, warehouse_name: name, category: category, remark: remark, warehouse_property: warehouse_property, disabled: disabled})
})
.then(r => r.json())
.then(res => {
btn.disabled = false;
btn.textContent = '💾 保存仓库';
if (res.code === 0) {
document.getElementById('whSaveMsg').innerHTML = '<span style="color:green">✅ ' + (res.msg || '保存成功') + '</span>';
document.getElementById('whCode').value = '';
document.getElementById('whName').value = '';
fetchWarehouses(1);
} else {
document.getElementById('whSaveMsg').innerHTML = '<span style="color:red">⚠️ ' + (res.msg || '保存失败') + '</span>';
}
}).catch(() => {
btn.disabled = false;
btn.textContent = '💾 保存仓库';
document.getElementById('whSaveMsg').innerHTML = '<span style="color:red">⚠️ 网络错误</span>';
});
}
// 仓库属性复选框变更
window.updateWhProperty = function(code) {
// 找到这一行对应的复选框
var rowCbs = [];
var rows = document.querySelectorAll('#whTbody tr');
for (var ri = 0; ri < rows.length; ri++) {
var codeCell = rows[ri].querySelector('td:nth-child(2) code');
if (codeCell && codeCell.textContent.trim() === code) {
rowCbs = rows[ri].querySelectorAll('input[type="checkbox"][data-wp]');
break;
}
}
var vals = [];
rowCbs.forEach(function(cb) { if (cb.checked) vals.push(cb.getAttribute('data-wp')); });
var propVal = vals.join(',');
fetch('/api/warehouse/update_field', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({warehouse_code: code, field: 'warehouse_property', value: propVal})
}).then(function(r) { return r.json(); }).then(function(res) {
if (res.code === 0) {
// 视觉反馈
} else {
alert('保存失败: ' + (res.msg || ''));
}
}).catch(function() {});
};
// 停用下拉变更
window.updateWhDisabled = function(code, val) {
fetch('/api/warehouse/update_field', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({warehouse_code: code, field: 'disabled', value: val})
}).then(function(r) { return r.json(); }).then(function(res) {
if (res.code !== 0) {
alert('保存失败: ' + (res.msg || ''));
}
}).catch(function() {});
};
window.editWarehouse = function(btn) {
// 从按钮所在行获取仓库数据
var row = btn.parentElement.parentElement;
var cells = row.cells;
if (cells.length < 7) return;
var code = cells[1].textContent.trim();
var name = cells[2].textContent.trim();
var category = cells[3].textContent.trim();
// 读取仓库属性(从复选框获取)
var wpCheckboxes = row.querySelectorAll('input[type="checkbox"][data-wp]');
var wpVals = [];
wpCheckboxes.forEach(function(cb) { if (cb.checked) wpVals.push(cb.getAttribute('data-wp')); });
var warehouseProperty = wpVals.join(',');
// 读取停用
var dpSelect = row.querySelector('select');
var disabledVal = dpSelect ? dpSelect.value : '否';
var remark = cells[7] ? cells[7].textContent.trim() : '';
// 移除旧弹窗
var oldModal = document.getElementById('editWarehouseModal');
if (oldModal) oldModal.remove();
// 创建弹窗
var modal = document.createElement('div');
modal.id = 'editWarehouseModal';
modal.className = 'modal-overlay';
modal.style.display = 'flex';
modal.innerHTML = '<div class="modal" style="width:480px">' +
'<div class="modal-header">' +
'<h3>编辑仓库</h3>' +
'<span class="modal-close" onclick="document.getElementById(\'editWarehouseModal\').style.display=\'none\'">&times;</span>' +
'</div>' +
'<div class="modal-body">' +
'<div class="form-group">' +
'<label>仓库编号</label>' +
'<input type="text" id="editWhCode" class="form-input" value="' + escHtml(code) + '" readonly>' +
'</div>' +
'<div class="form-group">' +
'<label>仓库名称</label>' +
'<input type="text" id="editWhName" class="form-input" value="' + escHtml(name) + '">' +
'</div>' +
'<div class="form-group">' +
'<label>仓库分类</label>' +
'<select id="editWhCategory" class="form-input">' +
'<option value="内部"' + (category === '内部' ? ' selected' : '') + '>内部</option>' +
'<option value="平台"' + (category === '平台' ? ' selected' : '') + '>平台</option>' +
'<option value="分销委外仓"' + (category === '分销委外仓' ? ' selected' : '') + '>分销委外仓</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label>仓库属性(可多选)</label>' +
'<div style="display:flex;gap:12px;margin-top:4px">' +
'<label style="cursor:pointer;font-size:13px;user-select:none"><input type="checkbox" id="editWhPropYuncang" value="云仓"' + (warehouseProperty.indexOf('云仓') >= 0 ? ' checked' : '') + '> 云仓</label>' +
'<label style="cursor:pointer;font-size:13px;user-select:none"><input type="checkbox" id="editWhPropWaicang" value="外仓"' + (warehouseProperty.indexOf('外仓') >= 0 ? ' checked' : '') + '> 外仓</label>' +
'<label style="cursor:pointer;font-size:13px;user-select:none"><input type="checkbox" id="editWhPropHaiwaicang" value="海外仓"' + (warehouseProperty.indexOf('海外仓') >= 0 ? ' checked' : '') + '> 海外仓</label>' +
'</div>' +
'</div>' +
'<div class="form-group">' +
'<label>停用</label>' +
'<select id="editWhDisabled" class="form-input">' +
'<option value="否"' + (disabledVal === '否' ? ' selected' : '') + '>否</option>' +
'<option value="是"' + (disabledVal === '是' ? ' selected' : '') + '>是</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label>备注</label>' +
'<input type="text" id="editWhRemark" class="form-input" value="' + escHtml(remark) + '">' +
'</div>' +
'<div id="editWhSaveMsg" style="margin-top:8px;font-size:13px;color:green"></div>' +
'</div>' +
'<div class="modal-footer">' +
'<button class="btn" onclick="document.getElementById(\'editWarehouseModal\').style.display=\'none\'">取消</button>' +
'<button class="btn btn-primary" onclick="saveEditedWarehouse()">💾 保存</button>' +
'</div>' +
'</div>';
document.body.appendChild(modal);
};
window.saveEditedWarehouse = function() {
var code = document.getElementById('editWhCode').value.trim();
var name = document.getElementById('editWhName').value.trim();
var category = document.getElementById('editWhCategory').value;
// 仓库属性
var wpVals = [];
if (document.getElementById('editWhPropYuncang').checked) wpVals.push('云仓');
if (document.getElementById('editWhPropWaicang').checked) wpVals.push('外仓');
if (document.getElementById('editWhPropHaiwaicang').checked) wpVals.push('海外仓');
var warehouseProperty = wpVals.join(',');
var disabled = document.getElementById('editWhDisabled').value;
var remark = document.getElementById('editWhRemark').value.trim();
var msgEl = document.getElementById('editWhSaveMsg');
if (!code || !name) {
msgEl.innerHTML = '<span style="color:red">⚠️ 编号和名称不能为空</span>';
return;
}
fetch('/api/warehouse/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({warehouse_code: code, warehouse_name: name, category: category, warehouse_property: warehouseProperty, disabled: disabled, remark: remark})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
msgEl.innerHTML = '<span style="color:green">✅ ' + (res.msg || '保存成功') + '</span>';
document.getElementById('editWarehouseModal').style.display = 'none';
fetchWarehouses(1);
} else {
msgEl.innerHTML = '<span style="color:red">⚠️ ' + (res.msg || '保存失败') + '</span>';
}
})
.catch(function() {
msgEl.innerHTML = '<span style="color:red">⚠️ 网络错误</span>';
});
};
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 = [
'<div class="modal" style="width:420px">',
'<div class="modal-header"><h3>导入仓库</h3><span class="modal-close" onclick="document.getElementById(\'whImportModal\').remove()">&times;</span></div>',
'<div class="modal-body" style="padding:20px;text-align:center">',
'<p style="font-size:13px;color:#888;margin-bottom:14px">请先下载模板填写数据,再上传CSV文件导入</p>',
'<a class="btn" href="/api/warehouse/export_template" target="_blank" style="display:inline-block;margin-bottom:16px">📥 下载导入模板</a>',
'<div style="border:2px dashed #e8e8e8;border-radius:8px;padding:20px;cursor:pointer"',
'onmouseenter="this.style.borderColor=\'var(--primary)\';" onmouseleave="this.style.borderColor=\'#e8e8e8\';"',
'onclick="document.getElementById(\'whImportFile\').click()">',
'<div style="font-size:36px;margin-bottom:8px">📂</div>',
'<div style="font-size:14px;color:#333">点击选择 CSV 文件</div>',
'</div>',
'<input type="file" id="whImportFile" accept=".csv" style="display:none" onchange="previewWhImportFile()">',
'<div id="whImportPreview" style="margin-top:10px;font-size:13px;color:#888"></div>',
'</div>',
'<div class="modal-footer">',
'<button class="btn" onclick="document.getElementById(\'whImportModal\').remove()">取消</button>',
'<button class="btn btn-primary" id="whImportBtn" disabled onclick="submitWhImport()">开始导入</button>',
'</div>',
'</div>'
].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 = `
<div class="page-header">
<h1>${title}</h1>
<p class="page-desc">${desc}</p>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px">
<select id="invDate" class="form-input" style="width:130px"><option value="">全部日期</option></select>
<input type="text" id="invProdCode" class="form-input" style="width:110px" placeholder="货品编号">
<input type="text" id="invProdName" class="form-input" style="width:130px" placeholder="货品名称">
<input type="text" id="invWarehouse" class="form-input" style="width:120px" placeholder="仓库名称">
<button class="btn btn-primary" onclick="searchInventory()">查询</button>
<button class="btn" onclick="resetInventoryFilters()">重置</button>
<span class="data-count" id="invCount"></span>
</div>
</div>
</div>
<div class="page-flex" style="margin-top:15px">
<div class="card" style="margin-top:15px">
<div class="card-header"><h3>${title}明细</h3></div>
<div class="card-body" style="padding:0;overflow:auto;flex:1">
<div class="card-header" style="display:flex;align-items:center;gap:8px;padding:8px 16px;border-bottom:1px solid var(--border)">
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px">
<input type="checkbox" id="invSelectAll" onchange="invToggleAll(this.checked)"> 全选
</label>
<span style="flex:1"></span>
<select id="invExportFormat" class="form-input" style="width:70px;font-size:12px;height:28px">
<option value="xlsx">xlsx</option>
<option value="xls">xls</option>
<option value="csv">csv</option>
</select>
<button class="btn btn-sm btn-primary" onclick="invExportSelected()">📥 导出</button>
<span id="invSelectedCount" style="font-size:12px;color:var(--text-light)"></span>
</div>
<table class="data-table" id="invTable">
<thead style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)"><tr><th style="width:36px;position:sticky;top:0;z-index:4;background:var(--bg,#fff)"><input type="checkbox" id="invCbAll" onchange="invToggleAll(this.checked)"></th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">序号</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">货品编号</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">货品名称</th>${_invProductType === '成品' ? '<th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">品牌</th>' : ''}<th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">仓库</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">可用库存</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">生产在途</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">采购在途</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">采购跟单</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">采购计划</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">供应商</th><th style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)">库存日期</th><th class="col-action" style="width:80px;min-width:80px;position:sticky;top:0;right:0;background:var(--bg,#fff);z-index:5;border-left:1px solid var(--border)">操作</th></tr></thead>
<tbody id="invTbody"></tbody>
</table>
</div>
<div id="invPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div>
</div>
</div>
`;
// 并行加载:筛选选项 + 表格数据,互不等待
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 = '<tr><td colspan="' + cols + '" style="text-align:center;padding:40px;color:var(--text-light)">暂无数据</td></tr>';
} 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 === '成品') ? '<td>' + escHtml(r.brand || '-') + '</td>' : '';
return '<tr><td style="text-align:center"><input type="checkbox" class="inv-row-cb" value="' + escHtml(code) + '" onchange="invUpdateCount()"></td><td>' + (startIdx + i + 1) + '</td><td><code>' + escHtml(code) + '</code></td><td>' + escHtml(r.product_name || '-') + '</td>' + brandCell + '<td>' + escHtml(r.warehouse_name || '-') + '</td><td>' + (r.available_qty || 0) + '</td><td>' + (r.production_in_transit || 0) + '</td><td>' + (r.purchase_in_transit || 0) + '</td><td>' + renderNameBadges(r['采购跟单']) + '</td><td>' + renderNameBadges(r['采购计划']) + '</td><td>' + escHtml(r['供应商'] || '-') + '</td><td>' + (r.data_date || '-') + '</td><td class="col-action" style="position:sticky;right:0;z-index:2;background:var(--bg,#fff);border-left:1px solid var(--border);white-space:nowrap"><button class="btn btn-sm btn-outline" onclick="showProductDetailEdit(\'' + escHtml(code) + '\')" title="编辑">编辑</button></td></tr>';
}).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 = '<div style="padding:16px 20px">';
html += '<h3 style="font-size:15px;margin-bottom:12px">选择导出字段</h3>';
html += '<label style="display:flex;align-items:center;gap:6px;padding:6px 0;cursor:pointer;border-bottom:1px solid var(--border);margin-bottom:6px">';
html += '<input type="checkbox" id="invFieldCbAll" checked onchange="(function(c){document.querySelectorAll(\'.inv-field-cb\').forEach(function(cb){cb.checked=c.checked});})(this)"> <strong>全选</strong></label>';
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 16px">';
INV_EXPORT_FIELDS.forEach(function(f) {
var checked = window._invExportSelectedFields.indexOf(f.key) >= 0;
html += '<label style="display:flex;align-items:center;gap:6px;padding:4px 0;cursor:pointer">';
html += '<input type="checkbox" class="inv-field-cb" value="' + f.key + '"' + (checked ? ' checked' : '') + '>';
html += f.label + '</label>';
});
html += '</div>';
html += '<div style="text-align:right;margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">';
html += '<button class="btn btn-sm" onclick="document.getElementById(\'invFieldModal\').remove()">取消</button>';
html += '<button class="btn btn-sm btn-primary" onclick="invConfirmExportFields()" style="margin-left:8px">确定</button></div></div>';
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 = '<div style="background:#fff;border-radius:12px;width:420px;max-width:90vw;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' + html + '</div>';
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 = '<div style="padding:20px 24px">';
html += '<h3 style="font-size:16px;margin-bottom:6px">选择导出字段</h3>';
html += '<p style="font-size:12px;color:var(--text-secondary);margin-bottom:12px">已选 ' + checked.length + ' 条数据,格式:' + fmt.toUpperCase() + '</p>';
html += '<div style="max-height:320px;overflow-y:auto;padding:4px 0">';
html += '<label style="display:flex;align-items:center;gap:6px;padding:6px 0;cursor:pointer;border-bottom:1px solid #f0f0f0;margin-bottom:6px">';
html += '<input type="checkbox" id="invExportFieldAll" checked onchange="(function(c){document.querySelectorAll(\'.inv-export-fc\').forEach(function(cb){cb.checked=c.checked})})(this)"> <strong>全选</strong></label>';
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:2px 16px">';
INV_EXPORT_FIELDS.forEach(function(f) {
var checked2 = curFields.indexOf(f.key) >= 0;
html += '<label style="display:flex;align-items:center;gap:6px;padding:5px 0;cursor:pointer">';
html += '<input type="checkbox" class="inv-export-fc" value="' + f.key + '"' + (checked2 ? ' checked' : '') + ' onchange="(function(){var all=document.getElementById(\'invExportFieldAll\');if(all){var cbs=document.querySelectorAll(\'.inv-export-fc\');all.checked=Array.from(cbs).every(function(cb){return cb.checked})}})()">';
html += f.label + '</label>';
});
html += '</div></div>';
html += '<div style="text-align:right;margin-top:14px;padding-top:14px;border-top:1px solid var(--border)">';
html += '<button class="btn" onclick="document.getElementById(\'invExportModal\').remove()">取消</button>';
html += '<button class="btn btn-primary" id="invExportDoBtn" style="margin-left:8px">确认导出</button></div></div>';
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 = '<div style="background:#fff;border-radius:12px;width:460px;max-width:90vw;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' + html + '</div>';
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 = `
<div class="page-header">
<h1>供应商管理</h1>
<p class="page-desc">供应商信息管理</p>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px">
<input type="text" id="supCode" class="form-input" style="width:120px" placeholder="供应商编码" onkeydown="if(event.key==='Enter')searchSuppliers()">
<input type="text" id="supName" class="form-input" style="width:150px" placeholder="供应商名称" onkeydown="if(event.key==='Enter')searchSuppliers()">
<button class="btn btn-primary" onclick="searchSuppliers()">查询</button>
<button class="btn" onclick="resetSupplierFilters()">重置</button>
<button class="btn" style="background:#e6fffb;color:#13c2c2;border:1px solid #87e8de" onclick="addSupplier()">+ 新增供应商</button>
<button class="btn btn-outline" onclick="exportSuppliers()">📤 导出</button>
<button class="btn btn-outline" onclick="importSuppliers()">📥 导入</button>
<span class="data-count" id="supCount"></span>
</div>
</div>
</div>
<div class="page-flex" style="flex:1;min-height:300px">
<div class="card" style="margin-top:15px">
<div class="card-header"><h3>供应商列表</h3></div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="supTable">
<thead style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)"><tr><th>序号</th><th>供应商编码</th><th>供应商名称</th><th>做货类型</th><th>简称</th><th>合同简称</th><th>供应商分组</th><th>审核人</th><th>审核日期</th><th>数据状态</th><th>禁用状态</th><th class="col-action" style="width:160px;min-width:160px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th></tr></thead>
<tbody id="supTbody"></tbody>
</table>
</div></div>
<div id="supPagination" class="card-footer pagination-sticky" style="padding:8px 16px;text-align:center"></div>
</div>
</div>
`;
// 清除筛选框搜索记忆
['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 = '<tr><td colspan="11" style="text-align:center;padding:40px;color:var(--text-light)">暂无数据</td></tr>';
} 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 = '<tr><td>' + (startIdx + i + 1) + '</td>';
html += '<td><code>' + escHtml(supCode) + '</code></td>';
html += '<td>' + escHtml(supName) + '</td>';
html += '<td>' + escHtml(r.production_type || '-') + '</td>';
html += '<td>' + escHtml(r.short_name || '-') + '</td>';
html += '<td>' + escHtml(r.contract_short_name || '-') + '</td>';
html += '<td>' + escHtml(r.supplier_group || '-') + '</td>';
html += '<td>' + escHtml(r.reviewer || '-') + '</td>';
html += '<td>' + (r.review_date ? r.review_date : '-') + '</td>';
html += '<td>' + escHtml(r.data_status || '-') + '</td>';
html += '<td>' + escHtml(r.disabled_status || '-') + '</td>';
html += '<td class="col-action" style="position:sticky;right:0;z-index:2;background:var(--bg,#fff);border-left:1px solid var(--border);white-space:nowrap">';
html += '<button class="btn btn-sm btn-outline" onclick="editSupplier(\'' + escHtml(supCode) + '\')" title="编辑">编辑</button>';
html += '<button class="btn btn-sm btn-outline-danger" onclick="deleteSupplier(\'' + escJs(supCode) + '\')" style="margin-left:4px;color:#f5222d;border-color:#f5222d">删除</button>';
html += '</td></tr>';
return html;
}).join('');
}
document.getElementById('supCount').textContent = '共 ' + (data.total || 0) + ' 条';
const pel = document.getElementById('supPagination');
if (pel) renderPaginationWithSize(pel, page, data.total || 0, SUP_PAGE_SIZE, 'loadSuppliers', 'SUP_PAGE_SIZE');
});
}
window.searchSuppliers = function() { loadSuppliers(1); };
window.resetSupplierFilters = function() {
document.getElementById('supCode').value = '';
document.getElementById('supName').value = '';
loadSuppliers(1);
};
// ─── 供应商导出 ───
function exportSuppliers() {
const code = (document.getElementById('supCode')?.value || '').trim();
const name = (document.getElementById('supName')?.value || '').trim();
let url = '/api/suppliers/export';
if (code) url += '?code=' + encodeURIComponent(code);
if (name) url += (code ? '&' : '?') + 'name=' + encodeURIComponent(name);
window.open(url, '_blank');
}
// ─── 供应商导入 ───
function importSuppliers() {
var overlayId = 'supImportOverlay-' + Date.now();
var overlay = document.createElement('div');
overlay.id = overlayId;
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:30000;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = '<div style="background:#fff;border-radius:12px;padding:24px;width:500px;max-width:94vw;max-height:90vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.18)">' +
'<h2 style="margin:0 0 16px 0;font-size:18px">📥 导入供应商简称</h2>' +
'<div style="margin-bottom:14px">' +
'<div style="display:flex;justify-content:space-between;align-items:center">' +
'<label style="display:block;font-size:13px;font-weight:600;margin:0;color:#555">上传Excel文件</label>' +
'<a href="/api/suppliers/import_template" target="_blank" style="font-size:13px;color:#1890ff;text-decoration:none">📄 下载模板</a>' +
'</div>' +
'<p style="font-size:12px;color:#999;margin:4px 0 10px">文件需包含<strong>供应商名称</strong>列,以及<strong>简称</strong>和/或<strong>合同简称</strong>列。系统将根据供应商名称匹配更新。</p>' +
'<div id="supImportDropzone" style="padding:30px 20px;text-align:center;border:2px dashed #d9d9d9;border-radius:8px;cursor:pointer;transition:all .2s" ondrop="handleSupImportDrop(event)" ondragover="handleSupImportDragOver(event)" ondragleave="handleSupImportDragLeave(event)" onclick="document.getElementById(\'supImportFile\').click()">' +
'<div style="font-size:36px;color:#bbb;margin-bottom:8px">📁</div>' +
'<div style="font-size:13px;color:#999">点击选择文件或拖拽文件到此处</div>' +
'<div style="font-size:11px;color:#ccc;margin-top:4px">仅支持 .xlsx 格式</div>' +
'<input type="file" id="supImportFile" accept=".xlsx" style="display:none" onchange="handleSupImportSelect(event)">' +
'</div>' +
'</div>' +
'<div id="supImportPreview" style="font-size:13px;color:#52c41a;margin:4px 0;min-height:20px"></div>' +
'<div style="display:flex;justify-content:center;gap:12px;margin-top:12px">' +
'<button style="padding:6px 20px;border:1px solid #d9d9d9;border-radius:6px;background:#fff;font-size:13px;cursor:pointer;color:#333" onclick="document.getElementById(\'' + overlayId + '\').remove()">取消</button>' +
'<button style="padding:6px 20px;border:none;border-radius:6px;background:#52c41a;color:#fff;font-size:13px;cursor:pointer" id="supImportBtn" disabled onclick="doImportSuppliers()">开始导入</button>' +
'</div></div>';
document.body.appendChild(overlay);
}
window._supImportFile = null;
function handleSupImportSelect(e) {
var file = e.target.files[0];
setSupImportFile(file);
}
function handleSupImportDrop(e) {
e.preventDefault();
e.stopPropagation();
var dz = document.getElementById('supImportDropzone');
if (dz) { dz.style.borderColor = '#d9d9d9'; dz.style.background = ''; }
var file = e.dataTransfer.files[0];
if (file && file.name.match(/\.xlsx?$/i)) {
setSupImportFile(file);
// 同步到隐藏的file input
var fi = document.getElementById('supImportFile');
if (fi) {
var dt = new DataTransfer();
dt.items.add(file);
fi.files = dt.files;
}
} else {
alert('请上传 .xlsx 格式的文件');
}
}
function handleSupImportDragOver(e) {
e.preventDefault();
e.stopPropagation();
var dz = document.getElementById('supImportDropzone');
if (dz) { dz.style.borderColor = '#1890ff'; dz.style.background = '#f0f8ff'; }
}
function handleSupImportDragLeave(e) {
e.preventDefault();
e.stopPropagation();
var dz = document.getElementById('supImportDropzone');
if (dz) { dz.style.borderColor = '#d9d9d9'; dz.style.background = ''; }
}
function setSupImportFile(file) {
window._supImportFile = file;
var btn = document.getElementById('supImportBtn');
var preview = document.getElementById('supImportPreview');
var dz = document.getElementById('supImportDropzone');
if (file) {
btn.disabled = false;
preview.textContent = '✅ 已选择:' + file.name;
if (dz) {
dz.style.borderColor = '#52c41a';
dz.style.background = '#f6ffed';
}
} else {
btn.disabled = true;
preview.textContent = '';
if (dz) {
dz.style.borderColor = '#d9d9d9';
dz.style.background = '';
}
}
}
function closeImportOverlay() {
var overlays = document.querySelectorAll('[id^="supImportOverlay-"]');
overlays.forEach(function(o) { o.remove(); });
}
function doImportSuppliers() {
if (!window._supImportFile) { alert('请先选择文件'); return; }
var btn = document.getElementById('supImportBtn');
btn.disabled = true;
btn.textContent = '⏳ 导入中...';
var formData = new FormData();
formData.append('file', window._supImportFile);
fetch('/api/suppliers/import', {
method: 'POST',
body: formData
})
.then(function(r) { return r.json(); })
.then(function(res) {
// 先移除导入弹窗,再显示结果
closeImportOverlay();
if (res.code !== 0) {
alert('导入失败:' + (res.msg || '未知错误'));
} else {
var d = res.data;
var msg = '✅ 导入完成\n\n' +
'成功更新:' + d.updated + ' 条\n' +
(d.not_found > 0 ? '未匹配:' + d.not_found + ' 条\n' : '') +
(d.skipped > 0 ? '跳过无数据行:' + d.skipped + ' 条\n' : '');
if (d.not_found_list && d.not_found_list.length > 0) {
msg += '\n未匹配的供应商名称(前20条):\n' + d.not_found_list.join('、');
}
alert(msg);
loadSuppliers(supCurrentPage);
}
})
.catch(function(err) {
closeImportOverlay();
alert('导入异常:' + err.message);
});
}
// ─── 人员信息管理 ───
function loadPersonnelPage(container) {
// ─── 采购跟单分组数据(从全局映射读取) ───
var followGroups = window._personnelGroups.filter(function(g) { return g.name !== '计划小组'; });
// ─── 采购计划分组数据 ───
var planGroups = window._personnelGroups.filter(function(g) { return g.name === '计划小组'; });
var cardsHtml = [
'<div class="personnel-section" style="margin-bottom:24px">',
'<div class="personnel-section-header" style="display:flex;align-items:center;gap:10px;margin-bottom:16px">',
'<span style="font-size:22px">👥</span>',
'<h2 style="margin:0;font-size:18px;color:#333">采购跟单</h2>',
'<span style="font-size:13px;color:var(--text-light)">' + (function(){
var total = 0; followGroups.forEach(function(g){ total += g.members.length; }); return '共 ' + total + ' 人';
})() + '</span>',
'</div>',
'<div class="personnel-group-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px">',
renderPersonnelCards(followGroups),
'</div>',
'</div>',
'<div class="personnel-section">',
'<div class="personnel-section-header" style="display:flex;align-items:center;gap:10px;margin-bottom:16px">',
'<span style="font-size:22px">📋</span>',
'<h2 style="margin:0;font-size:18px;color:#333">采购计划</h2>',
'<span style="font-size:13px;color:var(--text-light)">' + (function(){
var total = 0; planGroups.forEach(function(g){ total += g.members.length; }); return '共 ' + total + ' 人';
})() + '</span>',
'</div>',
'<div class="personnel-group-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px">',
renderPersonnelCards(planGroups),
'</div>',
'</div>'
].join('');
container.innerHTML = [
'<div class="page-header"><h1>人员信息管理</h1></div>',
'<div class="card"><div class="card-body" style="padding:20px 24px">',
'<style>',
'.personnel-card{background:#fff;border:1px solid #eee;border-radius:10px;overflow:hidden;transition:box-shadow .2s}',
'.personnel-card:hover{box-shadow:0 4px 14px rgba(0,0,0,0.08)}',
'.personnel-card-header{padding:12px 16px;display:flex;align-items:center;gap:8px;border-bottom:1px solid #f0f0f0}',
'.personnel-card-header .name{font-size:15px;font-weight:600;color:#333}',
'.personnel-card-body{padding:10px 16px 14px;display:flex;flex-wrap:wrap;gap:8px}',
'.personnel-tag{display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border-radius:20px;font-size:13px;background:#f5f5f5;color:#333;border:1px solid #e8e8e8;transition:all .15s}',
'.personnel-tag:hover{background:#e8e8e8}',
'.personnel-tag.leader{background:#e6f4ff;border-color:#91caff;color:#1677ff;font-weight:600}',
'.personnel-tag.leader .badge{font-size:11px;background:#1677ff;color:#fff;padding:0 6px;border-radius:8px;line-height:18px;margin-left:2px}',
'</style>',
cardsHtml,
'</div></div>'
].join('');
}
function renderPersonnelCards(groups) {
var html = '';
groups.forEach(function(g) {
html += '<div class="personnel-card">';
html += '<div class="personnel-card-header" style="background:' + (g.color || '#fafafa') + '">';
html += '<span style="font-size:16px">' + (g.icon || '👤') + '</span>';
html += '<span class="name">' + escHtml(g.name) + '</span>';
html += '<span style="font-size:12px;color:var(--text-light);margin-left:auto">' + g.members.length + '人</span>';
html += '</div>';
html += '<div class="personnel-card-body">';
g.members.forEach(function(m) {
var isLeader = m === g.leader && g.leader !== '';
var cls = 'personnel-tag' + (isLeader ? ' leader' : '');
html += '<span class="' + cls + '">';
if (isLeader) html += '⭐ ';
html += escHtml(m);
if (isLeader) html += '<span class="badge">组长</span>';
html += '</span>';
});
html += '</div>';
html += '</div>';
});
return html;
}
// ─── 用户管理 ───
function downloadUserTemplate() {
fetch('/api/users/template', {credentials: 'same-origin'})
.then(r => {
if (!r.ok) throw new Error('下载失败');
return r.blob();
})
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '用户导入模板.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
})
.catch(e => alert(e.message));
}
function handleImportUsers() {
const input = document.getElementById('userImportFile');
const file = input.files[0];
if (!file) { alert('请先选择CSV文件'); return; }
const btn = document.getElementById('importBtn');
if (btn) { btn.disabled = true; btn.textContent = '导入中...'; }
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function(e) {
const bytes = new Uint8Array(e.target.result);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
const base64 = btoa(binary);
fetch('/api/users/import', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({csv_base64: base64})
})
.then(r => {
if (!r.ok) throw new Error('请求失败(' + r.status + ')');
return r.json();
})
.then(res => {
if (res.code !== 0) { alert(res.msg); return; }
let msg = res.msg;
if (res.data.errors && res.data.errors.length > 0) {
msg += '\n\n错误详情:\n' + res.data.errors.join('\n');
}
alert(msg);
loadUsers();
})
.catch(err => alert('导入失败:' + err.message))
.finally(() => {
if (btn) { btn.disabled = false; btn.textContent = '导入用户'; }
input.value = '';
});
};
reader.readAsText(file, 'UTF-8');
}
function loadUsersPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>用户管理</h1>
<p class="page-desc">管理系统用户账号与权限</p>
</div>
<!-- 权限角色说明卡 -->
<div class="card" style="margin-bottom:15px">
<div class="card-body" style="padding:12px 16px">
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;font-size:13px">
<span style="font-weight:600;color:var(--text-secondary)">🔐 权限角色:</span>
<span class="tag tag-red" style="cursor:default">超级管理员</span> 全部权限
<span class="tag tag-orange" style="cursor:default">管理员</span> 管理权限
<span class="tag tag-green" style="cursor:default">查看者</span> 只读权限
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>用户列表</h3>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn" onclick="downloadUserTemplate()">下载模板</button>
<input type="file" id="userImportFile" accept=".csv" style="display:none" onchange="handleImportUsers()">
<button class="btn" id="importBtn" onclick="document.getElementById('userImportFile').click()">导入用户</button>
<button class="btn btn-primary" onclick="showUserModal()">+ 新增用户</button>
</div>
</div>
<!-- 搜索栏 -->
<div style="display:flex;gap:10px;padding:12px 16px;background:#fafafa;border-bottom:1px solid var(--border-color);flex-wrap:wrap;align-items:center">
<input type="text" id="userSearchInput" placeholder="🔍 姓名/岗位" class="form-input" style="width:200px" onkeydown="userSearchKeydown(event)" autocomplete="off">
<button class="btn btn-sm" onclick="userDoSearch()" style="background:#1677ff;color:#fff;border:none">搜索</button>
<button class="btn btn-sm" onclick="clearUserFilter()" style="background:#fff;color:#666;border:1px solid #d9d9d9">重置</button>
<select id="userRoleFilter" class="form-input" style="width:120px" onchange="userFilterChange()">
<option value="">全部角色</option>
<option value="super_admin">超级管理员</option>
<option value="admin">管理员</option>
<option value="viewer">查看者</option>
</select>
<select id="userStatusFilter" class="form-input" style="width:110px" onchange="userFilterChange()">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
<div class="card-body" style="padding:0;overflow-x:auto">
<table class="data-table" id="userTable">
<thead>
<tr>
<th>序号</th><th>用户名</th><th>姓名</th><th onclick="toggleRoleSort()" style="cursor:pointer;user-select:none">角色 <span id="roleSortIcon">↕</span></th><th>级别</th><th>岗位</th>
<th>手机</th><th>邮箱</th><th>状态</th><th>最后登录</th><th>操作</th>
</tr>
</thead>
<tbody id="userTbody"></tbody>
</table>
<div id="userPagination" class="pagination-controls" style="padding:8px 16px;text-align:center"></div>
</div>
</div>
<!-- 用户编辑弹窗 -->
<div class="modal-overlay" id="userModal" style="display:none">
<div class="modal" style="width:580px">
<div class="modal-header">
<h3 id="modalTitle">新增用户</h3>
<span class="modal-close" onclick="closeUserModal()">&times;</span>
</div>
<div class="modal-body">
<input type="hidden" id="editUserId">
<div class="form-row">
<div class="form-group" style="flex:1">
<label>用户名 <span style="color:red">*</span></label>
<input type="text" id="editUsername" class="form-input" placeholder="登录用户名" autocomplete="off">
</div>
<div class="form-group" style="flex:1">
<label>密码 <small id="pwHint">(必填)</small></label>
<input type="password" id="editPassword" class="form-input" placeholder="密码" autocomplete="new-password">
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>真实姓名</label>
<input type="text" id="editRealName" class="form-input" placeholder="真实姓名">
</div>
<div class="form-group" style="flex:1">
<label>岗位</label>
<select id="editPosition" class="form-input">
<option value="">请选择</option>
<option value="procurement_planner">📋 采购计划</option>
<option value="procurement_follower">📦 采购跟单</option>
<option value="product_developer">🔧 产品开发</option>
<option value="operator">📊 运营专员</option>
<option value="bp">🤝 BP</option>
<option value="procurement_trainee">🌱 采购管培生</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>角色权限</label>
<select id="editRole" class="form-input">
<option value="super_admin">🔴 超级管理员</option>
<option value="admin">🟠 管理员</option>
<option value="viewer">🟢 查看者</option>
</select>
</div>
<div class="form-group" style="flex:1">
<label>员工级别</label>
<select id="editUserLevel" class="form-input">
<option value="employee">👤 员工</option>
<option value="leader">👥 组长</option>
<option value="director">👑 主管</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>手机</label>
<input type="text" id="editPhone" class="form-input" placeholder="手机号">
</div>
<div class="form-group" style="flex:1">
<label>邮箱</label>
<input type="text" id="editEmail" class="form-input" placeholder="邮箱">
</div>
<div class="form-group" style="flex:0 0 100px">
<label>状态</label>
<select id="editStatus" class="form-input">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeUserModal()">取消</button>
<button class="btn btn-primary" onclick="saveUser()">保存</button>
</div>
</div>
</div>
`;
loadUsers();
}
let userPageSize = 10;
let userCurrentPage = 1;
let userAllData = [];
let _userFullSorted = []; // 缓存完整用户列表(未搜索时的排序),用于保留原始序号
let userRoleSortOrder = ''; // '' 'asc' 'desc'
function userSearchKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
userDoSearch();
}
}
function userDoSearch() {
userCurrentPage = 1;
loadUsers();
}
function userFilterChange() {
userCurrentPage = 1;
loadUsers();
}
function clearUserFilter() {
document.getElementById('userSearchInput').value = '';
document.getElementById('userRoleFilter').value = '';
document.getElementById('userStatusFilter').value = '';
userCurrentPage = 1;
loadUsers();
}
function toggleRoleSort() {
if (userRoleSortOrder === '' || userRoleSortOrder === 'desc') {
userRoleSortOrder = 'asc';
} else {
userRoleSortOrder = 'desc';
}
document.getElementById('roleSortIcon').textContent = userRoleSortOrder === 'asc' ? '↑' : '↓';
userCurrentPage = 1;
loadUsers();
}
const ROLE_MAP = {
'super_admin':'超级管理员',
'admin':'管理员',
'viewer':'查看者'
};
const ROLE_COLOR = {
'super_admin':'tag-red',
'admin':'tag-orange',
'viewer':'tag-green'
};
const POSITION_MAP = {
'procurement_planner':'采购计划',
'procurement_follower':'采购跟单',
'product_developer':'产品开发',
'operator':'运营专员',
'bp':'BP',
'procurement_trainee':'采购管培生'
};
const LEVEL_MAP = {'employee':'员工','leader':'组长','director':'主管'};
const LEVEL_STYLE = {
'employee':'background:#e6f7ff;padding:2px 8px;border-radius:4px;border:1px solid #91d5ff;font-size:12px',
'leader':'background:#fff7e6;padding:2px 8px;border-radius:4px;border:1px solid #ffd591;font-size:12px;font-weight:600',
'director':'background:#fff1f0;padding:2px 8px;border-radius:4px;border:1px solid #ffa39e;font-size:12px;font-weight:600'
};
function loadUsers(page) {
page = page || 1;
var search = document.getElementById('userSearchInput') ? document.getElementById('userSearchInput').value : '';
var role = document.getElementById('userRoleFilter') ? document.getElementById('userRoleFilter').value : '';
var positionFilter = document.getElementById('userPositionFilter') ? document.getElementById('userPositionFilter').value : '';
var status = document.getElementById('userStatusFilter') ? document.getElementById('userStatusFilter').value : '';
var sort = userRoleSortOrder ? 'role_' + userRoleSortOrder : '';
var url = '/api/users/list?search=' + encodeURIComponent(search) + '&role=' + encodeURIComponent(role) + '&position=' + encodeURIComponent(positionFilter) + '&status=' + encodeURIComponent(status) + '&sort=' + encodeURIComponent(sort);
fetch(url)
.then(r => r.json())
.then(res => {
if (res.code !== 0) return;
userAllData = res.data || [];
var totalPages = Math.max(1, Math.ceil(userAllData.length / userPageSize));
if (page > totalPages) page = totalPages;
// 缓存完整排序列表(不带任何筛选条件的全量数据),用于保留原始序号
var searchVal = (document.getElementById('userSearchInput') || {}).value || '';
var roleVal = (document.getElementById('userRoleFilter') || {}).value || '';
var statusVal = (document.getElementById('userStatusFilter') || {}).value || '';
var isFull = !searchVal && !roleVal && !statusVal;
if (isFull) {
_userFullSorted = (res.data || []).slice();
}
renderUserPage(page);
});
}
function renderUserPage(page) {
userCurrentPage = page;
const total = userAllData.length;
const totalPages = Math.max(1, Math.ceil(total / userPageSize));
const start = (page - 1) * userPageSize;
const end = Math.min(start + userPageSize, total);
const pageData = userAllData.slice(start, end);
const tbody = document.getElementById('userTbody');
tbody.innerHTML = '';
pageData.forEach((u, idx) => {
var level = u.user_level || 'employee';
// 从完整列表中查找原始序号(保持搜索前后一致)
var origIdx = -1;
for (var oi = 0; oi < _userFullSorted.length; oi++) {
if (_userFullSorted[oi].id === u.id) { origIdx = oi; break; }
}
var serialIdx = origIdx >= 0 ? origIdx : (start + idx);
tbody.innerHTML += `<tr>
<td><strong>DY${String(serialIdx + 1).padStart(4,'0')}</strong></td>
<td>${u.username}</td>
<td>${u.real_name || '-'}</td>
<td><span class="tag ${ROLE_COLOR[u.role] || 'tag-green'}">${ROLE_MAP[u.role] || u.role}</span></td>
<td><span style="${LEVEL_STYLE[level]}">${LEVEL_MAP[level] || level}</span></td>
<td>${POSITION_MAP[u.position] || u.position || '-'}</td>
<td>${u.phone || '-'}</td>
<td>${u.email || '-'}</td>
<td>${u.status==1 ? '<span class="status-dot active"></span>启用' : '<span class="status-dot"></span>禁用'}</td>
<td>${u.last_login ? new Date(u.last_login).toLocaleDateString('zh-CN', {year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-') : '-'}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="editUser(${u.id})">编辑</button>
${u.is_protected ? '<span class="tag tag-grey" style="cursor:default;opacity:0.6">受保护</span>' : '<button class="btn btn-sm btn-outline-danger" onclick="deleteUser(' + u.id + ',\'' + u.username + '\')">删除</button>'}
</td>
</tr>`;
});
const pagination = document.getElementById('userPagination');
if (!pagination) return;
if (totalPages <= 1) { pagination.innerHTML = ''; return; }
let html = '<div class="pagination-controls">';
if (page > 1) html += `<button class="btn btn-sm" onclick="renderUserPage(${page-1})">上一页</button>`;
html += `<span style="margin:0 12px;color:var(--text-light)">第 ${page}/${totalPages} 页,共 ${total} 条</span>`;
if (page < totalPages) html += `<button class="btn btn-sm" onclick="renderUserPage(${page+1})">下一页</button>`;
html += '</div>';
pagination.innerHTML = html;
}
window.showUserModal = function() {
document.getElementById('modalTitle').textContent = '新增用户';
document.getElementById('editUserId').value = '';
document.getElementById('editUsername').value = '';
document.getElementById('editPassword').value = '';
document.getElementById('editRealName').value = '';
document.getElementById('editRole').value = 'viewer';
document.getElementById('editPosition').value = '';
document.getElementById('editUserLevel').value = 'employee';
document.getElementById('editPhone').value = '';
document.getElementById('editEmail').value = '';
document.getElementById('editStatus').value = '1';
document.getElementById('pwHint').textContent = '(必填)';
document.getElementById('userModal').style.display = 'flex';
};
window.editUser = function(id) {
fetch('/api/users/list')
.then(r => r.json())
.then(res => {
const u = (res.data || []).find(x => x.id === id);
if (!u) return;
document.getElementById('modalTitle').textContent = '编辑用户';
document.getElementById('editUserId').value = u.id;
document.getElementById('editUsername').value = u.username;
document.getElementById('editPassword').value = '';
document.getElementById('editRealName').value = u.real_name || '';
document.getElementById('editRole').value = u.role || 'viewer';
document.getElementById('editUserLevel').value = u.user_level || 'employee';
document.getElementById('editPosition').value = u.position || '';
document.getElementById('editPhone').value = u.phone || '';
document.getElementById('editEmail').value = u.email || '';
document.getElementById('editStatus').value = u.status;
document.getElementById('pwHint').textContent = '(留空不修改)';
document.getElementById('userModal').style.display = 'flex';
});
};
window.closeUserModal = function() {
document.getElementById('userModal').style.display = 'none';
};
window.saveUser = function() {
const id = document.getElementById('editUserId').value;
const data = {
username: document.getElementById('editUsername').value,
password: document.getElementById('editPassword').value,
real_name: document.getElementById('editRealName').value,
position: document.getElementById('editPosition').value,
role: document.getElementById('editRole').value,
user_level: document.getElementById('editUserLevel').value,
phone: document.getElementById('editPhone').value,
email: document.getElementById('editEmail').value,
status: parseInt(document.getElementById('editStatus').value)
};
if (!id) data.id = 0;
else data.id = parseInt(id);
const url = id ? '/api/users/update' : '/api/users/create';
fetch(url, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)})
.then(r => r.json())
.then(res => {
if (res.code === 0) {
closeUserModal();
loadUsers(userCurrentPage);
} else {
alert(res.msg);
}
});
};
window.deleteUser = function(id, username) {
if (!confirm(`确定删除用户 "${username}"?`)) return;
fetch('/api/users/delete', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id})})
.then(r => r.json())
.then(res => {
if (res.code === 0) loadUsers(userCurrentPage);
else alert(res.msg);
});
};
// ─── 角色权限配置 ───
var _permPages = [];
var _permRoles = [];
var _permData = {}; // {role_page: 1|0}
function renderPermTable() {
// 加载页面列表和角色列表
Promise.all([
fetch('/api/permissions/pages').then(function(r) { return r.json(); }),
fetch('/api/permissions/roles').then(function(r) { return r.json(); }),
fetch('/api/permissions/list').then(function(r) { return r.json(); })
]).then(function(results) {
if (results[0].code !== 0 || results[1].code !== 0) return;
_permPages = results[0].data || [];
_permRoles = results[1].data || [];
var perms = results[2].data || [];
// 构建权限索引
_permData = {};
perms.forEach(function(p) {
_permData[p.role + '_' + p.page] = p.permission;
});
// 渲染表头
var thead = document.getElementById('permThead');
if (!thead) return;
var headHtml = '<tr><th style="position:sticky;left:0;background:var(--bg,#fff);z-index:1">页面</th>';
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 += '<th style="text-align:center"><span class="tag ' + colorClass + '" style="cursor:default">' + roleNames[r.id] + '</span><br><small style="color:#999;font-weight:400">全部权限</small></th>';
} else {
headHtml += '<th style="text-align:center"><span class="tag ' + colorClass + '" style="cursor:default">' + roleNames[r.id] + '</span></th>';
}
});
headHtml += '</tr>';
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 += '<tr style="background:#fafafa"><td colspan="' + (_permRoles.length + 1) + '" style="font-weight:600;font-size:13px;color:var(--text-secondary);padding:6px 12px">' + group + '</td></tr>';
groups[group].forEach(function(p) {
bodyHtml += '<tr><td style="position:sticky;left:0;background:var(--bg,#fff)">' + p.name + '</td>';
_permRoles.forEach(function(r) {
var key = r.id + '_' + p.id;
var checked = (_permData[key] == 1) ? 'checked' : '';
var disabled = (r.id === 'admin') ? 'disabled' : '';
bodyHtml += '<td style="text-align:center"><input type="checkbox" class="perm-cb" data-role="' + r.id + '" data-page="' + p.id + '" ' + checked + ' ' + disabled + '></td>';
});
bodyHtml += '</tr>';
});
});
tbody.innerHTML = bodyHtml;
});
}
window.savePermissions = function() {
var items = [];
document.querySelectorAll('.perm-cb').forEach(function(cb) {
items.push({
role: cb.dataset.role,
page: cb.dataset.page,
permission: cb.checked ? 1 : 0
});
});
fetch('/api/permissions/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({items: items})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ 权限配置已保存');
} else {
alert(res.msg || '保存失败');
}
})
.catch(function() { alert('保存失败'); });
};
// ─── 用户字段权限管理 ───
window.switchPermTab = function(tab) {
document.getElementById('permRolePane').style.display = tab === 'role' ? '' : 'none';
document.getElementById('permUserPane').style.display = tab === 'user' ? '' : 'none';
document.getElementById('permSaveBtn').style.display = tab === 'role' ? 'inline-block' : 'none';
document.getElementById('permFieldSaveBtn').style.display = tab === 'user' ? 'inline-block' : 'none';
var roleBtn = document.getElementById('permTabRole');
var userBtn = document.getElementById('permTabUser');
if (tab === 'role') {
roleBtn.style.cssText = 'background:#1890ff;color:#fff;border:none';
userBtn.style.cssText = 'background:#f0f0f0;color:#333;border:1px solid #d9d9d9';
} else {
roleBtn.style.cssText = 'background:#f0f0f0;color:#333;border:1px solid #d9d9d9';
userBtn.style.cssText = 'background:#1890ff;color:#fff;border:none';
loadFieldUserList();
}
};
window.loadFieldUserList = function() {
var sel = document.getElementById('permFieldUser');
if (sel.options.length > 1) return;
fetch('/api/permissions/users')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
(res.data || []).forEach(function(u) {
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.real_name + ' (' + (u.role||'') + ')';
sel.appendChild(opt);
});
});
};
window.loadUserFieldPerms = function() {
try {
var userId = document.getElementById('permFieldUser').value;
var wrap = document.getElementById('permFieldWrap');
if (!userId) { if(wrap)wrap.innerHTML='<div style="padding:20px;text-align:center;color:#999">请先选择用户</div>'; return; }
if (!wrap) { return; }
wrap.innerHTML = '<div style="padding:20px;text-align:center;color:#999">加载中...</div>';
// 单API获取字段级权限+有效页面级权限(后端已合并角色+用户)
fetch('/api/permissions/user/field-perms?user_id=' + userId + '&_t=' + Date.now())
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(fieldPermRes) {
if (fieldPermRes.code !== 0) { wrap.innerHTML = '<div style="padding:20px;text-align:center;color:red">API错误: ' + (fieldPermRes.msg || 'code=' + fieldPermRes.code) + '</div>'; return; }
var fieldPermData = fieldPermRes.data || {};
var userFieldPerms = fieldPermData.field_perms || {}; // 字段级
var roleFieldPerms = fieldPermData.role_perms || {}; // 页面级有效权限
var html = '';
// DEBUG: 确认新API版本运行中
html += '<div style="font-size:10px;color:#aaa;padding:4px 8px;border:1px dashed #ddd;margin-bottom:8px">✅ 新API v2 - data_summary有效=' + JSON.stringify(roleFieldPerms.data_summary || {}) + '</div>';
_permMenuTree.forEach(function(group) {
var items = group.items || [];
if (items.length === 0) return;
html += '<div style="margin-bottom:12px;border:1px solid #e8e8e8;border-radius:8px;overflow:hidden">';
html += '<div style="background:#fafafa;padding:8px 14px;font-weight:600;font-size:14px;color:#333;border-bottom:1px solid #e8e8e8">' + escHtml(group.name) + '</div>';
items.forEach(function(item) {
var page = item.page;
var feats = item.features || [];
var fields = userFieldPerms[page] || {};
var fieldKeys = Object.keys(fields).sort();
// 页面级操作权限
html += '<div style="padding:8px 14px;border-bottom:1px solid #f5f5f5">';
html += '<div style="display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:' + (fieldKeys.length ? '8px' : '0') + '">';
html += '<span style="font-size:13px;font-weight:600;color:#333;min-width:90px;margin-right:8px">' + escHtml(item.name) + '</span>';
feats.forEach(function(f) {
// 有效权限(后端已合并角色+用户)
var effPerms = roleFieldPerms[page] || {};
var effective = effPerms[f.id] === 1;
// roleEnabled=角色原本有(用于title提示)
var roleEnabled = effPerms[f.id] === 1;
html += '<label style="display:inline-flex;align-items:center;gap:3px;font-size:12px;cursor:pointer;padding:2px 6px;border-radius:4px;background:' + (effective ? '#e6f7ff' : '#f5f5f5') + ';border:1px solid ' + (effective ? '#91d5ff' : '#e8e8e8') + '" title="有效=' + (effective ? '✓' : '✗') + '">';
html += '<input type="checkbox" class="user-op-cb" data-page="' + page + '" data-op="' + f.id + '" ' + (effective ? 'checked' : '') + ' style="margin:0"> ' + escHtml(f.name || f.id) + '</label>';
});
html += '</div>';
// 字段级权限(仅当有字段时)
if (fieldKeys.length > 0) {
html += '<div style="padding:4px 0 0 8px">';
html += '<div style="font-size:11px;color:#888;cursor:pointer;margin-bottom:4px" onclick="var n=this.nextElementSibling;n.style.display=n.style.display==\'none\'?\'\':\'none\'">📋 字段权限 <span style="font-size:10px">▼</span></div>';
html += '<div style="overflow-x:auto;display:none">';
html += '<table style="width:100%;font-size:11px;border-collapse:collapse">';
html += '<tr style="background:#f5f5f5"><th style="padding:2px 6px;text-align:left">字段</th><th style="padding:2px 6px;text-align:center;width:60px">查看</th><th style="padding:2px 6px;text-align:center;width:60px">编辑</th></tr>';
fieldKeys.forEach(function(f) {
var fv = fields[f];
var viewChecked = fv['view'] === 1 ? 'checked' : '';
var editChecked = fv['edit'] === 1 ? 'checked' : '';
html += '<tr>';
html += '<td style="padding:2px 6px">' + escHtml(f) + '</td>';
html += '<td style="text-align:center"><input type="checkbox" data-page="' + page + '" data-field="' + escJs(f) + '" data-op="view" ' + viewChecked + ' onchange="markFieldPermChanged()"></td>';
html += '<td style="text-align:center"><input type="checkbox" data-page="' + page + '" data-field="' + escJs(f) + '" data-op="edit" ' + editChecked + ' onchange="markFieldPermChanged()"></td>';
html += '</tr>';
});
html += '</table></div></div>';
}
html += '</div>';
});
html += '</div>';
});
wrap.innerHTML = html;
window._fieldPermChanged = false;
}).catch(function(e) {
wrap.innerHTML = '<div style="padding:20px;text-align:center;color:red">加载权限数据失败: ' + (e.message || '') + '</div>';
});
} catch(e) {
var w = document.getElementById('permFieldWrap');
if(w) w.innerHTML = '<div style="padding:20px;text-align:center;color:red">JS错误: ' + escHtml(e.message) + '</div>';
}
};
window.markFieldPermChanged = function() {
window._fieldPermChanged = true;
};
window.saveUserFieldPerms = function() {
var userId = document.getElementById('permFieldUser').value;
if (!userId) return alert('请先选择用户');
// 先获取该用户当前的所有权限(包含page级权限),避免被覆盖
fetch('/api/permissions/users')
.then(function(r) { return r.json(); })
.then(function(usersRes) {
var users = usersRes.data || [];
var curUser = users.find(function(u) { return u.id == userId; });
var pagePerms = (curUser && curUser.permissions) || {};
var items = [];
// 页面级操作权限(从user-op-cb checkbox读取)
var processedOps = {};
document.querySelectorAll('#permFieldWrap .user-op-cb').forEach(function(cb) {
var page = cb.getAttribute('data-page');
var op = cb.getAttribute('data-op');
if (page && op) {
// 勾选=用户增加权限,取消=移除用户级覆盖(由后端角色权限兜底)
if (cb.checked) {
items.push({page: page, operation: op, permission: 1});
}
processedOps[page + '_' + op] = true;
}
});
// 保留已有用户级页面权限(本次没操作的)
Object.keys(pagePerms).forEach(function(p) {
Object.keys(pagePerms[p]).forEach(function(op) {
if (!op.startsWith('field:') && !processedOps[p + '_' + op]) {
items.push({page: p, operation: op, permission: pagePerms[p][op]});
}
});
});
// 字段级权限(从checkbox读取)
document.querySelectorAll('#permFieldWrap input[type="checkbox"]').forEach(function(cb) {
var page = cb.getAttribute('data-page');
var field = cb.getAttribute('data-field');
var op = cb.getAttribute('data-op');
if (page && field && op) {
items.push({
page: page,
operation: 'field:' + field + ':' + op,
permission: cb.checked ? 1 : 0
});
}
});
return fetch('/api/permissions/user/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_id: parseInt(userId), items: items})
});
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ 保存成功');
window._fieldPermChanged = false;
// 刷新字段权限显示
loadUserFieldPerms();
} else {
alert('❌ ' + (res.msg || '保存失败'));
}
})
.catch(function() { alert('保存失败'); });
};
// ─── 字段级权限应用到表格(隐藏无查看权限的列) ───
window._applyTableFieldPerms = function(tableId, pageId, fieldOrder) {
var table = document.getElementById(tableId);
if (!table) return;
var headerRow = table.querySelector('thead tr');
if (!headerRow) return;
var ths = headerRow.querySelectorAll('th');
ths.forEach(function(th, idx) {
// 优先用 data-field,否则用 th 文本内容匹配
var fieldName = th.getAttribute('data-field') || (th.textContent || '').trim().replace(/[^\u4e00-\u9fa5a-zA-Z0-9\-\/]/g, '');
if (fieldName && !window._canViewField(pageId, fieldName)) {
th.style.display = 'none';
table.querySelectorAll('tbody tr').forEach(function(tr) {
var td = tr.children[idx];
if (td) td.style.display = 'none';
});
}
});
};
// ─── 操作日志 ───
function loadLogsPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>操作日志</h1>
<p class="page-desc">系统操作记录</p>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px">
<input type="text" id="logUser" class="form-input" style="width:100px" placeholder="操作人">
<input type="text" id="logAction" class="form-input" style="width:120px" placeholder="操作类型">
<button class="btn btn-primary" onclick="loadLogs(1)">查询</button>
<button class="btn" onclick="resetLogFilters()">重置</button>
<span class="data-count" id="logCount"></span>
</div>
</div>
</div>
<div class="card" style="margin-top:15px">
<div class="card-header"><h3>日志列表</h3></div>
<div class="card-body" style="padding:0;overflow-x:auto">
<table class="data-table" id="logTable">
<thead><tr><th>时间</th><th>操作人</th><th>操作类型</th><th>详情</th><th>IP</th></tr></thead>
<tbody id="logTbody"></tbody>
</table>
<div id="logPagination" class="pagination-controls" style="padding:8px 16px"></div>
</div>
</div>
`;
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 = '<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-light)">暂无日志</td></tr>';
} else {
tbody.innerHTML = data.rows.map(r => {
return '<tr><td>' + (r.created_at || '-') + '</td><td>' + escHtml(r.real_name || '-') + '</td><td>' + escHtml(r.action || '-') + '</td><td>' + escHtml(r.detail || '-') + '</td><td>' + escHtml(r.ip_address || '-') + '</td></tr>';
}).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 = `
<div class="page-header">
<h1>访问记录</h1>
<p class="page-desc">用户登录系统记录</p>
</div>
<div class="card">
<div class="card-body" style="padding:12px 16px">
<div class="filter-bar" style="flex-wrap:wrap;gap:8px">
<input type="text" id="loginLogUser" class="form-input" style="width:130px" placeholder="搜索用户名/姓名">
<button class="btn btn-primary" onclick="loadLoginRecords(1)">查询</button>
<button class="btn" onclick="resetLoginLogFilters()">重置</button>
<button class="btn btn-outline" id="refreshLoginLogBtn" onclick="refreshLoginLog()">🔄 刷新</button>
<span class="data-count" id="loginLogCount"></span>
<span id="loginLogRefreshNotice" style="display:none;font-size:13px;padding:4px 10px;border-radius:4px;background:#f6ffed;border:1px solid #b7eb8f;color:#52c41a"></span>
</div>
</div>
</div>
<div class="card" style="margin-top:15px">
<div class="card-header"><h3>登录记录</h3></div>
<div class="card-body" style="padding:0;overflow-x:auto">
<table class="data-table" id="loginLogTable">
<thead><tr><th>时间</th><th>用户名</th><th>姓名</th><th>角色</th><th>IP地址</th></tr></thead>
<tbody id="loginLogTbody"></tbody>
</table>
<div id="loginLogPagination" class="pagination-controls" style="padding:8px 16px"></div>
</div>
</div>
`;
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 = '<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-light)">暂无登录记录</td></tr>';
} else {
tbody.innerHTML = data.rows.map(function(r) {
var roleLabel = r.role || '-';
return '<tr><td>' + escHtml(r.created_at || '-') + '</td><td><code>' + escHtml(r.username || '-') + '</code></td><td>' + escHtml(r.real_name || '-') + '</td><td>' + escHtml(roleLabel) + '</td><td><code>' + escHtml(r.ip_address || '-') + '</code></td></tr>';
}).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 = `
<div class="page-header">
<h1>权限配置</h1>
<p class="page-desc">按角色统一配置操作权限,也可单独配置每个用户</p>
</div>
<div id="permissionsContent">
<div style="text-align:center;padding:60px 20px;color:var(--text-light,#999)">
<div style="font-size:48px;margin-bottom:16px">⏳</div>
<div>加载中...</div>
</div>
</div>`;
loadPermissionsUsers();
}
// 全局缓存
var _permUsers = [];
var _permPages = [];
var _permOps = [];
var _permRoles = [];
var _roleOpPerms = [];
function loadPermissionsUsers() {
var el = document.getElementById('permissionsContent');
if (!el) return;
Promise.all([
fetch('/api/permissions/users').then(function(r) { return r.json(); }),
fetch('/api/permissions/pages').then(function(r) { return r.json(); }),
fetch('/api/permissions/operations').then(function(r) { return r.json(); }),
fetch('/api/permissions/roles').then(function(r) { return r.json(); }),
fetch('/api/permissions/role-operations').then(function(r) { return r.json(); })
])
.then(function(results) {
var usersRes = results[0];
var pagesRes = results[1];
var opsRes = results[2];
var rolesRes = results[3];
var roleOpRes = results[4];
if (usersRes.code !== 0) {
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败</div>';
return;
}
_permUsers = usersRes.data || [];
_permPages = pagesRes.data || [];
_permOps = opsRes.data || [];
_permRoles = rolesRes.data || [];
_roleOpPerms = roleOpRes.data || [];
// 收集去重的岗位和级别列表用于筛选下拉
var posSet = {};
var levelSet = {};
_permUsers.forEach(function(u) {
if (u.position) posSet[u.position] = true;
if (u.user_level) levelSet[u.user_level] = true;
});
var posList = Object.keys(posSet).sort();
var levelList = Object.keys(levelSet).sort();
var positionNames = {
'bp': 'BP', 'procurement_planner': '采购计划', 'procurement_follower': '采购跟单',
'procurement_trainee': '采购管培生', '采购开发': '采购开发'
};
var levelNames = {'employee': '员工', 'leader': '主管', 'director': '总监'};
// 筛选栏
var filterHtml = '<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;align-items:center">';
filterHtml += '<div style="display:flex;gap:4px"><input type="text" id="permFilterName" class="form-input" placeholder="🔍 姓名/账号" style="width:160px;padding:6px 10px" onkeydown="if(event.key===\'Enter\')renderPermUserGrid()" autocomplete="off"><button class="btn btn-sm btn-primary" onclick="renderPermUserGrid()" style="padding:4px 10px">搜索</button><button class="btn btn-sm" onclick="resetPermFilter()" style="padding:4px 10px;background:#fff;color:#666;border:1px solid #d9d9d9">重置</button></div>';
filterHtml += '<select id="permFilterPos" class="form-input" style="width:130px;padding:6px 8px" onchange="renderPermUserGrid()"><option value="">全部岗位</option>';
posList.forEach(function(p) {
var pn = positionNames[p] || p;
filterHtml += '<option value="' + escHtml(p) + '">' + escHtml(pn) + '</option>';
});
filterHtml += '</select>';
filterHtml += '<select id="permFilterLevel" class="form-input" style="width:110px;padding:6px 8px" onchange="renderPermUserGrid()"><option value="">全部级别</option>';
levelList.forEach(function(l) {
var ln = levelNames[l] || l;
filterHtml += '<option value="' + escHtml(l) + '">' + escHtml(ln) + '</option>';
});
filterHtml += '</select>';
filterHtml += '<span id="permFilterCount" style="font-size:13px;color:var(--text-light,#999)"></span>';
filterHtml += '</div>';
filterHtml += '<div id="permUserGrid"></div>';
// 角色操作权限卡片
el.innerHTML = filterHtml;
renderPermUserGrid();
})
.catch(function() {
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--danger)">❌ 加载失败</div>';
});
}
function resetPermFilter() {
var input = document.getElementById('permFilterName');
if (input) input.value = '';
var posEl = document.getElementById('permFilterPos');
if (posEl) posEl.value = '';
var levelEl = document.getElementById('permFilterLevel');
if (levelEl) levelEl.value = '';
renderPermUserGrid();
}
// ─── 角色操作权限 ───
var _roleOpEditingRole = '';
var _roleOpData = {}; // {role_page_operation: 1|0}
// 按功能ID给出偏安全的默认值(查看者以外的角色)
var featureDefaults = {
'view': 1, 'query': 1, 'export': 1, 'submit': 1,
'edit': 1, 'edit_product': 1, 'edit_article': 1, 'edit_warehouse': 1,
'add': 1, 'create': 1, 'delete': 1, 'delete_article': 1, 'delete_warehouse': 1,
'batch_confirm': 1, 'rollback': 1, 'field_setting': 1, 'frozen': 1,
'import': 1, 'lint': 1, 'graph': 1, 'reset': 1
};
window.openRoleOpPermModal = function(roleId) {
_roleOpEditingRole = roleId;
// 构建权限索引,未保存的按角色默认值填充
_roleOpData = {};
_roleOpPerms.forEach(function(p) {
_roleOpData[p.role + '_' + p.page + '_' + p.operation] = p.permission;
});
// 预填默认值
var isFullAdmin = (roleId === 'super_admin' || roleId === 'admin');
var isViewer = (roleId === 'viewer');
_permMenuTree.forEach(function(group) {
group.items.forEach(function(item) {
(item.features || []).forEach(function(feat) {
var key = roleId + '_' + item.page + '_' + feat.id;
if (!(key in _roleOpData)) {
if (isFullAdmin) {
_roleOpData[key] = 1;
} else if (featureDefaults[feat.id] !== undefined) {
_roleOpData[key] = featureDefaults[feat.id];
} else {
_roleOpData[key] = (feat.id === 'view') ? 1 : 0;
}
}
});
});
});
// 渲染弹窗
var roleNames = {
'super_admin': '超级管理员', 'admin': '管理员',
'procurement_planner': '采购计划', 'procurement_follower': '采购跟单',
'product_developer': '产品开发', 'operator': '运营专员',
'viewer': '查看者'
};
var modalId = 'roleOpPermModal-' + Date.now();
var modal = document.createElement('div');
modal.id = modalId;
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto;padding:20px';
modal.innerHTML = '<div style="background:#fff;border-radius:12px;width:90vw;max-width:1200px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);overflow:hidden">' +
'<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 24px;border-bottom:1px solid #eee;flex:0 0 auto">' +
'<h3 style="margin:0;font-size:16px">⚙️ 角色操作权限配置 — ' + escHtml(roleNames[roleId] || roleId) + '</h3>' +
'<button style="font-size:22px;border:none;background:none;cursor:pointer;padding:0 6px;line-height:1;color:#999" onclick="document.getElementById(\'' + modalId + '\').remove()">✕</button>' +
'</div>' +
'<div style="padding:12px 24px;overflow:auto;flex:1 1 auto" id="roleOpModalBody">' +
'<div id="roleOpModalCardWrap"></div>' +
'<table class="data-table" id="roleOpModalTable" style="display:none">' +
'<tbody id="roleOpModalTbody"></tbody>' +
'</table>' +
'</div>' +
'<div style="padding:10px 24px;border-top:1px solid #eee;text-align:right;flex:0 0 auto">' +
'<button class="btn" onclick="document.getElementById(\'' + modalId + '\').remove()" style="margin-right:8px">取消</button>' +
'<button class="btn btn-primary" onclick="saveRoleOpPerms(\'' + modalId + '\')">💾 保存</button>' +
'</div></div>';
modal.onclick = function(e) { if (e.target === modal) {} };
document.body.appendChild(modal);
renderRoleOpPermTable(roleId);
};
function renderRoleOpPermTable(roleId) {
var tbody = document.getElementById('roleOpModalTbody');
if (!tbody) return;
var bodyHtml = '';
_permMenuTree.forEach(function(group) {
var items = group.items || [];
if (items.length === 0) return;
// 每组一个卡片
bodyHtml += '<div style="margin-bottom:12px;border:1px solid #e8e8e8;border-radius:8px;overflow:hidden">';
bodyHtml += '<div style="background:#fafafa;padding:8px 14px;font-weight:600;font-size:14px;color:#333;border-bottom:1px solid #e8e8e8">' + escHtml(group.name) + '</div>';
items.forEach(function(item) {
var feats = item.features || [];
bodyHtml += '<div style="padding:8px 14px;display:flex;align-items:center;flex-wrap:wrap;gap:6px;border-bottom:1px solid #f5f5f5">';
bodyHtml += '<span style="font-size:13px;font-weight:600;color:#333;min-width:90px;margin-right:8px">' + escHtml(item.name) + '</span>';
feats.forEach(function(f) {
var key = roleId + '_' + item.page + '_' + f.id;
var checked = _roleOpData[key] == 1 ? 'checked' : '';
bodyHtml += '<label style="display:inline-flex;align-items:center;gap:3px;font-size:12px;cursor:pointer;padding:2px 6px;border-radius:4px;background:' + (checked ? '#e6f7ff' : '#f5f5f5') + ';border:1px solid ' + (checked ? '#91d5ff' : '#e8e8e8') + '">';
bodyHtml += '<input type="checkbox" class="role-op-cb" data-role="' + roleId + '" data-page="' + item.page + '" data-op="' + f.id + '" ' + checked + ' style="margin:0"> ' + escHtml(f.name || f.id) + '</label>';
});
bodyHtml += '</div>';
});
bodyHtml += '</div>';
});
tbody.innerHTML = '';
// 把卡片放到tbody的容器里(实际放在其父容器中)
var wrap = document.getElementById('roleOpModalTable');
if (wrap) {
var parent = wrap.parentNode;
// 隐藏table,用卡片替代
wrap.style.display = 'none';
var cardWrap = document.getElementById('roleOpModalCardWrap');
if (!cardWrap) {
cardWrap = document.createElement('div');
cardWrap.id = 'roleOpModalCardWrap';
parent.appendChild(cardWrap);
}
cardWrap.innerHTML = bodyHtml;
}
}
function saveRoleOpPerms(modalId) {
var items = [];
document.querySelectorAll('.role-op-cb').forEach(function(cb) {
items.push({
role: cb.dataset.role,
page: cb.dataset.page,
operation: cb.dataset.op,
permission: cb.checked ? 1 : 0
});
});
fetch('/api/permissions/role-operations/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({items: items})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
// 更新缓存,下次打开弹窗显示最新状态
_roleOpPerms.length = 0;
items.forEach(function(item) {
_roleOpPerms.push({
role: item.role,
page: item.page,
operation: item.operation,
permission: item.permission
});
});
alert('✅ 角色操作权限已保存');
if (modalId) document.getElementById(modalId)?.remove();
} else {
alert(res.msg || '保存失败');
}
})
.catch(function(e) { console.error(e); alert('保存失败: ' + (e.message || '网络错误')); });
}
function renderPermUserGrid() {
var el = document.getElementById('permUserGrid');
if (!el) return;
var kw = (document.getElementById('permFilterName')?.value || '').trim().toLowerCase();
var posFilter = document.getElementById('permFilterPos')?.value || '';
var levelFilter = document.getElementById('permFilterLevel')?.value || '';
var filtered = _permUsers.filter(function(u) {
if (kw) {
if (!(u.real_name || '').toLowerCase().includes(kw) && !(u.username || '').toLowerCase().includes(kw)) return false;
}
if (posFilter && u.position !== posFilter) return false;
if (levelFilter && u.user_level !== levelFilter) return false;
return true;
});
var countEl = document.getElementById('permFilterCount');
if (countEl) countEl.textContent = filtered.length + ' / ' + _permUsers.length + ' 人';
var roleNames = {
'super_admin': '超级管理员', 'admin': '管理员',
'procurement_planner': '采购计划', 'procurement_follower': '采购跟单',
'product_developer': '产品开发', 'operator': '运营专员',
'viewer': '查看者', '查看者': '查看者'
};
var roleColors = {
'super_admin': '#f5222d', 'admin': '#1890ff',
'procurement_planner': '#722ed1', 'procurement_follower': '#13c2c2',
'product_developer': '#eb2f96', 'operator': '#fa8c16',
'viewer': '#8c8c8c', '查看者': '#8c8c8c'
};
var roleBadgeColors = {
'super_admin': '#fff1f0', 'admin': '#e6f7ff',
'procurement_planner': '#f9f0ff', 'procurement_follower': '#e6fffb',
'product_developer': '#fff0f6', 'operator': '#fff7e6',
'viewer': '#f5f5f5', '查看者': '#f5f5f5'
};
var roleTextColors = {
'super_admin': '#cf1322', 'admin': '#096dd9',
'procurement_planner': '#531dab', 'procurement_follower': '#08979c',
'product_developer': '#c41d7f', 'operator': '#d46b08',
'viewer': '#595959', '查看者': '#595959'
};
// 按角色分组:超级管理员 → 管理员 → 查看者 → 其他
var groupSpecs = [
{role: 'super_admin', icon: '⭐', label: '超级管理员'},
{role: 'admin', icon: '🔐', label: '管理员'},
{role: 'viewer', icon: '👁️', label: '查看者'}
];
var roleGroups = {};
var otherUsers = [];
filtered.forEach(function(u) {
var found = false;
groupSpecs.forEach(function(gs) {
if (u.role === gs.role) {
if (!roleGroups[gs.role]) roleGroups[gs.role] = [];
roleGroups[gs.role].push(u);
found = true;
}
});
if (!found) otherUsers.push(u);
});
function renderUserSection(users, sectionInfo) {
var h = '';
if (!users || users.length === 0) return '';
var icon = sectionInfo.icon || '👤';
var label = sectionInfo.label || '成员';
var roleId = sectionInfo.role || '';
h += '<div style="font-weight:600;font-size:14px;color:#333;padding:8px 4px 12px;display:flex;align-items:center;gap:10px">';
h += '<span><span style="font-size:18px;margin-right:6px">' + icon + '</span> ' + label + '</span>';
h += '<button class="btn btn-sm" onclick="openRoleOpPermModal(\'' + roleId + '\')" style="font-size:12px;background:#f0f0f0;color:#333;border:1px solid #d9d9d9">⚙️ 权限配置</button></div>';
h += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px">';
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 += '<div class="perm-user-card" style="cursor:pointer;display:flex;align-items:center;gap:14px;padding:14px 18px;background:#fff;border:1px solid #e8e8e8;border-radius:10px;transition:all .2s ease" onclick="openUserPermPanel(' + u.id + ')">';
// 头像
h += '<div style="width:44px;height:44px;border-radius:50%;background:' + avatarBg + ';display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:18px;flex-shrink:0;box-shadow:0 2px 6px rgba(0,0,0,0.1)">' + escHtml(initial) + '</div>';
// 信息
h += '<div style="flex:1;min-width:0">';
h += '<div style="font-weight:600;font-size:15px;color:#262626;display:flex;align-items:center;gap:8px">';
h += escHtml(u.real_name || u.username);
h += '<span style="display:inline-block;font-size:11px;padding:1px 8px;border-radius:10px;background:' + badgeBg + ';color:' + badgeText + ';font-weight:500;line-height:20px">' + escHtml(roleName) + '</span>';
if (levelName) {
h += '<span style="display:inline-block;font-size:10px;padding:0 6px;border-radius:8px;background:#fff7e6;color:#d46b08;font-weight:500;line-height:18px">' + levelName + '</span>';
}
h += '</div>';
var infoParts = ['账号: ' + escHtml(u.username)];
if (posName) infoParts.push('岗位: ' + escHtml(posName));
if (levelName) infoParts.push('级别: ' + levelName);
if (permCount > 0) infoParts.push('已配置 ' + permCount + ' 个页面');
h += '<div style="font-size:12px;color:#8c8c8c;margin-top:3px">' + infoParts.join(' · ') + '</div>';
h += '</div>';
h += '<span style="color:#d9d9d9;font-size:20px;flex-shrink:0">›</span>';
h += '</div>';
});
h += '</div>';
return h;
}
var html = '';
var first = true;
groupSpecs.forEach(function(gs) {
var users = roleGroups[gs.role] || [];
if (users.length > 0) {
if (!first) html += '<div style="border:0;border-top:1px solid #f0f0f0;margin:16px 0 4px"></div>';
html += renderUserSection(users, gs);
first = false;
}
});
if (otherUsers.length > 0) {
if (!first) html += '<div style="border:0;border-top:1px solid #f0f0f0;margin:16px 0 4px"></div>';
html += renderUserSection(otherUsers, {icon:'👥', label:'其他成员', role:''});
}
html += '<style>';
html += '.perm-user-card:hover{border-color:#d9d9d9!important;box-shadow:0 4px 16px rgba(0,0,0,0.08);transform:translateY(-1px)}';
html += '.perm-user-card:active{transform:translateY(0);box-shadow:0 2px 8px rgba(0,0,0,0.06)}';
html += '</style>';
if (filtered.length === 0) {
html = '<div style="text-align:center;padding:60px;color:var(--text-light,#999)">无匹配用户</div>';
}
el.innerHTML = html;
}
// 打开用户权限面板 - 使用新API展示用户有效权限
window.openUserPermPanel = function(userId) {
// 关闭已有弹窗,防止重复堆积
var existing = document.querySelectorAll('.modal-overlay');
existing.forEach(function(el) { if (el.id !== 'permModal' && el.id !== 'userModal') el.remove(); });
// 从缓存中获取用户信息,避免额外API调用
var user = (_permUsers || []).find(function(u) { return u.id == userId; });
if (!user) { alert('未找到该用户'); return; }
fetch('/api/permissions/user/field-perms?user_id=' + userId + '&_t=' + Date.now())
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { alert('加载失败: ' + (res.msg || '')); return; }
var data = res.data || {};
var rolePerms = data.role_perms || {};
var html = '<div class="modal-overlay"><div class="modal" style="width:800px;max-height:85vh;display:flex;flex-direction:column"><div class="modal-header"><h3>用户权限配置 — ' + escHtml(user.real_name || user.username) + '</h3><span class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</span></div>';
html += '<div class="modal-body" style="padding:16px 20px;flex:1;overflow-y:auto">';
html += '<div style="font-size:13px;color:#888;margin-bottom:12px">角色: ' + escHtml(user.role) + ' — 勾选框为有效权限(角色级 + 用户级叠加),取消勾选即关闭用户级覆盖</div>';
_permMenuTree.forEach(function(group) {
var items = group.items || [];
if (items.length === 0) return;
html += '<div style="margin-bottom:12px;border:1px solid #e8e8e8;border-radius:8px;overflow:hidden">';
html += '<div style="background:#fafafa;padding:8px 14px;font-weight:600;font-size:14px;color:#333;border-bottom:1px solid #e8e8e8">' + escHtml(group.name) + '</div>';
items.forEach(function(item) {
var perms = rolePerms[item.page] || {};
var feats = item.features || [];
html += '<div style="padding:8px 14px;display:flex;align-items:center;flex-wrap:wrap;gap:6px;border-bottom:1px solid #f5f5f5">';
html += '<span style="font-size:13px;font-weight:600;color:#333;min-width:90px;margin-right:8px">' + escHtml(item.name) + '</span>';
feats.forEach(function(f) {
var checked = perms[f.id] === 1 ? 'checked' : '';
html += '<label style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:12px;font-size:12px;cursor:pointer;background:' + (checked ? '#e6f7ff' : '#f5f5f5') + ';border:1px solid ' + (checked ? '#91d5ff' : '#e8e8e8') + ';user-select:none;white-space:nowrap">';
html += '<input type="checkbox" class="user-edit-op-cb" data-page="' + item.page + '" data-op="' + f.id + '" ' + checked + ' style="margin:0;cursor:pointer">';
html += ' ' + escHtml(f.name) + '</label>';
});
html += '</div>';
});
html += '</div>';
});
html += '</div>';
html += '<div class="modal-footer" style="display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid #eee">';
html += '<button class="btn" onclick="this.closest(\'.modal-overlay\').remove()">关闭</button>';
html += '<button class="btn btn-primary" id="userPermSaveBtn" onclick="saveUserPermPanel(' + userId + ')">💾 保存</button>';
html += '</div></div></div>';
var div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
})
.catch(function(e) { alert('请求失败: ' + e.message); });
};
// 保存用户权限面板
window.saveUserPermPanel = function(userId) {
var items = [];
document.querySelectorAll('.user-edit-op-cb').forEach(function(cb) {
var page = cb.getAttribute('data-page');
var op = cb.getAttribute('data-op');
if (page && op) {
if (cb.checked) {
items.push({page: page, operation: op, permission: 1});
}
}
});
var saveBtn = document.getElementById('userPermSaveBtn');
if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '保存中...'; }
var body = JSON.stringify({user_id: parseInt(userId), items: items});
fetch('/api/permissions/user/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'same-origin',
body: body
})
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status + ' ' + r.statusText);
return r.json();
})
.then(function(res) {
alert(res.msg || (res.code === 0 ? '✅ 保存成功' : '❌ 保存失败'));
if (res.code === 0) {
document.querySelector('.modal-overlay')?.remove();
}
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; }
})
.catch(function(e) {
alert('保存失败: ' + e.message + (body ? ' (已发送' + items.length + '项)' : ''));
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; }
});
};
var _editingUserId = null;
var _rolePagePerms = {}; // {page: 1/0} - 角色默认的页面权限
// 菜单树结构(与侧边栏菜单一致)
// 菜单树结构:每个菜单项列出实际的功能按钮(不是通用的增删改查)
var _permMenuTree = [
{
name: '数据概览', icon: '📊', items: [
{page: 'dashboard', name: '数据概览', features: [{id:'view',name:'查看'}]}
]
},
{
name: '出库数据', icon: '📦', items: [
{page: 'data_summary', name: '出库汇总', features: [{id:'view',name:'查看'},{id:'query',name:'查询'},{id:'export',name:'导出'}]},
{page: 'data_detail', name: '出库明细', features: [{id:'view',name:'查看'},{id:'query',name:'查询'},{id:'export',name:'导出'}]}
]
},
{
name: '库存信息', icon: '📋', items: [
{page: 'inventory_finished', name: '成品库存', features: [{id:'view',name:'查看'},{id:'edit_product',name:'编辑产品'},{id:'export',name:'导出'}]},
{page: 'inventory_packaging', name: '包材库存', features: [{id:'view',name:'查看'},{id:'edit_product',name:'编辑产品'},{id:'export',name:'导出'}]}
]
},
{
name: '计划预测', icon: '📈', items: [
{page: 'planning', name: '计划预测', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑产品'},{id:'delete',name:'删除'},{id:'field_setting',name:'字段设置'},{id:'frozen',name:'冻结列'}]}
]
},
{
name: '采购订单', icon: '📄', items: [
{page: 'purchase', name: '待下单管理', features: [{id:'view',name:'查看'},{id:'batch_confirm',name:'批量确认下单'},{id:'rollback',name:'回退'},{id:'field_setting',name:'字段设置'},{id:'frozen',name:'冻结列'}]},
{page: 'purchase_ready', name: '下单备份管理', features: [{id:'view',name:'查看'},{id:'rollback',name:'回退'},{id:'field_setting',name:'字段设置'}]},
{page: 'orders_domestic', name: '国内订单管理', features: [{id:'view',name:'查看'}]},
{page: 'orders_overseas', name: '海外订单管理', features: [{id:'view',name:'查看'}]},
{page: 'orders_packaging', name: '包材订单管理', features: [{id:'view',name:'查看'}]},
{page: 'orders_finished', name: '成品提货表', features: [{id:'view',name:'查看'}]}
]
},
{
name: '测试问题', icon: '🐛', items: [
{page: 'feedback', name: '测试问题', features: [{id:'view',name:'查看'},{id:'submit',name:'提交反馈'}]}
]
},
{
name: '基础信息', icon: '📚', items: [
{page: 'products', name: '产品信息管理', features: [{id:'view',name:'查看'},{id:'add',name:'新增产品'},{id:'edit',name:'编辑产品'},{id:'field_setting',name:'字段设置'}]},
{page: 'bom', name: '物料清单管理', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'}]},
{page: 'warehouse', name: '仓库信息管理', features: [{id:'view',name:'查看'},{id:'create',name:'新增仓库'},{id:'edit',name:'编辑仓库'},{id:'delete',name:'删除仓库'}]},
{page: 'suppliers', name: '供应商管理', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'},{id:'create',name:'新增供应商'}]},
{page: 'personnel', name: '人员信息管理', features: [{id:'view',name:'查看'}]}
]
},
{
name: '知识库', icon: '📖', items: [
{page: 'knowledge', name: '知识库', features: [{id:'view',name:'查看'},{id:'create',name:'新建文章'},{id:'edit',name:'编辑文章'},{id:'delete',name:'删除文章'},{id:'import',name:'导入'},{id:'export',name:'导出'},{id:'lint',name:'运行检查'},{id:'graph',name:'关系图谱'}]}
]
},
{
name: '系统设置', icon: '⚙️', items: [
{page: 'users', name: '用户管理', features: [{id:'view',name:'查看'},{id:'create',name:'新增'},{id:'edit',name:'编辑'},{id:'delete',name:'删除'},{id:'import',name:'导入'}]},
{page: 'permissions', name: '权限配置', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'}]},
{page: 'login_records', name: '访问记录', features: [{id:'view',name:'查看'}]},
{page: 'logs', name: '操作日志', features: [{id:'view',name:'查看'}]},
{page: 'config', name: '参数配置', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'},{id:'reset',name:'恢复默认'}]},
{page: 'config_calibrate', name: '参数校准', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑'}]},
{page: 'contract_templates', name: '合同模板', features: [{id:'view',name:'查看'},{id:'edit',name:'编辑上传'},{id:'delete',name:'删除'}]},
{page: 'knowledge', name: '知识库', features: [{id:'view',name:'查看'},{id:'create',name:'新建文章'},{id:'edit',name:'编辑文章'},{id:'delete',name:'删除文章'},{id:'import',name:'导入'},{id:'export',name:'导出'}]}
]
}
];
// ─── 每个页面的字段/卡片定义 ───
window._permPageFields = {
// 数据概览:按图表和卡片设置权限
'dashboard': ['销量概览卡片','库存概览卡片','采购待办卡片','出库趋势图','品类分布图','库存排行表'],
// 出库数据
'data_summary': ['货品编号','货品名称','品牌','仓库','店铺','总数量','总成本','日期','数据来源'],
'data_detail': ['货品编号','货品名称','品牌','仓库','店铺','一级类目','二级类目','三级类目','货品数量','货品成本','日期','数据来源'],
// 库存信息
'inventory_finished': ['货品编号','货品名称','品牌','仓库','可用库存','生产在途','采购在途','供应商','库存日期','采购跟单','采购计划'],
'inventory_packaging': ['货品编号','货品名称','品牌','仓库','可用库存','生产在途','采购在途','供应商','库存日期','采购跟单','采购计划'],
// 计划预测:与字段设置保持一致
'planning': [], // 将在渲染时从 PLAN_COLUMNS 动态生成
// 采购订单
'purchase': ['产品类型','货品编号','货品名称','确认下单','产品分级','供应商名称','建议下单数量','正式下单数量','下单备注','货期','物流','总支撑天数','平台','运营','审核价格','下单人员','下单时间','采购跟单','采购计划','加入时间'],
'purchase_ready': ['产品类型','货品编号','货品名称','下单时间','下单人','生产单号','建议下单数量','正式下单数量','产品分级','供应商名称','下单备注','货期','物流','总支撑天数','采购跟单','采购计划','平台','运营','是否审核价格'],
'orders_domestic': ['日期','计划下单时间','新旧品','编码','名称','下单备注','单价','审核价格','数量','金额','供货商','仓库','金蝶采购单','采购员','下单人','进度','旺店通单号','采购开发','预计到货日期','金蝶入库'],
'orders_overseas': ['产品编码','货品名称','日期','计划下单时间','单价','审核价格','数量','金额','供应商','仓库','新旧品','下单备注','采购员','下单人','进度','旺店通单号','采购开发','预计到货日期'],
'orders_packaging': ['日期','计划下单时间','产品编码','单价','审核价格','数量','金额','供应商','仓库','货品名称','新旧品','采购员','下单人','进度','下单备注','采购开发','旺店通单号'],
'orders_finished': ['日期','编码','名称','单价','数量','合计','供应商','下单备注','仓库到货','进度','采购下单人','旺店通编号','到货日期','做货天数','包材开发'],
// 基础信息
'products': ['货品编号','货品名称','货品类型','品牌','供应商编码','供应商名称','货期','物流时间','采购跟单','采购计划','平台','运营','开发专员','创建时间','导入日期'],
'bom': ['成品编号','成品名称','物料编号','物料名称','用量','供应商','成本价','创建时间'],
'warehouse': ['仓库编号','仓库名称','仓库类型','地址','联系人','联系电话','创建时间'],
'suppliers': ['供应商编码','供应商名称','联系人','联系电话','地址','合作状态','创建时间'],
// 知识库
'knowledge': ['标题','主题分类','创建人','创建时间','更新时间','状态'],
// 系统设置
'users': ['用户名','姓名','角色','岗位','用户级别','手机号','邮箱','状态','最后登录'],
'permissions': ['用户名','姓名','角色','岗位'],
'login_records': ['用户名','登录时间','IP地址','操作'],
'logs': ['操作人','操作类型','操作详情','操作时间','IP地址'],
'config': ['配置项','配置值','说明','更新时间'],
'config_calibrate': ['参数名称','当前值','校准值','校准时间','操作人'],
'contract_templates': ['模板名称','适用类型','上传人','上传时间','文件大小'],
'feedback': ['编号','标题','反馈人','提交时间','状态','处理人','处理时间']
};
// 切换字段权限折叠
function loadConfigPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>参数配置</h1>
<p class="page-desc">趋势判断与销量预测基础配置参数</p>
</div>
<div id="configContent"><div class="page-loading"><div class="loading-spinner"></div></div></div>
`;
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 = '<div class="card" style="margin-top:15px"><div class="card-header"><h3>全局配置参数</h3></div><div class="card-body">';
globalHtml += '<table class="data-table"><thead><tr><th>参数名称</th><th>参数值</th><th>说明</th><th class="col-action" style="width:120px;min-width:120px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th></tr></thead><tbody>';
(d.global_config || []).forEach(function(g) {
globalHtml += '<tr>';
globalHtml += '<td><code>' + escHtml(g.config_key) + '</code></td>';
globalHtml += '<td><input type="text" id="cfg-' + escHtml(g.config_key) + '" class="form-input" style="width:100px" value="' + escHtml(g.config_value) + '"></td>';
globalHtml += '<td>' + escHtml(g.description || '') + '</td>';
globalHtml += '<td><button class="btn btn-sm btn-primary" onclick="saveGlobalConfig()">保存</button></td>';
globalHtml += '</tr>';
});
globalHtml += '</tbody></table></div></div>';
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 = '<div class="card" style="margin-bottom:20px">';
html += '<div class="card-header"><h3>' + title + '</h3></div>';
html += '<div class="card-body" style="padding:0;overflow-x:auto">';
html += '<table class="data-table"><thead><tr>';
labels.forEach(function(l) { html += '<th>' + l + '</th>'; });
html += '</tr></thead><tbody>';
data.forEach(function(r) {
html += '<tr>';
fields.forEach(function(f) {
var val = r[f];
if (val === null || val === undefined) val = '-';
html += '<td>' + escHtml(String(val)) + '</td>';
});
html += '</tr>';
});
html += '</tbody></table>';
html += '<div style="padding:10px 16px;font-size:12px;color:var(--text-light)">共 ' + data.length + ' 条记录</div>';
html += '</div></div>';
return html;
}
// 可排序的数值列(点击表头排序)
var SORTABLE_FIELDS = {
'昨日销量':1, '云仓昨日销量':1, '外仓昨日销量':1, '库存':1, '云仓库存':1, '外仓库存':1, '加工厂库存':1, '生产在途':1, '采购在途':1,
'日销量预测':1, '云仓日销量预测':1, '外仓日销量预测':1, '库存健康指数':1, '成本价':1, 'cost_price':1,
'3天平均销量':1, '云仓3天平均销量':1, '外仓3天平均销量':1, '7天平均销量':1, '云仓7天平均销量':1, '外仓7天平均销量':1, '14天平均销量':1, '云仓14天平均销量':1, '外仓14天平均销量':1,
'30天平均销量':1, '60天平均销量':1,
'3天最高销量':1, '7天最高销量':1, '14天最高销量':1, '30天最高销量':1,
'14天最低销量':1, '14天标准差':1,
'库存可支撑天数':1, '库存工厂可支撑天数':1, '加工厂库存支撑天数':1,
'库存工厂待生产可支撑天数':1, '总支撑天数':1,
'最低销量偏差率':1, '最高销量偏差率':1, '14天波动率':1,
'建议下单数量':1, '实际下单数量':1, '建议回货数量':1,
'生产货期':1, '物流时间':1, '最低起订量':1, '总库存':1, '总在途库存':1, '正式下单数量':1
};
// ─── 计划预测页面 ───
// 所有计划列定义(按照指定顺序和显隐)
const PLAN_COLUMNS = [
{field: 'product_type', label: '产品类型', defaultShow: true},
{field: '产品种类', label: '产品种类', defaultShow: false},
{field: 'product_code', label: '货品编号', defaultShow: true},
{field: 'product_name', label: '货品名称', defaultShow: true},
{field: 'brand', label: '品牌', defaultShow: true},
{field: 'bom', label: '下单清单', defaultShow: true, filterable: false},
{field: '产品分级', label: '产品分级', defaultShow: true},
{field: '昨日销量', label: '昨日销量', defaultShow: true},
{field: '云仓昨日销量', label: '云仓昨日销量', defaultShow: false},
{field: '外仓昨日销量', label: '外仓昨日销量', defaultShow: false},
{field: '销售趋势', label: '销售趋势', defaultShow: true},
{field: '总库存', label: '总库存', defaultShow: true},
{field: '库存', label: '库存', defaultShow: true},
{field: '总在途库存', label: '总在途库存', defaultShow: true},
{field: '云仓库存', label: '云仓库存', defaultShow: false},
{field: '外仓库存', label: '外仓库存', defaultShow: false},
{field: '加工厂库存', label: '加工厂库存', defaultShow: true},
{field: '生产在途', label: '生产在途', defaultShow: true},
{field: '采购在途', label: '采购在途', defaultShow: true},
{field: '日销量预测', label: '日销量预测', defaultShow: true},
{field: '云仓日销量预测', label: '云仓日销量预测', defaultShow: false},
{field: '外仓日销量预测', label: '外仓日销量预测', defaultShow: false},
{field: '建议下单数量', label: '建议下单数量', defaultShow: true},
{field: '实际下单数量', label: '实际下单数量', defaultShow: true},
{field: '正式下单数量', label: '正式下单数量', defaultShow: true},
{field: '库存健康指数', label: '库存健康指数', defaultShow: true},
{field: '风险把控', label: '风险把控', defaultShow: true},
{field: '供应商名称', label: '供应商名称', defaultShow: true},
{field: '采购跟单', label: '采购跟单', defaultShow: true},
{field: '采购计划', label: '采购计划', defaultShow: true},
{field: '开发专员', label: '开发专员', defaultShow: true},
{field: '归属平台', label: '归属平台', defaultShow: true},
{field: '运营专员', label: '运营专员', defaultShow: true},
{field: 'cost_price', label: '成本价', defaultShow: false},
{field: '3天平均销量', label: '3天平均销量', defaultShow: false},
{field: '云仓3天平均销量', label: '云仓3天平均销量', defaultShow: false},
{field: '外仓3天平均销量', label: '外仓3天平均销量', defaultShow: false},
{field: '7天平均销量', label: '7天平均销量', defaultShow: false},
{field: '云仓7天平均销量', label: '云仓7天平均销量', defaultShow: false},
{field: '外仓7天平均销量', label: '外仓7天平均销量', defaultShow: false},
{field: '14天平均销量', label: '14天平均销量', defaultShow: false},
{field: '云仓14天平均销量', label: '云仓14天平均销量', defaultShow: false},
{field: '外仓14天平均销量', label: '外仓14天平均销量', defaultShow: false},
{field: '库存可支撑天数', label: '库存可支撑天数', defaultShow: false},
{field: '库存工厂可支撑天数', label: '现货采购在途', defaultShow: false},
{field: '加工厂库存支撑天数', label: '加工厂库存支撑', defaultShow: false},
{field: '库存工厂待生产可支撑天数', label: '定制品在途', defaultShow: false},
{field: '总支撑天数', label: '总支撑天数', defaultShow: false},
{field: '建议回货数量', label: '建议回货数量', defaultShow: false},
{field: 'specification', label: '产品规格', defaultShow: false},
{field: '30天平均销量', label: '30天平均销量', defaultShow: false},
{field: '60天平均销量', label: '60天平均销量', defaultShow: false},
{field: '3天最高销量', label: '3天最高销量', defaultShow: false},
{field: '7天最高销量', label: '7天最高销量', defaultShow: false},
{field: '14天最高销量', label: '14天最高销量', defaultShow: false},
{field: '30天最高销量', label: '30天最高销量', defaultShow: false},
{field: '14天最低销量', label: '14天最低销量', defaultShow: false},
{field: '14天标准差', label: '14天标准差', defaultShow: false},
{field: '最低销量偏差率', label: '最低销量偏差率', defaultShow: false},
{field: '最高销量偏差率', label: '最高销量偏差率', defaultShow: false},
{field: '极值判断', label: '极值判断', defaultShow: false},
{field: '14天波动率', label: '14天波动率', defaultShow: false},
{field: '波动风险', label: '波动风险', defaultShow: false},
{field: '生产货期', label: '生产货期', defaultShow: false},
{field: '物流时间', label: '物流时间', defaultShow: false},
{field: '最低起订量', label: '最低起订量', defaultShow: false},
{field: '创建时间', label: '创建时间', defaultShow: false},
{field: '是否新品', label: '是否新品', defaultShow: false},
{field: '一级类目', label: '一级类目', defaultShow: false},
{field: '二级类目', label: '二级类目', defaultShow: false},
{field: '三级类目', label: '三级类目', defaultShow: false},
];
// 筛选字段预定义选项(下拉选择类型)
const PLAN_FILTER_OPTIONS = {
'product_type': ['成品', '包材', '现货', '赠品', '清库存', '其他'],
'产品分级': ['S级', 'A级', 'B级', 'C级', '不参与评级'],
'销售趋势': ['开始上升', '开始下跌', '平稳趋势', '上升趋稳', '下跌趋稳', '持续上升', '持续下跌', '急增', '急跌', '近期无销售'],
'是否新品': ['新品', '返单品'],
'风险把控': ['正常', '新品', '呆滞品', '需启动清库存'],
'波动风险': ['X', 'Y', 'Z'],
'极值判断': ['正常', '极大', '极小', '极大/极小'],
'产品种类': ['成品', '包材', '现货', '赠品', '清库存', '其他'],
'库存健康指数': ['999']
};
// 动态筛选字段中排除的计算字段(这些值来自后台计算,不可直接查询)
const PLAN_FILTER_EXCLUDE_FIELDS = {
'产品分级':1, '昨日销量':1, '云仓昨日销量':1, '外仓昨日销量':1, '销售趋势':1, '库存':1, '云仓库存':1, '外仓库存':1, '加工厂库存':1,
'生产在途':1, '采购在途':1, '日销量预测':1, '云仓日销量预测':1, '外仓日销量预测':1, '库存健康指数':1, '风险把控':1,
'库存可支撑天数':1, '库存工厂可支撑天数':1, '加工厂库存支撑天数':1,
'库存工厂待生产可支撑天数':1, '总支撑天数':1,
'建议下单数量':1, '实际下单数量':1, '建议回货数量':1,
'3天平均销量':1, '7天平均销量':1, '14天平均销量':1,
'30天平均销量':1, '60天平均销量':1, '3天最高销量':1, '7天最高销量':1,
'14天最高销量':1, '30天最高销量':1, '14天最低销量':1, '14天标准差':1,
'最低销量偏差率':1, '最高销量偏差率':1, '极值判断':1, '14天波动率':1, '波动风险':1,
'是否新品':1
};
// 字段显隐状态(实际生效的)
if (!window.planColVisible) {
window.planColVisible = {};
PLAN_COLUMNS.forEach((c, i) => { window.planColVisible[c.field] = c.defaultShow; window['_planColIdx_' + c.field] = i; });
}
// 三个Tab各自独立的字段显隐状态
if (!window._planViewVisibility) {
window._planViewVisibility = {'default': {}, 'cloud': {}, 'external': {}};
PLAN_COLUMNS.forEach(function(c) {
['default', 'cloud', 'external'].forEach(function(view) {
// 云仓库存/外仓库存/云仓昨日销量/外仓昨日销量仅国内品(cloud)视图默认显示
var cloudOnly = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量',
'云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量',
'云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测'];
if (cloudOnly.indexOf(c.field) >= 0 && view !== 'cloud') {
window._planViewVisibility[view][c.field] = false;
} else if (cloudOnly.indexOf(c.field) >= 0 && view === 'cloud') {
window._planViewVisibility[view][c.field] = true;
} else {
window._planViewVisibility[view][c.field] = c.defaultShow;
}
});
});
// 当前视图同步到planColVisible(默认default视图)
PLAN_COLUMNS.forEach(function(c) { window.planColVisible[c.field] = window._planViewVisibility['default'][c.field]; });
}
// 弹窗中的临时状态(确定后才生效)
if (!window._planPendingVisible) {
window._planPendingVisible = {};
PLAN_COLUMNS.forEach((c, i) => { window._planPendingVisible[c.field] = c.defaultShow; });
}
// 弹窗中拖拽后的列顺序(存储字段名数组)
if (!window._planColDragOrder) {
window._planColDragOrder = PLAN_COLUMNS.map(c => c.field);
}
// 行内编辑功能
window.prodInlineEditClick = function(el) {
inlineEditClick(el);
};
window.inlineEditClick = function(el) {
if (window._currentRole !== 'admin') return;
var code = el.dataset.code;
var field = el.dataset.field;
var val = el.dataset.val;
var input = document.createElement('input');
input.type = 'text';
input.className = 'form-input';
input.style.width = '80px';
input.style.height = '28px';
input.style.padding = '2px 6px';
input.value = val === '点击编辑' ? '' : val;
input.dataset.code = code;
input.dataset.field = field;
el.parentNode.replaceChild(input, el);
input.focus();
input.select();
function saveEdit() {
var newVal = input.value.trim();
var fieldMap = {'生产货期':'lead_time','物流时间':'logistics_time','最低起订量':'min_order_qty'};
var apiField = fieldMap[field] || field;
fetch('/api/product/update_fields', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code, field: apiField, value: newVal || '0'})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { alert(res.msg); }
loadPlanPage(planCurrentPage);
})
.catch(function(e) { alert('保存失败'); });
}
input.addEventListener('blur', saveEdit);
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { input.blur(); }
if (e.key === 'Escape') {
var span = document.createElement('span');
span.className = 'inline-edit';
span.dataset.code = code;
span.dataset.field = field;
span.dataset.val = val;
span.onclick = function() { inlineEditClick(this); };
span.textContent = val || (window._currentRole === 'admin' ? '点击编辑' : '-');
input.parentNode.replaceChild(span, input);
}
});
}
function applyPlanColVisible() {
PLAN_COLUMNS.forEach((c, i) => {
window['_planColIdx_' + c.field] = i;
const show = window.planColVisible[c.field];
document.querySelectorAll('.plan-col-' + i).forEach(el => {
el.style.display = show ? '' : 'none';
});
document.querySelectorAll('.plan-th-' + i).forEach(el => {
el.style.display = show ? '' : 'none';
});
});
}
function renderPlanColCheckboxes(grid) {
if (!grid) grid = document.getElementById('planColGrid');
if (!grid) return;
grid.innerHTML = '';
const order = window._planColDragOrder || PLAN_COLUMNS.map(c => c.field);
order.forEach((field, i) => {
const col = PLAN_COLUMNS.find(c => c.field === field);
if (!col) return;
// 云仓/外仓库存仅在国内品(cloud)视图的字段设置中显示
// 云仓相关字段仅在国内品(cloud)视图的字段设置中显示
var cloudOnlyFields = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量',
'云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量',
'云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测'];
if (planActiveView !== 'cloud' && cloudOnlyFields.indexOf(field) >= 0) return;
const checked = window._planPendingVisible[field] ? 'checked' : '';
const label = document.createElement('label');
label.draggable = true;
label.dataset.field = field;
label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px';
label.innerHTML = `<span style="color:#bbb;font-size:12px;cursor:grab">⠿</span><input type="checkbox" id="plan-cb-${field}" ${checked} onchange="togglePlanCol('${field}')"> ${col.label}`;
label.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', field);
this.style.opacity = '0.4';
});
label.addEventListener('dragend', function(e) {
this.style.opacity = '1';
document.querySelectorAll('#planColGrid label').forEach(el => el.style.border = '');
});
label.addEventListener('dragover', function(e) {
e.preventDefault();
document.querySelectorAll('#planColGrid label').forEach(el => el.style.border = '');
this.style.border = '2px dashed var(--primary)';
});
label.addEventListener('dragleave', function(e) {
this.style.border = '';
});
label.addEventListener('drop', function(e) {
e.preventDefault();
document.querySelectorAll('#planColGrid label').forEach(el => el.style.border = '');
const fromField = e.dataTransfer.getData('text/plain');
const toField = this.dataset.field;
if (fromField === toField) return;
const order = window._planColDragOrder;
const fromIdx = order.indexOf(fromField);
const toIdx = order.indexOf(toField);
if (fromIdx === -1 || toIdx === -1) return;
order.splice(fromIdx, 1);
order.splice(toIdx, 0, fromField);
renderPlanColCheckboxes();
});
grid.appendChild(label);
});
}
function openPlanColModal() {
// 初始化拖拽顺序
window._planColDragOrder = PLAN_COLUMNS.map(c => c.field);
const grid = document.getElementById('planColGrid');
if (grid) renderPlanColCheckboxes(grid);
// 把当前生效状态复制到临时状态
PLAN_COLUMNS.forEach((c, i) => {
window._planPendingVisible[c.field] = window.planColVisible[c.field];
const cb = document.getElementById('plan-cb-' + c.field);
if (cb) cb.checked = window.planColVisible[c.field];
});
// 同步全选checkbox状态
const allCb = document.getElementById('plan-cb-all');
if (allCb) {
allCb.checked = PLAN_COLUMNS.every(c => window.planColVisible[c.field]);
}
// 重置搜索框
const searchInput = document.getElementById('planColSearchInput');
if (searchInput) {
searchInput.value = '';
}
filterPlanColCheckboxes();
document.getElementById('planColModal').style.display = 'flex';
// 刷新方案列表
refreshPlanSchemeList();
}
function confirmPlanCol() {
// 应用拖拽后的新顺序
var newOrder = window._planColDragOrder;
if (newOrder) {
var reordered = [];
newOrder.forEach(function(field) {
var col = PLAN_COLUMNS.find(function(c) { return c.field === field; });
if (col) reordered.push(col);
});
PLAN_COLUMNS.forEach(function(c) {
if (newOrder.indexOf(c.field) === -1) reordered.push(c);
});
PLAN_COLUMNS.length = 0;
reordered.forEach(function(c) { PLAN_COLUMNS.push(c); });
PLAN_COLUMNS.forEach(function(c, i) { window['_planColIdx_' + c.field] = i; });
}
PLAN_COLUMNS.forEach(function(c, i) {
// 非cloud视图不允许修改云仓/外仓相关字段显隐
var cloudOnlyFields = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量',
'云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量',
'云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测'];
if (planActiveView !== 'cloud' && cloudOnlyFields.indexOf(c.field) >= 0) return;
window.planColVisible[c.field] = window._planPendingVisible[c.field];
// 保存到当前视图的独立状态
if (window._planViewVisibility && window._planViewVisibility[planActiveView]) {
window._planViewVisibility[planActiveView][c.field] = window._planPendingVisible[c.field];
}
});
planCurrentPage = Math.max(1, planCurrentPage || 1);
// 全量刷新页面(确保冻结列的 sticky 位置正确)
if (window._pageCache) delete window._pageCache['planning'];
loadPage('planning');
document.getElementById('planColModal').style.display = 'none';
}
function closePlanColModal() {
document.getElementById('planColModal').style.display = 'none';
}
function togglePlanCol(field) {
window._planPendingVisible[field] = !window._planPendingVisible[field];
const show = window._planPendingVisible[field];
const cb = document.getElementById('plan-cb-' + field);
if (cb) cb.checked = show;
}
function filterPlanColCheckboxes() {
const keyword = (document.getElementById('planColSearchInput')?.value || '').trim();
const labels = document.querySelectorAll('#planColCheckboxes label');
labels.forEach(label => {
if (!keyword) {
label.style.display = '';
} else {
const text = label.textContent || '';
label.style.display = text.includes(keyword) ? '' : 'none';
}
});
}
function togglePlanColPendingAll(checked) {
PLAN_COLUMNS.forEach((c, i) => {
// 非cloud视图不操作云仓/外仓相关字段
var cloudOnlyFields = ['云仓库存', '外仓库存', '云仓昨日销量', '外仓昨日销量',
'云仓3天平均销量', '外仓3天平均销量', '云仓7天平均销量', '外仓7天平均销量',
'云仓14天平均销量', '外仓14天平均销量', '云仓日销量预测', '外仓日销量预测'];
if (planActiveView !== 'cloud' && cloudOnlyFields.indexOf(c.field) >= 0) return;
window._planPendingVisible[c.field] = checked;
const cb = document.getElementById('plan-cb-' + c.field);
if (cb) cb.checked = checked;
});
}
function loadPlanningPage(container) {
// 冻结列:计算所有可见的冻结列及其 left 偏移
var frozenLefts = {};
if (planFrozen) {
var leftAccum = 50;
frozenLefts['_index'] = 0;
planFrozenFields.forEach(function(f) {
if (window.planColVisible[f]) {
frozenLefts[f] = leftAccum;
leftAccum += (FROZEN_WIDTHS[f] || 100);
}
});
}
var frozenStyle = 'position:sticky;top:0;z-index:2';
if (planFrozen) frozenStyle = 'position:sticky;left:0;top:0;z-index:999;background:var(--bg-card);transform:translateZ(0)';
let theadHtml = '<tr><th class="plan-col-index" style="width:50px;' + frozenStyle + '"><div style="text-align:center;width:100%">序号</div></th>';
PLAN_COLUMNS.forEach((c, i) => {
const visibleClass = window.planColVisible[c.field] ? '' : ' style="display:none"';
var sortArrow = (planSortField === c.field) ? (planSortAsc ? ' ▲' : ' ▼') : '';
var isSortable = (c.field === '产品分级') || SORTABLE_FIELDS[c.field];
var planTips = {
'日销量预测': '日销量预测 = (3天平均×w3 + 7天平均×w7 + 14天平均×w14) × 调整系数',
'库存可支撑天数': '库存可支撑天数 = 库存 / 日销量预测',
'加工厂库存支撑天数': '加工厂库存支撑 = 加工厂库存 / 日销量预测',
'库存工厂可支撑天数': '现货采购在途 = 采购在途 / 日销量预测',
'库存工厂待生产可支撑天数': '定制品在途 = 生产在途 / 日销量预测',
'总支撑天数': '总支撑天数 = (库存+加工厂库存+生产在途+采购在途) / 日销量预测',
'库存健康指数': '日销量预测>0:库存可支撑天数÷(货期+物流);=0:库存可支撑天数÷(货期+物流)',
'建议下单数量': '日销量预测=0且库存>0则0,否则(安全库存天数+货期+物流)×日销量预测',
'实际下单数量': '建议下单数量向上取整到1000',
'正式下单数量': '正式下单数量初始等于实际下单数量,可人工修改',
'建议回货数量': 'ROUNDUP((日销量预测×回货天数-库存)×10)/10,上限在途总数',
'是否新品': '创建时间5个月以内为新品,否则返单品',
'成本价': '从产品信息匹配的成本价格',
'销售趋势': '基于3/7/14天日均销量判断的9种趋势类型'
};
var tip = planTips[c.label] || '';
var thPos = 'position:sticky;top:0;z-index:10';
if (planFrozen && frozenLefts[c.field] !== undefined) {
thPos = 'position:sticky;top:0;left:' + frozenLefts[c.field] + 'px;z-index:999;background:var(--bg-card);transform:translateZ(0);min-width:' + (FROZEN_WIDTHS[c.field] || 100) + 'px';
}
var canFilter = c.filterable !== false;
var isActive = window._planFilters && window._planFilters[c.field];
var iconColor = isActive ? '#1890ff' : 'var(--text-light)';
var iconWeight = isActive ? 'bold' : 'normal';
var iconChar2 = isActive ? '▼' : '▾';
var filterIcon = canFilter ? '<span style="margin-left:4px;font-size:20px;padding:2px 6px;color:' + iconColor + ';cursor:pointer;font-weight:' + iconWeight + '" onclick="event.stopPropagation();openPlanFilterMenu(\'' + escJs(c.field) + '\', event)" title="筛选">' + iconChar2 + '</span>' : '';
var clickHandler = canFilter ? 'openPlanFilterMenu(\'' + escJs(c.field) + '\', event)' : '';
theadHtml += `<th class="plan-col-${i} plan-th-${i}"${visibleClass} onclick="${clickHandler}" style="cursor:${canFilter ? 'pointer' : 'default'};${isActive ? 'background:#e6f7ff;' : ''}${thPos}"${tip ? ' title="' + escHtml(tip) + '"' : ''}>${c.label}${sortArrow}${filterIcon}</th>`;
});
theadHtml += '<th class="plan-col-action" style="width:200px;min-width:200px;position:sticky;top:0;right:0;background:var(--bg,#fff);z-index:11;border-left:1px solid var(--border)">操作</th>';
theadHtml += '</tr>';
let colToggleHtml = '<button class="btn btn-sm" id="planFrozenBtn" style="margin-right:6px">🔒 冻结列</button>';
colToggleHtml += '<button class="btn btn-sm" onclick="openPlanColModal()">⚙ 字段设置</button>';
container.innerHTML = `
<style>
.filter-select-wrap { position:relative; display:inline-block; }
.filter-select-wrap select { padding-right:20px !important; }
.filter-clear-btn:hover { color:#ff4d4f !important; background:#fff1f0 !important; }
.filter-clear-btn { font-family:sans-serif; }
</style>
<div class="page-header" style="padding:8px 0;margin-bottom:4px">
<h1 style="font-size:18px;margin:0">计划预测</h1>
<p class="page-desc" style="font-size:12px;margin:2px 0 0">产品计划与预测数据</p>
</div>
<style>
.plan-tabs { display:flex;gap:4px;margin-bottom:14px;background:var(--bg-card,#f7f8fa);padding:4px;border-radius:10px; }
.plan-tab { padding:8px 22px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text-light,#888);border-radius:8px;transition:all .25s;user-select:none; }
.plan-tab:hover { color:var(--text,#333);background:rgba(0,0,0,0.04); }
.plan-tab:nth-child(1).active { color:#1890ff;background:#e6f7ff;box-shadow:0 1px 4px rgba(24,144,255,0.12); }
.plan-tab:nth-child(2).active { color:#13c2c2;background:#e6fffb;box-shadow:0 1px 4px rgba(19,194,194,0.12); }
.plan-tab:nth-child(3).active { color:#f5a623;background:#fff7e6;box-shadow:0 1px 4px rgba(245,166,35,0.12); }
</style>
<div class="plan-tabs" id="planTabs">
<div class="plan-tab active" data-view="default" onclick="switchPlanView('default')">计划预测</div>
<div class="plan-tab" data-view="cloud" onclick="switchPlanView('cloud')">计划预测-国内品</div>
<div class="plan-tab" data-view="external" onclick="switchPlanView('external')">计划预测-海外品</div>
</div>
<div class="card" style="margin-bottom:8px">
<div class="card-body" style="padding:8px 12px">
<div class="filter-bar" style="flex-wrap:wrap;gap:6px">
<input type="text" id="planSearch" class="form-input" style="width:200px" placeholder="🔍 货品编号/货品名称" autocomplete="off" onkeydown="if(event.key==='Enter')loadPlanPage(1)">
<span style="font-size:13px;color:var(--text-secondary);margin-left:4px">创建时间</span>
<select id="planDateCond" class="form-input" style="width:80px">
<option value="">全部</option>
<option value="lt">早于</option>
<option value="eq">等于</option>
<option value="gt">晚于</option>
</select>
<input type="date" id="planDateVal" class="form-input" style="width:200px">
<input type="text" id="planSupplierFilter" class="form-input" style="width:240px" placeholder="供应商名称" autocomplete="off" onkeydown="if(event.key==='Enter')planFilterApply()">
<span class="filter-select-wrap"><select id="planFollowerFilter" class="form-input" style="width:auto;min-width:80px">
<option value="">采购跟单</option>
</select></span>
<span class="filter-select-wrap"><select id="planPlannerFilter" class="form-input" style="width:auto;min-width:80px">
<option value="">采购计划</option>
</select></span>
<button class="btn btn-primary" onclick="searchPlanning()">查询</button>
<button class="btn" onclick="resetPlanningFilters()">重置</button>
<span class="data-count" id="planCount"></span>
</div>
</div>
</div>
<div class="page-flex" style="flex:1;min-height:300px;margin-top:15px">
<div class="card" style="margin-top:15px">
<div class="card-header" style="display:flex;align-items:center">
<h3 style="flex:0 0 auto">产品计划列表</h3>
<div style="display:flex;align-items:center;gap:8px;margin-left:16px">
</div>
<div style="margin-left:auto">
${colToggleHtml}
</div>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="planTable">
<thead>${theadHtml}</thead>
<tbody id="planTbody"></tbody>
</table>
</div>
</div>
<div id="planPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center;z-index:20"></div>
</div>
</div>
<!-- 字段设置弹窗 -->
<div class="modal-overlay" id="planColModal" style="display:none">
<div class="modal" style="width:720px">
<div class="modal-header">
<h3>字段设置</h3>
<span class="modal-close" onclick="closePlanColModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:65vh;overflow-y:auto;padding:16px 20px">
<!-- 方案管理 -->
<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="font-weight:600;font-size:14px;white-space:nowrap">📁 方案管理</span>
<input type="text" id="planSchemeNameInput" class="form-input" placeholder="输入方案名称..." style="flex:1;min-width:0">
<button class="btn btn-primary btn-sm" onclick="savePlanScheme()">💾 保存方案</button>
</div>
<div id="planSchemeList" style="display:flex;flex-wrap:wrap;gap:6px;min-height:28px">
<span style="color:var(--text-light);font-size:13px">暂无保存方案</span>
</div>
</div>
<div class="col-toggle-all" style="padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:8px">
<label class="cb-all"><input type="checkbox" id="plan-cb-all" checked onchange="togglePlanColPendingAll(this.checked)"> <strong>全选</strong></label>
</div>
<input type="text" id="planColSearchInput" class="form-input" placeholder="搜索字段名称..." oninput="filterPlanColCheckboxes()" style="margin-bottom:8px;width:100%;box-sizing:border-box">
<div id="planColCheckboxes"></div>
</div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:12px;color:var(--text-light)">拖拽字段名可调整列顺序</span>
<div>
<button class="btn" onclick="closePlanColModal()">取消</button>
<button class="btn btn-primary" onclick="confirmPlanCol()">确定</button>
</div>
</div>
</div>
</div>
<!-- 冻结列选择弹窗 -->
<div class="modal-overlay" id="planFrozenModal" style="display:none">
<div class="modal" style="width:400px">
<div class="modal-header">
<h3>🔒 冻结列设置</h3>
<span class="modal-close" onclick="closePlanFrozenModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:60vh;overflow-y:auto;padding:12px 20px" id="planFrozenModalBody">
</div>
<div class="modal-footer">
<button class="btn" onclick="closePlanFrozenModal()">取消</button>
<button class="btn btn-primary" onclick="confirmPlanFrozen()">确定</button>
</div>
</div>
</div>
`;
// 冻结列按钮 - 事件绑定
var frozenBtn = document.getElementById('planFrozenBtn');
if (frozenBtn) frozenBtn.onclick = openPlanFrozenModal;
// 填充字段设置弹窗(3列网格,可拖拽排序)
const cbContainer = document.getElementById('planColCheckboxes');
if (cbContainer) {
cbContainer.innerHTML = '<div id="planColGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px 12px;padding:4px 0"></div>';
const grid = document.getElementById('planColGrid');
window._planColDragOrder = PLAN_COLUMNS.map(c => c.field);
renderPlanColCheckboxes(grid);
}
// 禁用浏览器表单自动填充
var planSearchEl = document.getElementById('planSearch');
if (planSearchEl) planSearchEl.setAttribute('autocomplete', 'off');
// 初始化下拉清除按钮
setupFilterClearButtons();
// 恢复当前视图的tab高亮(避免从字段设置确认后tab跳回第一个)
document.querySelectorAll('.plan-tab').forEach(function(el) {
el.classList.toggle('active', el.dataset.view === planActiveView);
});
loadPlanPage(1);
// 加载默认方案
loadDefaultPlanScheme();
// 后台预拉取计划页所有列的筛选值
prefetchPlanColumnValues();
}
// 采购跟单/采购计划下拉框:选中值时悬浮显示×清除按钮
function setupFilterClearButtons() {
var items = [
{ el: document.getElementById('planFollowerFilter'), name: '采购跟单' },
{ el: document.getElementById('planPlannerFilter'), name: '采购计划' }
];
items.forEach(function(item) {
var sel = item.el;
if (!sel) return;
var wrap = sel.parentNode;
if (!wrap.classList.contains('filter-select-wrap')) return;
// 防重复
if (wrap.querySelector('.filter-clear-btn')) return;
var clearBtn = document.createElement('span');
clearBtn.className = 'filter-clear-btn';
clearBtn.innerHTML = '✕';
clearBtn.style.cssText = 'display:none;position:absolute;right:4px;top:50%;transform:translateY(-50%);width:16px;height:16px;line-height:16px;text-align:center;font-size:12px;cursor:pointer;color:#999;background:#fff;border-radius:50%;z-index:5;user-select:none';
clearBtn.title = '清除筛选';
wrap.style.position = 'relative';
wrap.appendChild(clearBtn);
// 更新清除按钮显隐
function updateClearBtn() {
clearBtn.style.display = sel.value ? 'block' : 'none';
}
updateClearBtn();
sel.addEventListener('change', function() {
updateClearBtn();
// 选中值变化时自动查询
searchPlanning();
});
sel.addEventListener('mouseenter', function() { if (sel.value) clearBtn.style.display = 'block'; });
sel.addEventListener('mouseleave', function() { clearBtn.style.display = 'none'; });
clearBtn.addEventListener('mouseenter', function() { clearBtn.style.display = 'block'; });
clearBtn.addEventListener('mouseleave', function() { clearBtn.style.display = 'none'; });
clearBtn.addEventListener('click', function(e) {
e.stopPropagation();
sel.value = '';
clearBtn.style.display = 'none';
searchPlanning();
});
});
}
let planCurrentPage = 1;
let planTotal = 0;
let PLAN_PAGE_SIZE = 20;
let planSortField = '';
let planSortAsc = false;
const GRADE_ORDER = {'S级':0, 'A级':1, 'B级':2, 'C级':3, '不参与评级':4};
function planSortRows(rows) {
if (!planSortField) return rows;
return rows.sort(function(a, b) {
var va = a[planSortField], vb = b[planSortField];
if (planSortField === '产品分级') {
va = GRADE_ORDER[va] !== undefined ? GRADE_ORDER[va] : 999;
vb = GRADE_ORDER[vb] !== undefined ? GRADE_ORDER[vb] : 999;
} else {
if (va === null || va === undefined || va === '') va = '';
if (vb === null || vb === undefined || vb === '') vb = '';
va = isNaN(Number(va)) ? String(va).toLowerCase() : Number(va);
vb = isNaN(Number(vb)) ? String(vb).toLowerCase() : Number(vb);
}
if (va < vb) return planSortAsc ? -1 : 1;
if (va > vb) return planSortAsc ? 1 : -1;
return 0;
});
}
function loadPlanPage(page) {
planCurrentPage = page;
const search = document.getElementById('planSearch')?.value || '';
const dateCond = document.getElementById('planDateCond')?.value || '';
const dateVal = document.getElementById('planDateVal')?.value || '';
const supplierFilter = document.getElementById('planSupplierFilter')?.value || '';
const followerFilter = document.getElementById('planFollowerFilter')?.value || '';
const plannerFilter = document.getElementById('planPlannerFilter')?.value || '';
// 从全局筛选状态读取
var filterValue = '';
if (window._planFilters) {
var activeCount = 0;
var filters = [];
Object.keys(window._planFilters).forEach(function(f) {
var fp = window._planFilters[f];
if (fp) { fp.field = f; filters.push(fp); activeCount++; }
});
if (activeCount > 0) {
filterValue = '__multi__:' + JSON.stringify(filters);
}
}
// 如果有计算字段筛选条件,需要拿全量数据做客户端过滤
var hasExcludedFilter = false;
if (window._planFilters) {
Object.keys(window._planFilters).forEach(function(ff) {
if (PLAN_FILTER_EXCLUDE_FIELDS[ff]) hasExcludedFilter = true;
});
}
var dataSize = hasExcludedFilter ? 10000 : PLAN_PAGE_SIZE;
let url = '/api/planning/list?page=' + page + '&size=' + dataSize;
if (search) url += '&search=' + encodeURIComponent(search);
if (filterValue) url += '&custom_field=__multi__&custom_value=' + encodeURIComponent(filterValue);
if (dateCond && dateVal) url += '&date_cond=' + encodeURIComponent(dateCond) + '&date_val=' + encodeURIComponent(dateVal);
if (supplierFilter) url += '&supplier=' + encodeURIComponent(supplierFilter);
if (followerFilter) url += '&procurement_follower=' + encodeURIComponent(followerFilter);
if (plannerFilter) url += '&procurement_planner=' + encodeURIComponent(plannerFilter);
if (planSortField) url += '&sort_by=' + encodeURIComponent(planSortField) + '&sort_order=' + (planSortAsc ? 'asc' : 'desc');
if (planActiveView !== 'default') url += '&view=' + encodeURIComponent(planActiveView);
fetch(url)
.then(r => r.json())
.then(res => {
if (res.code !== 0) return;
const data = res.data;
planTotal = data.total;
// 缓存全量行数据(用于计算字段的值列表提取)
if (!window._planAllRows) window._planAllRows = {};
if (data.rows) {
data.rows.forEach(function(r) { window._planAllRows[r.product_code] = r; });
}
// 客户端过滤计算字段(后端无法过滤的字段)
if (data.rows && window._planFilters && Object.keys(window._planFilters).length > 0) {
var excludedSet = {};
Object.keys(PLAN_FILTER_EXCLUDE_FIELDS).forEach(function(f) { excludedSet[f] = 1; });
var clientFiltered = data.rows.filter(function(rr) {
var pass = true;
Object.keys(window._planFilters).forEach(function(ff) {
if (!excludedSet[ff]) return; // DB字段后端已处理
var fp = window._planFilters[ff];
if (!fp || !fp.type) return;
var rrVal = rr[ff];
if (rrVal === undefined || rrVal === null) rrVal = '';
rrVal = String(rrVal).trim();
if (fp.type === 'list' && fp.values) {
if (fp.values.indexOf(rrVal) < 0) { pass = false; }
} else if (fp.type === 'search' && fp.value) {
if (rrVal.indexOf(fp.value) < 0) { pass = false; }
} else if (fp.type === 'num') {
var numVal = parseFloat(rrVal) || 0;
var condVal = parseFloat(fp.val) || 0;
if (fp.cond === 'eq' && numVal !== condVal) pass = false;
else if (fp.cond === 'neq' && numVal === condVal) pass = false;
else if (fp.cond === 'gt' && numVal <= condVal) pass = false;
else if (fp.cond === 'gte' && numVal < condVal) pass = false;
else if (fp.cond === 'lt' && numVal >= condVal) pass = false;
else if (fp.cond === 'lte' && numVal > condVal) pass = false;
else if (fp.cond === 'range') {
var parts = (fp.val || '').split(',').map(function(x){return parseFloat(x.trim())||0;});
if (parts.length === 2 && (numVal < parts[0] || numVal > parts[1])) pass = false;
}
}
});
return pass;
});
var hadExcludedFilterApplied = false;
Object.keys(window._planFilters).forEach(function(cf) {
if (excludedSet[cf]) hadExcludedFilterApplied = true;
});
if (hadExcludedFilterApplied) {
data.rows = clientFiltered;
data.total = clientFiltered.length;
planTotal = clientFiltered.length;
}
}
window._planCurrentAllCodes = data.all_codes || [];
renderPlanTable(planSortRows(data.rows));
document.getElementById('planCount').textContent = '共 ' + data.total + ' 条';
// 根据当前视图调整列显隐和名称
updatePlanViewColumns();
// 填充采购跟单/采购计划下拉选项(仅首次加载或当页面为第1页时更新)
if (page === 1) { populatePlanPersonFilter(data.rows); setupFilterClearButtons(); }
const pel = document.getElementById('planPagination');
if (pel) renderPaginationWithSize(pel, planCurrentPage, planTotal, PLAN_PAGE_SIZE, 'loadPlanPage', 'PLAN_PAGE_SIZE');
});
}
function renderPlanTable(rows) {
const tbody = document.getElementById('planTbody');
// 按权限过滤列(不按显隐过滤,thead用display:none控制显隐)
var permFilteredCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField('planning', c.label); });
if (!rows || rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="' + (permFilteredCols.length + 1) + '" style="text-align:center;color:var(--text-light);padding:40px">暂无数据</td></tr>';
return;
}
var permFilterSet = {};
permFilteredCols.forEach(function(c) { permFilterSet[c.field] = true; });
const startIdx = (planCurrentPage - 1) * PLAN_PAGE_SIZE;
const typeMap = {'成品':'blue','包材':'orange','赠品':'green','小样':'purple','物料':'orange','其他':'grey'};
tbody.innerHTML = rows.map((r, rowIdx) => {
const vals = permFilteredCols.map(c => {
const raw = r[c.field];
if (c.field === 'product_type') {
const color = typeMap[raw] || 'grey';
return `<span class="tag tag-${color}">${escHtml(raw || '-')}</span>`;
}
if (c.field === '产品种类') {
const color = typeMap[raw] || 'grey';
return `<span class="tag tag-${color}">${escHtml(raw || '-')}</span>`;
}
if (c.field === '产品种类') {
const color = typeMap[raw] || 'grey';
return `<span class="tag tag-${color}">${escHtml(raw || '-')}</span>`;
}
if (c.field === 'product_code') {
return `<a href="javascript:;" onclick="showProductDetail('${escHtml(raw)}')" class="sku-link"><code>${escHtml(raw || '-')}</code></a>`;
}
if (c.field === 'product_name') {
return escHtml(truncate(raw || '-', 25));
}
if (c.field === 'bom') {
var pcode = (r.product_code || '');
var pname = escHtml(r.product_name || '');
var suggQty = r['建议下单数量'] || 0;
var actQty = r['实际下单数量'] || 0;
var remark = escHtml(r['下单备注'] || '');
return '<button class="btn btn-sm btn-outline" onclick="planToggleBom(\'' + escHtml(pcode) + '\',\'' + pname + '\',' + suggQty + ',' + actQty + ',\'' + remark + '\')" title="查看下单清单" style="font-size:12px;padding:2px 10px">📋 BOM</button>';
}
if (c.field === '生产货期' || c.field === '物流时间' || c.field === '最低起订量') {
var isAdmin = window._currentRole === 'admin';
var val = raw ?? '';
return '<span class="inline-edit" data-code="' + escHtml(r.product_code || '') + '" data-field="' + c.field + '" data-val="' + val + '" ' + (isAdmin ? 'onclick="inlineEditClick(this)"' : '') + '>' + (val === '' || val === null || val === undefined ? (isAdmin ? '点击编辑' : '-') : val) + '</span>';
}
if (c.field === '创建时间') {
return raw || '-';
}
if (c.field === '昨日销量' || c.field === '云仓昨日销量' || c.field === '外仓昨日销量' || c.field === '3天平均销量' || c.field === '7天平均销量' || c.field === '14天平均销量' || c.field === '30天平均销量' || c.field === '60天平均销量' || c.field === '3天最高销量' || c.field === '7天最高销量' || c.field === '14天最高销量' || c.field === '30天最高销量' || c.field === '14天最低销量' || c.field === '14天标准差') {
return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw);
}
if (c.field === '库存' || c.field === '云仓库存' || c.field === '外仓库存' || c.field === '加工厂库存' || c.field === '生产在途' || c.field === '采购在途') {
return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw);
}
if (c.field === 'cost_price') {
var val = parseFloat(raw);
return isNaN(val) ? '-' : '¥' + val.toFixed(2);
}
if (c.field === '最低销量偏差率' || c.field === '最高销量偏差率' || c.field === '14天波动率') {
return (raw === 0 || raw === null || raw === undefined) ? '0' : String(raw);
}
if (c.field === '极值判断') {
var cls = 'tag-grey';
if (raw === '极大/极小') cls = 'tag-red';
else if (raw === '极小') cls = 'tag-orange';
else if (raw === '极大') cls = 'tag-orange';
else if (raw === '正常') cls = 'tag-green';
return '<span class="tag ' + cls + '">' + (raw || '-') + '</span>';
}
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 '<span class="tag ' + cls + '">' + (raw || '-') + '</span>';
}
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 '<span class="tag ' + cls + '" style="font-weight:700">' + (raw || '-') + '</span>';
}
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 '<span class="tag ' + cls + '">' + (raw || '-') + '</span>';
}
if (c.field === '是否新品') {
return (raw === '新品') ? '<span class="tag tag-blue">新品</span>' : '<span class="tag tag-grey">返单品</span>';
}
if (c.field === '日销量预测') {
var val = parseFloat(raw);
if (isNaN(val) || val === 0) return '0';
return '<span style="font-weight:600;color:var(--primary)">' + Math.round(val) + '</span>';
}
if (c.field === '库存可支撑天数') {
var val = parseFloat(raw);
if (isNaN(val)) return '-';
if (val === 999) return '<span class="tag tag-grey">999</span>';
return val.toFixed(1);
}
if (c.field === '库存工厂可支撑天数') {
var val = parseFloat(raw);
if (isNaN(val)) return '-';
if (val === 999) return '<span class="tag tag-grey">999</span>';
return val.toFixed(1);
}
if (c.field === '加工厂库存支撑天数') {
var val = parseFloat(raw);
if (isNaN(val)) return '-';
if (val === 999) return '<span class="tag tag-grey">999</span>';
return val.toFixed(1);
}
if (c.field === '库存工厂待生产可支撑天数') {
var val = parseFloat(raw);
if (isNaN(val)) return '-';
if (val === 999) return '<span class="tag tag-grey">999</span>';
return val.toFixed(1);
}
if (c.field === '总支撑天数') {
var val = parseFloat(raw);
if (isNaN(val)) return '-';
if (val === 999) return '<span class="tag tag-grey">999</span>';
return val.toFixed(1);
}
if (c.field === '库存健康指数') {
var val = parseFloat(raw);
if (isNaN(val) || val === 999) return '<span class="tag tag-grey">999</span>';
if (val >= 1.5) return '<span class="tag tag-green">' + val.toFixed(1) + '</span>';
if (val >= 0.8) return '<span class="tag tag-orange">' + val.toFixed(1) + '</span>';
return '<span class="tag tag-red">' + val.toFixed(1) + '</span>';
}
if (c.field === '风险把控') {
var riskColors = {'正常':'tag-green','新品':'tag-blue','呆滞品':'tag-orange','需启动清库存':'tag-red'};
var cls = riskColors[raw] || 'tag-grey';
return '<span class="tag ' + cls + '">' + (raw || '-') + '</span>';
}
if (c.field === '建议下单数量') {
var val = parseInt(raw);
if (isNaN(val) || val === 0) return '0';
return '<span style="font-weight:600;color:var(--primary)">' + val + '</span>';
}
if (c.field === '实际下单数量') {
var val = parseInt(raw);
if (isNaN(val) || val === 0) return '0';
return '<span style="font-weight:600;color:#d4380d">' + val + '</span>';
}
if (c.field === '正式下单数量') {
var currentVal = parseInt(r['实际下单数量']) || 0;
var code = escHtml(r.product_code || '');
var canEditFormal = window._canEditField && window._canEditField('planning', '正式下单数量');
return '<span class="formal-qty-wrap" style="display:inline-flex;align-items:center;gap:4px">' +
'<span class="formal-qty-val" data-code="' + code + '" data-val="' + currentVal + '" id="fqv-' + code + '">' + currentVal + '</span>' +
(canEditFormal ? '<span class="formal-qty-edit" onclick="editFormalQty(\'' + code + '\')" style="cursor:pointer;color:var(--primary);font-size:14px" title="编辑">✏️</span>' : '') +
'</span>';
}
if (c.field === '建议回货数量') {
var val = parseFloat(raw);
if (isNaN(val) || val === 0) return '0';
return '<span style="font-weight:600;color:#cf1322">' + val.toFixed(1) + '</span>';
}
// 人员列:彩色标签
if (c.field === '采购跟单' || c.field === '采购计划' || c.field === '开发专员' || c.field === '运营专员') {
return renderNameBadges(raw);
}
return escHtml(raw || '-');
});
// 冻结列:计算可见冻结列的 left 偏移
// 冻结列:计算可见冻结列的 left 偏移
var colLefts = {};
if (planFrozen) {
var acc = 50;
colLefts['_index'] = 0;
planFrozenFields.forEach(function(f) {
if (window.planColVisible[f]) {
colLefts[f] = acc;
acc += (FROZEN_WIDTHS[f] || 100);
}
});
}
var idxSticky = planFrozen ? 'position:sticky;left:0;z-index:9;background:var(--bg-card);transform:translateZ(0);min-width:50px' : '';
var code2 = r.product_code || '';
let colsHtml = `<td class="plan-col-index row-index" style="text-align:center;width:50px;${idxSticky}">${startIdx + rowIdx + 1}</td>`;
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 `<td class="plan-col-${i}"${disp} style="${extraStyle}">${vals[i]}</td>`;
}).join('');
var code = r.product_code || '';
// 序列化下单所需数据
var formalQty = parseInt(r['实际下单数量']) || 0;
var suggestedQty = parseInt(r['建议下单数量']) || 0;
var orderData = JSON.stringify({
product_code: r.product_code || '',
product_name: r.product_name || '',
product_type: r.product_type || '',
product_grade: r['产品分级'] || '',
supplier_name: r['供应商名称'] || '',
lead_time: r['生产货期'] || 0,
logistics_time: r['物流时间'] || 0,
total_support_days: r['总支撑天数'] || 0,
procurement_follower: r['采购跟单'] || '',
procurement_planner: r['采购计划'] || '',
platform: r['归属平台'] || '',
operator: r['运营专员'] || '',
formal_order_qty: formalQty,
suggested_qty: suggestedQty,
remark: r['下单备注'] || ''
});
colsHtml += '<td class="plan-col-action" style="position:sticky;right:0;z-index:1;background:var(--bg,#fff);border-left:1px solid var(--border);white-space:nowrap">';
colsHtml += '<button class="btn btn-sm btn-outline" onclick="showProductDetail(\'' + escHtml(code) + '\')" title="展开详情">详情</button> ';
if (window._hasPagePerm('planning', 'edit')) colsHtml += '<button class="btn btn-sm btn-outline" onclick="showProductDetailEdit(\'' + escHtml(code) + '\')" title="编辑">编辑</button>';
if (!window._planOrderDataCache) window._planOrderDataCache = {};
window._planOrderDataCache[code] = orderData;
colsHtml += '<button class="btn btn-sm" style="background:#e6fffb;color:#13c2c2;border:1px solid #87e8de;margin-left:4px" onclick="planOrder(\'' + escHtml(code) + '\')" title="下单">📦 下单</button>';
if (window._hasPagePerm('planning', 'delete')) colsHtml += '<button class="btn btn-sm btn-outline-danger" onclick="planDeleteProduct(\'' + escHtml(code) + '\')" title="从计划预测中移除" style="margin-left:4px;font-size:11px">删除</button>';
colsHtml += '</td>';
return `<tr>${colsHtml}</tr>`;
}).join('');
// 渲染完成后动态修正冻结列位置(实际列宽与 FROZEN_WIDTHS 有偏差时修正)
// 更新表头排序箭头状态(在冻结列之前,因为 buildPlanTableHeader 会替换 thead DOM)
var planTbl = document.getElementById('planTable');
if (planTbl) buildPlanTableHeader(planTbl);
// 在表头重建后重新应用筛选图标
setTimeout(function() { if (typeof _restoreFilterIcons === 'function') _restoreFilterIcons(); }, 50);
// 渲染完成后动态修正冻结列位置(实际列宽与 FROZEN_WIDTHS 有偏差时修正)
updatePlanFrozenPositions();
// 更新筛选图标状态
if (typeof _restoreFilterIcons === 'function') _restoreFilterIcons();
}
/**
* 动态计算冻结列正确的 left 偏移(基于实际渲染宽度,而非固定 FROZEN_WIDTHS)
*/
function updatePlanFrozenPositions() {
if (!planFrozen) return;
var table = document.getElementById('planTable');
if (!table) return;
var headerRow = table.querySelector('thead tr');
if (!headerRow) return;
// 获取所有可见冻结列的实际宽度
var actualWidths = {};
// 序号列
var indexTh = headerRow.querySelector('.plan-col-index');
if (indexTh) actualWidths['_index'] = indexTh.offsetWidth;
planFrozenFields.forEach(function(field) {
var idx = window['_planColIdx_' + field];
if (idx === undefined) return;
var th = headerRow.querySelector('.plan-col-' + idx + '.plan-th-' + idx);
if (th && window.planColVisible[field]) {
actualWidths[field] = th.offsetWidth;
}
});
// 用实际宽度重新计算 left 偏移,并应用到所有 th 和 td
var acc = 0;
// 序号列
if (indexTh && actualWidths['_index']) {
indexTh.style.position = 'sticky';
indexTh.style.top = '0';
indexTh.style.left = acc + 'px';
indexTh.style.zIndex = '999';
indexTh.style.background = 'var(--bg-card)';
acc += actualWidths['_index'];
} else {
acc += 50;
}
planFrozenFields.forEach(function(field) {
if (!window.planColVisible[field]) return;
var idx = window['_planColIdx_' + field];
if (idx === undefined) return;
var leftVal = acc + 'px';
// 更新表头 th(必须同时设 position/top/z-index,否则可能因 buildPlanTableHeader 而被抹掉)
headerRow.querySelectorAll('.plan-col-' + idx + '.plan-th-' + idx).forEach(function(el) {
el.style.position = 'sticky';
el.style.top = '0';
el.style.left = leftVal;
el.style.zIndex = '999';
el.style.background = 'var(--bg-card)';
});
// 更新所有数据行中的 td
table.querySelectorAll('tbody .plan-col-' + idx).forEach(function(el) {
el.style.position = 'sticky';
el.style.left = leftVal;
el.style.zIndex = '9';
el.style.background = 'var(--bg-card)';
});
acc += actualWidths[field] || FROZEN_WIDTHS[field] || 100;
});
// 第二次修正:延迟确保渲染完成后重新计算(解决首次加载时可能计算不准)
setTimeout(function() {
// 重新测量实际宽度
var widths2 = {};
var idxTh2 = headerRow.querySelector('.plan-col-index');
if (idxTh2) widths2['_index'] = idxTh2.offsetWidth;
planFrozenFields.forEach(function(field) {
if (!window.planColVisible[field]) return;
var idx = window['_planColIdx_' + field];
if (idx === undefined) return;
var th = headerRow.querySelector('.plan-col-' + idx + '.plan-th-' + idx);
if (th) widths2[field] = th.offsetWidth;
});
var acc2 = 0;
acc2 += widths2['_index'] || 50;
if (idxTh2) {
idxTh2.style.left = '0px';
table.querySelectorAll('tbody .plan-col-index').forEach(function(el) {
el.style.left = '0px';
});
}
planFrozenFields.forEach(function(field) {
if (!window.planColVisible[field]) return;
var idx = window['_planColIdx_' + field];
if (idx === undefined) return;
var lv = acc2 + 'px';
headerRow.querySelectorAll('.plan-col-' + idx + '.plan-th-' + idx).forEach(function(el) {
el.style.left = lv;
});
table.querySelectorAll('tbody .plan-col-' + idx).forEach(function(el) {
el.style.left = lv;
});
acc2 += widths2[field] || FROZEN_WIDTHS[field] || 100;
});
}, 50);
}
// ─── 冻结列弹窗 ───
function openPlanFrozenModal() {
// 从当前视图加载冻结列设置
var viewFrozen = window._planViewFrozenFields[planActiveView] || planFrozenFields;
window._planFrozenPendingFields = viewFrozen.slice();
var html = '<div style="padding:8px 0">';
html += '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer">';
html += '<input type="checkbox" id="planFrozenCbAll" checked onchange="planFrozenToggleAll(this.checked)"> <strong>全选/取消</strong>';
html += '</label><div style="border-top:1px solid var(--border);padding-top:8px;margin-top:4px">';
var candidates = [
{field:'_index', label:'序号'},
{field:'product_type', label:'产品类型'},
{field:'product_code', label:'货品编号'},
{field:'product_name', label:'货品名称'}
];
candidates.forEach(function(c) {
var checked = window._planFrozenPendingFields.indexOf(c.field) >= 0;
html += '<label style="display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer">';
html += '<input type="checkbox" class="plan-frozen-cb" value="' + c.field + '"' + (checked ? ' checked' : '') + ' onchange="planFrozenCbChange()">';
html += '<span>' + c.label + '</span></label>';
});
html += '</div></div>';
document.getElementById('planFrozenModalBody').innerHTML = html;
document.getElementById('planFrozenModal').style.display = 'flex';
}
window.planFrozenCbChange = function() {
var checked = [];
document.querySelectorAll('.plan-frozen-cb:checked').forEach(function(cb) { checked.push(cb.value); });
window._planFrozenPendingFields = checked;
var allCb = document.getElementById('planFrozenCbAll');
if (allCb) allCb.checked = checked.length === document.querySelectorAll('.plan-frozen-cb').length;
};
window.planFrozenToggleAll = function(checked) {
document.querySelectorAll('.plan-frozen-cb').forEach(function(cb) { cb.checked = checked; });
planFrozenCbChange();
};
function closePlanFrozenModal() {
document.getElementById('planFrozenModal').style.display = 'none';
}
function confirmPlanFrozen() {
planFrozenFields = window._planFrozenPendingFields.slice();
planFrozen = planFrozenFields.length > 0;
// 保存到当前视图的独立设置
window._planViewFrozenFields[planActiveView] = planFrozenFields.slice();
var page = 'planning';
if (window._pageCache && window._pageCache[page]) delete window._pageCache[page];
loadPage(page);
}
// ─── 表头筛选菜单 ───
var _planFilterMenuField = '';
// ─── Excel风格表头筛选 ───
var _planFilterMenuField = '';
var _planFilterSelected = {};
function openPlanFilterMenu(field, event) {
event = event || window.event;
event.stopPropagation();
var existing = document.getElementById('planFilterMenu');
if (existing) existing.remove();
_planFilterMenuField = field;
var th = event.currentTarget;
var rect = th.getBoundingClientRect();
// 跟随表头滚动:监听滚动事件更新菜单位置
document.addEventListener('scroll', planFilterScrollHandler, true);
var label = '';
PLAN_COLUMNS.forEach(function(c) { if (c.field === field) label = c.label; });
// 判断是否为数字列
// 产品分级虽然可排序,但只有 S/A/B/C 四种值,用列表筛选
var productGradeFields = ['产品分级'];
var isNumericCol = SORTABLE_FIELDS[field] && !productGradeFields.includes(field);
var menu = null;
if (isNumericCol) {
menu = document.createElement('div');
menu.id = 'planFilterMenu';
var isSorted = planSortField === field;
var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : '';
_buildNumericFilterMenu(field, label, rect, isSorted, currentOrder, menu);
} else {
// 获取该列所有唯一值
var values = [];
var predefined = PLAN_FILTER_OPTIONS[field];
var hasPlanFilter = window._planFilters && Object.keys(window._planFilters).length > 0;
var isExcludedField = PLAN_FILTER_EXCLUDE_FIELDS[field] ? true : false;
// 联动筛选:有筛选时从表格提取唯一值(基于已筛选行);但当前字段已有筛选时用预定义列表保持全量选项
if (predefined && hasPlanFilter && window._planFilters[field]) {
// 当前字段本身已有筛选 → 用完整预定义列表,勾选状态由 _populateFilterMenuList 回显
values = predefined.slice();
} else if (predefined && hasPlanFilter) {
// 其他字段有筛选 → 从表格提取当前已筛选行的唯一值(联动筛选)
values = [];
var tbody = document.getElementById('planTbody');
if (tbody) {
var permCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField && window._canViewField('planning', c.label); });
var colIdx = permCols.findIndex(function(c) { return c.field === field; });
if (colIdx >= 0) {
colIdx = colIdx + 1; // +1 for 序号列
var seen = {};
tbody.querySelectorAll('tr').forEach(function(tr) {
var td = tr.cells[colIdx];
if (td) {
var txt = td.textContent.trim();
if (txt && !seen[txt]) { seen[txt] = 1; values.push(txt); }
}
});
}
}
if (values.length === 0) values = predefined.slice();
} else if (predefined && !hasPlanFilter) {
values = predefined.slice();
} else if (field === '产品分级' && hasPlanFilter && isExcludedField) {
// 从表格提取
values = [];
var tbody2 = document.getElementById('planTbody');
if (tbody2) {
var permCols2 = PLAN_COLUMNS.filter(function(c) { return window._canViewField && window._canViewField('planning', c.label); });
var colIdx2 = permCols2.findIndex(function(c) { return c.field === field; });
if (colIdx2 >= 0) {
colIdx2 = colIdx2 + 1; // +1 for 序号列
var seen2 = {};
tbody2.querySelectorAll('tr').forEach(function(tr) {
var td = tr.cells[colIdx2];
if (td) {
var txt = td.textContent.trim();
if (txt && !seen2[txt]) { seen2[txt] = 1; values.push(txt); }
}
});
}
}
if (values.length === 0) values = ['S', 'A', 'B', 'C'];
} else if (field === '产品分级' && (!hasPlanFilter || !isExcludedField)) {
values = ['S', 'A', 'B', 'C'];
} else if (field === 'brand') {
// 品牌列:优先使用缓存
var planFilterParam = '';
var hasPF = window._planFilters && Object.keys(window._planFilters).length > 0;
if (hasPF) {
planFilterParam = (planActiveView !== 'default' ? '&' : '?') + 'plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters));
}
var cacheKey = 'plan:brand:' + (planActiveView || 'default') + (hasPF ? ':filtered' : '');
var cached = !hasPF && window._planColumnCache && window._planColumnCache[cacheKey];
if (cached) {
// 命中缓存:直接构建完整菜单
if (!_planFilterSelected[field]) _planFilterSelected[field] = {};
var isSorted = planSortField === field;
var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : '';
var menuBrand = document.createElement('div');
menuBrand.id = 'planFilterMenu';
_setupPlanFilterMenuHTML(menuBrand, field, label, rect, currentOrder, isSorted);
document.body.appendChild(menuBrand);
_populateFilterMenuList(field, cached, false);
setupFilterMenuClose(menuBrand, th);
} else {
var menu = _createFilterMenuShell(field, label, rect);
document.body.appendChild(menu);
var viewParam = planActiveView !== 'default' ? '?view=' + encodeURIComponent(planActiveView) : '';
fetch('/api/planning/brands' + viewParam + planFilterParam)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) {
if (!hasPF) {
window._planColumnCache = window._planColumnCache || {};
window._planColumnCache[cacheKey] = res.data;
}
_populateFilterMenuList(field, res.data, false);
}
});
setupFilterMenuClose(menu, th);
}
return;
} else {
// 非预定义非品牌的文本列
var isExcl = PLAN_FILTER_EXCLUDE_FIELDS[field] ? true : false;
var hasPF = window._planFilters && Object.keys(window._planFilters).length > 0;
// 计算字段+联动筛选:从表格提取
if (hasPF && isExcl) {
values = [];
if (window._planAllRows) {
var seen3 = {};
Object.keys(window._planAllRows).forEach(function(pc) {
var r = window._planAllRows[pc];
var rawVal = r[field];
if (rawVal !== undefined && rawVal !== null && rawVal !== '') {
var txt = String(rawVal).trim();
if (txt && txt !== '-' && !seen3[txt]) { seen3[txt] = 1; values.push(txt); }
}
});
values.sort();
}
if (values.length === 0) values = ['暂无'];
} else {
// 非预定义文本列:优先使用缓存
var planFilterParam2 = '';
if (hasPF) {
planFilterParam2 = '&plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters));
}
var cacheKey2 = 'plan:field:' + field + ':' + (planActiveView || 'default') + (hasPF ? ':filtered' : '');
var cached2 = !hasPF && window._planColumnCache && window._planColumnCache[cacheKey2];
if (cached2) {
// 命中缓存:直接构建完整菜单
if (!_planFilterSelected[field]) _planFilterSelected[field] = {};
var isSorted2 = planSortField === field;
var currentOrder2 = isSorted2 ? (planSortAsc ? 'asc' : 'desc') : '';
var menuField = document.createElement('div');
menuField.id = 'planFilterMenu';
_setupPlanFilterMenuHTML(menuField, field, label, rect, currentOrder2, isSorted2);
document.body.appendChild(menuField);
// 采购跟单/采购计划:人员姓名转组别名
var valsCached = cached2;
if (field === 'procurement_follower' || field === 'procurement_planner') {
var groupNames = {};
valsCached.forEach(function(v) {
var names = String(v).split(',');
names.forEach(function(n) {
n = n.trim();
if (!n) return;
var g = window._nameToGroup && window._nameToGroup[n];
if (g && g.name) groupNames[g.name] = 1;
else groupNames[n] = 1;
});
});
valsCached = Object.keys(groupNames).sort();
}
_populateFilterMenuList(field, valsCached, false);
setupFilterMenuClose(menuField, th);
} else {
// 未命中缓存:从后端API获取全量唯一值
var menu = _createFilterMenuShell(field, label, rect);
document.body.appendChild(menu);
var viewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : '';
fetch('/api/planning/column-values?field=' + encodeURIComponent(field) + viewParam + planFilterParam2)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) {
var vals = res.data;
// 采购跟单/采购计划:人员姓名转组别名(需拆逗号分隔值)
if (field === 'procurement_follower' || field === 'procurement_planner') {
var groupNames = {};
vals.forEach(function(v) {
var names = String(v).split(',');
names.forEach(function(n) {
n = n.trim();
if (!n) return;
var g = window._nameToGroup && window._nameToGroup[n];
if (g && g.name) groupNames[g.name] = 1;
else groupNames[n] = 1;
});
});
vals = Object.keys(groupNames).sort();
}
if (!hasPF) {
window._planColumnCache = window._planColumnCache || {};
window._planColumnCache[cacheKey2] = res.data;
}
_populateFilterMenuList(field, vals, false);
}
});
setupFilterMenuClose(menu, th);
}
return;
}
}
if (!_planFilterSelected[field]) _planFilterSelected[field] = {};
var isSorted = planSortField === field;
var currentOrder = isSorted ? (planSortAsc ? 'asc' : 'desc') : '';
menu = document.createElement('div');
menu.id = 'planFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="planFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="planFilterSearchList(this.value)" onkeydown="if(event.key===\'Enter\')planFilterApply()"></div>';
html += '<div style="margin-bottom:6px;display:flex;gap:4px">';
html += '<button class="btn btn-sm ' + (currentOrder === 'asc' ? 'btn-primary' : 'btn-outline') + '" onclick="planFilterSort(\'' + field + '\', \'asc\')" style="font-size:11px;padding:4px 8px">▲ 升序</button>' +
'<button class="btn btn-sm ' + (currentOrder === 'desc' ? 'btn-primary' : 'btn-outline') + '" onclick="planFilterSort(\'' + field + '\', \'desc\')" style="font-size:11px;padding:4px 8px">▼ 降序</button>';
html += '</div>';
var savedPf = window._planFilters && window._planFilters[field];
var isListPf = savedPf && savedPf.type === 'list' && savedPf.values;
var isSearchPf = savedPf && savedPf.type === 'search' && savedPf.value;
var allChecked = (isListPf || isSearchPf) ? 0 : 1;
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="planFilterSelectAll" ' + (allChecked ? 'checked' : '') + ' onchange="planFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="planFilterClear()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
html += '<div id="planFilterList" style="overflow-y:auto;flex:1;max-height:200px;border-top:1px solid #f0f0f0;padding-top:4px">';
if (isSearchPf && savedPf.value) {
var pfSearchInput = document.getElementById('planFilterSearch');
if (pfSearchInput) {
pfSearchInput.value = savedPf.value;
setTimeout(function() {
if (typeof planFilterSearchList === 'function') planFilterSearchList(savedPf.value);
}, 50);
}
}
values.forEach(function(v) {
var key = escHtml(v).replace(/'/g, "\\'");
var checked = 'checked';
if (isListPf) {
checked = (savedPf.values.indexOf(v) >= 0) ? 'checked' : '';
} else if (isSearchPf) {
checked = (v.indexOf(savedPf.value) >= 0) ? 'checked' : '';
}
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="plan-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="planFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
html += '</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="planFilterApply()" style="font-size:12px;padding:4px 12px">确定</button><button class="btn btn-sm" onclick="planFilterClear()" style="font-size:12px;padding:4px 10px;margin-left:6px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
}
_setupFilterMenuCommon(field, menu, th);
}
// 构建计划页完整筛选菜单HTML(缓存命中时用,含排序按钮+全选)
function _setupPlanFilterMenuHTML(menu, field, label, rect, currentOrder, isSorted) {
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:450px;font-size:13px;display:flex;flex-direction:column';
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="planFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="planFilterSearchList(this.value)" onkeydown="if(event.key===\'Enter\')planFilterApply()"></div>';
html += '<div style="margin-bottom:6px;display:flex;gap:4px">';
html += '<button class="btn btn-sm ' + (currentOrder === 'asc' ? 'btn-primary' : 'btn-outline') + '" onclick="planFilterSort(\'' + escJs(field) + '\', \'asc\')" style="font-size:11px;padding:4px 8px">▲ 升序</button>' +
'<button class="btn btn-sm ' + (currentOrder === 'desc' ? 'btn-primary' : 'btn-outline') + '" onclick="planFilterSort(\'' + escJs(field) + '\', \'desc\')" style="font-size:11px;padding:4px 8px">▼ 降序</button>';
html += '</div>';
html += '<div id="planFilterList" style="overflow-y:auto;flex:1;max-height:200px;min-height:40px"></div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="planFilterApply()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="planFilterClear()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
}
// 创建筛选菜单外壳(用于异步加载的字段)
function _createFilterMenuShell(field, label, rect) {
var existing = document.getElementById('planFilterMenu');
if (existing) existing.remove();
_planFilterMenuField = field;
if (!_planFilterSelected[field]) _planFilterSelected[field] = {};
var menu = document.createElement('div');
menu.id = 'planFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="planFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="planFilterSearchList(this.value)" onkeydown="if(event.key===\'Enter\')planFilterApply()"></div>';
html += '<div id="planFilterList" style="overflow-y:auto;flex:1;max-height:200px;min-height:40px"></div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="planFilterApply()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="planFilterClear()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
return menu;
}
// 填充筛选列表
function _populateFilterMenuList(field, values, hasSortBtns) {
var listEl = document.getElementById('planFilterList');
if (!listEl) return;
if (!_planFilterSelected[field]) _planFilterSelected[field] = {};
// 读取已保存的筛选状态
var savedFilter = window._planFilters && window._planFilters[field];
var isListFilter = savedFilter && savedFilter.type === 'list' && savedFilter.values;
var isSearchFilter = savedFilter && savedFilter.type === 'search' && savedFilter.value;
// 预填搜索框(搜索模式回显)
if (isSearchFilter) {
var si = document.getElementById('planFilterSearch');
if (si) {
si.value = savedFilter.value;
setTimeout(function() {
if (typeof planFilterSearchList === 'function') {
planFilterSearchList(si.value);
}
}, 50);
}
}
var html = '';
var allChecked = (isListFilter || isSearchFilter) ? 0 : 1;
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="planFilterSelectAll" ' + (allChecked ? 'checked' : '') + ' onchange="planFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="planFilterClear()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
values.forEach(function(v) {
var checked = 'checked';
if (isListFilter) {
checked = (savedFilter.values.indexOf(v) >= 0) ? 'checked' : '';
} else if (isSearchFilter) {
checked = (v.indexOf(savedFilter.value) >= 0) ? 'checked' : '';
}
var labelText = (v === '__EMPTY__') ? '空' : escHtml(v);
var valAttr = escHtml(v);
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="plan-filter-cb" value="' + valAttr + '" ' + checked + ' onchange="planFilterCbChange()"> ' + labelText;
html += '</label>';
});
listEl.innerHTML = html;
// 搜索自动聚焦
setTimeout(function() {
var inp = document.getElementById('planFilterSearch');
if (inp) inp.focus();
}, 150);
}
function setupFilterMenuClose(menu, th) {
document.addEventListener('scroll', planFilterScrollHandler, true);
var handler = function(e) {
if (!menu.contains(e.target) && e.target !== th && !th.contains(e.target)) {
menu.remove();
document.removeEventListener('click', handler);
}
};
setTimeout(function() { document.addEventListener('click', handler); }, 10);
}
function _setupFilterMenuCommon(field, menu, th) {
// 跟随表头滚动
document.addEventListener('scroll', planFilterScrollHandler, true);
// 搜索自动聚焦
setTimeout(function() {
var inp = document.getElementById('planFilterSearch');
if (inp) inp.focus();
}, 150);
// 点击外部关闭
var handler = function(e) {
if (!menu.contains(e.target) && e.target !== th && !th.contains(e.target)) {
menu.remove();
document.removeEventListener('click', handler);
}
};
setTimeout(function() { document.addEventListener('click', handler); }, 10);
}
// ─── 数字列条件筛选 ───
function _buildNumericFilterMenu(field, label, rect, isSorted, currentOrder, menu) {
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:260px;max-width:320px;font-size:13px;display:flex;flex-direction:column;max-height:450px';
var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none';
var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none';
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="display:flex;margin-bottom:6px;border-bottom:1px solid #d9d9d9;padding-bottom:0">';
html += '<span id="planTabContent" style="' + tabAct + '" onclick="planNumSwitchTab(\'content\')">内容筛选</span>';
html += '<span id="planTabCond" style="' + tabDef + '" onclick="planNumSwitchTab(\'cond\')">条件筛选</span></div>';
// 排序按钮
html += '<div style="margin-bottom:6px;display:flex;gap:4px">';
html += '<button class="btn btn-sm ' + (currentOrder === 'asc' ? 'btn-primary' : 'btn-outline') + '" onclick="planFilterSort(\'' + escJs(field) + '\', \'asc\')" style="font-size:11px;padding:4px 8px">▲ 升序</button>' +
'<button class="btn btn-sm ' + (currentOrder === 'desc' ? 'btn-primary' : 'btn-outline') + '" onclick="planFilterSort(\'' + escJs(field) + '\', \'desc\')" style="font-size:11px;padding:4px 8px">▼ 降序</button>';
html += '</div>';
html += '<div id="planNumContentPanel">';
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="planNumSelAll" checked onchange="planNumToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="planFilterClear()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
html += '<input type="text" id="planNumSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px;margin-bottom:4px" placeholder="搜索数值..." autocomplete="off" oninput="planNumSearchList(this.value)">';
html += '<div id="planNumList" style="overflow-y:auto;flex:1;max-height:180px;border:1px solid #f0f0f0;border-radius:4px;padding:4px 6px;margin-bottom:4px">加载中...</div></div>';
html += '<div id="planNumCondPanel" style="display:none">';
html += '<select id="planNumCond" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px;margin-bottom:4px">';
html += '<option value="">-- 选择条件 --</option>';
html += '<option value="eq">等于</option>';
html += '<option value="neq">不等于</option>';
html += '<option value="gt">大于</option>';
html += '<option value="gte">大于等于</option>';
html += '<option value="lt">小于</option>';
html += '<option value="lte">小于等于</option>';
html += '<option value="range">介于(逗号分隔)</option>';
html += '</select>';
html += '<input type="text" id="planNumVal" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="输入数值..." autocomplete="off" onkeydown="if(event.key===\'Enter\')planNumFilterApply()">';
html += '</div>';
html += '<div style="border-top:1px solid #f0f0f0;padding-top:6px;margin-top:4px;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm" onclick="planFilterClear()" style="font-size:12px;padding:4px 10px">清除</button>';
html += '<button class="btn btn-sm btn-primary" onclick="planNumFilterApply()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="planFilterClear()" style="font-size:12px;padding:4px 10px;margin-left:6px">重置</button>';
html += '</div>';
menu.innerHTML = html;
document.body.appendChild(menu);
// 从后端API获取唯一值:无筛选全量,有筛选联动
var viewParam = planActiveView !== 'default' ? '&view=' + encodeURIComponent(planActiveView) : '';
var planFilterParam = '';
if (window._planFilters && Object.keys(window._planFilters).length > 0) {
planFilterParam = '&plan_filter=' + encodeURIComponent(JSON.stringify(window._planFilters));
}
fetch('/api/planning/column-values?field=' + encodeURIComponent(field) + viewParam + planFilterParam)
.then(function(r) { return r.json(); })
.then(function(res) {
var numValues = [];
if (res.code === 0 && res.data) {
// 后端返回全量唯一值
numValues = res.data.map(function(v) { return String(v).trim(); }).filter(function(v) { return v && v !== '-'; });
}
// 排序
numValues.sort(function(a,b) { return parseFloat(a) - parseFloat(b); });
renderNumList(numValues);
});
function renderNumList(vals) {
var list = document.getElementById('planNumList');
if (!list) return;
if (vals.length === 0) { list.innerHTML = '<div style="text-align:center;padding:20px;color:#999">暂无数据</div>'; return; }
var html = '';
vals.forEach(function(v) {
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap">';
html += '<input type="checkbox" class="plan-num-cb" value="' + escHtml(v) + '" checked onchange="planNumUpdateAll()"> ' + escHtml(v);
html += '</label>';
});
list.innerHTML = html;
}
// 回显已保存的数字筛选状态
var savedNumFilter = window._planFilters && window._planFilters[field];
if (savedNumFilter && savedNumFilter.type === 'num') {
var condSel = document.getElementById('planNumCond');
var valInput = document.getElementById('planNumVal');
if (condSel) condSel.value = savedNumFilter.cond || '';
if (valInput) valInput.value = savedNumFilter.val || '';
// 切换到条件Tab
planNumSwitchTab('cond');
} else if (savedNumFilter && savedNumFilter.type === 'list' && savedNumFilter.values) {
var cbs = document.querySelectorAll('.plan-num-cb');
cbs.forEach(function(cb) {
cb.checked = savedNumFilter.values.indexOf(cb.value) >= 0;
});
}
setTimeout(function() { var inp = document.getElementById('planNumVal'); if (inp) inp.focus(); }, 100);
}
function planNumFilterApply() {
var field = _planFilterMenuField;
var condPanel = document.getElementById('planNumCondPanel');
var isCondTab = condPanel && condPanel.style.display !== 'none';
if (isCondTab) {
// 条件筛选
var cond = document.getElementById('planNumCond')?.value || '';
var val = document.getElementById('planNumVal')?.value || '';
if (!cond || !val) {
alert('请选择条件并输入数值');
return;
}
if (!window._planFilters) window._planFilters = {};
window._planFilters[field] = {type:'num', cond: cond, val: val};
} else {
// 内容筛选:复选框值创建列表筛选
var cbs = document.querySelectorAll('.plan-num-cb:checked');
var checked = [];
cbs.forEach(function(cb) { checked.push(cb.value); });
if (!window._planFilters) window._planFilters = {};
if (checked.length === 0 || checked.length === document.querySelectorAll('.plan-num-cb').length) {
delete window._planFilters[field];
} else {
window._planFilters[field] = {type:'list', values: checked};
}
}
_syncFiltersToUI();
closePlanFilterMenu();
loadPlanPage(1);
}
function planFilterApply() {
var field = _planFilterMenuField;
// 先检查搜索框是否有内容
var searchInput = document.getElementById('planFilterSearch');
var searchText = searchInput ? searchInput.value.trim() : '';
if (searchText) {
// 搜索框有内容 → 文本模糊匹配
if (!window._planFilters) window._planFilters = {};
window._planFilters[field] = {type: 'search', value: searchText};
} else {
// 搜索框为空 → 用复选框选择
var cbs = document.querySelectorAll('.plan-filter-cb:checked');
var checked = [];
cbs.forEach(function(cb) { checked.push(cb.value); });
if (!window._planFilters) window._planFilters = {};
if (checked.length === 0 || checked.length === document.querySelectorAll('.plan-filter-cb').length) {
// 全选或没选 → 清除该列筛选
delete window._planFilters[field];
} else {
window._planFilters[field] = {type:'list', values: checked};
}
}
_syncFiltersToUI();
closePlanFilterMenu();
loadPlanPage(1);
}
function _syncFiltersToUI() {
// 仅用于更新表头筛选图标状态
var activeFilters = [];
Object.keys(window._planFilters || {}).forEach(function(f) {
var filter = window._planFilters[f];
if (filter) { filter.field = f; activeFilters.push(filter); }
});
_updateFilterIcons(activeFilters);
}
function _updateFilterIcons(activeFilters) {
var activeFields = {};
activeFilters.forEach(function(f) { activeFields[f.field] = true; });
PLAN_COLUMNS.forEach(function(c, i) {
var th = document.querySelector('.plan-th-' + i);
if (th) {
var isActive = activeFields[c.field];
var icon = th.querySelector('span[title="筛选"]');
if (icon) {
icon.style.color = isActive ? '#1890ff' : 'var(--text-light)';
icon.style.fontWeight = isActive ? 'bold' : 'normal';
icon.textContent = isActive ? '▼' : '▾';
}
}
});
}
function _restoreFilterIcons() {
var activeFilters = [];
Object.keys(window._planFilters || {}).forEach(function(f) {
var fp = window._planFilters[f];
if (fp) { fp.field = f; activeFilters.push(fp); }
});
_updateFilterIcons(activeFilters);
}
function planFilterSearchList(q) {
var items = document.querySelectorAll('#planFilterList label');
q = (q || '').toLowerCase();
items.forEach(function(label) {
var txt = label.textContent.toLowerCase();
label.style.display = (!q || txt.indexOf(q) >= 0) ? '' : 'none';
});
}
function planFilterToggleAll(checked) {
document.querySelectorAll('.plan-filter-cb').forEach(function(cb) { cb.checked = checked; });
}
function planFilterCbChange() {
var all = document.querySelectorAll('.plan-filter-cb');
var checked = document.querySelectorAll('.plan-filter-cb:checked');
var cb = document.getElementById('planFilterSelectAll');
if (cb) {
cb.checked = all.length === checked.length;
cb.indeterminate = checked.length > 0 && checked.length < all.length;
}
}
function planFilterSort(field, order) {
if (order === 'asc') { planSortField = field; planSortAsc = true; }
else if (order === 'desc') { planSortField = field; planSortAsc = false; }
else { planSortField = ''; }
closePlanFilterMenu();
loadPlanPage(1);
}
function planFilterClear() {
var field = _planFilterMenuField;
if (window._planFilters) delete window._planFilters[field];
_syncFiltersToUI();
closePlanFilterMenu();
loadPlanPage(1);
}
// 重置所有筛选条件
window.planFilterResetAll = function() {
window._planFilters = {};
window._planFilterSelected = {};
planSortField = '';
planSortAsc = true;
// 清空主搜索框和下拉筛选
['planSearch','planSupplierFilter','planFollowerFilter','planPlannerFilter','planDateCond','planDateVal'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
_syncFiltersToUI();
closePlanFilterMenu();
loadPlanPage(1);
};
// ─── 数字列双模式筛选辅助函数 ───
function planNumSwitchTab(tab) {
var contentPanel = document.getElementById('planNumContentPanel');
var condPanel = document.getElementById('planNumCondPanel');
var tabContent = document.getElementById('planTabContent');
var tabCond = document.getElementById('planTabCond');
var tabDef = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #d9d9d9;border-radius:4px 4px 0 0;background:#f5f5f5;color:#666;margin-right:2px;user-select:none';
var tabAct = 'cursor:pointer;padding:4px 10px;font-size:12px;border:1px solid #1890ff;border-radius:4px 4px 0 0;background:#fff;color:#1890ff;margin-right:2px;font-weight:600;user-select:none';
if (tab === 'content') {
if (contentPanel) contentPanel.style.display = '';
if (condPanel) condPanel.style.display = 'none';
if (tabContent) tabContent.style.cssText = tabAct;
if (tabCond) tabCond.style.cssText = tabDef;
} else {
if (contentPanel) contentPanel.style.display = 'none';
if (condPanel) condPanel.style.display = '';
if (tabContent) tabContent.style.cssText = tabDef;
if (tabCond) tabCond.style.cssText = tabAct;
}
}
function planNumSearchList(q) {
var items = document.querySelectorAll('#planNumList label');
q = (q || '').toLowerCase();
items.forEach(function(label) {
var txt = label.textContent.toLowerCase();
label.style.display = (!q || txt.indexOf(q) >= 0) ? '' : 'none';
});
}
function planNumToggleAll(checked) {
document.querySelectorAll('.plan-num-cb').forEach(function(cb) { cb.checked = checked; });
}
function planNumUpdateAll() {
var all = document.querySelectorAll('.plan-num-cb');
var checked = document.querySelectorAll('.plan-num-cb:checked');
var cb = document.getElementById('planNumSelAll');
if (cb) {
cb.checked = all.length === checked.length;
cb.indeterminate = checked.length > 0 && checked.length < all.length;
}
}
function closePlanFilterMenu() {
var m = document.getElementById('planFilterMenu');
if (m) m.remove();
document.removeEventListener('scroll', planFilterScrollHandler, true);
}
var planFilterScrollHandler = function() {
var menu = document.getElementById('planFilterMenu');
if (!menu) return;
var colIdx = window['_planColIdx_' + _planFilterMenuField];
if (colIdx === undefined) return;
var th = document.querySelector('.plan-th-' + colIdx);
if (th) {
var r = th.getBoundingClientRect();
menu.style.top = (r.bottom + 4) + 'px';
menu.style.left = Math.max(r.left, 10) + 'px';
}
};
function togglePlanSort(field) {
if (planSortField === field) {
planSortAsc = !planSortAsc;
} else {
planSortField = field;
planSortAsc = true;
}
loadPlanPage(1);
}
function searchPlanning() { loadPlanPage(1); }
// 根据视图调整列显隐和名称(仅cloud视图显示云仓/外仓库存)
function updatePlanViewColumns() {
var isCloud = planActiveView === 'cloud';
// 同步所有列的显隐状态(planColVisible → DOM)
PLAN_COLUMNS.forEach(function(c, i) {
var show = window.planColVisible[c.field];
var els = document.querySelectorAll('.plan-col-' + i + ', .plan-th-' + i);
els.forEach(function(el) { el.style.display = show ? '' : 'none'; });
});
}
var planActiveView = 'default';
function switchPlanView(view) {
// 保存当前视图的字段显隐状态
if (window._planViewVisibility) {
PLAN_COLUMNS.forEach(function(c) {
window._planViewVisibility[planActiveView][c.field] = window.planColVisible[c.field];
});
// 切换到新视图的显隐状态
PLAN_COLUMNS.forEach(function(c) {
window.planColVisible[c.field] = window._planViewVisibility[view][c.field];
});
}
// 保存/切换冻结列设置
if (window._planViewFrozenFields) {
window._planViewFrozenFields[planActiveView] = planFrozenFields.slice();
planFrozenFields = (window._planViewFrozenFields[view] || ['product_type', 'product_code', 'product_name']).slice();
}
planActiveView = view;
document.querySelectorAll('.plan-tab').forEach(function(el) {
el.classList.toggle('active', el.dataset.view === view);
});
// 切换视图时重置筛选条件并加载
resetPlanningFiltersNoSearch();
loadPlanPage(1);
}
function resetPlanningFiltersNoSearch() {
document.getElementById('planSearch').value = '';
document.getElementById('planDateCond').value = '';
document.getElementById('planDateVal').value = '';
document.getElementById('planSupplierFilter').value = '';
document.getElementById('planFollowerFilter').value = '';
document.getElementById('planPlannerFilter').value = '';
window._planFilters = {};
setupFilterClearButtons();
}
// 从数据行中提取采购跟单/采购计划去重值,填充下拉选项
function populatePlanPersonFilter(rows) {
function getDistinct(field) {
var set = new Set();
(rows || []).forEach(function(r) {
var v = r[field];
if (v && v !== '-' && v.trim()) {
v.split(',').forEach(function(name) {
name = name.trim();
if (name) set.add(name);
});
}
});
return Array.from(set).sort();
}
['planFollowerFilter', 'planPlannerFilter'].forEach(function(elId) {
var sel = document.getElementById(elId);
if (!sel) return;
var currentVal = sel.value;
var field = elId === 'planFollowerFilter' ? '采购跟单' : '采购计划';
var label = elId === 'planFollowerFilter' ? '采购跟单' : '采购计划';
var names = getDistinct(field);
sel.innerHTML = '<option value="">全部' + label + '</option>' +
names.map(function(n) { return '<option value="' + escHtml(n) + '">' + escHtml(n) + '</option>'; }).join('');
sel.value = currentVal;
});
}
function resetPlanningFilters() {
document.getElementById('planSearch').value = '';
document.getElementById('planDateCond').value = '';
document.getElementById('planDateVal').value = '';
document.getElementById('planSupplierFilter').value = '';
document.getElementById('planFollowerFilter').value = '';
document.getElementById('planPlannerFilter').value = '';
// 清除所有列筛选状态
window._planFilters = {};
// 清除排序状态
planSortField = '';
planSortAsc = true;
// 更新清除按钮状态
setupFilterClearButtons();
loadPlanPage(1);
}
// ─── 列方案管理 ───
// 构建当前列配置JSON
function buildPlanColumnsJSON() {
var order = window._planColDragOrder || PLAN_COLUMNS.map(function(c) { return c.field; });
var cols = [];
order.forEach(function(field) {
var colDef = null;
PLAN_COLUMNS.forEach(function(c) {
if (c.field === field) colDef = c;
});
if (colDef) {
cols.push({
field: colDef.field,
label: colDef.label,
visible: window._planPendingVisible[field] !== undefined ? window._planPendingVisible[field] : true
});
}
});
PLAN_COLUMNS.forEach(function(c) {
var found = false;
cols.forEach(function(col) { if (col.field === c.field) found = true; });
if (!found) {
cols.push({
field: c.field,
label: c.label,
visible: window._planPendingVisible[c.field] !== undefined ? window._planPendingVisible[c.field] : c.defaultShow
});
}
});
return JSON.stringify(cols);
}
// 保存方案
window.savePlanScheme = function() {
var name = document.getElementById('planSchemeNameInput');
if (!name) return;
var schemeName = name.value.trim();
if (!schemeName) {
alert('请输入方案名称');
name.focus();
return;
}
var columnsJson = buildPlanColumnsJSON();
fetch('/api/planning/scheme/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
scheme_name: schemeName,
columns_json: columnsJson,
page: 'planning',
is_default: 0
})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
name.value = '';
refreshPlanSchemeList();
} else {
alert(res.msg || '保存失败');
}
})
.catch(function() { alert('保存失败,请检查网络'); });
};
// 加载方案并预览(在弹窗中勾选对应字段,不关闭弹窗)
window.loadPlanScheme = function(schemeId) {
if (schemeId === undefined || schemeId === null) return;
fetch('/api/planning/schemes')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var schemes = res.data || [];
var target = null;
for (var i = 0; i < schemes.length; i++) {
if (schemes[i].id == schemeId) {
target = schemes[i];
break;
}
}
if (!target) {
alert('方案不存在或已被删除');
refreshPlanSchemeList();
return;
}
var cols;
try {
cols = JSON.parse(target.columns_json);
} catch(e) {
alert('方案数据异常');
return;
}
if (!cols || !Array.isArray(cols) || cols.length === 0) {
alert('方案数据为空');
return;
}
// 只更新临时状态(弹窗内的勾选),不关闭弹窗
var newOrder = [];
var pendingVisible = {};
cols.forEach(function(c) {
if (c && c.field) {
newOrder.push(c.field);
pendingVisible[c.field] = c.visible !== false;
}
});
PLAN_COLUMNS.forEach(function(c) {
if (pendingVisible[c.field] === undefined) {
newOrder.push(c.field);
pendingVisible[c.field] = false;
}
});
// 更新临时勾选状态和拖拽顺序
window._planColDragOrder = newOrder.slice();
Object.keys(pendingVisible).forEach(function(field) {
window._planPendingVisible[field] = pendingVisible[field];
});
window._planPendingVisible = pendingVisible;
// 刷新弹窗内的 checkbox 状态
var grid = document.getElementById('planColGrid');
if (grid) renderPlanColCheckboxes(grid);
// 同步全选状态
var allCb = document.getElementById('plan-cb-all');
if (allCb) {
var allChecked = true;
PLAN_COLUMNS.forEach(function(c) {
if (!window._planPendingVisible[c.field]) allChecked = false;
});
allCb.checked = allChecked;
}
})
.catch(function(err) {
console.error('loadPlanScheme error:', err);
});
};
// 构建表头
function buildPlanTableHeader(planTable) {
// 按权限过滤列
var permFilteredCols = PLAN_COLUMNS.filter(function(c) { return window._canViewField('planning', c.label); });
var newThead = '<tr><th class="plan-col-index" style="width:50px"><div style="text-align:center;width:100%">序号</div></th>';
permFilteredCols.forEach(function(c, i) {
var disp = window.planColVisible[c.field] ? '' : ' style="display:none"';
var isNumericSortable = (c.field === '产品分级') || SORTABLE_FIELDS[c.field];
var sortArrowHtml = '';
if (isNumericSortable) {
var isSorted = planSortField === c.field;
var upActive = isSorted && planSortAsc;
var downActive = isSorted && !planSortAsc;
var upStyle = upActive ? 'color:#cf1322;font-weight:700;font-size:12px' : 'color:#d9d9d9;font-size:11px';
var downStyle = downActive ? 'color:#cf1322;font-weight:700;font-size:12px' : 'color:#d9d9d9;font-size:11px';
sortArrowHtml = ' <span style="font-size:11px"><span style="' + upStyle + '">▲</span><span style="' + downStyle + '">▼</span></span>';
}
var planTips = {'日销量预测':'日销量预测 = (3天平均×w3 + 7天平均×w7 + 14天平均×w14) × 调整系数','库存可支撑天数':'库存可支撑天数 = 库存 / 日销量预测,为0时显示999','加工厂库存支撑天数':'加工厂库存支撑 = 加工厂库存 / 日销量预测,为0时显示999','库存工厂可支撑天数':'现货采购在途 = 采购在途 / 日销量预测','库存工厂待生产可支撑天数':'定制品在途 = 生产在途 / 日销量预测','总支撑天数':'总支撑天数 = (库存 + 加工厂库存 + 生产在途 + 采购在途) / 日销量预测','库存健康指数':'日销量预测>0:库存可支撑天数÷(货期+物流);=0:库存可支撑天数÷(货期+物流)','建议下单数量':'日销量预测=0且库存>0则0,否则(安全库存天数+货期+物流)×日销量预测','实际下单数量':'建议下单数量向上取整到1000','正式下单数量':'正式下单数量初始等于实际下单数量,可人工修改','建议回货数量':'ROUNDUP((日销量预测×回货天数-库存)×10)/10,上限在途总数','是否新品':'创建时间5个月以内为新品,否则返单品','成本价':'从产品信息匹配的成本价格','销售趋势':'基于3/7/14天日均销量判断的9种趋势类型'};
var tip = planTips[c.label] || '';
var canFilter = c.filterable !== false;
var isActive = window._planFilters && window._planFilters[c.field];
var iconColor = isActive ? '#1890ff' : 'var(--text-light)';
var iconWeight = isActive ? 'bold' : 'normal';
var iconChar = isActive ? '▼' : '▾';
var filterIcon = canFilter ? ' <span style="margin-left:2px;font-size:20px;color:' + iconColor + ';cursor:pointer;font-weight:' + iconWeight + '" onclick="event.stopPropagation();openPlanFilterMenu(\'' + escJs(c.field) + '\',event)" title="筛选">' + iconChar + '</span>' : '';
var clickHandler = canFilter ? 'openPlanFilterMenu(\'' + escJs(c.field) + '\', event)' : '';
newThead += '<th class="plan-col-' + i + ' plan-th-' + i + '"' + disp + ' onclick="' + clickHandler + '" style="cursor:' + (canFilter ? 'pointer' : 'default') + ';' + (isActive ? 'background:#e6f7ff;' : '') + '"' + (tip ? ' title="' + tip.replace(/"/g,'&quot;') + '"' : '') + '>' + c.label + sortArrowHtml + filterIcon + '</th>';
});
newThead += '<th class="plan-col-action" style="width:200px;min-width:200px;position:sticky;top:0;right:0;background:var(--bg,#fff);z-index:11;border-left:1px solid var(--border)">操作</th>';
newThead += '</tr>';
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 = '<span style="color:var(--text-light);font-size:13px">暂无保存方案</span>';
return;
}
var html = '';
schemes.forEach(function(s) {
var isDefault = s.is_default == 1;
html += '<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;font-size:13px;background:' + (isDefault ? '#e6f7ff' : '#f5f5f5') + ';border:1px solid ' + (isDefault ? '#1890ff' : '#e8e8e8') + '">';
html += '<span style="cursor:pointer" onclick="loadPlanScheme(' + s.id + ')">' + escHtml(s.scheme_name) + '</span>';
if (isDefault) {
html += '<span style="color:#1890ff;font-size:11px">★默认</span>';
}
html += '<span style="color:#bbb;font-size:14px;cursor:pointer;margin-left:2px" onclick="setDefaultPlanScheme(' + s.id + ')" title="设为默认">📌</span>';
html += '<span style="color:#bbb;font-size:14px;cursor:pointer" onclick="deletePlanScheme(' + s.id + ')" title="删除">✕</span>';
html += '</span>';
});
container.innerHTML = html;
});
}
// 加载默认方案(页面初始化时调用)
function loadDefaultPlanScheme() {
fetch('/api/planning/schemes')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var schemes = res.data || [];
var defaultScheme = null;
schemes.forEach(function(s) {
if (s.is_default == 1) defaultScheme = s;
});
if (defaultScheme) {
window.loadPlanScheme(defaultScheme.id);
}
});
}
// ─── 采购订单-待下单管理 ───
var _pendingItems = [];
var pendingOrderFilter = {}; // 列筛选状态
var pendingFilterMenu = null; // 当前打开的筛选弹窗
var pendingFilterField = ''; // 当前筛选字段
// 待下单列定义(含类型,用于筛选菜单)
// 待下单列中文名→数据库字段名映射
var _pendingFieldMap = {
'产品类型': 'product_type',
'货品编号': 'product_code',
'货品名称': 'product_name',
'产品分级': 'product_grade',
'供应商名称': 'supplier_name',
'建议下单数量': 'suggested_qty',
'正式下单数量': 'formal_order_qty',
'下单备注': 'remark',
'货期': 'lead_time',
'物流': 'logistics_time',
'总支撑天数': 'total_support_days',
'平台': 'platform',
'运营': 'operator',
'审核价格': 'audit_price',
'下单人员': 'created_by',
'采购跟单': 'procurement_follower',
'采购计划': 'procurement_planner',
};
var PENDING_FILTER_COLS = [
{field:'产品类型', label:'产品类型', type:'text'},
{field:'货品编号', label:'货品编号', type:'text'},
{field:'货品名称', label:'货品名称', type:'text'},
{field:'产品分级', label:'产品分级', type:'text'},
{field:'供应商名称', label:'供应商名称', type:'text'},
{field:'建议下单数量', label:'建议下单数量', type:'number'},
{field:'正式下单数量', label:'正式下单数量', type:'number'},
{field:'下单备注', label:'下单备注', type:'text'},
{field:'货期', label:'货期', type:'number'},
{field:'物流', label:'物流', type:'number'},
{field:'总支撑天数', label:'总支撑天数', type:'number'},
{field:'平台', label:'平台', type:'text'},
{field:'运营', label:'运营', type:'text'},
{field:'审核价格', label:'审核价格', type:'number'},
{field:'下单人员', label:'下单人员', type:'text'},
{field:'采购跟单', label:'采购跟单', type:'text'},
{field:'采购计划', label:'采购计划', type:'text'},
];
function loadPurchasePage(container) {
container.innerHTML = `
<div class="page-header">
<h1>待下单管理</h1>
</div>
<div class="card">
<div class="card-header" style="display:flex;align-items:center;gap:12px">
<h3>📋 待下单商品</h3>
<span style="font-size:13px;color:var(--text-light)" id="pendingCount"></span>
<span id="pendingSelectedCount" style="font-size:13px;color:var(--primary);font-weight:600;margin-left:8px"></span>
<div style="margin-left:auto;gap:8px">
<button class="btn btn-sm btn-outline" onclick="loadPendingList()">🔄 刷新</button>
<button class="btn btn-sm" id="pendingFrozenBtn" onclick="togglePendingFrozen()">🔒 冻结列</button>
<button class="btn btn-sm" onclick="openPendingColModal()">⚙ 字段设置</button>
<button class="btn btn-sm" style="background:#e6fffb;color:#13c2c2;border:1px solid #87e8de" onclick="pendingBatchConfirm()">📦 批量确认下单</button>
</div>
</div>
<div class="card-body" style="overflow-x:auto">
<table class="data-table" id="pendingTable">
<thead>
<tr>
<th style="width:30px;text-align:center"><input type="checkbox" id="pendingSelectAllCb" onchange="pendingToggleAll(this.checked)" title="全选"></th>
<th style="width:40px;text-align:center">序号</th>
<th>产品类型 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('产品类型',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>货品编号 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('货品编号',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>货品名称 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('货品名称',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="width:100px">确认下单</th>
<th>产品分级 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('产品分级',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>供应商名称 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('供应商名称',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>建议下单数量 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('建议下单数量',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>正式下单数量 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('正式下单数量',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>下单备注 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('下单备注',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>货期 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('货期',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>物流 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('物流',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>总支撑天数 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('总支撑天数',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>平台 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('平台',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>运营 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('运营',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>审核价格 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('审核价格',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>下单人员 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('下单人员',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>下单时间</th>
<th>采购跟单 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('采购跟单',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>采购计划 <span class="order-filt-icon" onclick="event.stopPropagation();openPendingFilterMenu('采购计划',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>加入时间</th>
<th class="col-action" style="width:140px;min-width:140px;position:sticky;right:0;top:0;background:var(--bg,#fff);z-index:11;border-left:1px solid var(--border)">操作</th>
</tr>
</thead>
<tbody id="pendingTbody"></tbody>
</table>
</div>
</div>
`;
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 = '<tr><td colspan="23" style="text-align:center;padding:30px;color:var(--text-light)"><div class="loading-spinner"></div></td></tr>';
fetch('/api/purchase/pending_list')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
_pendingItems = res.data || [];
// 清筛选缓存(数据已刷新)
invalidateOrderColumnCache('pending');
_syncPendingFilterIcons();
_renderFilteredPending();
});
}
// 提取待下单行渲染为独立函数(供 loadPendingList 和 _renderFilteredPending 共用)
function _pendingRowsHtml(items) {
return items.map(function(item, idx) {
var code = escHtml(item.product_code);
return '<tr>' +
'<td style="text-align:center"><input type="checkbox" class="pending-row-cb" value="' + code + '" onchange="pendingUpdateCount()"></td>' +
'<td style="text-align:center">' + (idx + 1) + '</td>' +
'<td>' + escHtml(item.product_type || '-') + '</td>' +
'<td><code>' + code + '</code></td>' +
'<td>' + escHtml(item.product_name || '-') + '</td>' +
'<td style="text-align:center"><button class="btn btn-sm" style="background:#e6fffb;color:#13c2c2;border:1px solid #87e8de" onclick="pendingConfirmOrder(\'' + code + '\')">✅ 确认下单</button></td>' +
'<td>' + escHtml(item.product_grade || '-') + '</td>' +
'<td>' + escHtml(item.supplier_name || '-') + '</td>' +
'<td style="font-weight:600;color:var(--primary)">' + (item.suggested_qty || 0) + '</td>' +
'<td><input type="number" class="form-input" style="width:80px" value="' + (item.formal_order_qty || 0) + '" onchange="updateFormalQty(\'' + code + '\', this.value)"></td>' +
'<td><input type="text" class="form-input" style="width:130px" value="' + escHtml(item.remark || '') + '" data-pcode="' + code + '" onchange="savePendingRemark(this)" onkeydown="remarkEnterSave(event)" placeholder="下单备注"></td>' +
'<td>' + (item.lead_time || 0) + '</td>' +
'<td>' + (item.logistics_time || 0) + '</td>' +
'<td>' + (item.total_support_days || 0) + '</td>' +
'<td>' + escHtml(item.platform || '-') + '</td>' +
'<td>' + renderNameBadges(item.operator) + '</td>' +
'<td><input type="number" step="0.01" class="form-input" style="width:90px" value="' + (item.audit_price || 0) + '" onchange="updateAuditPrice(\'' + code + '\', this.value)"></td>' +
'<td>' + escHtml(item.created_by || '-') + '</td>' +
'<td>' + ((item.created_at || '').toString().slice(0, 19) || '-') + '</td>' +
'<td>' + renderNameBadges(item.procurement_follower) + '</td>' +
'<td>' + renderNameBadges(item.procurement_planner) + '</td>' +
'<td>' + ((item.created_at || '').toString().slice(0, 19) || '-') + '</td>' +
'<td class="col-action" style="text-align:center;white-space:nowrap;position:sticky;right:0;background:var(--bg,#fff);z-index:2;border-left:1px solid var(--border)">' +
'<button class="btn btn-sm btn-outline" onclick="pendingDetail(\'' + code + '\')" title="详情">详情</button> ' +
'<button class="btn btn-sm btn-outline-danger" onclick="removePending(\'' + code + '\')" title="回退">回退</button>' +
'</td>' +
'</tr>';
}).join('');
}
// ─── 待下单表头筛选 ───
function _pendingGetColType(field) {
for (var i = 0; i < PENDING_FILTER_COLS.length; i++) {
if (PENDING_FILTER_COLS[i].field === field) return PENDING_FILTER_COLS[i].type;
}
return 'text';
}
function openPendingFilterMenu(field, event) {
event = event || window.event;
event.stopPropagation();
if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; }
pendingFilterField = field;
var th = event.currentTarget;
while (th && th.tagName !== 'TH') th = th.parentNode;
var rect = th.getBoundingClientRect();
var label = field;
var colType = _pendingGetColType(field);
var filterActive = pendingOrderFilter[field];
if (colType === 'number') {
_buildPendingNumericFilter(field, label, rect);
} else {
// 从已加载数据提取唯一值(已缓存则直接用)
var cacheKey = 'pending:' + field;
var cached = window._orderColumnCache && window._orderColumnCache[cacheKey];
if (cached) {
_buildPendingListFilter(field, label, rect, cached);
} else {
// 显示外壳 + 异步提取
_buildPendingFilterShell(field, label, rect);
var vals = [];
var seen = {};
_pendingItems.forEach(function(item) {
var v = String(item[_pendingFieldMap[field] || field] || '').trim();
if (v && !seen[v]) { seen[v] = 1; vals.push(v); }
});
vals.sort();
window._orderColumnCache = window._orderColumnCache || {};
window._orderColumnCache[cacheKey] = vals;
_populatePendingFilterList(field, vals);
}
}
if (pendingFilterMenu) {
var handler = function(e) {
if (pendingFilterMenu && !pendingFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) {
pendingFilterMenu.remove(); pendingFilterMenu = null;
document.removeEventListener('click', handler);
}
};
setTimeout(function() { document.addEventListener('click', handler); }, 10);
setTimeout(function() {
var inp = document.getElementById('pendingFilterSearch');
if (inp) inp.focus();
}, 150);
}
}
function _buildPendingFilterShell(field, label, rect) {
var menu = document.createElement('div');
menu.id = 'pendingFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="pendingFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="pendingFilterSearch(this.value)" onkeydown="if(event.key===\'Enter\')applyPendingFilter()"></div>';
html += '<div id="pendingFilterList" style="overflow-y:auto;flex:1;max-height:200px;text-align:center;padding:20px;color:#999">加载中...</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyPendingFilter()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="clearPendingFilter()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
pendingFilterMenu = menu;
}
function _buildPendingListFilter(field, label, rect, values) {
var menu = document.createElement('div');
menu.id = 'pendingFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var sel = pendingOrderFilter[field] || {};
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="pendingFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="pendingFilterSearch(this.value)" onkeydown="if(event.key===\'Enter\')applyPendingFilter()"></div>';
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="pendingFilterSelectAll" checked onchange="pendingFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="clearPendingFilter()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
html += '<div id="pendingFilterList" style="overflow-y:auto;flex:1;max-height:200px;border-top:1px solid #f0f0f0;padding-top:4px">';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="pending-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="pendingFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
html += '</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyPendingFilter()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="clearPendingFilter()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
pendingFilterMenu = menu;
}
function _populatePendingFilterList(field, values) {
var listEl = document.getElementById('pendingFilterList');
if (!listEl) return;
var sel = pendingOrderFilter[field] || {};
var html = '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="pendingFilterSelectAll" checked onchange="pendingFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="clearPendingFilter()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="pending-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="pendingFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
listEl.innerHTML = html;
}
function _buildPendingNumericFilter(field, label, rect) {
var menu = document.createElement('div');
menu.id = 'pendingFilterMenu';
var current = pendingOrderFilter[field] || {};
var cond = current.cond || '';
var val = current.val || '';
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px">';
html += '<select id="pendingNumCond" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px">';
html += '<option value="">-- 选择条件 --</option>';
html += '<option value="eq"' + (cond === 'eq' ? ' selected' : '') + '>等于</option>';
html += '<option value="neq"' + (cond === 'neq' ? ' selected' : '') + '>不等于</option>';
html += '<option value="gt"' + (cond === 'gt' ? ' selected' : '') + '>大于</option>';
html += '<option value="gte"' + (cond === 'gte' ? ' selected' : '') + '>大于等于</option>';
html += '<option value="lt"' + (cond === 'lt' ? ' selected' : '') + '>小于</option>';
html += '<option value="lte"' + (cond === 'lte' ? ' selected' : '') + '>小于等于</option>';
html += '</select></div>';
html += '<div style="margin-bottom:6px">';
html += '<input type="text" id="pendingNumVal" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="输入数值..." value="' + escHtml(val) + '" autocomplete="off" onkeydown="if(event.key===\'Enter\')applyPendingNumFilter()">';
html += '</div>';
html += '<div style="border-top:1px solid #f0f0f0;padding-top:6px;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm" onclick="clearPendingFilter()" style="font-size:12px;padding:4px 10px">清除</button>';
html += '<button class="btn btn-sm btn-primary" onclick="applyPendingNumFilter()" style="font-size:12px;padding:4px 12px">确定</button></div>';
menu.innerHTML = html;
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:200px;font-size:13px';
document.body.appendChild(menu);
pendingFilterMenu = menu;
}
function pendingFilterSearch(q) {
var items = document.querySelectorAll('#pendingFilterList label');
q = (q || '').toLowerCase();
items.forEach(function(item) {
item.style.display = item.textContent.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
});
}
function pendingFilterToggleAll(checked) {
document.querySelectorAll('#pendingFilterList .pending-filter-cb').forEach(function(cb) { cb.checked = checked; });
}
function pendingFilterCbChange() {
var all = document.querySelectorAll('#pendingFilterList .pending-filter-cb');
var checked = document.querySelectorAll('#pendingFilterList .pending-filter-cb:checked');
var sa = document.getElementById('pendingFilterSelectAll');
if (sa) sa.checked = checked.length === all.length;
}
function applyPendingFilter() {
var field = pendingFilterField;
var searchInput = document.getElementById('pendingFilterSearch');
var searchText = searchInput ? searchInput.value.trim() : '';
if (searchText) {
pendingOrderFilter[field] = {type: 'search', value: searchText};
} else {
var cbs = document.querySelectorAll('#pendingFilterList .pending-filter-cb:checked');
var checked = [];
cbs.forEach(function(cb) { checked.push(cb.value); });
var allCbs = document.querySelectorAll('#pendingFilterList .pending-filter-cb');
if (checked.length === 0 || checked.length === allCbs.length) {
delete pendingOrderFilter[field];
} else {
pendingOrderFilter[field] = {type: 'list', values: checked};
}
}
if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; }
_syncPendingFilterIcons();
_renderFilteredPending();
}
function applyPendingNumFilter() {
var field = pendingFilterField;
var cond = document.getElementById('pendingNumCond')?.value || '';
var val = document.getElementById('pendingNumVal')?.value || '';
if (!cond || !val) { alert('请选择条件并输入数值'); return; }
pendingOrderFilter[field] = {type: 'num', cond: cond, val: val};
if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; }
_syncPendingFilterIcons();
_renderFilteredPending();
}
function clearPendingFilter() {
delete pendingOrderFilter[pendingFilterField];
if (pendingFilterMenu) { pendingFilterMenu.remove(); pendingFilterMenu = null; }
_syncPendingFilterIcons();
_renderFilteredPending();
}
function _applyPendingFilters(items) {
var keys = Object.keys(pendingOrderFilter);
if (keys.length === 0) return items;
return items.filter(function(item) {
return keys.every(function(f) {
var fp = pendingOrderFilter[f];
if (!fp) return true;
var v = String(item[_pendingFieldMap[f] || f] || '');
if (fp.type === 'search') {
return v.toLowerCase().indexOf((fp.value || '').toLowerCase()) >= 0;
}
if (fp.type === 'list') {
return fp.values.indexOf(v) >= 0;
}
if (fp.type === 'num') {
var n = parseFloat(v);
var tv = parseFloat(fp.val);
if (isNaN(n) || isNaN(tv)) return false;
switch (fp.cond) {
case 'eq': return n === tv;
case 'neq': return n !== tv;
case 'gt': return n > tv;
case 'gte': return n >= tv;
case 'lt': return n < tv;
case 'lte': return n <= tv;
}
}
return true;
});
});
}
function _renderFilteredPending() {
var filtered = _applyPendingFilters(_pendingItems);
var countEl = document.getElementById('pendingCount');
if (countEl) countEl.textContent = '共 ' + filtered.length + ' / ' + _pendingItems.length + ' 条';
var tbody = document.getElementById('pendingTbody');
if (!tbody) return;
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="23" style="text-align:center;padding:60px;color:var(--text-light)">暂无匹配数据</td></tr>';
return;
}
tbody.innerHTML = _pendingRowsHtml(filtered);
pendingUpdateCount();
}
function _syncPendingFilterIcons() {
var icons = document.querySelectorAll('#pendingTable .order-filt-icon');
icons.forEach(function(icon) {
var field = icon.parentNode.textContent.trim().replace(/▾/,'').trim();
var hasFilter = pendingOrderFilter[field];
icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)';
icon.style.fontWeight = hasFilter ? 'bold' : 'normal';
});
}
function pendingToggleAll(checked) {
document.querySelectorAll('.pending-row-cb').forEach(function(cb) { cb.checked = checked; });
pendingUpdateCount();
}
function pendingUpdateCount() {
var total = document.querySelectorAll('.pending-row-cb').length;
var checked = document.querySelectorAll('.pending-row-cb:checked').length;
var el = document.getElementById('pendingSelectedCount');
if (el) el.textContent = checked > 0 ? '已选 ' + checked + '/' + total : '';
document.getElementById('pendingSelectAllCb').checked = total > 0 && checked === total;
}
function showPendingConfirmModal(item, onConfirm) {
// 先显示 loading 弹窗,同时加载BOM数据
var modal = document.createElement('div');
modal.id = 'pendingConfirmModal';
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto';
modal.innerHTML = '<div style="background:#fff;border-radius:12px;width:600px;max-width:90vw;max-height:80vh;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' +
'<div style="padding:24px;text-align:center;color:#999">加载中...</div></div>';
document.body.appendChild(modal);
// 异步加载BOM详情(获取包材供应商和单价)
fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(item.product_code))
.then(function(r) { return r.json(); })
.then(function(res) {
var bomMaterials = (res.code === 0 && res.data && res.data.bom_materials) ? res.data.bom_materials : [];
renderPendingConfirmModal(modal, item, bomMaterials, onConfirm);
})
.catch(function() {
renderPendingConfirmModal(modal, item, [], onConfirm);
});
}
function renderPendingConfirmModal(modal, item, bomMaterials, onConfirm) {
var productCode = item.product_code || '';
var supplierName = item.supplier_name || '-';
var createdBy = item.created_by || '-';
var suggestedQty = item.suggested_qty || 0;
var actualQty = item.formal_order_qty || 0;
var auditPrice = item.audit_price || 0;
var hasBom = bomMaterials.length > 0;
// 成品信息字段:2列布局
var prodPairs = [
[['货品编号', productCode], ['货品名称', item.product_name || '-']],
[['供应商名称', supplierName], ['下单人员', createdBy]],
[['建议下单数量', suggestedQty], ['实际下单数量', actualQty]]
];
// 包材信息字段:2列布局
// 每行展示一个物料的所有4个字段:包材编号 | 包材名称 | 供应商名称 | 单价
var formalQtyConfirm = item.formal_order_qty || 0;
// 获取已保存的包材实际下单数量
var savedActualMap = {};
if (item.bom_items) {
try {
var savedBom = typeof item.bom_items === 'string' ? JSON.parse(item.bom_items) : item.bom_items;
savedBom.forEach(function(s) {
if (s.material_code) savedActualMap[s.material_code] = s.actual_quantity || 0;
});
} catch(e) {}
}
var bomRows = [];
if (hasBom) {
// 过滤掉内料
bomMaterials = bomMaterials.filter(function(b) { return !(b.material_name && b.material_name.indexOf('内料') !== -1); });
bomMaterials.forEach(function(b) {
var matSupplier = b.material_supplier || '-';
var matPrice = b.material_cost !== undefined ? '¥' + parseFloat(b.material_cost || 0).toFixed(4) : '-';
var bomQty = b.bom_qty || 0;
var suggestedQty = Math.ceil(formalQtyConfirm * bomQty);
var actualQty = savedActualMap[b.material_code] || 0;
bomRows.push({
code: b.material_code || '',
name: b.material_name || '',
suggested_qty: suggestedQty,
actual_qty: actualQty,
supplier: matSupplier,
price: matPrice
});
});
}
var cellBorder = 'border:1px solid #e8e8e8';
var thBaseLight = 'padding:7px 10px;' + cellBorder + ';text-align:center;background:#e8e8e8;color:#555;font-weight:500;white-space:nowrap';
var thBaseDark = 'padding:7px 10px;' + cellBorder + ';text-align:center;background:#555;color:#fff;font-weight:500;white-space:nowrap';
var tdBase = 'padding:7px 10px;' + cellBorder + ';text-align:center';
var html = '<div style="padding:20px 24px 8px">';
html += '<h3 style="margin-bottom:16px;font-size:17px">请核对商品信息</h3>';
// ─── 成品信息 ───
html += '<div class="pd-card" style="margin-bottom:12px">';
html += '<div class="pd-card-header"><span>🏭 成品信息</span></div>';
html += '<div class="pd-card-body" style="padding:6px 14px">';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
prodPairs.forEach(function(pair) {
var left = pair[0], right = pair[1];
html += '<tr>';
html += '<th style="' + thBaseLight + ';width:100px">' + left[0] + '</th>';
html += '<td style="' + tdBase + ';font-weight:600">' + escHtml(String(left[1] || '-')) + '</td>';
html += '<th style="' + thBaseLight + ';width:100px">' + right[0] + '</th>';
html += '<td style="' + tdBase + ';font-weight:600">' + escHtml(String(right[1] || '-')) + '</td>';
html += '</tr>';
});
html += '</table></div></div>';
// ─── 包材信息 ───
if (hasBom && bomRows.length > 0) {
html += '<div class="pd-card">';
html += '<div class="pd-card-header"><span>📦 包材信息</span></div>';
html += '<div class="pd-card-body" style="padding:0;overflow-x:auto">';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
html += '<thead><tr style="background:#555;color:#fff">';
html += '<th style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">包材编号</th>';
html += '<th style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">包材名称</th>';
html += '<th style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">建议下单数量</th>';
html += '<th style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">实际下单数量</th>';
html += '<th style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">供应商名称</th>';
html += '<th style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">单价</th>';
html += '</tr></thead><tbody>';
bomRows.forEach(function(b, i) {
var bg = i % 2 === 0 ? '#fff' : '#f5f5f5';
html += '<tr style="background:' + bg + '">';
html += '<td style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;font-weight:600;white-space:nowrap"><code style="font-weight:700;color:#333">' + escHtml(b.code) + '</code></td>';
html += '<td style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#333;white-space:nowrap">' + escHtml(b.name) + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#1890ff;white-space:nowrap">' + (b.suggested_qty || 0).toLocaleString() + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#d4380d;white-space:nowrap">' + (b.actual_qty || 0).toLocaleString() + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">' + escHtml(b.supplier) + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">' + escHtml(b.price) + '</td>';
html += '</tr>';
});
html += '</tbody></table></div></div>';
} else {
html += '<div class="pd-card">';
html += '<div class="pd-card-header"><span>📦 包材信息</span></div>';
html += '<div class="pd-card-body" style="padding:16px 14px;text-align:center;color:#999;font-size:13px">暂无包材数据</div></div>';
}
html += '</div>';
html += '<div style="padding:16px 24px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px">';
html += '<button class="btn" id="pendingConfirmCancelBtn">取消</button>';
html += '<button class="btn btn-primary" id="pendingConfirmOkBtn">✅ 确认无误,下单</button></div>';
modal.innerHTML = '<div style="background:#fff;border-radius:12px;width:860px;max-width:95vw;max-height:85vh;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' + html + '</div>';
document.getElementById('pendingConfirmCancelBtn').onclick = function() { modal.remove(); };
document.getElementById('pendingConfirmOkBtn').onclick = function() {
modal.remove();
if (onConfirm) onConfirm();
};
}
function showConfirmRedirect(hasProduct, hasPackaging) {
var links = [];
if (hasProduct) {
links.push({ label: '📦 国内/海外订单管理', page: '/orders/domestic' });
}
if (hasPackaging) {
links.push({ label: '📦 包材订单管理', page: '/orders/packaging' });
}
// 下单备份始终存在(已标记为ordered的记录)
links.push({ label: '📋 下单备份管理', page: '/purchase' });
if (links.length === 0) return;
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:30000;display:flex;align-items:center;justify-content:center;';
var html = '<div style="background:#fff;border-radius:12px;padding:24px;width:400px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,0.18);text-align:center">' +
'<div style="font-size:40px;margin-bottom:10px">✅</div>' +
'<h3 style="margin:0 0 6px 0;font-size:16px">下单完成</h3>' +
'<p style="color:#666;font-size:13px;margin:0 0 16px 0">数据已生成,请前往对应页面查看</p>';
links.forEach(function(l) {
html += '<div style="margin:6px 0"><a href="' + l.page + '" style="display:block;padding:10px 16px;background:#f5f7fa;border-radius:8px;color:#1890ff;text-decoration:none;font-size:14px;font-weight:600" onclick="document.getElementById(\'confirmRedirectModal\').remove()">' + l.label + ' →</a></div>';
});
html += '<div style="margin-top:12px"><button style="padding:8px 24px;background:#f5f5f5;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;cursor:pointer" onclick="document.getElementById(\'confirmRedirectModal\').remove()">我知道了</button></div></div>';
overlay.innerHTML = html;
overlay.id = 'confirmRedirectModal';
document.body.appendChild(overlay);
}
function pendingBatchConfirm() {
var checkedCodes = [];
document.querySelectorAll('.pending-row-cb:checked').forEach(function(cb) {
checkedCodes.push(cb.value);
});
if (checkedCodes.length === 0) {
showCenteredAlert('提示', '请先勾选要确认下单的商品');
return;
}
showCenteredConfirm('批量下单确认', '确认对选中的 <strong>' + checkedCodes.length + '</strong> 个商品进行下单操作?').then(function(confirmed) {
if (!confirmed) return;
// 收集这些商品的BOM前端修改数据(如有)
var bomMap = {};
if (window._pendingBomData) {
checkedCodes.forEach(function(code) {
if (window._pendingBomData[code]) {
bomMap[code] = window._pendingBomData[code];
}
});
}
fetch('/api/purchase/confirm_order', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_codes: checkedCodes, bom_data_map: bomMap})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
showCenteredAlert('操作成功', '✅ 批量下单成功,共 ' + checkedCodes.length + ' 个', function() {
showConfirmRedirect(res.has_product_order, res.has_packaging_order);
});
loadPendingList();
} else {
showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败'));
}
})
.catch(function(err) {
showCenteredAlert('网络错误', '❌ 网络错误: ' + (err.message || '请求失败'));
});
});
}
function pendingConfirmOrder(productCode) {
// 查找该商品在列表中的数据
var item = null;
(_pendingItems || []).forEach(function(it) {
if (it.product_code === productCode) item = it;
});
if (!item) { showCenteredAlert('提示', '未找到该商品数据'); return; }
showCenteredConfirm('下单确认', '确认对 <strong>[' + escHtml(productCode) + ']</strong> 进行下单操作?').then(function(confirmed) {
if (!confirmed) return;
// 携带前端已保存的BOM修改数据(如有)
var bomMap = {};
if (window._pendingBomData && window._pendingBomData[productCode]) {
bomMap[productCode] = window._pendingBomData[productCode];
}
fetch('/api/purchase/confirm_order', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_codes: [productCode], bom_data_map: bomMap})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
showCenteredAlert('操作成功', '✅ 下单成功', function() {
showConfirmRedirect(res.has_product_order, res.has_packaging_order);
});
loadPendingList();
} else {
showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败'));
}
})
.catch(function(err) {
showCenteredAlert('网络错误', '❌ 网络错误: ' + (err.message || '请求失败'));
});
});
}
// ─── 下单备注保存 ───
window.remarkEnterSave = function(e) {
if (e.key === 'Enter') {
e.preventDefault();
e.target.blur();
}
};
window.savePendingRemark = function(input) {
var remark = input.value.trim();
var code = input.getAttribute('data-pcode');
if (!code) return;
// 同步更新本地缓存
(_pendingItems || []).forEach(function(it) {
if (it.product_code === code) it.remark = remark;
});
fetch('/api/purchase/update_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code, remark: remark})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) console.warn('保存备注失败:', res.msg);
})
.catch(function(e){ console.warn('保存备注出错:', e); });
};
// 待下单冻结列功能
window._pendingFrozen = false;
// 待下单管理 - 列字段设置
window._pendingColVisible = {};
window._pendingColOrder = [
'checkbox','index','product_type','product_code','product_name',
'confirm_order','product_grade','supplier_name','remark',
'lead_time','logistics_time','total_support_days',
'procurement_follower','procurement_planner','platform','operator',
'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action'
];
var PENDING_COL_LABELS = {
checkbox:'✔','index':'序号','product_type':'产品类型','product_code':'货品编号','product_name':'货品名称',
confirm_order:'确认下单','product_grade':'产品分级','supplier_name':'供应商名称','remark':'下单备注',
lead_time:'货期','logistics_time':'物流','total_support_days':'总支撑天数',
procurement_follower:'采购跟单','procurement_planner':'采购计划','platform':'归属平台','operator':'运营专员',
audit_price:'审核价格',created_by:'下单人员',created_time:'下单时间',
'suggested_qty':'建议下单数量','formal_qty':'正式下单数量',
created_at:'加入时间','action':'操作'
};
window.openPendingColModal = function() {
// 确保字段顺序完整无重复
var defaultOrder = ['checkbox','index','product_type','product_code','product_name',
'confirm_order','product_grade','supplier_name','remark',
'lead_time','logistics_time','total_support_days',
'procurement_follower','procurement_planner','platform','operator',
'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action'];
if (!window._pendingColOrder || window._pendingColOrder.length < 10) {
window._pendingColOrder = defaultOrder.slice();
}
// 去重
var seen = {};
window._pendingColOrder = window._pendingColOrder.filter(function(f) {
if (seen[f]) return false;
seen[f] = true;
return true;
});
// 补齐缺失字段
defaultOrder.forEach(function(f) {
if (window._pendingColOrder.indexOf(f) === -1) {
window._pendingColOrder.push(f);
}
});
var order = window._pendingColOrder.slice();
var modalId = 'pendingColModal-' + Date.now();
var modal = document.createElement('div');
modal.id = modalId;
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px';
var bodyHtml = '<div style="background:#fff;border-radius:12px;width:480px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);overflow:hidden">' +
'<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #eee">' +
'<h3 style="margin:0;font-size:16px">⚙ 字段设置</h3>' +
'<button style="font-size:22px;border:none;background:none;cursor:pointer;color:#999" onclick="document.getElementById(\'' + modalId + '\').remove()">✕</button>' +
'</div>' +
'<div style="padding:8px 0;overflow-y:auto;flex:1">' +
'<div style="padding:6px 20px;border-bottom:1px solid #f5f5f5;">' +
'<label style="font-size:13px;cursor:pointer"><input type="checkbox" id="' + modalId + '-all" onchange="var c=this.checked;document.querySelectorAll(\'#' + modalId + '-body .pending-col-cb\').forEach(function(b){b.checked=c})"> 全选</label>' +
'</div>' +
'<div id="' + modalId + '-body" style="column-count:3;column-gap:8px;padding:8px 16px">';
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 += '<div class="pending-col-row" draggable="true" data-field="' + field + '" style="display:flex;align-items:center;gap:6px;padding:6px 8px;font-size:13px;cursor:grab;border-radius:4px;break-inside:avoid" ' +
'ondragstart="event.dataTransfer.setData(\'text/plain\',this.dataset.field)" ' +
'ondrop="event.preventDefault();var f=event.dataTransfer.getData(\'text/plain\');var t=this.dataset.field;var p=this.parentNode;var items=Array.from(p.querySelectorAll(\'.pending-col-row\'));var fi=items.findIndex(function(x){return x.dataset.field===f});var ti=items.findIndex(function(x){return x.dataset.field===t});if(fi>=0&&ti>=0){var el=items[fi];if(fi<ti){p.insertBefore(el,items[ti].nextSibling)}else{p.insertBefore(el,items[ti])}}" ' +
'ondragover="event.preventDefault()">' +
'<span style="color:#bbb;font-size:12px;cursor:grab">⠿</span>' +
'<input type="checkbox" class="pending-col-cb" data-field="' + field + '"' + checked + ' style="width:15px;height:15px"> ' + label +
'</div>';
});
bodyHtml += '</div></div>' +
'<div style="padding:10px 20px;border-top:1px solid #eee;text-align:right;display:flex;gap:8px;justify-content:flex-end">' +
'<button class="btn btn-sm" onclick="document.getElementById(\'' + modalId + '\').remove()">取消</button>' +
'<button class="btn btn-sm btn-primary" onclick="savePendingColSettings(\'' + modalId + '\')">💾 保存</button>' +
'</div></div>';
modal.innerHTML = bodyHtml;
document.body.appendChild(modal);
};
window.savePendingColSettings = function(modalId) {
// 读取顺序
var rows = document.querySelectorAll('#' + modalId + '-body .pending-col-row');
var newOrder = ['checkbox', 'index'];
rows.forEach(function(row) {
newOrder.push(row.dataset.field);
});
newOrder.push('action');
window._pendingColOrder = newOrder;
// 读取可见性
document.querySelectorAll('#' + modalId + '-body .pending-col-cb').forEach(function(cb) {
window._pendingColVisible[cb.dataset.field] = cb.checked;
});
document.getElementById(modalId).remove();
applyPendingColSettings();
};
function applyPendingColSettings() {
var order = window._pendingColOrder || [];
var tbl = document.getElementById('pendingTable');
if (!tbl) return;
var thead = tbl.querySelector('thead tr');
var tbody = document.getElementById('pendingTbody');
if (!thead) return;
// 获取字段到TH/TD索引的映射(按当前DOM顺序)
var fieldToThIdx = {};
var ths = thead.querySelectorAll('th');
var fieldMap = ['checkbox','index','product_type','product_code','product_name',
'confirm_order','product_grade','supplier_name','remark',
'lead_time','logistics_time','total_support_days',
'procurement_follower','procurement_planner','platform','operator',
'audit_price','created_by','created_time','suggested_qty','formal_qty','created_at','action'];
ths.forEach(function(th, i) {
if (i < fieldMap.length) fieldToThIdx[fieldMap[i]] = i;
});
// 重新排列表头
order.forEach(function(field, newIdx) {
var oldIdx = fieldToThIdx[field];
if (oldIdx === undefined || oldIdx >= ths.length) return;
var target = thead.children[newIdx];
var el = ths[oldIdx];
if (el !== target) {
thead.insertBefore(el, target || null);
}
});
// 重新排列数据行
if (tbody) {
var fieldToTdIdx = {};
fieldMap.forEach(function(f, i) { fieldToTdIdx[f] = i; });
tbody.querySelectorAll('tr').forEach(function(tr) {
var tds = tr.querySelectorAll('td');
order.forEach(function(field, newIdx) {
var oldIdx = fieldToTdIdx[field];
if (oldIdx === undefined || oldIdx >= tds.length) return;
var target = tr.children[newIdx];
var el = tds[oldIdx];
if (el !== target) {
tr.insertBefore(el, target || null);
}
});
});
}
// 设置可见性
order.forEach(function(field) {
var vis = window._pendingColVisible[field];
var hide = vis === false || (field !== 'checkbox' && field !== 'index' && field !== 'action' && vis === false);
// 简化:隐藏字段通过样式
});
// 刷新冻结
if (window._pendingFrozenActive) {
applyPendingFrozen(window._pendingFrozenFields);
}
}
// ─── 下单备份管理 - 字段设置 ───
if (!window._backupColVisible) { window._backupColVisible = {}; window._backupColOrder = null; }
var BACKUP_COL_ORDER = ['index','product_type','product_code','product_name',
'ordered_at','created_by','production_order_no','suggested_qty','formal_qty',
'product_grade','supplier_name','remark',
'lead_time','logistics_time','total_support_days',
'procurement_follower','procurement_planner','platform','operator',
'audit_price','created_at','action'];
var BACKUP_COL_LABELS = {
index:'序号',product_type:'产品类型',product_code:'货品编号',product_name:'货品名称',
ordered_at:'下单时间',product_grade:'产品分级',supplier_name:'供应商名称',remark:'下单备注',
lead_time:'货期',logistics_time:'物流',total_support_days:'总支撑天数',
procurement_follower:'采购跟单',procurement_planner:'采购计划',platform:'归属平台',operator:'运营专员',created_by:'下单人员',
audit_price:'是否审核价格',production_order_no:'生产单号',suggested_qty:'建议下单数量',formal_qty:'正式下单数量',
created_at:'加入时间',action:'操作'
};
window.openBackupColModal = function() {
if (!window._backupColOrder) { window._backupColOrder = BACKUP_COL_ORDER.slice(); }
var order = window._backupColOrder.slice();
var modalId = 'backupColModal-' + Date.now();
var modal = document.createElement('div');
modal.id = modalId;
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px';
var bodyHtml = '<div style="background:#fff;border-radius:12px;width:480px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);overflow:hidden">' +
'<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #eee">' +
'<h3 style="margin:0;font-size:16px">⚙ 字段设置(下单备份)</h3>' +
'<button style="font-size:22px;border:none;background:none;cursor:pointer;color:#999" onclick="document.getElementById(\'' + modalId + '\').remove()">✕</button>' +
'</div>' +
'<div style="padding:8px 0;overflow-y:auto;flex:1">' +
'<div style="padding:6px 20px;border-bottom:1px solid #f5f5f5">' +
'<label style="font-size:13px;cursor:pointer"><input type="checkbox" id="' + modalId + '-all" onchange="var c=this.checked;document.querySelectorAll(\'#' + modalId + '-body .backup-col-cb\').forEach(function(b){b.checked=c})"> 全选</label>' +
'</div>' +
'<div id="' + modalId + '-body" style="column-count:3;column-gap:8px;padding:8px 16px">';
order.forEach(function(field) {
if (field === 'action') return;
var label = BACKUP_COL_LABELS[field] || field;
var checked = window._backupColVisible[field] !== false ? ' checked' : '';
bodyHtml += '<label style="display:flex;align-items:center;gap:6px;padding:5px 8px;font-size:13px;break-inside:avoid;cursor:pointer">' +
'<input type="checkbox" class="backup-col-cb" data-field="' + field + '"' + checked + ' style="width:15px;height:15px"> ' + label +
'</label>';
});
bodyHtml += '</div></div>' +
'<div style="padding:10px 20px;border-top:1px solid #eee;text-align:right;display:flex;gap:8px;justify-content:flex-end">' +
'<button class="btn btn-sm" onclick="document.getElementById(\'' + modalId + '\').remove()">取消</button>' +
'<button class="btn btn-sm btn-primary" onclick="saveBackupColSettings(\'' + modalId + '\')">💾 保存</button>' +
'</div></div>';
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 = '<div style="background:#fff;border-radius:12px;width:400px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);overflow:hidden">' +
'<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #eee">' +
'<h3 style="margin:0;font-size:16px">🔒 选择冻结列</h3>' +
'<button style="font-size:22px;border:none;background:none;cursor:pointer;color:#999" onclick="document.getElementById(\'' + modalId + '\').remove()">✕</button>' +
'</div>' +
'<div style="padding:12px 20px;overflow:auto;flex:1" id="' + modalId + '-body">';
labels.forEach(function(label, i) {
var checked = _pendingFrozenFields.indexOf(i + 1) !== -1 ? ' checked' : '';
html += '<label style="display:flex;align-items:center;padding:6px 0;cursor:pointer;font-size:14px">' +
'<input type="checkbox" data-col="' + (i + 1) + '"' + checked + ' style="margin-right:10px;width:16px;height:16px"> ' + label +
'</label>';
});
html += '</div>' +
'<div style="padding:10px 20px;border-top:1px solid #eee;text-align:right;display:flex;gap:8px;justify-content:flex-end">' +
'<button class="btn btn-sm" onclick="_pendingFrozenFields=[];document.getElementById(\'' + modalId + '\').remove();applyPendingFrozen([])">取消冻结</button>' +
'<button class="btn btn-sm btn-primary" onclick="var cbs=document.querySelectorAll(\'#' + modalId + '-body input[type=checkbox]:checked\');var arr=[];cbs.forEach(function(cb){arr.push(parseInt(cb.getAttribute(\'data-col\')))});_pendingFrozenFields=arr;document.getElementById(\'' + modalId + '\').remove();applyPendingFrozen(arr)">✅ 确认</button>' +
'</div></div>';
modal.innerHTML = html;
modal.onclick = function(e) { if (e.target === modal) modal.remove(); };
document.body.appendChild(modal);
};
function applyPendingFrozen(frozenIdx) {
_pendingFrozenActive = frozenIdx.length > 0;
var btn = document.getElementById('pendingFrozenBtn');
if (btn) {
btn.style.background = _pendingFrozenActive ? '#bae7ff' : '';
btn.style.border = _pendingFrozenActive ? '1px solid #1890ff' : '';
btn.textContent = _pendingFrozenActive ? '🔓 取消冻结' : '🔒 冻结列';
}
var tbl = document.getElementById('pendingTable');
if (!tbl) return;
// 清除所有冻结
tbl.querySelectorAll('[style*="sticky"]').forEach(function(el) {
el.style.position = ''; el.style.left = ''; el.style.zIndex = ''; el.style.background = '';
});
tbl.querySelectorAll('th, td').forEach(function(el) {
el.style.position = ''; el.style.left = ''; el.style.zIndex = ''; el.style.background = '';
});
var ths = tbl.querySelectorAll('thead tr th');
var rows = tbl.querySelectorAll('tbody tr');
// 按从左到右顺序冻结选中列
frozenIdx.sort(function(a,b){return a-b;});
// 先冻结th
frozenIdx.forEach(function(idx) {
if (!ths[idx]) return;
var left = 0;
for (var i = 0; i < idx; i++) {
if (frozenIdx.indexOf(i) !== -1) {
left += (ths[i] ? (ths[i].offsetWidth || ths[i].getBoundingClientRect().width || 50) : 50);
}
}
ths[idx].style.cssText = 'position:sticky;left:' + left + 'px;z-index:11;background:var(--bg,#f5f5f5)';
});
// 同步冻结数据行
frozenIdx.forEach(function(idx) {
rows.forEach(function(tr) {
var tds = tr.querySelectorAll('td');
if (!tds[idx]) return;
var left = 0;
for (var i = 0; i < idx; i++) {
if (frozenIdx.indexOf(i) !== -1) {
left += (tds[i] ? (tds[i].offsetWidth || tds[i].getBoundingClientRect().width || 50) : 50);
}
}
tds[idx].style.cssText = 'position:sticky;left:' + left + 'px;z-index:2;background:var(--card-bg,#fff)';
});
});
}
// 采购跟单/计划按名称生成颜色标签
function colorForName(name) {
if (!name || name === '-') return null;
var colors = ['#4d9aff','#e8579e','#7ecf4f','#fba84a','#9d5ee0','#4dd4d4','#f062ae','#fbc44d','#5f7af0','#b8e54a','#f75a62','#52abff','#63be3f','#e56540','#7d4ac4'];
var hash = 0;
for (var i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); }
return colors[Math.abs(hash) % colors.length];
}
function textColorForBG(bg) {
if (!bg) return '#333';
var c = bg.replace('#','');
var r = parseInt(c.substr(0,2),16), g = parseInt(c.substr(2,2),16), b = parseInt(c.substr(4,2),16);
return (r*0.299 + g*0.587 + b*0.114) > 140 ? '#333' : '#fff';
}
// ─── 采购跟单/采购计划 分组映射 ───
window._personnelGroups = [
{ name: '跟单一组', leader: '方园霞', members: ['方园霞', '邝碧君', '余瑞霞', '卢浩然'], color: '#e6f4ff', tagColor: '#1677ff' },
{ name: '跟单二组', leader: '王加飞', members: ['许晓珊', '刘伟', '王加飞', '邓文婷'], color: '#f0f5ff', tagColor: '#722ed1' },
{ name: '跟单三组', leader: '蔡少铃', members: ['闫伟华', '蔡少铃', '颜于凯', '张文菁'], color: '#e6fffb', tagColor: '#13c2c2' },
{ name: '现货小组', leader: '', members: ['莫碧霞', '范亚欢'], color: '#fff7e6', tagColor: '#fa8c16' },
{ name: '计划小组', leader: '汤定毅', members: ['汤定毅', '李媛', '黄静', '胡孟怡', '张莹纯', '范世澄', '陈瑜'], color: '#f6ffed', tagColor: '#52c41a' }
];
// 构建名字→分组 快速查找
window._nameToGroup = {};
window._personnelGroups.forEach(function(g) {
g.members.forEach(function(m) {
_nameToGroup[m] = g;
});
});
function renderNameBadges(names) {
if (!names || names === '-' || names === '') return '<span style="color:#bbb">-</span>';
var nameList = names.split(',').map(function(n) { return n.trim(); }).filter(function(n) { return n; });
if (nameList.length === 0) return '<span style="color:#bbb">-</span>';
// 检查所有人是否属于同一个分组(仅对采购跟单分组生效,不合并"计划小组")
var group = null;
var allInSameGroup = true;
nameList.forEach(function(name) {
var g = _nameToGroup[name];
if (!g || g.name === '计划小组') { allInSameGroup = false; }
else if (!group) { group = g; }
else if (g.name !== group.name) { allInSameGroup = false; }
});
// 如果同属一个采购跟单分组,显示分组名
if (allInSameGroup && group) {
var namesStr = group.members.join('、');
return '<span style="display:inline-block;padding:2px 14px;border-radius:12px;font-size:13px;font-weight:600;margin:1px 3px;background:' + group.color + ';color:' + group.tagColor + ';border:1px solid ' + group.tagColor + ';cursor:pointer" title="' + escHtml(namesStr) + '">📦 ' + escHtml(group.name) + '</span>';
}
// 否则按原来的方式渲染单个标签
var html = '';
nameList.forEach(function(name) {
var bg = colorForName(name) || '#e6f7ff';
var fc = textColorForBG(bg);
html += '<span style="display:inline-block;padding:2px 10px;border-radius:12px;font-size:13px;font-weight:600;margin:1px 3px;background:' + bg + ';color:' + fc + '">' + escHtml(name) + '</span>';
});
return html;
}
// 将人名渲染为独立彩色标签(跳过分组合并)
function renderPersonBadges(names) {
if (!names || names === '-' || names === '') return '<span style="color:#bbb">-</span>';
var nameList = names.split(',').map(function(n) { return n.trim(); }).filter(function(n) { return n; });
if (nameList.length === 0) return '<span style="color:#bbb">-</span>';
var html = '';
nameList.forEach(function(name) {
var bg = colorForName(name) || '#e6f7ff';
var fc = textColorForBG(bg);
html += '<span style="display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:500;margin:1px 2px;background:' + bg + ';color:' + fc + '">' + escHtml(name) + '</span>';
});
return html;
}
// 渲染下单人下拉选择(采购跟单人员)
function renderOrdererSelect(row) {
var current = (row['下单人'] || '').trim();
if (domesticFollowers.length === 0) {
return '<span style="color:#bbb">-</span>';
}
var initBg = '#fff', initFc = '#333';
if (current && domesticFollowers.indexOf(current) >= 0) {
initBg = colorForName(current) || '#e6f7ff';
initFc = textColorForBG(initBg);
}
var html = '<select class="orderer-select" data-id="' + row.id + '" style="width:100%;border:1px solid #d9d9d9;border-radius:3px;font-size:12px;padding:3px 4px;background:' + initBg + ';color:' + initFc + ';cursor:pointer;font-weight:500">';
html += '<option value="">— 无 —</option>';
domesticFollowers.forEach(function(name) {
var sel = (name === current) ? ' selected' : '';
var bg = colorForName(name) || '#e6f7ff';
var fc = textColorForBG(bg);
html += '<option value="' + escHtml(name) + '"' + sel + ' style="background:' + bg + ';color:' + fc + ';font-weight:500">' + escHtml(name) + '</option>';
});
html += '</select>';
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 = '<div style="overflow-x:auto;overscroll-behavior:contain"><table style="font-size:13px;border-collapse:collapse;width:100%">';
bomContent += '<thead><tr style="background:#e6f7ff"><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">物料编号</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">物料名称</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">用量</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">建议下单数量</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">实际下单数量</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单前包材</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单后包材</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单备注</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">更换稿件</th><th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">操作</th></tr></thead><tbody>';
bomList.forEach(function(b) {
var matCode = escHtml(b.material_code || '');
var qty = b.quantity || 0;
var orderQty = Math.ceil(formalQty * qty);
// 使用已保存的实际下单数量(如有)
var actualQty = (b.actual_quantity !== undefined && b.actual_quantity !== null) ? b.actual_quantity : orderQty;
bomContent += '<tr><td style="padding:6px 10px;border:1px solid #eee;word-break:break-all"><code style="font-size:14px;font-weight:600">' + matCode + '</code></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee;word-break:break-word;font-size:14px">' + escHtml(b.material_name || '') + '</td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee">' + qty + '</td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input" style="width:55px" value="' + orderQty + '" data-material="' + matCode + '" data-product="' + escHtml(productCode) + '"></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input bom-actual-qty" style="width:55px;font-weight:600;color:#d4380d" value="' + actualQty + '" data-material="' + matCode + '" data-product="' + escHtml(productCode) + '" placeholder="实下单"></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input" style="width:50px" value="0"></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input" style="width:50px" value="0"></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><input type="text" class="form-input" style="width:100%;min-width:180px;box-sizing:border-box" value=""></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><select class="form-input bom-manuscript" style="width:60px;font-weight:600" data-material="' + matCode + '" onchange="this.style.color=this.value===\'是\'?\'#f5222d\':\'#333\'"><option value="">否</option><option value="是">是</option></select></td>';
bomContent += '<td style="padding:6px 10px;border:1px solid #eee"><button class="btn btn-sm" style="background:#e6fffb;color:#13c2c2;border:1px solid #87e8de" onclick="generateMaterialOrder(\'' + escHtml(productCode) + '\', \'' + matCode + '\')">生成</button></td></tr>';
});
bomContent += '</tbody></table></div>';
// 弹窗展示
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 = '<div style="background:#fff;border-radius:12px;width:95vw;max-width:1600px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);overflow:hidden">' +
'<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 24px;border-bottom:1px solid #eee;flex:0 0 auto">' +
'<h3 style="margin:0;font-size:16px">📋 物料清单 [<strong>' + escHtml(productCode) + '</strong>]</h3>' +
'<button style="font-size:22px;border:none;background:none;cursor:pointer;padding:0 6px;line-height:1;color:#999" onclick="document.getElementById(\'' + modalId + '\').remove()">✕</button>' +
'</div>' +
'<div style="padding:16px 24px;overflow:auto;flex:1 1 auto">' + bomContent + '</div>' +
'<div style="padding:10px 24px;border-top:1px solid #eee;text-align:right;flex:0 0 auto;display:flex;gap:8px;justify-content:flex-end">' +
'<button class="btn btn-primary" onclick="saveBomData(\'' + escHtml(productCode) + '\')">💾 保存</button>' +
'<button class="btn" onclick="document.getElementById(\'' + modalId + '\').remove()">关闭</button>' +
'</div></div>';
modal.onclick = function(e) { if (e.target === modal) modal.remove(); };
document.body.appendChild(modal);
}
window.saveBomData = function(productCode) {
var inputs = document.querySelectorAll('.bom-actual-qty[data-product="' + productCode + '"]');
var materials = [];
// 读取成品下单信息
var productActualInput = document.querySelector('.bom-product-actual-qty[data-product="' + productCode + '"]');
var productRemarkInput = document.querySelector('.bom-product-remark[data-product="' + productCode + '"]');
var productActualQty = productActualInput ? (parseInt(productActualInput.value) || 0) : 0;
var productRemark = productRemarkInput ? productRemarkInput.value.trim() : '';
// 保存成品数据(单独存,允许为0)
materials.push({
material_code: '成品_' + productCode,
actual_quantity: productActualQty,
remark: productRemark
});
inputs.forEach(function(inp) {
var qty = parseInt(inp.value) || 0;
{
var matCode = inp.getAttribute('data-material');
// 读取同行建议下单数量
var orderInput = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]');
var bomQty = orderInput ? (parseInt(orderInput.value) || 0) : 0;
// 读取物料名称(从BOM弹窗内的表格行中获取)
var matName = '';
if (orderInput) {
var row = orderInput.closest('tr');
if (row) {
var nameCell = row.querySelector('td:nth-child(2)');
if (nameCell) matName = nameCell.textContent.trim();
}
}
// 读取同行备注
var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]');
var remarkVal = remarkInput ? remarkInput.value.trim() : '';
// 读取是否修改稿件
var manuscriptSelect = document.querySelector('.bom-manuscript[data-material="' + matCode + '"]');
var manuscriptVal = manuscriptSelect ? manuscriptSelect.value : '';
materials.push({
material_code: matCode,
material_name: matName,
quantity: bomQty,
actual_quantity: qty,
remark: remarkVal,
change_manuscript: manuscriptVal
});
}
});
// 过滤完所有物料后,检查是否至少有一条物料记录
var nonProductMaterials = materials.filter(function(m) { return m.material_code.indexOf('成品_') !== 0; });
if (materials.length === 0 || nonProductMaterials.length === 0) {
alert('没有需要保存的物料');
return;
}
// 存到前端变量,下单时一起提交
if (!window._pendingBomData) window._pendingBomData = {};
window._pendingBomData[productCode] = materials;
// 关闭BOM弹窗
var bomInput = document.querySelector('.bom-actual-qty[data-product="' + productCode + '"]');
if (bomInput) {
var modal = bomInput.closest('[style*="z-index: 10000"]') || bomInput.closest('[id^="bom-modal-"]');
if (modal) modal.remove();
}
// 持久化到后端:BOM数据和备注同步到 pending_orders
var updatePayload = {product_code: productCode, bom_items: materials};
if (productRemark) updatePayload.remark = productRemark;
fetch('/api/purchase/update_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(updatePayload)
}).then(function(r) { return r.json(); }).then(function(res) {
if (res.code !== 0) console.warn('同步BOM数据失败:', res.msg);
}).catch(function(e) { console.warn('同步BOM数据出错:', e); });
showCenteredAlert('保存成功', '✅ BOM已保存并同步到数据库,确认下单将使用最新包材数据');
};
// ─── 计划预测 BOM 物料清单(完整编辑功能) ───
function planToggleBom(productCode, productName, suggestedQty, actualQty, orderRemark) {
// 从已保存的BOM数据中恢复成品实际下单数量和备注
if (window._pendingBomData && window._pendingBomData[productCode]) {
window._pendingBomData[productCode].forEach(function(m) {
if (m.material_code === '成品_' + productCode) {
// 修复:允许恢复为0(用户手动设0表示不下单)
if (m.actual_quantity >= 0) actualQty = m.actual_quantity;
if (m.remark) orderRemark = m.remark;
}
});
}
fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode))
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0 || !res.data) {
alert('❌ 获取物料清单失败');
return;
}
var bom = res.data.bom_materials || [];
if (bom.length === 0) {
alert('该商品暂无物料清单');
return;
}
// 获取正式下单数量(从计划预测页面元素获取)
var formalQtySpan = document.getElementById('fqv-' + productCode);
var formalQty = formalQtySpan ? (parseInt(formalQtySpan.dataset.val) || 0) : 0;
var modalId = 'bom-modal-' + Date.now();
var productSummaryHtml =
'<div style="margin-bottom:16px;background:#fafafa;border-radius:8px;padding:12px 16px">' +
'<div style="font-size:14px;font-weight:600;color:#333;margin-bottom:10px">📦 成品下单信息</div>' +
'<table style="font-size:13px;border-collapse:collapse;width:100%">' +
'<thead><tr style="background:#e6f7ff">' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">货品编号</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">货品名称</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">建议下单数量</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">实际下单数量</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单备注</th>' +
'</tr></thead>' +
'<tbody>' +
'<tr>' +
'<td style="padding:6px 10px;border:1px solid #eee"><code style="font-size:13px;font-weight:600">' + escHtml(productCode) + '</code></td>' +
'<td style="padding:6px 10px;border:1px solid #eee">' + escHtml(productName) + '</td>' +
'<td style="padding:6px 10px;border:1px solid #eee;font-weight:600;color:#999;background:#f5f5f5;text-align:center">' + suggestedQty + '</td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input bom-product-actual-qty" style="width:80px;font-weight:600;color:#d4380d" value="' + actualQty + '" data-product="' + escHtml(productCode) + '" placeholder="实下单"' + (window._canEditField && window._canEditField('planning','实际下单数量') ? '' : ' readonly style="width:80px;font-weight:600;color:#999;background:#f5f5f5"') + '></td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="text" class="form-input bom-product-remark" style="width:100%;min-width:180px;box-sizing:border-box" value="' + (orderRemark || '') + '" data-product="' + escHtml(productCode) + '" placeholder="备注"' + (window._canEditField && window._canEditField('planning','下单备注') ? '' : ' readonly style="width:100%;min-width:180px;box-sizing:border-box;background:#f5f5f5;color:#999"') + '></td>' +
'</tr>' +
'</tbody>' +
'</table>' +
'</div>';
var bomContent = productSummaryHtml +
'<div style="margin-top:16px;background:#fafafa;border-radius:8px;padding:12px 16px">' +
'<div style="font-size:14px;font-weight:600;color:#333;margin-bottom:10px">📦 包材下单信息</div>' +
'<div style="overflow-x:auto;overscroll-behavior:contain"><table style="font-size:13px;border-collapse:collapse;width:100%">' +
'<thead><tr style="background:#e6f7ff">' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">物料编号</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">物料名称</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">用量</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">建议下单数量</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">实际下单数量</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单前包材</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单后包材</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">下单备注</th>' +
'<th style="padding:6px 10px;border:1px solid #d9d9d9;text-align:left;white-space:nowrap">更换稿件</th>' +
'</tr></thead><tbody>';
// 读取之前保存的包材数据
var savedBom = window._pendingBomData && window._pendingBomData[productCode] ? window._pendingBomData[productCode] : [];
var savedMap = {};
var savedRemarkMap = {};
var savedManuscriptMap = {};
savedBom.forEach(function(s) {
savedMap[s.material_code] = s.actual_quantity;
savedRemarkMap[s.material_code] = s.remark || '';
savedManuscriptMap[s.material_code] = s.change_manuscript || '';
});
bom.forEach(function(b) {
// 过滤掉含有内料的包材
if (b.material_name && b.material_name.indexOf('内料') !== -1) return;
var matCode = escHtml(b.material_code || '');
var qty = b.bom_qty || 0;
var orderQty = Math.ceil(formalQty * qty);
// 修复:物料在 savedMap 中时(即使实际数量为0),使用保存值;否则用默认的建议值
var hasSaved = b.material_code in savedMap;
var savedActual = savedMap[b.material_code];
var actualQty = hasSaved ? (savedActual === undefined ? orderQty : savedActual) : orderQty;
bomContent += '<tr>' +
'<td style="padding:6px 10px;border:1px solid #eee;word-break:break-all"><code style="font-size:13px;font-weight:600">' + matCode + '</code></td>' +
'<td style="padding:6px 10px;border:1px solid #eee;word-break:break-word;font-size:13px">' + escHtml(b.material_name || '') + '</td>' +
'<td style="padding:6px 10px;border:1px solid #eee">' + qty + '</td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input bom-order-qty" style="width:70px;background:#f5f5f5;color:#999" value="' + orderQty + '" data-material="' + matCode + '" data-product="' + escHtml(productCode) + '" readonly></td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input bom-actual-qty" style="width:70px;font-weight:600;color:#d4380d" value="' + actualQty + '" data-material="' + matCode + '" data-product="' + escHtml(productCode) + '" placeholder="实下单"' + (window._canEditField && window._canEditField('planning','实际下单数量') ? '' : ' readonly style="width:70px;font-weight:600;color:#999;background:#f5f5f5"') + '></td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input" style="width:60px" value="0"' + (window._canEditField && window._canEditField('planning','下单前包材') ? '' : ' readonly style="width:60px;background:#f5f5f5;color:#999"') + '></td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="number" class="form-input" style="width:50px" value="0"' + (window._canEditField && window._canEditField('planning','下单后包材') ? '' : ' readonly style="width:50px;background:#f5f5f5;color:#999"') + '></td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><input type="text" class="form-input bom-remark" style="width:100%;min-width:180px;box-sizing:border-box" value="' + escHtml(savedRemarkMap[b.material_code] || '') + '" data-material="' + matCode + '" placeholder="备注"' + (window._canEditField && window._canEditField('planning','下单备注') ? '' : ' readonly style="width:100%;min-width:180px;box-sizing:border-box;background:#f5f5f5;color:#999"') + '></td>' +
'<td style="padding:6px 10px;border:1px solid #eee"><select class="form-input bom-manuscript" style="width:60px;font-weight:600" data-material="' + matCode + '" onchange="this.style.color=this.value===\'是\'?\'#f5222d\':\'#333\'"' + (window._canEditField && window._canEditField('planning','更换稿件') ? '' : ' disabled style="width:60px;font-weight:600;background:#f5f5f5;color:#999"') + '><option value="">否</option><option value="是">是</option></select></td></tr>';
});
bomContent += '</tbody></table></div>';
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 = '<div style="background:#fff;border-radius:12px;width:95vw;max-width:1600px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);overflow:hidden">' +
'<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 24px;border-bottom:1px solid #eee;flex:0 0 auto">' +
'<h3 style="margin:0;font-size:16px">📋 下单清单 [<strong>' + escHtml(productCode) + '</strong>]</h3>' +
'<button style="font-size:22px;border:none;background:none;cursor:pointer;padding:0 6px;line-height:1;color:#999" onclick="document.getElementById(\'' + modalId + '\').remove()">✕</button>' +
'</div>' +
'<div style="padding:16px 24px;overflow:auto;flex:1 1 auto">' + bomContent + '</div>' +
'<div style="padding:10px 24px;border-top:1px solid #eee;text-align:right;flex:0 0 auto;display:flex;gap:8px;justify-content:flex-end">' +
'<button class="btn btn-primary" onclick="saveBomData(\'' + escHtml(productCode) + '\')"' + (window._canEditField && window._canEditField('planning','实际下单数量') ? '' : ' disabled style="background:#d9d9d9;border-color:#d9d9d9;color:#999;cursor:not-allowed"') + '>💾 保存</button>' +
'<button class="btn" onclick="document.getElementById(\'' + modalId + '\').remove()">关闭</button>' +
'</div></div>';
modal.onclick = function(e) { if (e.target === modal) {} };
document.body.appendChild(modal);
// 恢复已保存的更换稿件状态
modal.querySelectorAll('.bom-manuscript').forEach(function(sel) {
var matCode = sel.getAttribute('data-material');
if (savedManuscriptMap[matCode]) {
sel.value = savedManuscriptMap[matCode];
}
sel.style.color = sel.value === '是' ? '#f5222d' : '#333';
});
// 实际下单数量输入框:点击自动全选 + 回车跳到下一行
var actualInputs = modal.querySelectorAll('.bom-actual-qty');
actualInputs.forEach(function(inp, idx) {
// 点击自动全选
inp.addEventListener('focus', function() {
this.select();
});
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
var matCode = inp.getAttribute('data-material');
var qty = parseInt(inp.value) || 0;
if (!window._pendingBomData) window._pendingBomData = {};
if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = [];
// 更新或添加当前物料
var found = false;
window._pendingBomData[productCode].forEach(function(m) {
if (m.material_code === matCode) {
m.actual_quantity = qty;
// 同时更新其他字段
var orderInput = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]');
if (orderInput) {
m.quantity = parseInt(orderInput.value) || 0;
var row = orderInput.closest('tr');
if (row) {
var nameCell = row.querySelector('td:nth-child(2)');
if (nameCell) m.material_name = nameCell.textContent.trim();
}
}
var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]');
if (remarkInput) m.remark = remarkInput.value.trim();
found = true;
}
});
if (!found && qty > 0) {
// 读取同行建议下单数量、物料名称和备注
var orderInput = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]');
var bomQty = orderInput ? (parseInt(orderInput.value) || 0) : 0;
var matName = '';
var remarkVal = '';
if (orderInput) {
var row = orderInput.closest('tr');
if (row) {
var nameCell = row.querySelector('td:nth-child(2)');
if (nameCell) matName = nameCell.textContent.trim();
}
}
// 读取备注
var remarkInput = document.querySelector('.bom-remark[data-material="' + matCode + '"]');
if (remarkInput) remarkVal = remarkInput.value.trim();
window._pendingBomData[productCode].push({
material_code: matCode,
material_name: matName,
quantity: bomQty,
actual_quantity: qty,
remark: remarkVal
});
}
inp.style.borderColor = '#52c41a';
setTimeout(function() { inp.style.borderColor = ''; }, 800);
// 跳到下一行
var nextInput = actualInputs[idx + 1];
if (nextInput) {
nextInput.focus();
nextInput.select();
}
// 没有下一行:不做处理,保存完成
}
});
});
// 成品下单信息 - 实际下单数量:点击全选 + 回车保存
var productActualInputs = modal.querySelectorAll('.bom-product-actual-qty');
productActualInputs.forEach(function(inp) {
inp.addEventListener('focus', function() { this.select(); });
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
var newQty = parseInt(inp.value) || 0;
showCenteredConfirm('确认保存', '确认保存成品实际下单数量为 <strong>' + newQty + '</strong> 吗?保存后将同步更新下方包材的实际下单数量。').then(function(confirmed) {
if (!confirmed) return;
// 保存成品数量
if (!window._pendingBomData) window._pendingBomData = {};
if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = [];
var found = false;
window._pendingBomData[productCode].forEach(function(m) {
if (m.material_code === '成品_' + productCode) {
m.actual_quantity = newQty;
found = true;
}
});
if (!found) {
window._pendingBomData[productCode].push({
material_code: '成品_' + productCode,
material_name: '',
quantity: 0,
actual_quantity: newQty,
remark: ''
});
}
// 退出编辑状态
inp.blur();
inp.style.borderColor = '#52c41a';
setTimeout(function() { inp.style.borderColor = ''; }, 800);
// 同步更新下方包材实际下单数量
if (bom && bom.length > 0) {
var materialActualInputs = modal.querySelectorAll('.bom-actual-qty');
materialActualInputs.forEach(function(mInp) {
var matCode = mInp.getAttribute('data-material');
// 查找该物料的BOM用量
var bomQty = 0;
bom.forEach(function(b) {
if (b.material_code === matCode) bomQty = b.bom_qty || 0;
});
if (bomQty > 0) {
var newMatQty = Math.ceil(newQty * bomQty);
mInp.value = newMatQty;
// 更新存储
var fMat = false;
window._pendingBomData[productCode].forEach(function(m) {
if (m.material_code === matCode) {
m.actual_quantity = newMatQty;
var orderInput = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]');
if (orderInput) {
m.quantity = parseInt(orderInput.value) || 0;
var row = orderInput.closest('tr');
if (row) {
var nameCell = row.querySelector('td:nth-child(2)');
if (nameCell) m.material_name = nameCell.textContent.trim();
}
}
var rInp = document.querySelector('.bom-remark[data-material="' + matCode + '"]');
if (rInp) m.remark = rInp.value.trim();
fMat = true;
}
});
if (!fMat && newMatQty > 0) {
var oInp = document.querySelector('.bom-order-qty[data-product="' + productCode + '"][data-material="' + matCode + '"]');
var bQty = oInp ? (parseInt(oInp.value) || 0) : 0;
var mName = '';
if (oInp) {
var row = oInp.closest('tr');
if (row) {
var nCell = row.querySelector('td:nth-child(2)');
if (nCell) mName = nCell.textContent.trim();
}
}
var rVal = '';
var rInp2 = document.querySelector('.bom-remark[data-material="' + matCode + '"]');
if (rInp2) rVal = rInp2.value.trim();
window._pendingBomData[productCode].push({
material_code: matCode,
material_name: mName,
quantity: bQty,
actual_quantity: newMatQty,
remark: rVal
});
}
}
});
showCenteredAlert('保存成功', '✅ 成品实际下单数量已保存,下方包材数量已同步更新');
}
});
}
});
});
// 成品下单信息 - 下单备注:回车保存
var productRemarkInputs = modal.querySelectorAll('.bom-product-remark');
productRemarkInputs.forEach(function(inp) {
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
if (!window._pendingBomData) window._pendingBomData = {};
if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = [];
var found = false;
window._pendingBomData[productCode].forEach(function(m) {
if (m.material_code === '成品_' + productCode) {
m.remark = inp.value;
found = true;
}
});
if (!found) {
window._pendingBomData[productCode].push({
material_code: '成品_' + productCode,
material_name: '',
quantity: 0,
actual_quantity: 0,
remark: inp.value
});
}
inp.style.borderColor = '#52c41a';
setTimeout(function() { inp.style.borderColor = ''; }, 800);
}
});
});
// 包材下单信息 - 下单备注:回车保存
var remarkInputs = modal.querySelectorAll('.bom-remark');
remarkInputs.forEach(function(inp) {
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
var matCode = inp.getAttribute('data-material');
var val = inp.value;
if (!window._pendingBomData) window._pendingBomData = {};
if (!window._pendingBomData[productCode]) window._pendingBomData[productCode] = [];
var found = false;
window._pendingBomData[productCode].forEach(function(m) {
if (m.material_code === matCode) {
m.remark = val;
found = true;
}
});
if (!found) {
window._pendingBomData[productCode].push({
material_code: matCode,
remark: val
});
}
inp.style.borderColor = '#52c41a';
setTimeout(function() { inp.style.borderColor = ''; }, 800);
}
});
});
})
.catch(function() { alert('❌ 网络错误'); });
}
// 计划预测 BOM - 生成物料订单(不依赖 _pendingItems)
function planGenerateMatOrder(productCode, materialCode, materialName) {
var input = document.querySelector('.bom-order-qty[data-material="' + materialCode + '"][data-product="' + productCode + '"]');
var qty = input ? parseInt(input.value) || 0 : 0;
if (qty <= 0) {
alert('请先填写有效的下单数量');
return;
}
if (!confirm('确认生成物料 [' + materialCode + '] 的采购订单,数量:' + qty + '?')) return;
fetch('/api/purchase/create_material_order', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
product_code: productCode,
material_code: materialCode,
material_name: materialName,
order_qty: qty
})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ 物料采购单已生成: ' + res.order_no);
} else {
alert('❌ ' + (res.msg || '生成失败'));
}
});
}
function generateMaterialOrder(productCode, materialCode) {
var input = document.querySelector('.bom-order-qty[data-material="' + materialCode + '"][data-product="' + productCode + '"]');
var qty = input ? parseInt(input.value) || 0 : 0;
if (qty <= 0) {
alert('请先填写有效的下单数量');
return;
}
if (!confirm('确认生成物料 [' + materialCode + '] 的采购订单,数量:' + qty + '?')) return;
// 查找物料名称
var item = null;
(_pendingItems || []).forEach(function(it) {
if (it.product_code === productCode) item = it;
});
var bomList = [];
if (item && item.bom_items) {
try { bomList = typeof item.bom_items === 'string' ? JSON.parse(item.bom_items) : item.bom_items; } catch(e) {}
}
var matName = '';
bomList.forEach(function(b) {
if (b.material_code === materialCode) matName = b.material_name;
});
// 调用后端生成物料采购订单
fetch('/api/purchase/create_material_order', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
product_code: productCode,
material_code: materialCode,
material_name: matName,
order_qty: qty
})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ 物料采购单已生成: ' + res.order_no);
} else {
alert('❌ ' + (res.msg || '生成失败'));
}
});
}
function updateFormalQty(productCode, val) {
var qty = parseInt(val) || 0;
fetch('/api/purchase/add_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, formal_order_qty: qty})
});
}
function pendingDetail(productCode) {
var item = null;
_pendingItems.forEach(function(i) {
if (i.product_code === productCode) item = i;
});
if (!item) {
showProductDetail(productCode);
return;
}
var old = document.getElementById('pendingDetailModal');
if (old) old.remove();
var modal = document.createElement('div');
modal.id = 'pendingDetailModal';
modal.className = 'modal-overlay';
modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:flex-start;justify-content:center;padding:40px 0;overflow-y:auto';
modal.onclick = function(e) { if (e.target === modal) {} };
var html = '<div class="modal" style="width:1500px"><div class="modal-header"><h3>📋 待下单详情</h3><span class="modal-close" onclick="document.getElementById(\'pendingDetailModal\').remove()">&times;</span></div>';
html += '<div class="modal-body" style="padding:12px 16px;max-height:70vh;overflow-y:auto">';
// 成品信息卡片
html += '<div class="pd-card" style="margin-bottom:10px">';
html += '<div class="pd-card-header"><span>🏭 成品信息</span></div>';
html += '<div class="pd-card-body" style="padding:10px 14px">';
html += '<div class="pd-fields pd-fields-3col" id="pendingProdFields">';
html += '<div style="text-align:center;padding:20px;color:var(--text-light)">加载中...</div>';
html += '</div></div></div>';
// 包材信息卡片
html += '<div class="pd-card">';
html += '<div class="pd-card-header"><span>📦 包材信息</span></div>';
html += '<div class="pd-card-body" style="padding:0;overflow-x:auto;min-height:60px">';
html += '<div id="pendingBomContent" style="text-align:center;padding:20px;color:var(--text-light)">加载中...</div>';
html += '</div></div>';
html += '</div>';
html += '<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center">';
html += '<div><button class="btn btn-primary" onclick="saveDetailBom(\'' + escHtml(productCode) + '\')">💾 保存</button></div>';
html += '<div><button class="btn" onclick="document.getElementById(\'pendingDetailModal\').remove()">关闭</button></div></div></div>';
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 = '<input type="text" class="form-input" style="width:100%;box-sizing:border-box;padding:3px 6px;font-size:13px" value="' + valHtml + '" onchange="updatePendingProdField(\'' + item.product_code + '\',\'' + f.label + '\',this.value)">';
} else if (f.readonly) {
valHtml = '<span style="font-weight:600;color:#8c8c8c">' + valHtml + '</span>';
} else {
valHtml = '<span style="font-weight:600">' + valHtml + '</span>';
}
prodFieldsHtml += '<div class="pd-row pd-row-3col"><span class="pd-label">' + f.label + '</span><span class="pd-value">' + valHtml + '</span></div>';
});
var pf = document.getElementById('pendingProdFields');
if (pf) pf.innerHTML = prodFieldsHtml;
// BOM
var container = document.getElementById('pendingBomContent');
if (!container) return;
var bom = (bomRes.code === 0 && bomRes.data && bomRes.data.bom_materials) ? bomRes.data.bom_materials : [];
// 过滤掉内料,只显示非内料包材
bom = bom.filter(function(b) { return !(b.material_name && b.material_name.indexOf('内料') !== -1); });
if (bom.length === 0) {
container.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-light)">暂无包材物料清单</div>';
return;
}
var formalQty = item.formal_order_qty || 0;
// 读取已保存的包材实际下单数量
var savedBomActuals = {};
var savedBomRemarks = {};
var savedBomManuscript = {};
if (item.bom_items) {
try {
var savedBomList = typeof item.bom_items === 'string' ? JSON.parse(item.bom_items) : item.bom_items;
savedBomList.forEach(function(s) {
if (s.material_code) {
if (s.actual_quantity !== undefined) savedBomActuals[s.material_code] = s.actual_quantity;
if (s.remark) savedBomRemarks[s.material_code] = s.remark;
if (s.change_manuscript) savedBomManuscript[s.material_code] = s.change_manuscript;
}
});
} catch(e) {}
}
var cellBorder = 'border:1px solid #d9d9d9';
var thBase = 'padding:7px 8px;' + cellBorder + ';text-align:left;white-space:nowrap';
var tdBase = 'padding:7px 8px;' + cellBorder + ';text-align:left;white-space:nowrap';
var tableHtml = '<table style="width:100%;font-size:12px;border-collapse:collapse"><thead><tr style="background:#555;color:#fff">' +
'<th style="' + thBase + '">物料编号</th>' +
'<th style="' + thBase + '">物料名称</th>' +
'<th style="' + thBase + '">用量</th>' +
'<th style="' + thBase + '">建议下单数量</th>' +
'<th style="' + thBase + '">实际下单数量</th>' +
'<th style="' + thBase + '">下单前包材</th>' +
'<th style="' + thBase + '">下单后包材</th>' +
'<th style="' + thBase + '">下单备注</th>' +
'<th style="' + thBase + '">更换稿件</th>' +
'</tr></thead><tbody>';
bom.forEach(function(b) {
var matCode = escHtml(b.material_code || '');
var qty = b.bom_qty || 0;
var orderQty = Math.ceil(formalQty * qty);
// 使用已保存的实际下单数量(包括0),没有保存则用建议值
var actualQty = b.material_code in savedBomActuals ? (savedBomActuals[b.material_code] || 0) : orderQty;
tableHtml += '<tr>' +
'<td style="' + tdBase + ';font-weight:600"><code style="font-weight:700;font-size:13px;color:#333">' + matCode + '</code></td>' +
'<td style="' + tdBase + ';font-weight:600;color:#333">' + escHtml(b.material_name || '') + '</td>' +
'<td style="' + tdBase + ';font-weight:600;color:#333">' + qty + '</td>' +
'<td style="' + tdBase + ';font-weight:600;color:#1890ff">' + orderQty + '</td>' +
'<td style="' + tdBase + '"><input type="number" class="form-input pend-detail-editable" style="width:70px;text-align:center;font-size:12px;padding:3px 4px;font-weight:600;color:#d4380d" value="' + actualQty + '" data-material="' + matCode + '" data-col="actual_qty"></td>' +
'<td style="' + tdBase + '"><input type="number" class="form-input pend-detail-editable" style="width:60px;text-align:center;font-size:12px;padding:3px 4px" value="0" data-material="' + matCode + '" data-col="pre_pack"></td>' +
'<td style="' + tdBase + '"><input type="number" class="form-input pend-detail-editable" style="width:60px;text-align:center;font-size:12px;padding:3px 4px" value="0" data-material="' + matCode + '" data-col="post_pack"></td>' +
'<td style="' + tdBase + '"><input type="text" class="form-input pend-detail-editable" style="width:100%;box-sizing:border-box;text-align:left;font-size:12px;padding:3px 4px" value="' + escHtml(savedBomRemarks[b.material_code] || '') + '" data-material="' + matCode + '" data-col="remark"></td>' +
'<td style="' + tdBase + '"><select class="form-input bom-manuscript pend-detail-editable" style="width:55px;text-align:center;font-size:12px;padding:3px 4px;font-weight:600" data-material="' + matCode + '" data-col="manuscript" onchange="this.style.color=this.value===\'是\' ? \'#f5222d\' : \'#333\'"><option value="">否</option><option value="是">是</option></select></td>' +
'</tr>';
});
tableHtml += '</tbody></table>';
container.innerHTML = tableHtml;
// 恢复已保存的更换稿件状态
container.querySelectorAll('.bom-manuscript').forEach(function(sel) {
var mc = sel.getAttribute('data-material');
if (savedBomManuscript[mc]) {
sel.value = savedBomManuscript[mc];
}
sel.style.color = sel.value === '是' ? '#f5222d' : '#333';
});
// 可编辑输入框:回车保存当前项并跳转到下一项
var editableInputs = container.querySelectorAll('.pend-detail-editable');
// 按 data-col 分组,同列内从上往下跳转
var colGroups = {};
editableInputs.forEach(function(inp) {
var col = inp.getAttribute('data-col');
if (!colGroups[col]) colGroups[col] = [];
colGroups[col].push(inp);
});
editableInputs.forEach(function(inp) {
inp.addEventListener('focus', function() { this.select(); });
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
inp.style.borderColor = '#52c41a';
setTimeout(function() { inp.style.borderColor = ''; }, 600);
var col = inp.getAttribute('data-col');
var group = colGroups[col] || [];
var idx = group.indexOf(inp);
var next = group[idx + 1];
if (next) {
next.focus();
next.select();
}
}
});
});
});
}
// 详情弹窗:保存BOM编辑数据
function saveDetailBom(productCode) {
var modal = document.getElementById('pendingDetailModal');
if (!modal) return;
var inputs = modal.querySelectorAll('.pend-detail-editable');
var materials = [];
var seen = {};
inputs.forEach(function(inp) {
var matCode = inp.getAttribute('data-material');
var col = inp.getAttribute('data-col');
var val = inp.value.trim();
if (!matCode) return;
if (!seen[matCode]) {
var row = inp.closest('tr');
var matName = '';
var bomQty = 0;
if (row) {
// 物料名称在第2个td
var nameCell = row.querySelector('td:nth-child(2)');
if (nameCell) matName = nameCell.textContent.trim();
// 用量在第3个td
var qtyCell = row.querySelector('td:nth-child(3)');
if (qtyCell) bomQty = parseInt(qtyCell.textContent.trim()) || 0;
}
seen[matCode] = {material_code: matCode, material_name: matName, bom_qty: bomQty};
materials.push(seen[matCode]);
}
if (col === 'actual_qty') seen[matCode].actual_quantity = parseInt(val) || 0;
else if (col === 'pre_pack') seen[matCode].pre_pack_qty = parseFloat(val) || 0;
else if (col === 'post_pack') seen[matCode].post_pack_qty = parseFloat(val) || 0;
else if (col === 'price') seen[matCode].unit_price = parseFloat(val) || 0;
else if (col === 'remark') seen[matCode].remark = val;
else if (col === 'manuscript') seen[matCode].change_manuscript = val;
});
// 只保存实际下单数量>0的物料
var toSave = materials.filter(function(m) { return m.actual_quantity >= 0; });
if (toSave.length === 0) {
alert('没有实际下单数量>=0的物料');
return;
}
// 从 _pendingItems 获取当前数据,避免保存时其他字段被清空
// 优先从DOM读取最新值(用户可能在表行或详情弹窗中修改过)
var curItem = null;
(_pendingItems || []).forEach(function(it) {
if (it.product_code === productCode) curItem = it;
});
// 从表格行输入框读取最新备注(最可靠来源)
var tableRemarkInput = document.querySelector('input[data-pcode="' + productCode + '"]');
var latestRemark = tableRemarkInput ? tableRemarkInput.value : (curItem ? (curItem.remark || '') : '');
// 从表格行输入框读取最新正式下单数量
// 在pendingTable中,正式下单数量的input没有data属性,只能从缓存获取
var saveData = {
product_code: productCode,
product_name: curItem ? curItem.product_name : '',
product_type: curItem ? curItem.product_type : '',
product_grade: curItem ? curItem.product_grade : '',
supplier_name: curItem ? curItem.supplier_name : '',
lead_time: curItem ? curItem.lead_time : 0,
logistics_time: curItem ? curItem.logistics_time : 0,
total_support_days: curItem ? curItem.total_support_days : 0,
procurement_follower: curItem ? curItem.procurement_follower : '',
procurement_planner: curItem ? curItem.procurement_planner : '',
platform: curItem ? curItem.platform : '',
operator: curItem ? curItem.operator : '',
formal_order_qty: curItem ? curItem.formal_order_qty : 0,
suggested_qty: curItem ? curItem.suggested_qty : 0,
actual_order_qty: curItem ? curItem.actual_order_qty : 0,
audit_price: curItem ? curItem.audit_price : 0,
remark: latestRemark,
bom_items: JSON.stringify(toSave)
};
fetch('/api/purchase/add_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(saveData)
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
if (modal) modal.remove();
loadPendingList();
showCenteredAlert('保存成功', '✅ 包材数据已保存');
} else {
alert('❌ ' + (res.msg || '保存失败'));
}
})
.catch(function() { alert('❌ 网络错误'); });
}
// 成品信息字段更新(附带所有字段,防止ON DUPLICATE KEY UPDATE清零其他字段)
window.updatePendingProdField = function(productCode, field, val) {
var curItem = null;
(_pendingItems || []).forEach(function(it) {
if (it.product_code === productCode) curItem = it;
});
var saveData = {
product_code: productCode,
product_name: curItem ? curItem.product_name : '',
product_type: curItem ? curItem.product_type : '',
product_grade: curItem ? curItem.product_grade : '',
supplier_name: curItem ? curItem.supplier_name : '',
lead_time: curItem ? curItem.lead_time : 0,
logistics_time: curItem ? curItem.logistics_time : 0,
total_support_days: curItem ? curItem.total_support_days : 0,
procurement_follower: curItem ? curItem.procurement_follower : '',
procurement_planner: curItem ? curItem.procurement_planner : '',
platform: curItem ? curItem.platform : '',
operator: curItem ? curItem.operator : '',
formal_order_qty: curItem ? curItem.formal_order_qty : 0,
suggested_qty: curItem ? curItem.suggested_qty : 0,
actual_order_qty: curItem ? curItem.actual_order_qty : 0,
audit_price: curItem ? curItem.audit_price : 0,
remark: curItem ? (curItem.remark || '') : ''
};
// 覆盖当前修改的字段
if (field === '实际下单数量') {
saveData.formal_order_qty = parseFloat(val) || 0;
} else if (field === '下单备注') {
saveData.remark = val;
}
fetch('/api/purchase/add_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(saveData)
}).then(function(r){return r.json()}).then(function(res){
if (res.code === 0) {
if (typeof loadPendingList === 'function') loadPendingList();
} else {
console.warn('更新失败:', res.msg);
}
}).catch(function(e){ console.warn('更新出错:', e); });
};
function removePending(productCode) {
showCenteredConfirm('回退确认', '确认从待下单列表移除 <strong>[' + escHtml(productCode) + ']</strong>?').then(function(confirmed) {
if (!confirmed) return;
fetch('/api/purchase/remove_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_codes: [productCode]})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
loadPendingList();
} else {
showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败'));
}
});
});
}
function updateAuditPrice(productCode, val) {
var price = parseFloat(val) || 0;
fetch('/api/purchase/add_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, audit_price: price})
});
}
// ─── 采购订单-下单备份管理 ───
var _backupItems = [];
var _backupAllItems = []; // 原始全量数据(未受搜索过滤)
var backupOrderFilter = {}; // 表头筛选状态
var backupFilterMenu = null; // 当前筛选弹窗
var backupFilterField = ''; // 当前筛选字段
// 备份列中文名→数据库字段名映射
var _backupFieldMap = {
'产品类型': 'product_type',
'货品编号': 'product_code',
'货品名称': 'product_name',
'下单人': 'created_by',
'生产单号': 'production_order_no',
'建议下单数量': 'suggested_qty',
'正式下单数量': 'formal_order_qty',
'产品分级': 'product_grade',
'供应商名称': 'supplier_name',
'下单备注': 'remark',
'货期': 'lead_time',
'物流': 'logistics_time',
'总支撑天数': 'total_support_days',
'采购跟单': 'procurement_follower',
'采购计划': 'procurement_planner',
'平台': 'platform',
'运营': 'operator',
'是否审核价格': 'audit_price',
};
var BACKUP_FILTER_COLS = [
{field:'产品类型', label:'产品类型', type:'text'},
{field:'货品编号', label:'货品编号', type:'text'},
{field:'货品名称', label:'货品名称', type:'text'},
{field:'下单人', label:'下单人', type:'text'},
{field:'生产单号', label:'生产单号', type:'text'},
{field:'建议下单数量', label:'建议下单数量', type:'number'},
{field:'正式下单数量', label:'正式下单数量', type:'number'},
{field:'产品分级', label:'产品分级', type:'text'},
{field:'供应商名称', label:'供应商名称', type:'text'},
{field:'下单备注', label:'下单备注', type:'text'},
{field:'货期', label:'货期', type:'number'},
{field:'物流', label:'物流', type:'number'},
{field:'总支撑天数', label:'总支撑天数', type:'number'},
{field:'采购跟单', label:'采购跟单', type:'text'},
{field:'采购计划', label:'采购计划', type:'text'},
{field:'平台', label:'平台', type:'text'},
{field:'运营', label:'运营', type:'text'},
{field:'是否审核价格', label:'是否审核价格', type:'number'},
];
var BACKUP_PAGE_SIZE = 50;
var backupCurrentPage = 1;
function loadPurchaseReadyPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>下单备份管理</h1>
</div>
<div class="page-flex" style="flex:1;min-height:300px">
<div class="card">
<div class="card-header" style="display:flex;align-items:center;gap:12px">
<h3>📋 已下单记录</h3>
<span style="font-size:13px;color:var(--text-light)" id="backupCount"></span>
<div style="margin-left:auto;display:flex;gap:8px">
<button class="btn btn-sm btn-outline" onclick="loadBackupList()">🔄 刷新</button>
<button class="btn btn-sm" onclick="openBackupColModal()">⚙ 字段设置</button>
</div>
</div>
<div style="display:flex;gap:8px;padding:8px 16px;background:#fafafa;border-bottom:1px solid var(--border-color);flex-wrap:wrap;align-items:center">
<input type="text" id="backupSearch" class="form-input" placeholder="🔍 编号/名称" style="width:180px" onkeydown="if(event.key==='Enter')loadBackupList()" autocomplete="off">
<select id="backupOrderer" class="form-input" style="width:150px" onchange="loadBackupList()"><option value="">👤 全部下单人</option></select>
<select id="backupPlanner" class="form-input" style="width:150px" onchange="loadBackupList()"><option value="">📋 全部采购计划</option></select>
<button class="btn btn-sm btn-primary" onclick="loadBackupList()" style="background:#1677ff;color:#fff;border:none">搜索</button>
<button class="btn btn-sm" onclick="clearBackupFilter()" style="background:#fff;color:#666;border:1px solid #d9d9d9">重置</button>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="backupTable">
<thead>
<tr>
<th style="width:40px;text-align:center">序号</th>
<th>产品类型 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('产品类型',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>货品编号 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('货品编号',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>货品名称 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('货品名称',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>下单时间</th>
<th>下单人 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('下单人',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>生产单号 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('生产单号',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>建议下单数量 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('建议下单数量',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>正式下单数量 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('正式下单数量',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>产品分级 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('产品分级',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>供应商名称 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('供应商名称',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:240px">下单备注 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('下单备注',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>货期 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('货期',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>物流 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('物流',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>总支撑天数 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('总支撑天数',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>采购跟单 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('采购跟单',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>采购计划 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('采购计划',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>平台 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('平台',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>运营 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('运营',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th>是否审核价格 <span class="bk-filt-icon" onclick="event.stopPropagation();openBackupFilterMenu('是否审核价格',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th class="col-action" style="width:130px;min-width:130px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th>
</tr>
</thead>
<tbody id="backupTbody"></tbody>
</table>
</div></div>
<div id="backupPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div>
</div>
</div>
`;
loadBackupList();
if (!window._pageReloaders) window._pageReloaders = {};
if (!window._pageDataCache) window._pageDataCache = {};
window._pageReloaders['purchase_ready'] = function() { loadBackupList(); };
window._pageDataCache['purchase_ready'] = { _ts: Date.now() };
}
function clearBackupFilter() {
['backupSearch','backupOrderer','backupPlanner'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
backupOrderFilter = {};
_syncBackupFilterIcons();
_renderFilteredBackup();
}
function loadBackupList(resetPage) {
if (resetPage !== false) backupCurrentPage = 1;
var tbody = document.getElementById('backupTbody');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="22" style="text-align:center;padding:30px;color:var(--text-light)"><div class="loading-spinner"></div></td></tr>';
fetch('/api/purchase/pending_list?status=ordered')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
_backupAllItems = res.data || []; // 保存原始全量数据
_backupItems = _backupAllItems.slice(); // 搜索过滤前先复制
invalidateOrderColumnCache('backup');
// 填充下拉选项
['backupOrderer','backupPlanner'].forEach(function(selId) {
var sel = document.getElementById(selId);
if (!sel) return;
var currentVal = sel.value;
var field = selId === 'backupOrderer' ? 'created_by' : 'procurement_planner';
var values = {};
_backupAllItems.forEach(function(it) {
var v = (it[field] || '').trim();
if (v) values[v] = true;
});
var keys = Object.keys(values).sort();
sel.innerHTML = '<option value="">' + (selId === 'backupOrderer' ? '👤 全部下单人' : '📋 全部采购计划') + '</option>';
keys.forEach(function(k) {
var selected = (k === currentVal) ? ' selected' : '';
sel.innerHTML += '<option value="' + escHtml(k) + '"' + selected + '>' + escHtml(k) + '</option>';
});
});
_syncBackupFilterIcons();
_renderFilteredBackup();
});
}
// 提取下单备份行渲染为独立函数
function _backupRowsHtml(items, startIdx) {
return items.map(function(item, idx) {
var code = escHtml(item.product_code);
return '<tr>' +
'<td style="text-align:center">' + (startIdx + idx + 1) + '</td>' +
'<td>' + escHtml(item.product_type || '-') + '</td>' +
'<td><code>' + code + '</code></td>' +
'<td>' + escHtml(item.product_name || '-') + '</td>' +
'<td>' + ((item.ordered_at || '').toString().slice(0, 19) || '-') + '</td>' +
'<td>' + escHtml(item.created_by || '-') + '</td>' +
'<td>' + escHtml(item.production_order_no || '-') + '</td>' +
'<td>' + (item.suggested_qty || 0) + '</td>' +
'<td>' + (item.formal_order_qty || 0) + '</td>' +
'<td>' + escHtml(item.product_grade || '-') + '</td>' +
'<td>' + escHtml(item.supplier_name || '-') + '</td>' +
'<td>' + escHtml(item.remark || '-') + '</td>' +
'<td>' + (item.lead_time || 0) + '</td>' +
'<td>' + (item.logistics_time || 0) + '</td>' +
'<td>' + (item.total_support_days || 0) + '</td>' +
'<td>' + renderNameBadges(item.procurement_follower) + '</td>' +
'<td>' + renderNameBadges(item.procurement_planner) + '</td>' +
'<td>' + escHtml(item.platform || '-') + '</td>' +
'<td>' + renderNameBadges(item.operator) + '</td>' +
'<td>' + (item.audit_price || 0) + '</td>' +
'<td style="position:sticky;right:0;background:var(--card-bg,#fff);border-left:1px solid var(--border);white-space:nowrap">' +
'<button class="btn btn-sm btn-outline" onclick="viewBackupDetail(\'' + code + '\')" title="详情" style="font-size:11px">详情</button> ' +
'<button class="btn btn-sm btn-outline-danger" onclick="showBackupRollbackConfirm(\'' + code + '\')" title="回退" style="font-size:11px">⏪ 回退</button> ' +
'<button class="btn btn-sm btn-outline-danger" onclick="deleteBackup(\'' + code + '\')" title="删除" style="font-size:11px">删除</button>' +
'</td>' +
'</tr>';
}).join('');
}
function _applyBackupSearch(items) {
var kw = (document.getElementById('backupSearch')?.value || '').trim().toLowerCase();
var orderer = (document.getElementById('backupOrderer')?.value || '').trim().toLowerCase();
var planner = (document.getElementById('backupPlanner')?.value || '').trim().toLowerCase();
if (!kw && !orderer && !planner) return items;
return items.filter(function(it) {
if (kw) {
var name = (it.product_name || '').toLowerCase();
var code = (it.product_code || '').toLowerCase();
if (!name.includes(kw) && !code.includes(kw)) return false;
}
if (orderer && !(it.created_by || '').toLowerCase().includes(orderer)) return false;
if (planner && !(it.procurement_planner || '').toLowerCase().includes(planner)) return false;
return true;
});
}
function _applyBackupHeaderFilter(items) {
var keys = Object.keys(backupOrderFilter);
if (keys.length === 0) return items;
return items.filter(function(item) {
return keys.every(function(f) {
var fp = backupOrderFilter[f];
if (!fp) return true;
var v = String(item[_backupFieldMap[f] || f] || '');
if (fp.type === 'search') {
return v.toLowerCase().indexOf((fp.value || '').toLowerCase()) >= 0;
}
if (fp.type === 'list') {
return fp.values.indexOf(v) >= 0;
}
if (fp.type === 'num') {
var n = parseFloat(v);
var tv = parseFloat(fp.val);
if (isNaN(n) || isNaN(tv)) return false;
switch (fp.cond) {
case 'eq': return n === tv;
case 'neq': return n !== tv;
case 'gt': return n > tv;
case 'gte': return n >= tv;
case 'lt': return n < tv;
case 'lte': return n <= tv;
}
}
return true;
});
});
}
function _renderFilteredBackup() {
var items = _backupAllItems.slice(); // 从原始全量数据开始
items = _applyBackupSearch(items); // 先应用搜索栏筛选
items = _applyBackupHeaderFilter(items); // 再应用表头筛选
_backupItems = items;
var countEl = document.getElementById('backupCount');
if (countEl) countEl.textContent = '共 ' + items.length + ' / ' + _backupAllItems.length + ' 条';
var tbody = document.getElementById('backupTbody');
if (!tbody) return;
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="22" style="text-align:center;padding:60px;color:var(--text-light)">暂无匹配数据</td></tr>';
return;
}
var total = items.length;
var totalPages = Math.ceil(total / BACKUP_PAGE_SIZE);
if (backupCurrentPage > totalPages) backupCurrentPage = totalPages;
if (backupCurrentPage < 1) backupCurrentPage = 1;
var startIdx = (backupCurrentPage - 1) * BACKUP_PAGE_SIZE;
var pageItems = items.slice(startIdx, startIdx + BACKUP_PAGE_SIZE);
tbody.innerHTML = _backupRowsHtml(pageItems, startIdx);
var pel = document.getElementById('backupPagination');
if (pel && typeof renderPaginationWithSize === 'function') {
renderPaginationWithSize(pel, backupCurrentPage, total, BACKUP_PAGE_SIZE, 'loadBackupListPage', 'BACKUP_PAGE_SIZE');
}
}
// ─── 下单备份表头筛选 ───
function _backupGetColType(field) {
for (var i = 0; i < BACKUP_FILTER_COLS.length; i++) {
if (BACKUP_FILTER_COLS[i].field === field) return BACKUP_FILTER_COLS[i].type;
}
return 'text';
}
function openBackupFilterMenu(field, event) {
event = event || window.event;
event.stopPropagation();
if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; }
backupFilterField = field;
var th = event.currentTarget;
while (th && th.tagName !== 'TH') th = th.parentNode;
var rect = th.getBoundingClientRect();
var label = field;
var colType = _backupGetColType(field);
if (colType === 'number') {
_buildBackupNumericFilter(field, label, rect);
} else {
var cacheKey = 'backup:' + field;
var cached = window._orderColumnCache && window._orderColumnCache[cacheKey];
if (cached) {
_buildBackupListFilter(field, label, rect, cached);
} else {
_buildBackupFilterShell(field, label, rect);
var vals = [];
var seen = {};
// 从搜索过滤后的数据提取唯一值(联动搜索条件)
var searchFiltered = _applyBackupSearch(_backupAllItems);
searchFiltered.forEach(function(item) {
var v = String(item[_backupFieldMap[field] || field] || '').trim();
if (v && !seen[v]) { seen[v] = 1; vals.push(v); }
});
vals.sort();
window._orderColumnCache = window._orderColumnCache || {};
window._orderColumnCache[cacheKey] = vals;
_populateBackupFilterList(field, vals);
}
}
if (backupFilterMenu) {
var handler = function(e) {
if (backupFilterMenu && !backupFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) {
backupFilterMenu.remove(); backupFilterMenu = null;
document.removeEventListener('click', handler);
}
};
setTimeout(function() { document.addEventListener('click', handler); }, 10);
setTimeout(function() {
var inp = document.getElementById('backupFilterSearch');
if (inp) inp.focus();
}, 150);
}
}
function _buildBackupFilterShell(field, label, rect) {
var menu = document.createElement('div');
menu.id = 'backupFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="backupFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="backupFilterSearch(this.value)" onkeydown="if(event.key===\'Enter\')applyBackupFilter()"></div>';
html += '<div id="backupFilterList" style="overflow-y:auto;flex:1;max-height:200px;text-align:center;padding:20px;color:#999">加载中...</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyBackupFilter()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="clearBackupHeaderFilter()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
backupFilterMenu = menu;
}
function _buildBackupListFilter(field, label, rect, values) {
var menu = document.createElement('div');
menu.id = 'backupFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var sel = backupOrderFilter[field] || {};
allChecked = true;
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="backupFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="backupFilterSearch(this.value)" onkeydown="if(event.key===\'Enter\')applyBackupFilter()"></div>';
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="backupFilterSelectAll" checked onchange="backupFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="clearBackupHeaderFilter()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
html += '<div id="backupFilterList" style="overflow-y:auto;flex:1;max-height:200px;border-top:1px solid #f0f0f0;padding-top:4px">';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="backup-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="backupFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
html += '</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyBackupFilter()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="clearBackupHeaderFilter()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
backupFilterMenu = menu;
}
function _buildBackupNumericFilter(field, label, rect) {
var menu = document.createElement('div');
menu.id = 'backupFilterMenu';
var current = backupOrderFilter[field] || {};
var cond = current.cond || '';
var val = current.val || '';
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><select id="backupNumCond" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px">';
html += '<option value="">-- 选择条件 --</option>';
html += '<option value="eq"' + (cond === 'eq' ? ' selected' : '') + '>等于</option>';
html += '<option value="neq"' + (cond === 'neq' ? ' selected' : '') + '>不等于</option>';
html += '<option value="gt"' + (cond === 'gt' ? ' selected' : '') + '>大于</option>';
html += '<option value="gte"' + (cond === 'gte' ? ' selected' : '') + '>大于等于</option>';
html += '<option value="lt"' + (cond === 'lt' ? ' selected' : '') + '>小于</option>';
html += '<option value="lte"' + (cond === 'lte' ? ' selected' : '') + '>小于等于</option>';
html += '</select></div>';
html += '<div style="margin-bottom:6px"><input type="text" id="backupNumVal" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="输入数值..." value="' + escHtml(val) + '" autocomplete="off" onkeydown="if(event.key===\'Enter\')applyBackupNumFilter()"></div>';
html += '<div style="border-top:1px solid #f0f0f0;padding-top:6px;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm" onclick="clearBackupHeaderFilter()" style="font-size:12px;padding:4px 10px">清除</button>';
html += '<button class="btn btn-sm btn-primary" onclick="applyBackupNumFilter()" style="font-size:12px;padding:4px 12px">确定</button></div>';
menu.innerHTML = html;
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:200px;font-size:13px';
document.body.appendChild(menu);
backupFilterMenu = menu;
}
function _populateBackupFilterList(field, values) {
var listEl = document.getElementById('backupFilterList');
if (!listEl) return;
var sel = backupOrderFilter[field] || {};
var html = '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="backupFilterSelectAll" checked onchange="backupFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="clearBackupHeaderFilter()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="backup-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="backupFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
listEl.innerHTML = html;
}
function backupFilterSearch(q) {
var items = document.querySelectorAll('#backupFilterList label');
q = (q || '').toLowerCase();
items.forEach(function(item) {
item.style.display = item.textContent.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
});
}
function backupFilterToggleAll(checked) {
document.querySelectorAll('#backupFilterList .backup-filter-cb').forEach(function(cb) { cb.checked = checked; });
}
function backupFilterCbChange() {
var all = document.querySelectorAll('#backupFilterList .backup-filter-cb');
var checked = document.querySelectorAll('#backupFilterList .backup-filter-cb:checked');
var sa = document.getElementById('backupFilterSelectAll');
if (sa) sa.checked = checked.length === all.length;
}
function applyBackupFilter() {
var field = backupFilterField;
var searchInput = document.getElementById('backupFilterSearch');
var searchText = searchInput ? searchInput.value.trim() : '';
if (searchText) {
backupOrderFilter[field] = {type: 'search', value: searchText};
} else {
var cbs = document.querySelectorAll('#backupFilterList .backup-filter-cb:checked');
var checked = [];
cbs.forEach(function(cb) { checked.push(cb.value); });
var allCbs = document.querySelectorAll('#backupFilterList .backup-filter-cb');
if (checked.length === 0 || checked.length === allCbs.length) {
delete backupOrderFilter[field];
} else {
backupOrderFilter[field] = {type: 'list', values: checked};
}
}
if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; }
_syncBackupFilterIcons();
_renderFilteredBackup();
}
function applyBackupNumFilter() {
var field = backupFilterField;
var cond = document.getElementById('backupNumCond')?.value || '';
var val = document.getElementById('backupNumVal')?.value || '';
if (!cond || !val) { alert('请选择条件并输入数值'); return; }
backupOrderFilter[field] = {type: 'num', cond: cond, val: val};
if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; }
_syncBackupFilterIcons();
_renderFilteredBackup();
}
function clearBackupHeaderFilter() {
delete backupOrderFilter[backupFilterField];
if (backupFilterMenu) { backupFilterMenu.remove(); backupFilterMenu = null; }
_syncBackupFilterIcons();
_renderFilteredBackup();
}
function _syncBackupFilterIcons() {
var icons = document.querySelectorAll('#backupTable .bk-filt-icon');
icons.forEach(function(icon) {
var field = icon.parentNode.textContent.trim().replace(/▾/, '').trim();
var hasFilter = backupOrderFilter[field];
icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)';
icon.style.fontWeight = hasFilter ? 'bold' : 'normal';
});
}
function loadBackupListPage(page) {
backupCurrentPage = page;
_renderFilteredBackup();
}
function loadBackupListPage(page) {
backupCurrentPage = page;
loadBackupList(false);
}
// ─── 下单备份管理:详情+删除 ───
window._backupItems = [];
window.viewBackupDetail = function(code) {
var item = null;
(_backupItems || []).forEach(function(it) {
if (it.product_code === code) item = it;
});
if (!item) { alert('未找到数据'); return; }
// 解析包材数据
var bomList = [];
try {
var raw = item.bom_items;
if (raw) {
bomList = typeof raw === 'string' ? JSON.parse(raw) : (Array.isArray(raw) ? raw : []);
}
} catch(e) { bomList = []; }
var html = '<div style="padding:24px 24px 8px">';
html += '<h3 style="margin-bottom:16px;font-size:17px">📋 下单详情 - ' + escHtml(code) + '</h3>';
// ─── 板块1:成品信息 ───
html += '<h4 style="font-size:14px;color:#333;margin:0 0 10px 0;padding:8px 12px;background:#e6f7ff;border-radius:6px">📦 成品信息</h4>';
// 第一列字段
var col1Fields = [
['产品类型', item.product_type],
['货品编号', item.product_code],
['货品名称', item.product_name],
['供应商名称', item.supplier_name],
['采购计划', item.procurement_planner],
['采购跟单', item.procurement_follower]
];
// 第二列字段
var col2Fields = [
['建议下单数量', item.suggested_qty],
['实际下单数量', item.actual_order_qty],
['下单人', item.created_by],
['下单时间', (item.ordered_at || '').toString().slice(0, 19)],
['下单备注', item.remark],
['生产单号', item.production_order_no || '']
];
html += '<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:16px">';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed">';
for (var g = 0; g < 6; g++) {
var f1 = col1Fields[g];
var f2 = col2Fields[g];
html += '<tr>';
// 第一列
html += '<th style="padding:10px 8px;background:#e8e8e8;color:#555;font-weight:600;text-align:left;border:1px solid #d9d9d9;white-space:nowrap;width:15%">' + f1[0] + '</th>';
html += '<td style="padding:10px 8px;font-weight:600;border:1px solid var(--border);width:35%;word-break:break-all">' + escHtml(String(f1[1] || '-')) + '</td>';
// 第二列
var isProdNo = f2[0] === '生产单号';
html += '<th style="padding:10px 8px;background:#e8e8e8;color:#555;font-weight:600;text-align:left;border:1px solid #d9d9d9;white-space:nowrap;width:15%">' + f2[0] + '</th>';
if (isProdNo) {
html += '<td style="padding:6px 8px;font-weight:600;border:1px solid var(--border);width:35%"><input type="text" id="backupProdNo_' + escHtml(code) + '" class="form-input" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + escHtml(String(f2[1] || '')) + '" onkeydown="if(event.key===\'Enter\'){event.preventDefault();saveBackupProdNo(\'' + escHtml(code) + '\');}" placeholder="输入单号,回车保存" autocomplete="off"></td>';
} else {
html += '<td style="padding:10px 8px;font-weight:600;border:1px solid var(--border);width:35%;word-break:break-all">' + escHtml(String(f2[1] || '-')) + '</td>';
}
html += '</tr>';
}
html += '</table></div>';
// ─── 板块2:包材信息 ───
html += '<h4 style="font-size:14px;color:#333;margin:12px 0 10px 0;padding:8px 12px;background:#fff7e6;border-radius:6px">📦 包材信息</h4>';
if (bomList.length > 0) {
html += '<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden">';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
html += '<thead><tr style="background:#555;color:#fff">';
html += '<th style="padding:8px 12px;border:1px solid #666;text-align:left">物料编号</th>';
html += '<th style="padding:8px 12px;border:1px solid #666;text-align:left">物料名称</th>';
html += '<th style="padding:8px 12px;border:1px solid #666;text-align:left">下单数量</th>';
html += '<th style="padding:8px 12px;border:1px solid #666;text-align:left">备注</th>';
html += '<th style="padding:8px 12px;border:1px solid #666;text-align:left">更换稿件</th><th style="padding:8px 12px;border:1px solid #666;text-align:left">采购单号</th>';
html += '</tr></thead><tbody>';
bomList.forEach(function(b) {
html += '<tr>';
html += '<td style="padding:8px 12px;border:1px solid #eee"><code>' + escHtml(b.material_code || '-') + '</code></td>';
html += '<td style="padding:8px 12px;border:1px solid #eee">' + escHtml(b.material_name || '-') + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #eee">' + (b.actual_quantity || 0) + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #eee">' + escHtml(b.remark || '-') + '</td>';
html += '<td style="padding:8px 12px;border:1px solid #eee">' + (b.change_manuscript || '-') + '</td>';
html += '<td style="padding:2px 8px;border:1px solid #eee"><input type="text" class="form-input backup-purchase-no" style="width:130px;box-sizing:border-box;padding:3px 4px;font-size:12px" value="' + escHtml(b.purchase_order_no || '') + '" data-pcode="' + escHtml(code) + '" data-matcode="' + escHtml(b.material_code) + '" onkeydown="if(event.key===\'Enter\'){event.preventDefault();saveBackupMatPurchaseNo(this)}" placeholder="单号,回车保存" autocomplete="off"></td>'
html += '</tr>';
});
html += '</tbody></table></div>';
} else {
html += '<div style="padding:20px;text-align:center;color:var(--text-light,#999);border:1px dashed #d9d9d9;border-radius:8px">暂无包材数据</div>';
}
html += '</div>';
html += '<div style="padding:16px 24px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">';
html += '<div style="font-size:12px;color:#999">修改后点击保存按钮可同时保存生产单号和采购单号</div>';
html += '<div style="display:flex;gap:10px">';
html += '<button class="btn btn-primary" onclick="saveBackupAll(\'' + code + '\',this)">💾 保存</button>';
html += '<button class="btn" onclick="this.closest(\'.modal-overlay\').remove()">关闭</button></div></div>';
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = '<div style="background:#fff;border-radius:12px;width:1200px;max-width:94vw;max-height:85vh;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' + html + '</div>';
document.body.appendChild(overlay);
};
// 包材生产单号回车保存
window.backupProdNoKeydown = function(e, input, materialCode) {
if (e.key === 'Enter') {
e.preventDefault();
var val = input.value.trim();
// infer product_code from the modal/overlay
var overlay = input.closest('.modal-overlay');
if (!overlay) return;
var codeEl = overlay.querySelector('h3');
if (!codeEl) return;
var match = codeEl.textContent.match(/\[(.+?)\]/);
var productCode = match ? match[1] : '';
if (!productCode) return;
fetch('/api/purchase/backup/update_mat_prod_no', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, material_code: materialCode, production_order_no: val})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) showCenteredAlert('保存失败', res.msg || '保存失败');
});
}
};
// 保存成品生产单号
window.saveBackupProdNo = function(code) {
var inp = document.getElementById('backupProdNo_' + code);
if (!inp) return;
var val = inp.value.trim();
fetch('/api/purchase/backup/update_prod_no', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code, production_order_no: val})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
inp.blur();
// 更新缓存数据
(_backupItems || []).forEach(function(it) {
if (it.product_code === code) it.production_order_no = val;
});
// 同步更新表格中的生产单号列
var tbody = document.getElementById('backupTbody');
if (tbody) {
tbody.querySelectorAll('tr').forEach(function(tr) {
var codeEl = tr.querySelector('code');
if (codeEl && codeEl.textContent.trim() === code) {
var tds = tr.querySelectorAll('td');
// 生产单号是第7列(序号0,产品类型1,货品编号2,货品名称3,下单时间4,下单人5,生产单号6)
if (tds.length > 6) {
tds[6].textContent = val || '-';
}
}
});
}
showCenteredAlert('保存成功', '✅ 生产单号已保存');
} else {
inp.style.borderColor = '#f5222d';
setTimeout(function(){ inp.style.borderColor = ''; }, 1500);
}
});
};
// 保存包材采购单号
window.saveBackupMatPurchaseNo = function(input) {
if (!input) return;
var productCode = input.getAttribute('data-pcode');
var materialCode = input.getAttribute('data-matcode');
if (!productCode || !materialCode) return;
var val = input.value.trim();
fetch('/api/purchase/backup/update_mat_purchase_no', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, material_code: materialCode, purchase_order_no: val})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
input.blur();
// 同步更新前端缓存
(_backupItems || []).forEach(function(it) {
if (it.product_code === productCode && it.bom_items) {
try {
var bom = typeof it.bom_items === 'string' ? JSON.parse(it.bom_items) : it.bom_items;
if (Array.isArray(bom)) {
bom.forEach(function(b) {
if (b.material_code === materialCode) {
b.purchase_order_no = val;
}
});
it.bom_items = JSON.stringify(bom);
}
} catch(e) {}
}
});
// 后台刷新列表数据
if (typeof loadBackupList === 'function') {
loadBackupList();
}
showCenteredAlert('保存成功', '✅ 采购单号已保存');
} else {
input.style.borderColor = '#f5222d';
setTimeout(function(){ input.style.borderColor = ''; }, 1500);
}
});
};
// 保存生产单号+采购单号(统一保存按钮)
window.saveBackupAll = function(code, saveBtn) {
if (!code) return;
if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '⏳ 保存中...'; }
// 1. 收集生产单号
var prodNoInp = document.getElementById('backupProdNo_' + code);
// 2. 收集所有包材采购单号(含空值,用于清空已有数据)
var orderInputs = [];
var overlay = saveBtn ? saveBtn.closest('.modal-overlay') : null;
if (overlay) {
overlay.querySelectorAll('.backup-purchase-no').forEach(function(inp) {
var pc = inp.getAttribute('data-pcode');
var mc = inp.getAttribute('data-matcode');
if (pc && mc) {
orderInputs.push({input: inp, productCode: pc, materialCode: mc, val: inp.value.trim()});
}
});
}
// 3. 依次保存
var promises = [];
// 保存生产单号
if (prodNoInp) {
var prodVal = prodNoInp.value.trim();
promises.push(
fetch('/api/purchase/backup/update_prod_no', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code, production_order_no: prodVal})
}).then(function(r){return r.json()})
);
}
// 保存采购单号
orderInputs.forEach(function(item) {
promises.push(
fetch('/api/purchase/backup/update_mat_purchase_no', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: item.productCode, material_code: item.materialCode, purchase_order_no: item.val})
}).then(function(r){return r.json()})
);
});
// 4. 全部完成后刷新缓存并提示
Promise.all(promises).then(function(results) {
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; }
var hasError = results.some(function(r) { return r.code !== 0; });
if (hasError) {
showCenteredAlert('保存失败', '❌ 部分数据保存失败,请重试');
} else {
// ---- 同步更新前端缓存,确保关闭后再打开看到新数据 ----
var newProdNo = prodNoInp ? prodNoInp.value.trim() : '';
(_backupItems || []).forEach(function(it) {
if (it.product_code === code) {
// 更新生产单号
it.production_order_no = newProdNo;
// 更新包材采购单号(bom_items JSON内)
if (it.bom_items) {
try {
var bom = typeof it.bom_items === 'string' ? JSON.parse(it.bom_items) : it.bom_items;
if (Array.isArray(bom)) {
orderInputs.forEach(function(oi) {
bom.forEach(function(b) {
if (b.material_code === oi.materialCode) {
b.purchase_order_no = oi.val;
}
});
});
it.bom_items = JSON.stringify(bom);
}
} catch(e) {}
}
}
});
// 同步更新表格中的生产单号列
var tbody = document.getElementById('backupTbody');
if (tbody) {
tbody.querySelectorAll('tr').forEach(function(tr) {
var codeEl = tr.querySelector('code');
if (codeEl && codeEl.textContent.trim() === code) {
var tds = tr.querySelectorAll('td');
if (tds.length > 6) {
tds[6].textContent = newProdNo || '-';
}
}
});
}
// 后台刷新列表数据(用户下次查看时拉取最新)
if (typeof loadBackupList === 'function') {
loadBackupList();
}
showCenteredAlert('保存成功', '✅ 生产单号、采购单号已保存');
}
}).catch(function() {
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '💾 保存'; }
showCenteredAlert('保存失败', '❌ 网络错误,请重试');
});
};
// 下单回退:确认弹窗 + 调用后端
window.showBackupRollbackConfirm = function(code) {
showCenteredConfirm('回退确认',
'确认将 <strong>[' + escHtml(code) + ']</strong> 回退到待下单列表?<br>' +
'将从国内/海外订单、包材订单中同步删除对应数据。'
).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('删除确认', '确认删除此记录?<br><strong>编码: ' + escHtml(code) + '</strong>').then(function(confirmed) {
if (!confirmed) return;
fetch('/api/purchase/remove_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_codes: [code]})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
loadBackupList();
} else {
showCenteredAlert('删除失败', '删除失败: ' + (res.msg || ''));
}
});
});
};
// ─── 采购订单-订单管理(国内/海外/包材)───
// ──────────── 国内订单管理 ────────────
var DOMESTIC_PAGE_SIZE = 100;
var domesticOrderPage = 1;
var domesticSearch = '';
var domesticFilterStatus = '';
var domesticFilterSupplier = '';
var domesticFilterOrderer = '';
var domesticOrderFilter = {}; // 列筛选状态 {field:{type:'list'|'num', ...}}
var domesticOrderFilterActive = {}; // 高亮标记
var domesticOrderFilterMenu = null; // 当前打开的筛选弹窗
var domesticOrderFilterField = ''; // 当前筛选字段
// ─── 列筛选值缓存(避免每次打开筛选都查数据库)───
window._orderColumnCache = window._orderColumnCache || {};
window._planColumnCache = window._planColumnCache || {};
// ─── 后台预拉取订单表所有列的筛选值 ───
function prefetchOrderColumnValues(type) {
var cols = _getOrderColumns(type);
var apiMap = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'};
cols.forEach(function(col) {
var cacheKey = type + ':' + col.field;
if (window._orderColumnCache[cacheKey]) return;
var colType = _getOrderColType(type, col.field);
if (colType === 'number') return;
fetch(apiMap[type] + '?field=' + encodeURIComponent(col.field))
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data && res.data.length > 0) {
window._orderColumnCache[cacheKey] = res.data;
}
})
.catch(function() { /* 静默失败 */ });
});
}
// ─── 后台预拉取计划页所有列的筛选值 ───
function prefetchPlanColumnValues() {
var view = planActiveView || 'default';
// 品牌列
var brandKey = 'plan:brand:' + view;
if (!window._planColumnCache[brandKey]) {
var brandUrl = '/api/planning/brands' + (view !== 'default' ? '?view=' + encodeURIComponent(view) : '');
fetch(brandUrl)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) {
window._planColumnCache[brandKey] = res.data;
}
})
.catch(function() { /* 静默失败 */ });
}
// 非预定义非品牌非数字的非排除列
var productGradeFields = ['产品分级'];
PLAN_COLUMNS.forEach(function(c) {
if (PLAN_FILTER_OPTIONS[c.field]) return; // 预定义,skip
if (c.field === 'brand') return; // 品牌已处理
if (SORTABLE_FIELDS[c.field] && !productGradeFields.includes(c.field)) return; // 数字列
if (PLAN_FILTER_EXCLUDE_FIELDS[c.field]) return; // 排除字段
var key = 'plan:field:' + c.field + ':' + view;
if (window._planColumnCache[key]) return;
var url = '/api/planning/column-values?field=' + encodeURIComponent(c.field) +
(view !== 'default' ? '&view=' + encodeURIComponent(view) : '');
fetch(url)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) {
window._planColumnCache[key] = res.data;
}
})
.catch(function() { /* 静默失败 */ });
});
}
// ─── 清除筛选值缓存(数据变更后调用)───
function invalidateOrderColumnCache(type) {
if (!type) {
window._orderColumnCache = {};
window._planColumnCache = {};
return;
}
var keys = Object.keys(window._orderColumnCache);
keys.forEach(function(k) {
if (k.indexOf(type + ':') === 0) delete window._orderColumnCache[k];
});
}
var domesticFrozen = localStorage.getItem('domesticFrozen') !== 'false';
var domesticFollowers = []; // 采购跟单人员列表
// 冻结列索引列表(默认冻结前6列),存JSON数组
// 冻结列默认值
var domesticFrozenFields = [0,1,2];
var domesticFrozen = true;
// ─── SKU详情弹窗(点击编码/SKU) ───
window.showSkuInfoModal = function(el, title) {
var code = el.textContent.trim();
if (!code) return;
// 显示加载中的弹窗
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center;overflow-y:auto;padding:20px';
overlay.onclick = function(e) { if (e.target === overlay) {} };
overlay.innerHTML = '<div class="modal" style="width:500px"><div class="modal-body" style="text-align:center;padding:40px"><div class="spinner"></div><p style="color:#999;margin-top:8px">加载中...</p></div></div>';
document.body.appendChild(overlay);
// 通过API获取商品详情
fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(code))
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.data) {
overlay.innerHTML = '<div class="modal" style="width:500px;border-radius:12px;overflow:hidden"><div class="modal-body" style="text-align:center;padding:40px;color:#999">未找到商品信息</div><div class="modal-footer" style="padding:10px 20px;border-top:1px solid #eee;text-align:right"><button class="btn" onclick="this.closest(\'.modal-overlay\').remove()">关闭</button></div></div>';
return;
}
var d = res.data && res.data.product || {};
var fields = [];
if (d.product_code) fields.push(['货品编号', d.product_code]);
if (d.product_name) fields.push(['货品名称', d.product_name]);
if (d.brand) fields.push(['品牌', d.brand]);
if (d.product_type) fields.push(['产品类型', d.product_type]);
if (d.product_grade) fields.push(['产品分级', d.product_grade]);
if (d.cost_price) fields.push(['成本价', d.cost_price]);
if (d.supplier_name) fields.push(['供应商', d.supplier_name]);
if (d.procurement_follower) fields.push(['采购跟单', d.procurement_follower]);
if (d.procurement_planner) fields.push(['采购计划', d.procurement_planner]);
if (d.developer) fields.push(['开发专员', d.developer]);
if (d.operator) fields.push(['运营专员', d.operator]);
if (d.platform) fields.push(['平台', d.platform]);
if (d.lead_time) fields.push(['生产货期', d.lead_time + '天']);
if (d.logistics_time) fields.push(['物流时间', d.logistics_time + '天']);
if (d.min_order_qty) fields.push(['最低起订量', d.min_order_qty]);
if (d.created_at) fields.push(['创建时间', d.created_at]);
var bomCount = (res.data.bom_materials || []).length;
if (bomCount > 0) fields.push(['关联包材', bomCount + ' 种']);
// 调试:记录API返回
if (typeof console !== 'undefined') {
var _debugTransit = res.data.orders_in_transit || [];
console.log('[SKU] ' + code + ' 在途订单: ' + _debugTransit.length + ' 条');
}
// 构建弹窗HTML
var html = '<div class="modal" style="width:650px;max-height:85vh;display:flex;flex-direction:column;border-radius:12px;overflow:hidden">';
html += '<div class="modal-header" style="padding:14px 20px;border-bottom:1px solid #eee;display:flex;align-items:center;justify-content:space-between;background:#fafafa">';
html += '<h3 style="margin:0;font-size:16px">📋 ' + escHtml(code) + '</h3>';
html += '<span class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()" style="font-size:22px;cursor:pointer;color:#999">&times;</span>';
html += '</div><div class="modal-body" style="padding:16px 20px;overflow:auto;flex:1">';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
fields.forEach(function(f, idx) {
var k = f[0], v = f[1] !== undefined && f[1] !== null ? String(f[1]) : '-';
if (idx % 2 === 0) html += '<tr>';
html += '<td style="padding:7px 10px;border:1px solid #eee;font-size:12px;color:#888;background:#fafafa;white-space:nowrap;width:150px">' + escHtml(k) + '</td>';
html += '<td style="padding:7px 10px;border:1px solid #eee;color:#333;word-break:break-all">' + escHtml(v) + '</td>';
if (idx % 2 === 1) html += '</tr>';
});
if (fields.length % 2 === 1) html += '<td style="border:1px solid #eee"></td><td style="border:1px solid #eee"></td></tr>';
html += '</table>';
// 订单在途列表
var transitOrders = res.data.orders_in_transit || [];
html += '<div style="margin-top:16px">';
html += '<div style="font-size:14px;font-weight:600;color:#333;margin-bottom:8px">📦 订单在途 (' + transitOrders.length + ')</div>';
html += '<table style="width:100%;border-collapse:collapse;font-size:12px">';
html += '<thead><tr style="background:#e6f7ff">';
html += '<th style="padding:5px 8px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">下单时间</th>';
html += '<th style="padding:5px 8px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">编号</th>';
html += '<th style="padding:5px 8px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">名称</th>';
html += '<th style="padding:5px 8px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">下单数量</th>';
html += '<th style="padding:5px 8px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">采购跟单</th>';
html += '<th style="padding:5px 8px;border:1px solid #d9d9d9;text-align:center;white-space:nowrap">采购计划</th>';
html += '</tr></thead><tbody>';
if (transitOrders.length > 0) {
transitOrders.forEach(function(o) {
html += '<tr>';
html += '<td style="padding:4px 8px;border:1px solid #eee;text-align:center;white-space:nowrap">' + escHtml(o['下单时间'] || '-') + '</td>';
html += '<td style="padding:4px 8px;border:1px solid #eee;text-align:center"><code style="font-size:11px">' + escHtml(o['编码'] || '') + '</code></td>';
html += '<td style="padding:4px 8px;border:1px solid #eee;text-align:center;max-width:180px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(o['名称'] || '') + '">' + escHtml(o['名称'] || '-') + '</td>';
html += '<td style="padding:4px 8px;border:1px solid #eee;text-align:center;font-weight:600">' + (parseInt(o['下单数量']) || 0).toLocaleString() + '</td>';
html += '<td style="padding:4px 8px;border:1px solid #eee;text-align:center">' + escHtml(o['采购跟单'] || '-') + '</td>';
html += '<td style="padding:4px 8px;border:1px solid #eee;text-align:center">' + escHtml(o['采购计划'] || '-') + '</td>';
html += '</tr>';
});
} else {
html += '<tr><td style="padding:4px 8px;border:1px solid #eee;text-align:center;color:#ccc">-</td><td style="padding:4px 8px;border:1px solid #eee;text-align:center;color:#ccc">-</td><td style="padding:4px 8px;border:1px solid #eee;text-align:center;color:#ccc">-</td><td style="padding:4px 8px;border:1px solid #eee;text-align:center;color:#ccc">0</td><td style="padding:4px 8px;border:1px solid #eee;text-align:center;color:#ccc">-</td><td style="padding:4px 8px;border:1px solid #eee;text-align:center;color:#ccc">-</td></tr>';
}
html += '</tbody></table></div>';
html += '</div>';
html += '<div class="modal-footer" style="padding:10px 20px;border-top:1px solid #eee;text-align:right;background:#fafafa">';
html += '<button class="btn" onclick="this.closest(\'.modal-overlay\').remove()">关闭</button></div></div>';
overlay.innerHTML = html;
})
.catch(function() {
overlay.innerHTML = '<div class="modal" style="width:500px;border-radius:12px;overflow:hidden"><div class="modal-body" style="text-align:center;padding:40px;color:red">加载失败</div><div class="modal-footer" style="padding:10px 20px;border-top:1px solid #eee;text-align:right"><button class="btn" onclick="this.closest(\'.modal-overlay\').remove()">关闭</button></div></div>';
});
}
// ─── 订单详情弹窗(通用) ───
function showOrderDetail(encodedStr, title) {
// 兼容两种调用方式:URI编码的行数据 或 直接行对象
var row;
if (typeof encodedStr === 'string') {
try { row = JSON.parse(decodeURIComponent(encodedStr)); } catch(e) { row = JSON.parse(encodedStr); }
} else {
row = encodedStr;
}
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center';
overlay.onclick = function(e) { if (e.target === overlay) {} };
// 字段配置:label, dbField, editable
var col1, col2, apiUrl, refreshFn;
if (title && title.indexOf('海外') !== -1) {
apiUrl = '/api/overseas/orders/update';
refreshFn = 'overseasLoadOrders';
col1 = [
{label:'产品类型', field:'product_type', editable:false},
{label:'货品编号', field:'产品编码', editable:false},
{label:'供应商名称', field:'供应商', editable:false},
{label:'计划下单时间', field:'计划下单时间', editable:false},
{label:'单价', field:'单价', editable:true},
{label:'采购跟单', field:'采购员', editable:true},
{label:'订单所属平台', field:'platform', editable:true},
{label:'金蝶是否开单', field:'是否已做采购单', editable:true, type:'select', options:['是','否']},
{label:'旺店通是否已录入', field:'是否已录入到旺店通', editable:true, type:'select', options:['是','否']},
{label:'预计到货时间', field:'预计到货日期', editable:true, type:'date'},
{label:'订单完结状态', field:'状态', editable:true, type:'select', options:['已完结','未完结','已取消']}
];
col2 = [
{label:'产品种类', field:'product_grade', editable:false},
{label:'货品名称', field:'货品名称', editable:false},
{label:'生产货期', field:'lead_time', editable:false},
{label:'订单下达时间', field:'订单下达供应商时间', editable:true, type:'date'},
{label:'数量', field:'数量', editable:true},
{label:'采购计划', field:'procurement_planner', editable:false},
{label:'产品开发', field:'采购开发', editable:false},
{label:'金蝶是否入库', field:'是否做入库', editable:true, type:'select', options:['是','否']},
{label:'旺店通单号', field:'旺店通生产单号', editable:true},
{label:'待提数量', field:'待提数量', editable:true},
{label:'云仓库存支撑天数', field:'total_support_days', editable:true}
];
} else if (title && title.indexOf('包材') !== -1) {
apiUrl = '/api/packaging/orders/update';
refreshFn = 'packagingLoadOrders';
col1 = [
{label:'货品编号', field:'编码', editable:false},
{label:'供应商名称', field:'供应商', editable:true},
{label:'采购计划', field:'采购计划', editable:false},
{label:'单价', field:'单价', editable:true},
{label:'计划下单时间', field:'下单时间', editable:false},
{label:'产品种类', field:'产品种类', editable:true, type:'select', options:['新品','返单品','迭代品']},
{label:'金蝶是否开单', field:'是否已做采购单', editable:true, type:'select', options:['是','否']},
{label:'旺店通是否录入', field:'是否已录入旺店通', editable:true, type:'select', options:['是','否']},
{label:'状态', field:'状态', editable:true, type:'select', options:['已完成','分批送货']}
];
col2 = [
{label:'货品名称', field:'名称', editable:false},
{label:'到货仓库', field:'仓库到货', editable:true},
{label:'包材开发', field:'包材开发', editable:false},
{label:'数量', field:'数量', editable:true},
{label:'订单下达时间', field:'订单下达时间', editable:true, type:'date'},
{label:'有无新稿件', field:'有无新稿件', editable:true, type:'select', options:['有','无']},
{label:'旺店通编号', field:'旺店通编号', editable:false},
{label:'金蝶云是否录入', field:'是否已做入库', editable:true, type:'select', options:['是','否']},
{label:'包材异常状态', field:'包材异常状态', editable:true}
];
} else {
apiUrl = '/api/domestic/orders/update';
refreshFn = 'domesticLoadOrders';
col1 = [
{label:'产品类型', field:'product_type', editable:false},
{label:'货品编号', field:'编码', editable:false},
{label:'供应商名称', field:'供货商', editable:true},
{label:'计划下单时间', field:'计划下单时间', editable:false},
{label:'单价', field:'单价', editable:true},
{label:'采购跟单', field:'采购员', editable:true},
{label:'订单所属平台', field:'platform', editable:true},
{label:'金蝶是否开单', field:'金蝶采购单', editable:true, type:'select', options:['是','否']},
{label:'旺店通是否已录入', field:'是否已录入旺店通', editable:true, type:'select', options:['是','否']},
{label:'预计到货时间', field:'预计到货日期', editable:true, type:'date'},
{label:'订单完结状态', field:'状态', editable:true, type:'select', options:['已完结','未完结','已取消']}
];
col2 = [
{label:'产品种类', field:'product_grade', editable:false},
{label:'货品名称', field:'名称', editable:false},
{label:'生产货期', field:'lead_time', editable:false},
{label:'订单下达时间', field:'订单下达供应商时间', editable:true, type:'date'},
{label:'实际下单数量', field:"数量", editable:true},
{label:'采购计划', field:'procurement_planner', editable:false},
{label:'产品开发', field:'采购开发', editable:false},
{label:'金蝶是否入库', field:'金蝶入库', editable:true, type:'select', options:['是','否']},
{label:'旺店通单号', field:'旺店通生产单号', editable:true},
{label:'待提数量', field:'待提数量', editable:true},
{label:'云仓库存支撑天数', field:'total_support_days', editable:true}
];
}
var renderValue = function(cfg, val) {
if (val === null || val === undefined) val = '';
var sv = String(val);
if (!cfg.editable) {
return '<span class="detail-val">' + escHtml(sv || '-') + '</span>';
}
if (cfg.type === 'select' && cfg.options) {
var opts = cfg.options.map(function(o) {
var sel = (o === sv) ? ' selected' : '';
return '<option value="' + escHtml(o) + '"' + sel + '>' + escHtml(o) + '</option>';
}).join('');
return '<select class="detail-input detail-select" data-id="' + row.id + '" data-field="' + escHtml(cfg.field) + '">' + opts + '</select>';
}
if (cfg.type === 'date') {
// 去掉时间部分,只取日期
var dateVal = window._padDateStr(sv.split(' ')[0]);
return '<input type="date" class="detail-input" value="' + escHtml(dateVal) + '" data-id="' + row.id + '" data-field="' + escHtml(cfg.field) + '">';
}
return '<input type="text" class="detail-input" value="' + escHtml(sv) + '" data-id="' + row.id + '" data-field="' + escHtml(cfg.field) + '" onkeydown="if(event.key===\'Enter\'){event.preventDefault();if(confirm(\'确认保存 \'+this.previousElementSibling?.textContent||\'\'+\' 为 \'+this.value+\'?\')){this.blur();var btn=this.closest(\'.modal-overlay\').querySelector(\'.pd-footer .btn-primary\');if(btn)btn.click()}}">';
};
var html = '<div style="text-align:left"><div class="detail-two-col">';
html += '<div class="detail-col">';
col1.forEach(function(cfg) {
var val = row[cfg.field];
html += '<div class="detail-pair"><span class="detail-lbl">' + escHtml(cfg.label) + '</span><div class="detail-val-wrap">' + renderValue(cfg, val) + '</div></div>';
});
html += '</div>';
html += '<div class="detail-col">';
col2.forEach(function(cfg) {
var val = row[cfg.field];
html += '<div class="detail-pair"><span class="detail-lbl">' + escHtml(cfg.label) + '</span><div class="detail-val-wrap">' + renderValue(cfg, val) + '</div></div>';
});
html += '</div>';
html += '</div></div>';
// 进度字段(全宽、多行文本框)
var progressVal = row['进度'];
if (progressVal === null || progressVal === undefined) progressVal = '';
html += '<div style="margin-top:6px"><div class="detail-pair" style="min-height:70px"><span class="detail-lbl" style="min-height:68px">进度</span><div class="detail-val-wrap" style="padding:4px 12px"><textarea class="detail-textarea" data-id="' + row.id + '" data-field="进度" style="width:100%;height:60px;box-sizing:border-box;font-size:13px;border:1px solid #d9d9d9;border-radius:4px;padding:6px 8px;color:#333;background:#fff;resize:vertical;font-family:inherit">' + escHtml(String(progressVal)) + '</textarea></div></div></div>';
// 到货批次及数量列表(动态加载)
html += '<div style="margin-top:16px">';
html += '<div style="display:flex;align-items:center;margin:0 0 8px 0">';
html += '<h4 style="margin:0;font-size:14px;color:#555;font-weight:600">📋 到货批次及数量</h4>';
html += '<div style="margin-left:auto"><button id="addBatchBtn" class="btn btn-sm btn-outline" onclick="addArrivalBatchRow(this)" data-order-id="' + row.id + '" style="font-size:12px">➕ 添加一行</button></div>';
html += '</div>';
html += '<table id="arrivalBatchTable" style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed" data-order-qty="' + (parseInt(row['数量']) || 0) + '"><thead><tr>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;width:10%;text-align:center">批次编号</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;width:20%;text-align:center">到货日期</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;width:20%;text-align:center">到货数量</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;width:20%;text-align:center">订单未完结数量</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;width:30%;text-align:center">备注</th>';
html += '</tr></thead><tbody id="arrivalBatchBody"></tbody>';
html += '<tfoot id="arrivalBatchFoot"><tr style="font-weight:600;background:#fafafa">';
html += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">合计</td>';
html += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center"></td>';
html += '<td id="arrivalTotalQty" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>';
html += '<td id="arrivalUnfinishedTotal" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>';
html += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center"></td>';
html += '</tr></tfoot></table>';
html += '</div>';
// 包材信息(动态加载)
html += '<div style="margin-top:16px">';
html += '<h4 style="margin:0 0 8px 0;font-size:14px;color:#555;font-weight:600">📦 包材信息</h4>';
html += '<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">下单时间</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">货品编号</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">货品名称</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">下单数量</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">到货数量</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">到货日期</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 10px;background:#f5f5f5;color:#555;font-weight:600;text-align:center">进度</th>';
html += '</tr></thead><tbody id="packagingMaterialBody"><tr><td colspan="7" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载中...</td></tr></tbody></table>';
html += '</div>';
overlay.innerHTML = '<div class="pd-overlay" style="z-index:99999"><div class="pd-modal pd-modal-wide" style="width:1400px;display:flex;flex-direction:column;padding:0;overflow:hidden">' +
'<div class="pd-header" style="flex-shrink:0;padding:20px 28px 16px"><span class="pd-title">📋 ' + escHtml(title || '产品详情明细') + '</span><button class="pd-close-btn" onclick="this.closest(\'.modal-overlay\').remove()"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>' +
'<div class="pd-body" style="padding:16px 28px 20px;overflow:auto;flex:1;background:#e8e8e8">' + html + '</div>' +
'<div class="pd-footer" style="display:flex;justify-content:flex-end;gap:8px;padding:12px 24px;border-top:1px solid #e8ecf0;flex-shrink:0"><button class="btn btn-outline" onclick="this.closest(\'.modal-overlay\').remove()" style="border-radius:6px;padding:6px 20px;font-size:13px">取消</button><button class="btn btn-primary" onclick="saveDetailChanges(this)" data-order-id="' + row.id + '" data-api="' + apiUrl + '" data-refresh="' + refreshFn + '" style="border-radius:6px;padding:6px 20px;font-size:13px">💾 保存</button></div></div></div>';
document.body.appendChild(overlay);
// 实际下单数量变更时实时重新计算未完结数量
overlay.addEventListener('input', function(e) {
if (e.target.matches('.detail-input[data-field="数量"]') || e.target.matches('.batch-qty')) {
recalcArrivalUnfinished();
}
});
// 动态加载到货批次 + 包材信息
(function() {
var orderId = row.id;
var productCode = row['产品编码'] || row['编码'] || '';
fetch('/api/domestic/orders/detail_extras?order_id=' + encodeURIComponent(orderId) + '&product_code=' + encodeURIComponent(productCode))
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) {
var abBody = overlay.querySelector('#arrivalBatchBody');
if (abBody) abBody.innerHTML = '<tr><td colspan="5" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">暂无数据</td></tr>';
var pmBody = overlay.querySelector('#packagingMaterialBody');
if (pmBody) pmBody.innerHTML = '<tr><td colspan="7" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">暂无数据</td></tr>';
return;
}
var data = res.data;
// 到货批次
var abBody = overlay.querySelector('#arrivalBatchBody');
var totalQtyEl = overlay.querySelector('#arrivalTotalQty');
if (abBody) {
var abHtml = '';
var totalQty = 0;
var batchCount = data.arrival_batches ? data.arrival_batches.length : 0;
// 数据行
data.arrival_batches.forEach(function(b, idx) {
var qty = parseInt(b.arrival_qty) || 0;
totalQty += qty;
var dateVal = (b.arrival_date || '').substring(0, 10);
var isManual = (b.from_type === 'add');
var batchId = b.id || '';
abHtml += '<tr>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;font-weight:700">' + (idx + 1) + '</td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" value="' + dateVal + '" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' + (isManual ? '' : '') +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-qty" value="' + qty + '" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' +
'<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center">' +
'<input type="text" class="batch-remark" value="' + escHtml(b.remark || '') + '" style="width:66%;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px">' +
(isManual ? '<button onclick="saveArrivalBatch(this,' + batchId + ',' + orderId + ')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button><button onclick="deleteArrivalBatch(this,' + batchId + ')" title="删除" style="padding:2px 6px;font-size:11px;border:1px solid #ff4d4f;background:#fff1f0;color:#ff4d4f;border-radius:3px;cursor:pointer;margin-left:2px;white-space:nowrap">✕</button>' : '') +
'</div></td>';
abHtml += '</tr>';
});
// 补齐最少3行空白占位(可编辑)
var placeholderRows = Math.max(0, 3 - batchCount);
for (var i = 0; i < placeholderRows; i++) {
abHtml += '<tr class="batch-placeholder">';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;font-weight:700;color:#1890ff">' + (batchCount + i + 1) + '</td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" style="width:66%;box-sizing:border-box;font-size:12px;color:#999;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-qty" placeholder="数量" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>';
abHtml += '<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;color:#ccc">-</td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center"><input type="text" class="batch-remark" placeholder="备注" style="width:66%;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"><button onclick="saveNewArrivalBatch(this,' + orderId + ')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button></div></td>';
abHtml += '</tr>';
}
abBody.innerHTML = abHtml;
if (totalQtyEl) totalQtyEl.textContent = totalQty;
recalcArrivalUnfinished();
}
// 包材信息
var pmBody = overlay.querySelector('#packagingMaterialBody');
if (pmBody && data.packaging_materials) {
if (data.packaging_materials.length === 0) {
pmBody.innerHTML = '<tr><td colspan="7" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">无关联包材</td></tr>';
} else {
var pmHtml = '';
data.packaging_materials.forEach(function(m) {
pmHtml += '<tr>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">' + escHtml(m.order_time || '-') + '</td>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center"><code>' + escHtml(m.material_code || '-') + '</code></td>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">' + escHtml(m.material_name || '-') + '</td>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">' + m.order_qty + '</td>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">' + m.arrival_qty + '</td>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">' + escHtml(m.arrival_date || '-') + '</td>';
pmHtml += '<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center"><span class="progress-tip" data-progress="' + (m.progress || '-').replace(/"/g, '&quot;') + '">📊 ' + escHtml(m.progress || '-') + '</span></td>';
pmHtml += '</tr>';
});
pmBody.innerHTML = pmHtml;
}
}
})
.catch(function(err) {
console.error('加载详情额外数据失败:', err);
var abBody = overlay.querySelector('#arrivalBatchBody');
if (abBody) abBody.innerHTML = '<tr><td colspan="5" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载失败</td></tr>';
var pmBody = overlay.querySelector('#packagingMaterialBody');
if (pmBody) pmBody.innerHTML = '<tr><td colspan="7" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载失败</td></tr>';
});
})();
}
function addArrivalBatchRow(btn) {
btn.disabled = true;
btn.textContent = '⋯';
var overlay = btn.closest('.modal-overlay');
if (!overlay) {
var tbl = btn.closest('table');
if (tbl) overlay = tbl.closest('.modal-overlay');
}
var orderId = btn.getAttribute('data-order-id');
var tbody = overlay.querySelector('#arrivalBatchBody');
if (!tbody) { btn.disabled = false; btn.textContent = '➕ 添加一行'; return; }
if (tbody.querySelector('.batch-editing')) { btn.disabled = false; btn.textContent = '➕ 添加一行'; return; }
// 计算下一个批次编号
var lastNum = 0;
var rows = tbody.querySelectorAll('tr:not(.batch-placeholder)');
rows.forEach(function(r) {
var firstTd = r.querySelector('td');
if (firstTd) {
var num = parseInt(firstTd.textContent);
if (!isNaN(num) && num > lastNum) lastNum = num;
}
});
var nextNum = Math.max(lastNum + 1, 4);
// 插入编辑行(在最后一个数据行之后,占位行之前)
var tr = document.createElement('tr');
tr.className = 'batch-editing';
tr.innerHTML = '' +
'<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;font-weight:700;color:#1890ff">' + nextNum + '</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-qty" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px" placeholder="数量"></td>' +
'<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:4px 6px"></td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center">' +
'<input type="text" class="batch-remark" style="width:66%;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px" placeholder="备注">' +
'<button onclick="saveNewArrivalBatch(this,' + orderId + ')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button>' +
'<button onclick="cancelArrivalBatchRow(this)" title="取消" style="padding:2px 6px;font-size:11px;border:1px solid #ff4d4f;background:#fff1f0;color:#ff4d4f;border-radius:3px;cursor:pointer;margin-left:2px;white-space:nowrap">✕</button>' +
'</div></td>';
// 追加到表格末尾(合计行上方)
tbody.appendChild(tr);
recalcArrivalUnfinished();
var inp = tr.querySelector('input[type="date"]');
if (inp) inp.focus();
}
/**
* 取消新增行(删除未保存的行)
*/
function cancelArrivalBatchRow(btn) {
if (!confirm('确认取消本行?未保存的内容将丢失。')) return;
var tr = btn.closest('tr');
if (!tr) return;
// 恢复添加按钮状态
var addBtn = document.getElementById('addBatchBtn');
if (addBtn) { addBtn.disabled = false; addBtn.textContent = '➕ 添加一行'; }
tr.remove();
recalcArrivalUnfinished();
}
function saveNewArrivalBatch(btn, orderId) {
var tr = btn.closest('tr');
if (!tr) return;
var date = tr.querySelector('.batch-date')?.value || '';
var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0;
var remark = tr.querySelector('.batch-remark')?.value || '';
if (!date && !qty) { alert('请输入到货日期或数量'); return; }
btn.disabled = true;
btn.textContent = '⋯';
fetch('/api/domestic/orders/add_batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({order_id: orderId, arrival_date: date, arrival_qty: qty, remark: remark})
}).then(function(r){return r.json()}).then(function(res){
if (res.code === 0) {
var batchNum = '';
var firstTd = tr.querySelector('td');
if (firstTd) batchNum = firstTd.textContent || '';
var batchId = res.data && res.data.id || '';
tr.className = '';
tr.innerHTML = '' +
'<td style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;font-weight:700">' + batchNum + '</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" value="' + date + '" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-qty" value="' + qty + '" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' +
'<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center"><input type="text" class="batch-remark" value="' + escHtml(remark) + '" style="width:66%;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"><button onclick="saveArrivalBatch(this,' + batchId + ',' + orderId + ')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button><button onclick="deleteArrivalBatch(this,' + batchId + ')" title="删除" style="padding:2px 6px;font-size:11px;border:1px solid #ff4d4f;background:#fff1f0;color:#ff4d4f;border-radius:3px;cursor:pointer;margin-left:2px;white-space:nowrap">✕</button></div></td>';
// 更新合计
var totalEl = tr.closest('.modal-overlay').querySelector('#arrivalTotalQty');
if (totalEl) {
var cur = parseInt(totalEl.textContent) || 0;
totalEl.textContent = cur + qty;
}
recalcArrivalUnfinished();
var addBtn = tr.closest('.modal-overlay').querySelector('#addBatchBtn');
if (addBtn) { addBtn.disabled = false; addBtn.textContent = '➕ 添加一行'; }
} else {
alert('保存失败: ' + (res.msg || '未知错误'));
btn.disabled = false;
btn.textContent = '✅';
}
}).catch(function(){
alert('网络错误');
btn.disabled = false;
btn.textContent = '✅';
});
}
function saveArrivalBatch(btn, batchId, orderId) {
var tr = btn.closest('tr');
if (!tr) return;
var date = tr.querySelector('.batch-date')?.value || '';
var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0;
var remark = tr.querySelector('.batch-remark')?.value || '';
btn.disabled = true;
btn.textContent = '⋯';
fetch('/api/domestic/orders/update_batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: batchId, arrival_date: date, arrival_qty: qty, remark: remark})
}).then(function(r){return r.json()}).then(function(res){
if (res.code === 0) {
btn.textContent = '✅';
recalcArrivalUnfinished();
setTimeout(function() { btn.textContent = '✅'; }, 500);
} else {
alert('保存失败: ' + (res.msg || '未知错误'));
}
btn.disabled = false;
}).catch(function(){
alert('网络错误');
btn.disabled = false;
btn.textContent = '✅';
});
}
function deleteArrivalBatch(btn, batchId) {
if (!confirm('确认删除本行数据?删除后不可恢复。')) return;
var tr = btn.closest('tr');
if (!tr) return;
var overlay = tr.closest('.modal-overlay');
var qty = parseInt(tr.querySelector('.batch-qty')?.value) || 0;
btn.disabled = true;
btn.textContent = '⋯';
fetch('/api/domestic/orders/delete_batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: batchId})
}).then(function(r){return r.json()}).then(function(res){
if (res.code === 0) {
tr.remove();
recalcArrivalUnfinished();
if (overlay) {
var totalEl = overlay.querySelector('#arrivalTotalQty');
if (totalEl) {
var cur = parseInt(totalEl.textContent) || 0;
totalEl.textContent = Math.max(0, cur - qty);
}
}
} else {
alert('删除失败: ' + (res.msg || '未知错误'));
btn.disabled = false;
btn.textContent = '✕';
}
}).catch(function(){
alert('网络错误');
btn.disabled = false;
});
}
/**
* 到货数量输入框回车保存
*/
function batchQtyKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
var tr = event.target.closest('tr');
if (!tr) return;
var saveBtn = tr.querySelector('button[onclick*="saveArrivalBatch"], button[onclick*="saveNewArrivalBatch"]');
if (saveBtn) {
saveBtn.click();
setTimeout(function() { event.target.blur(); }, 100);
}
}
}
/**
* 滚动计算订单未完结数量
* 实际下单数量 - 累计到货数量 = 当前行未完结数量
*/
function recalcArrivalUnfinished() {
var table = document.getElementById('arrivalBatchTable');
if (!table) return;
// 从详情表单中的实际下单数量输入框读取值
var overlay = table.closest('.modal-overlay');
var orderInput = overlay ? overlay.querySelector('.detail-input[data-field="数量"]') : null;
var orderQty = orderInput ? (parseInt(orderInput.value) || 0) : (parseInt(table.getAttribute('data-order-qty')) || 0);
var body = table.querySelector('#arrivalBatchBody');
if (!body) return;
var rows = body.querySelectorAll('tr');
var cumulative = 0;
var lastUnfinished = 0;
var hasUnfinishedValue = false;
rows.forEach(function(tr) {
var qtyInput = tr.querySelector('.batch-qty');
var unFinishedTd = tr.querySelector('.batch-unfinished');
if (qtyInput && unFinishedTd) {
var arrivalQty = parseInt(qtyInput.value) || 0;
cumulative += arrivalQty;
var remaining = orderQty - cumulative;
if (qtyInput.getAttribute('placeholder') === '数量' && !qtyInput.value) {
unFinishedTd.textContent = '-';
unFinishedTd.style.color = '#ccc';
} else {
unFinishedTd.textContent = remaining;
unFinishedTd.style.color = remaining < 0 ? '#f5222d' : '';
lastUnfinished = remaining;
hasUnfinishedValue = true;
}
}
});
// 合计行显示最后一个有数据行的未完结数量(总剩余)
var footEl = table.querySelector('#arrivalUnfinishedTotal');
if (footEl) {
// 没有填写任何到货批次时,未完结数量 = 实际下单数量(即全部未到货)
var totalVal = hasUnfinishedValue ? lastUnfinished : orderQty;
footEl.textContent = totalVal;
footEl.style.color = totalVal < 0 ? '#f5222d' : '';
}
// 更新到货数量合计(遍历所有行的 batch-qty 累加)
var totalQtyEl = table.querySelector('#arrivalTotalQty');
if (totalQtyEl) {
var sumQty = 0;
body.querySelectorAll('tr .batch-qty').forEach(function(inp) {
sumQty += parseInt(inp.value) || 0;
});
totalQtyEl.textContent = sumQty;
}
}
function saveDetailChanges(btn) {
var overlay = btn.closest('.modal-overlay');
var orderId = btn.getAttribute('data-order-id');
var apiUrl = btn.getAttribute('data-api') || '/api/domestic/orders/update';
var refreshFn = btn.getAttribute('data-refresh') || 'domesticLoadOrders';
var inputs = overlay.querySelectorAll('.detail-input, .detail-textarea');
var changed = 0;
var total = inputs.length;
var hasError = false;
var promises = [];
inputs.forEach(function(inp) {
var field = inp.getAttribute('data-field');
var value = inp.value;
promises.push(
fetch(apiUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: orderId, field: field, value: value})
}).then(function(r){return r.json()}).then(function(res){
if (res.code !== 0) hasError = true;
}).catch(function(){
hasError = true;
})
);
});
Promise.all(promises).then(function() {
btn.disabled = false;
btn.textContent = '💾 保存';
if (hasError) {
alert('部分字段保存失败,请重试');
} else {
overlay.remove();
var pageVar = refreshFn === 'packagingLoadOrders' ? 'packagingOrderPage' : refreshFn === 'overseasLoadOrders' ? 'overseasOrderPage' : 'domesticOrderPage';
if (typeof window[refreshFn] === 'function') {
window[refreshFn](window[pageVar] || 1);
}
}
});
// 如果没有可编辑字段,直接关闭
if (total === 0) {
overlay.remove();
}
// 禁用按钮防重复点击
btn.disabled = true;
btn.textContent = '保存中...';
}
window._padDateStr = window._padDateStr || function(str) {
if (!str) return str;
str = str.replace(/\//g, '-');
var parts = str.split(' ');
if (parts.length >= 1) {
var dp = parts[0].split('-');
if (dp.length >= 2) dp[1] = ('0' + dp[1]).slice(-2);
if (dp.length >= 3) dp[2] = ('0' + dp[2]).slice(-2);
parts[0] = dp.join('-');
}
if (parts.length >= 2) {
var tp = parts[1].split(':');
if (tp.length >= 1) tp[0] = ('0' + tp[0]).slice(-2);
parts[1] = tp.join(':');
}
return parts.join(' ');
};
function loadDomesticOrdersPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>国内订单管理</h1>
<div style="display:flex;gap:10px;margin-top:8px;flex-wrap:wrap;align-items:center">
<div class="search-box" style="max-width:300px">
<input type="text" id="domesticSearchInput" class="form-input" placeholder="搜索编码/名称..." autocomplete="off"
onkeydown="if(event.key==='Enter') domesticDoSearch()" style="padding-left:32px;padding-right:8px;height:32px;font-size:13px">
<svg style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#999" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
<input type="text" id="domesticSupplierFilter" class="form-input" placeholder="供应商..." style="width:auto;min-width:120px;height:32px;font-size:13px" autocomplete="off" onkeydown="if(event.key==='Enter') domesticApplyFilters()">
<select id="domesticStatusFilter" class="form-input" style="width:auto;min-width:120px;height:32px;font-size:13px" onchange="domesticApplyFilters()">
<option value="">全部状态</option>
</select>
<select id="domesticOrdererFilter" class="form-input" style="width:auto;min-width:100px;height:32px;font-size:13px" onchange="domesticApplyFilters()">
<option value="">全部下单人</option>
</select>
<button class="btn btn-sm btn-primary" onclick="domesticDoSearch()">查询</button>
<button class="btn btn-sm btn-outline" onclick="domesticResetFilters()">重置</button>
<span id="domesticTotalInfo" style="font-size:13px;color:var(--text-light);margin-left:auto"></span>
</div>
</div>
<div id="domesticContent" class="page-flex">
<div class="card">
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
<h3>📦 国内采购下单明细</h3>
<div>
<button class="btn btn-sm btn-outline" onclick="domesticBatchGenerateContract()" style="color:#52c41a;border-color:#52c41a">📄 批量生成合同</button>
<button class="btn btn-sm btn-primary" onclick="domesticAddRow()">+ 新增</button>
<button class="btn btn-sm btn-outline" onclick="domesticExport()">导出</button>
<button class="btn btn-sm btn-outline" onclick="openDomesticFrozenModal()">🔒 冻结列</button>
<button class="btn btn-sm btn-outline" onclick="openDomesticColModal()">⚙ 字段设置</button>
</div>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="domesticOrdersTable" style="font-size:12px;white-space:nowrap;min-width:6000px">
<thead style="position:sticky;top:0;z-index:10;background:var(--bg,#f5f5f5)"><tr id="domesticOrdersHeader">
<th style="position:sticky;left:0;z-index:3;background:var(--bg,#f5f5f5);min-width:40px;text-align:center"><input type="checkbox" id="domesticSelectAll" onchange="domesticToggleAll(this)"></th>
<th style="position:sticky;left:40px;z-index:3;background:var(--bg,#f5f5f5);min-width:50px">序号</th>`;
// 动态生成表头列
(function() {
var cols = DOMESTIC_COLUMNS;
var order = window._domesticDragOrder || cols.map(function(c) { return c.field; });
order.forEach(function(field) {
var col = cols.find(function(c) { return c.field === field; });
if (!col) return;
if (!window.domesticColVisible[field]) return;
var label = col.label;
var fieldAttr = col.field;
var thStyle = '';
if (fieldAttr === '货品编码') thStyle = 'min-width:120px;position:sticky;left:90px;top:0;z-index:11;background:var(--bg,#f5f5f5)';
else if (fieldAttr === '货品名称') thStyle = 'min-width:200px;position:sticky;left:210px;top:0;z-index:11;background:var(--bg,#f5f5f5)';
else if (fieldAttr === '日期') thStyle = 'position:sticky;left:410px;z-index:3;background:var(--bg,#f5f5f5)';
else if (fieldAttr === '计划下单时间' || fieldAttr === '新旧品') thStyle = 'white-space:nowrap';
else if (fieldAttr === '下单备注') thStyle = 'min-width:150px;white-space:normal';
else if (fieldAttr === '单价' || fieldAttr === '是否审核价格' || fieldAttr === '数量' || fieldAttr === '金额' || fieldAttr === '供货商') thStyle = '';
else if (fieldAttr === '仓库') thStyle = 'min-width:80px';
else if (fieldAttr === '金蝶采购单') thStyle = 'min-width:100px';
else if (fieldAttr === '采购员') thStyle = 'min-width:150px';
else if (fieldAttr === '下单人') thStyle = 'min-width:70px';
else if (fieldAttr === '进度') thStyle = 'min-width:80px';
else if (fieldAttr === '旺店通生产单号') thStyle = 'min-width:120px';
else if (fieldAttr === '采购开发') thStyle = 'min-width:100px';
else if (fieldAttr === '实际到货日期' || fieldAttr === '预计到货日期') thStyle = 'min-width:100px';
else { thStyle = ''; }
container.querySelector('#domesticOrdersHeader').innerHTML += '<th data-field="' + fieldAttr + '" style="' + thStyle + '">' + label + ' <span class="order-filt-icon" onclick="event.stopPropagation();openOrderFilterMenu(\'domestic\',\'' + fieldAttr + '\',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>';
});
})();
container.querySelector('#domesticOrdersHeader').innerHTML += '<th style="min-width:70px">做货周期</th>';
container.querySelector('#domesticOrdersHeader').innerHTML += '<th style="width:1%;min-width:150px;white-space:nowrap;padding:12px 8px;position:sticky;right:0;top:0;z-index:13;background:var(--bg,#f5f5f5);border-left:1px solid var(--border);text-align:center">操作</th>';
container.querySelector('#domesticOrdersTable').innerHTML += '</thead><tbody id="domesticOrdersTbody"><tr><td colspan="28" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>';
// 添加供应商 datalist(模糊搜索用)
container.querySelector('#domesticContent').innerHTML += '</div></div><div id="domesticOrdersPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div></div>';
// 冻结列弹窗 + 字段设置弹窗
container.innerHTML += `
<div class="modal-overlay" id="domesticFrozenModal" style="display:none">
<div class="modal" style="width:400px">
<div class="modal-header">
<h3>🔒 冻结列设置</h3>
<span class="modal-close" onclick="closeDomesticFrozenModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:60vh;overflow-y:auto;padding:12px 20px" id="domesticFrozenModalBody">
</div>
<div class="modal-footer">
<button class="btn" onclick="closeDomesticFrozenModal()">取消</button>
<button class="btn btn-primary" onclick="confirmDomesticFrozen()">确定</button>
</div>
</div>
</div>
<!-- 字段设置弹窗 -->
<div class="modal-overlay" id="domesticColModal" style="display:none">
<div class="modal" style="width:720px">
<div class="modal-header">
<h3>字段设置</h3>
<span class="modal-close" onclick="closeOrderColModal('domestic')">&times;</span>
</div>
<div class="modal-body" style="max-height:65vh;overflow-y:auto;padding:16px 20px">
<!-- 方案管理 -->
<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="font-weight:600;font-size:14px;white-space:nowrap">📁 方案管理</span>
<input type="text" id="domesticSchemeNameInput" class="form-input" placeholder="输入方案名称..." style="flex:1;min-width:0">
<button class="btn btn-primary btn-sm" onclick="saveOrderScheme('domestic')">💾 保存方案</button>
</div>
<div id="domesticSchemeList" style="display:flex;flex-wrap:wrap;gap:6px;min-height:28px">
<span style="color:var(--text-light);font-size:13px">暂无保存方案</span>
</div>
</div>
<div class="col-toggle-all" style="padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:8px">
<label class="cb-all"><input type="checkbox" id="domestic-cb-all" checked onchange="toggleOrderColAll('domestic',this.checked)"> <strong>全选</strong></label>
</div>
<input type="text" id="domesticColSearchInput" class="form-input" placeholder="搜索字段名称..." oninput="filterOrderColCheckboxes('domestic')" style="margin-bottom:8px;width:100%;box-sizing:border-box">
<div id="domesticColCheckboxes"><div id="domesticColGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px 12px;padding:4px 0"></div></div>
</div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:12px;color:var(--text-light)">拖拽字段名可调整列顺序</span>
<div>
<button class="btn" onclick="closeOrderColModal('domestic')">取消</button>
<button class="btn btn-primary" onclick="confirmOrderCol('domestic')">确定</button>
</div>
</div>
</div>
</div>`;
// 初始化字段设置checkbox
var dcGrid = document.getElementById('domesticColGrid');
if (dcGrid) {
window._domesticDragOrder = DOMESTIC_COLUMNS.map(function(c) { return c.field; });
renderOrderColCheckboxes('domestic');
}
// 加载采购跟单人员列表
fetch('/api/users/followers').then(function(r){return r.json()}).then(function(res){
if (res.code === 0) domesticFollowers = res.data || [];
});
domesticLoadFilters();
domesticLoadOrders(1);
loadDefaultOrderScheme('domestic');
// 后台预拉取所有列的筛选值
prefetchOrderColumnValues('domestic');
}
function domesticLoadFilters() {
fetch('/api/domestic/orders/filters')
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var ss = document.getElementById('domesticStatusFilter');
var so = document.getElementById('domesticOrdererFilter');
(res.data.statuses || []).forEach(function(s) {
var opt = document.createElement('option');
opt.value = s; opt.textContent = s;
ss.appendChild(opt);
});
(res.data.orderers || []).forEach(function(s) {
var opt = document.createElement('option');
opt.value = s; opt.textContent = s;
so.appendChild(opt);
});
// 填充供应商 datalist(模糊搜索用)
});
}
function domesticDoSearch() {
domesticApplyFilters();
}
function domesticApplyFilters() {
domesticSearch = document.getElementById('domesticSearchInput').value.trim();
domesticFilterSupplier = document.getElementById('domesticSupplierFilter').value.trim();
domesticFilterStatus = document.getElementById('domesticStatusFilter').value;
domesticFilterOrderer = document.getElementById('domesticOrdererFilter').value;
domesticLoadOrders(1);
}
function domesticResetFilters() {
document.getElementById('domesticSearchInput').value = '';
document.getElementById('domesticSupplierFilter').value = '';
document.getElementById('domesticStatusFilter').value = '';
document.getElementById('domesticOrdererFilter').value = '';
domesticSearch = '';
domesticFilterSupplier = '';
domesticFilterStatus = '';
domesticFilterOrderer = '';
domesticOrderFilter = {};
domesticOrderFilterActive = {};
_syncOrderFilterIcons('domestic');
domesticLoadOrders(1);
}
function domesticLoadOrders(page) { domesticOrderPage = page;
var params = '?page=' + page + '&size=' + DOMESTIC_PAGE_SIZE;
if (domesticSearch) params += '&search=' + encodeURIComponent(domesticSearch);
if (domesticFilterStatus) params += '&status=' + encodeURIComponent(domesticFilterStatus);
if (domesticFilterSupplier) params += '&supplier=' + encodeURIComponent(domesticFilterSupplier);
if (domesticFilterOrderer) params += '&orderer=' + encodeURIComponent(domesticFilterOrderer);
var domesticFilterStr = '';
if (Object.keys(domesticOrderFilter).length > 0) {
domesticFilterStr = encodeURIComponent(JSON.stringify(domesticOrderFilter));
params += '&order_filter=' + domesticFilterStr;
}
fetch('/api/domestic/orders/list' + params)
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var data = res.data;
var tbody = document.getElementById('domesticOrdersTbody');
var totalInfo = document.getElementById('domesticTotalInfo');
if (totalInfo) totalInfo.textContent = '共 ' + data.total + ' 条';
if (!data.rows || data.rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="28" style="text-align:center;padding:60px;color:var(--text-light)">暂无数据</td></tr>';
} else {
var startIdx = (page - 1) * DOMESTIC_PAGE_SIZE;
var html = '';
// 国内订单单元格渲染器映射
var domesticCellRenderers = {
'货品编码': function(r) { return '<td><code style="cursor:pointer;color:#1890ff" onclick="showSkuInfoModal(this,\'国内订单详情\')">' + escHtml(r['编码'] || '') + '</code></td>'; },
'货品名称': function(r) { return '<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(r['名称'] || '') + '">' + escHtml(r['名称'] || '') + '</td>'; },
'日期': function(r) { return '<td><span class="date-text">' + escHtml(window._padDateStr(r['日期'] || '')) + '</span> <span class="date-edit-btn" onclick="event.stopPropagation();editDateField(this.parentNode,' + r.id + ',\'日期\',\'/api/domestic/orders/update\')" style="cursor:pointer;color:#1890ff;font-size:12px">📅</span></td>'; },
'计划下单时间': function(r) {
var t = window._padDateStr(r['计划下单时间'] || '');
if (t.length > 16) t = t.slice(0, 16);
return '<td>' + escHtml(t) + '</td>';
},
'新旧品': function(r) { return '<td>' + escHtml(r['新旧品'] || '') + '</td>'; },
'下单备注': function(r) { return '<td title="' + escHtml(r['下单备注'] || '') + '" style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(r['下单备注'] || '-') + '</td>'; },
'单价': function(r) { return '<td style="padding:2px 4px"><input type="text" class="price-inline" value="' + (r['单价'] ? parseFloat(r['单价']) : '') + '" oninput="recalcAmount(this)" data-pid="' + r.id + '" data-pfield="单价" data-papi="/api/domestic/orders/update" style="width:70px;border:1px solid #d9d9d9;border-radius:3px;padding:2px 4px;font-size:12px;text-align:right;background:var(--bg,#fff);color:var(--text,#333)"></td>'; },
'是否审核价格': function(r) { return '<td>' + escHtml(r['是否审核价格'] || '') + '</td>'; },
'数量': function(r) { return '<td class="qty-cell"><strong>' + (parseInt(r['数量']) || 0).toLocaleString() + '</strong></td>'; },
'金额': function(r) {
var price = parseFloat(r['单价']) || 0;
var qty = parseInt(r['数量']) || 0;
var amt = price * qty;
return '<td class="amt-cell">' + (amt ? '¥' + amt.toLocaleString() : '') + '</td>';
},
'供货商': function(r) { return '<td style="padding:2px 4px"><input type="text" class="supplier-inline" value="' + escHtml(r['供货商'] || '') + '" data-pid="' + r.id + '" data-pfield="供货商" data-papi="/api/domestic/orders/update" style="width:100%;min-width:80px;border:1px solid #d9d9d9;border-radius:3px;padding:2px 4px;font-size:12px;background:var(--bg,#fff);color:var(--text,#333)" autocomplete="off"></td>'; },
'仓库': function(r) { return '<td><input type="text" class="form-input" style="width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px;min-width:80px" value="' + escHtml(r['仓库'] || '') + '" data-pid="' + r.id + '" data-pfield="仓库" data-papi="/api/domestic/orders/update" onkeydown="if(event.key===\'Enter\'){event.preventDefault();var inp=this;var v=inp.value;if(confirm(\'确认保存仓库为 \'+v+\'?\')){fetch(\'/api/domestic/orders/update\',{method:\'POST\',headers:{\'Content-Type\':\'application/json\'},body:JSON.stringify({id:inp.dataset.pid,field:\'仓库\',value:v})}).then(function(r){return r.json()}).then(function(res){if(res.code===0){inp.blur();inp.style.borderColor=\'#52c41a\';setTimeout(function(){inp.style.borderColor=\'\'},800)}else{alert(res.msg||\'保存失败\')}}).catch(function(){alert(\'网络错误\')})}}"></td>'; },
'金蝶采购单': function(r) { return '<td><code>' + escHtml(r['金蝶采购单'] || '') + '</code></td>'; },
'采购员': function(r) { return '<td style="font-size:11px">' + renderNameBadges(r['采购员']) + '</td>'; },
'下单人': function(r) { return '<td>' + renderOrdererSelect(r) + '</td>'; },
'进度': function(r) { return '<td>' + escHtml(r['进度'] || '') + '</td>'; },
'旺店通生产单号': function(r) { return '<td><code>' + escHtml(r['旺店通生产单号'] || '') + '</code></td>'; },
'是否已录入旺店通': function(r) { return '<td>' + escHtml(r['是否已录入旺店通'] || '') + '</td>'; },
'采购开发': function(r) { return '<td>' + renderNameBadges(r['采购开发']) + '</td>'; },
'实际到货日期': function(r) { return '<td>' + escHtml(r['实际到货日期'] || '') + '</td>'; },
'未到货数': function(r) { return '<td><strong>' + (parseInt(r['未到货数']) || 0).toLocaleString() + '</strong></td>'; },
'预计到货日期': function(r) { return '<td>' + escHtml(r['预计到货日期'] || '') + '</td>'; },
'金蝶入库': function(r) { return '<td>' + escHtml(r['金蝶入库'] || '') + '</td>'; },
'状态': function(r) { return '<td><span class="badge">' + escHtml(r['状态'] || '') + '</span></td>'; },
'实际到货数量': function(r) { return '<td>' + ((r['实际到货数量'] || 0) > 0 ? parseInt(r['实际到货数量']).toLocaleString() : '0') + '</td>'; }
};
data.rows.forEach(function(r, i) {
var rowNum = startIdx + i + 1;
html += '<tr>';
html += '<td style="position:sticky;left:0;z-index:2;background:var(--card-bg,#fff);text-align:center"><input type="checkbox" class="domestic-checkbox" data-id="' + r.id + '"></td>';
html += '<td style="position:sticky;left:40px;z-index:2;background:var(--card-bg,#fff);text-align:center;color:#999">' + rowNum + '</td>';
// 按列顺序和可见性渲染单元格
var order = window._domesticDragOrder || DOMESTIC_COLUMNS.map(function(c) { return c.field; });
order.forEach(function(field) {
if (!window.domesticColVisible[field]) return;
var renderer = domesticCellRenderers[field];
if (renderer) {
html += renderer(r);
} else {
// 未定义的字段默认渲染
html += '<td>' + escHtml(r[field] || '') + '</td>';
}
});
// 做货周期 = 实际到货日期 - 跟单下单日期
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 += '<td style="text-align:center;font-weight:600">' + manuDays + '</td>';
html += '<td style="position:sticky;right:0;z-index:9;background:var(--card-bg,#fff);border-left:1px solid var(--border);white-space:nowrap;padding:10px 8px">';
html += '<button class="btn btn-sm btn-outline" onclick="showOrderDetail(\'' + encodeURIComponent(JSON.stringify(r)).replace(/'/g, '%27') + '\',\'国内订单详情\')" title="详情" style="font-size:11px;color:#1890ff">详情</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="generateContractForOrders(\'domestic\',[' + r.id + '])" title="生成合同" style="font-size:11px;color:#52c41a;border-color:#52c41a">生成合同</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="domesticDeleteRow(' + r.id +')" title="删除" style="font-size:11px;color:#f5222d;border-color:#f5222d">删除</button>';
html += '</td>';
html += '</tr>';
});
tbody.innerHTML = html;
// 下单人多选自动保存
tbody.querySelectorAll('.orderer-select').forEach(function(sel) {
// 初始化显示选中项的颜色
updateOrdererSelectColor(sel);
sel.addEventListener('change', function() {
var id = this.getAttribute('data-id');
var value = this.value;
updateOrdererSelectColor(this);
fetch('/api/domestic/orders/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, field: '下单人', value: value})
}).then(function(r){return r.json()});
});
});
function updateOrdererSelectColor(selEl) {
var selectedOpt = selEl.options[selEl.selectedIndex];
if (selectedOpt && selectedOpt.value) {
var bg = selectedOpt.style.background || '#fff';
var fc = selectedOpt.style.color || '#333';
selEl.style.background = bg;
selEl.style.color = fc;
} else {
selEl.style.background = '#fff';
selEl.style.color = '#333';
}
}
}
window._applyTableFieldPerms && window._applyTableFieldPerms('domesticOrdersTable', 'orders_domestic');
// 先恢复保存的列宽,再计算冻结列位置(基于实际宽度)
restoreColumnWidths('domesticOrdersTable', 'domesticColWidths');
applyDomesticFrozenPositions();
enableColumnResize('domesticOrdersTable', 'domesticColWidths');
var pel = document.getElementById('domesticOrdersPagination');
if (pel) {
if (typeof renderPaginationWithSize === 'function') {
renderPaginationWithSize(pel, page, data.total || 0, DOMESTIC_PAGE_SIZE, 'domesticLoadOrders', 'DOMESTIC_PAGE_SIZE');
} else {
// 简单分页
var totalPages = Math.ceil((data.total || 0) / DOMESTIC_PAGE_SIZE);
pel.innerHTML = '<div style="display:flex;gap:5px;align-items:center;justify-content:center">';
var phtml = '';
if (page > 1) phtml += '<button class="btn btn-sm" onclick="domesticLoadOrders(' + (page-1) + ')">上一页</button>';
phtml += '<span style="padding:0 10px;font-size:13px">第 ' + page + '/' + totalPages + ' 页</span>';
if (page < totalPages) phtml += '<button class="btn btn-sm" onclick="domesticLoadOrders(' + (page+1) + ')">下一页</button>';
pel.innerHTML += phtml + '</div>';
}
}
});
}
// ─── 状态筛选框同步:根据列表内容更新状态选项 ───
function syncStatusFilter(type, rows) {
if (!rows || rows.length === 0) return;
var selectId = type + 'StatusFilter';
var sel = document.getElementById(selectId);
if (!sel) return;
var currentVal = sel.value;
// 收集已有选项
var existing = {};
[].forEach.call(sel.options, function(opt) {
if (opt.value) existing[opt.value] = true;
});
// 补充数据中有的状态(不替换已有选项)
(rows || []).forEach(function(r) {
var s = r['状态'] || '';
if (s && !existing[s]) {
existing[s] = true;
var opt = document.createElement('option');
opt.value = s;
opt.textContent = s;
sel.appendChild(opt);
}
});
if (currentVal) sel.value = currentVal;
}
// ─── 恢复保存的列宽(在冻结列位置计算之前调用)───
function restoreColumnWidths(tableId, storageKey) {
var table = document.getElementById(tableId);
if (!table) return;
var thead = table.querySelector('thead');
if (!thead) return;
var ths = thead.querySelectorAll('tr:first-child th');
var lastRestore = ths.length - 1;
try {
var raw = localStorage.getItem(storageKey);
var saved = null;
if (raw) {
saved = JSON.parse(raw);
if (!saved._v || saved._v !== ths.length || !saved._c) {
saved = null;
localStorage.removeItem(storageKey);
}
}
// 没有保存的宽度时,只锁住非自适应列(跳过操作列)
if (!saved) {
saved = {_v: ths.length, _c: 1};
for (var i = 0; i < ths.length; i++) {
if (i === lastRestore || ths[i].classList.contains('ac')) continue;
saved[i] = ths[i].offsetWidth + 'px';
}
try { localStorage.setItem(storageKey, JSON.stringify(saved)); } catch(e){}
}
// 恢复宽度(跳过操作列;自适应列只在被拖拽过(有保存值)时才恢复)
for (var i = 0; i < ths.length; i++) {
if (i === lastRestore) continue;
if (saved[i] && (!ths[i].classList.contains('ac') || saved._acSet)) {
ths[i].style.width = saved[i];
}
}
} catch(e){}
}
// ─── 列宽拖拽调整 ───
var _resizeData = null;
function enableColumnResize(tableId, storageKey) {
var table = document.getElementById(tableId);
if (!table) return;
var thead = table.querySelector('thead');
if (!thead) return;
var ths = thead.querySelectorAll('tr:first-child th');
// 为每个 th 添加拖拽手柄(跳过最后一列的操作列)
var lastTh = ths.length - 1;
for (var i = 0; i < ths.length; i++) {
var th = ths[i];
// 跳过最后一列(操作列)
if (i === lastTh) continue;
// 已有手柄则不重复添加
if (th.querySelector('.col-resize-handle')) continue;
var handle = document.createElement('div');
handle.className = 'col-resize-handle';
handle.style.cssText = 'position:absolute;right:-1px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:20';
handle.addEventListener('mousedown', function(idx) {
return function(ev) {
ev.preventDefault();
ev.stopPropagation();
var thEl = ths[idx];
// 保存所有列的当前宽度,拖拽时锁定防止重排
var allW = {};
for (var ai = 0; ai < ths.length; ai++) {
if (ai === ths.length - 1) continue;
allW[ai] = ths[ai].offsetWidth;
}
_resizeData = {
tableId: tableId,
storageKey: storageKey,
table: table,
th: thEl,
idx: idx,
startX: ev.clientX,
startW: thEl.offsetWidth,
allWidths: allW
};
};
}(i));
// 已有 sticky 的不覆盖(冻结列需要保持 sticky)
if (th.style.position !== 'sticky') {
th.style.position = 'relative';
}
th.appendChild(handle);
}
}
// 全局 mousemove/mouseup 处理列拖拽
if (!window._colResizeInit) {
window._colResizeInit = true;
document.addEventListener('mousemove', function(e) {
if (!_resizeData) return;
var diff = e.clientX - _resizeData.startX;
var newW = Math.max(30, _resizeData.startW + diff);
_resizeData.th.style.width = newW + 'px';
_resizeData.th.style.minWidth = newW + 'px';
// 拖拽时锁定其他所有列的宽度,防止浏览器自动重排
if (_resizeData.allWidths && _resizeData.table) {
try {
var allThs = _resizeData.table.querySelectorAll('thead tr:first-child th');
var lIdx = allThs.length - 1;
for (var ti = 0; ti < allThs.length; ti++) {
if (ti === lIdx || ti === _resizeData.idx) continue;
if (_resizeData.allWidths[ti]) {
allThs[ti].style.width = _resizeData.allWidths[ti] + 'px';
}
}
} catch(e){}
}
});
document.addEventListener('mouseup', function(e) {
if (!_resizeData) return;
// 保存所有列当前宽度,防止其他列被自动重排影响
var table = document.getElementById(_resizeData.tableId);
if (table) {
try {
var allThs = table.querySelectorAll('thead tr:first-child th');
var allW = {_v: allThs.length, _c: 1};
var lastSaveIdx = allThs.length - 1;
for (var ti = 0; ti < allThs.length; ti++) {
if (ti === lastSaveIdx) continue;
if (allThs[ti].classList.contains('ac')) allW._acSet = true;
allW[ti] = allThs[ti].offsetWidth + 'px';
}
localStorage.setItem(_resizeData.storageKey, JSON.stringify(allW));
} catch(e){}
}
// 重新应用冻结列位置(因为列宽变了)
if (_resizeData.tableId === 'domesticOrdersTable') {
applyDomesticFrozenPositions();
} else if (_resizeData.tableId === 'overseasOrdersTable') {
applyOverseasFrozenPositions();
} else if (_resizeData.tableId === 'packagingOrdersTable') {
applyPackagingFrozenPositions();
}
_resizeData = null;
});
}
// ─── 动态计算国内订单冻结列位置(基于实际渲染宽度) ───
function applyDomesticFrozenPositions() {
if (!domesticFrozen || !domesticFrozenFields || domesticFrozenFields.length === 0) return;
var OFFSET = 1; // 对话框索引偏移(勾选框始终冻结且不在对话框内)
var table = document.getElementById('domesticOrdersTable');
if (!table) return;
var headerRow = table.querySelector('thead tr');
if (!headerRow) return;
var scope = headerRow.querySelectorAll('th');
var frozenSet = {0: true}; // 勾选框始终冻结
for (var fi = 0; fi < domesticFrozenFields.length; fi++) frozenSet[domesticFrozenFields[fi] + OFFSET] = true;
// 清除非冻结列 th 的 sticky(保留最后一列的操作列)
var lastThIdx = scope.length - 1;
for (var i = 0; i < scope.length; i++) {
if (i === lastThIdx) continue;
if (!frozenSet[i]) {
if (!scope[i].querySelector('.col-resize-handle')) {
scope[i].style.position = '';
} else {
scope[i].style.position = 'relative';
}
scope[i].style.left = '';
scope[i].style.zIndex = '';
}
}
// 一次性测量所有冻结列宽度(勾选框排首位)
var widths = [];
var acc = 0;
var cbW = scope[0].offsetWidth;
widths.push(cbW);
acc += cbW;
for (var fi = 0; fi < domesticFrozenFields.length; fi++) {
var idx = domesticFrozenFields[fi] + OFFSET;
if (idx >= scope.length) break;
var th = scope[idx];
var w = th.offsetWidth;
widths.push(w);
var leftVal = acc + 'px';
th.style.position = 'sticky';
th.style.top = '0';
th.style.left = leftVal;
th.style.zIndex = fi < 2 ? '3' : '11';
th.style.background = 'var(--bg,#f5f5f5)';
acc += w;
}
// 更新 td
var tbody = table.querySelector('tbody');
if (tbody && widths.length > 0) {
var rows = tbody.rows;
for (var ri = 0; ri < rows.length; ri++) {
var tr = rows[ri];
var tds = tr.cells;
var lastIdx = tds.length - 1;
for (var i = 0; i < tds.length; i++) {
// 跳过最后一列(操作列),始终保留 sticky-right
if (i === lastIdx) continue;
if (!frozenSet[i]) {
tds[i].style.position = '';
tds[i].style.left = '';
tds[i].style.zIndex = '';
}
}
var acc2 = 0;
// 勾选框 td 始终冻结
tds[0].style.position = 'sticky';
tds[0].style.left = acc2 + 'px';
tds[0].style.zIndex = '2';
tds[0].style.background = 'var(--card-bg,#fff)';
acc2 += widths[0];
for (var fi = 0; fi < domesticFrozenFields.length; fi++) {
var idx = domesticFrozenFields[fi] + OFFSET;
if (idx >= tds.length) break;
tds[idx].style.position = 'sticky';
tds[idx].style.left = acc2 + 'px';
tds[idx].style.zIndex = '2';
tds[idx].style.background = 'var(--card-bg,#fff)';
acc2 += widths[fi + 1];
}
}
}
}
// ─── 动态计算海外订单冻结列位置 ───
function applyOverseasFrozenPositions() {
if (!overseasFrozen || !overseasFrozenFields || overseasFrozenFields.length === 0) return;
var OFFSET = 1;
var table = document.getElementById('overseasOrdersTable');
if (!table) return;
var headerRow = table.querySelector('thead tr');
if (!headerRow) return;
var scope = headerRow.querySelectorAll('th');
var frozenSet = {0: true};
for (var fi = 0; fi < overseasFrozenFields.length; fi++) frozenSet[overseasFrozenFields[fi] + OFFSET] = true;
// 清除非冻结列 th 的 sticky(保留拖拽手柄和右侧操作列)
for (var i = 0; i < scope.length; i++) {
if (!frozenSet[i]) {
if (scope[i].style.position === 'sticky' && scope[i].style.right !== '') continue;
if (!scope[i].querySelector('.col-resize-handle')) {
scope[i].style.position = '';
} else {
scope[i].style.position = 'relative';
}
scope[i].style.left = '';
scope[i].style.zIndex = '';
}
}
// 一次性测量所有冻结列宽度(勾选框排首位)
var widths = [];
var acc = 0;
var cbW = scope[0].offsetWidth;
widths.push(cbW);
acc += cbW;
for (var fi = 0; fi < overseasFrozenFields.length; fi++) {
var idx = overseasFrozenFields[fi] + OFFSET;
if (idx >= scope.length) break;
var th = scope[idx];
var w = th.offsetWidth;
widths.push(w);
var leftVal = acc + 'px';
th.style.position = 'sticky';
th.style.top = '0';
th.style.left = leftVal;
th.style.zIndex = fi < 2 ? '3' : '11';
th.style.background = 'var(--bg,#f5f5f5)';
acc += w;
}
// 更新 td
var tbody = table.querySelector('tbody');
if (tbody && widths.length > 0) {
var rows = tbody.rows;
for (var ri = 0; ri < rows.length; ri++) {
var tr = rows[ri];
var tds = tr.cells;
for (var i = 0; i < tds.length; i++) {
if (!frozenSet[i]) {
if (tds[i].style.position === 'sticky' && tds[i].style.right !== '') continue;
tds[i].style.position = '';
tds[i].style.left = '';
tds[i].style.zIndex = '';
}
}
var acc2 = 0;
tds[0].style.position = 'sticky';
tds[0].style.left = acc2 + 'px';
tds[0].style.zIndex = '2';
tds[0].style.background = 'var(--card-bg,#fff)';
acc2 += widths[0];
for (var fi = 0; fi < overseasFrozenFields.length; fi++) {
var idx = overseasFrozenFields[fi] + OFFSET;
if (idx >= tds.length) break;
tds[idx].style.position = 'sticky';
tds[idx].style.left = acc2 + 'px';
tds[idx].style.zIndex = '2';
tds[idx].style.background = 'var(--card-bg,#fff)';
acc2 += widths[fi + 1];
}
}
}
}
window.domesticEditRow = function(id) {
// 简单编辑弹窗
var row = document.querySelector('#domesticOrdersTbody tr:nth-child(' + (id) + ')');
var newVal = prompt('输入新值(格式: 字段名=值),例如: 状态=已完结');
if (!newVal) return;
var parts = newVal.split('=');
if (parts.length < 2) { alert('格式错误,请用: 字段名=值'); return; }
var field = parts[0].trim();
var value = parts.slice(1).join('=').trim();
fetch('/api/domestic/orders/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, field: field, value: value})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) { domesticLoadOrders(domesticOrderPage); }
else { alert(res.msg || '更新失败'); }
});
};
window.domesticDeleteRow = function(id) {
if (!confirm('确认删除此记录?')) return;
fetch('/api/domestic/orders/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('domestic');
domesticLoadOrders(domesticOrderPage);
}
else { alert(res.msg || '删除失败'); }
});
};
function domesticToggleAll(master) {
var cbs = document.querySelectorAll('.domestic-checkbox');
cbs.forEach(function(cb) { cb.checked = master.checked; });
}
function domesticBatchGenerateContract() {
var cbs = document.querySelectorAll('.domestic-checkbox:checked');
var ids = [];
cbs.forEach(function(cb) { ids.push(parseInt(cb.getAttribute('data-id'))); });
if (ids.length === 0) { alert('请先勾选要生成合同的订单'); return; }
generateContractForOrders('domestic', ids);
}
window.domesticAddRow = function() {
var code = prompt('请输入编码(必填):');
if (!code) return;
var name = prompt('请输入名称:');
fetch('/api/domestic/orders/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({编码: code, 名称: name || ''})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('domestic');
domesticLoadOrders(1);
}
else { alert(res.msg || '添加失败'); }
});
};
window.domesticExport = function() {
// 简单 CSV 导出
var tbody = document.getElementById('domesticOrdersTbody');
if (!tbody) return;
var lines = [];
var headers = ['#','货品编码','货品名称','跟单下单日期','计划下单时间','新旧品','下单备注','单价','审核价格','数量','金额','供货商','仓库','金蝶采购单','采购组别','下单人','进度','旺店通单号','录入旺店通','采购开发','实际到货日期','未到货数','预计到货日期','金蝶入库','状态'];
lines.push(headers.join(','));
var rows = tbody.querySelectorAll('tr');
rows.forEach(function(tr) {
var cols = tr.querySelectorAll('td');
var row = [];
cols.forEach(function(td) {
var txt = td.textContent.trim().replace(/,/g, ',').replace(/"/g, '""');
row.push('"' + txt + '"');
});
lines.push(row.join(','));
});
if (lines.length <= 1) return;
var blob = new Blob([lines.join('\n')], {type: 'text/csv;charset=utf-8'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '国内订单_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
};
// ──────────── 海外订单管理 ────────────
var OVERSEAS_PAGE_SIZE = 100;
var overseasOrderPage = 1;
var overseasSearch = '';
var overseasFilterStatus = '';
var overseasFilterSupplier = '';
var overseasFilterOrderer = '';
var overseasOrderFilter = {};
var overseasOrderFilterActive = {};
var overseasOrderFilterMenu = null;
var overseasOrderFilterField = '';
var overseasFrozen = localStorage.getItem('overseasFrozen') !== 'false';
var overseasFrozenFields = (function(){
var arr;
try { arr = JSON.parse(localStorage.getItem('overseasFrozenFields')) || [0,1,2]; }
catch(e){ arr = [0,1,2]; }
return arr.filter(function(v) { return v < 3; });
})();
function loadOverseasOrdersPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>海外订单管理</h1>
<div style="display:flex;gap:10px;margin-top:8px;flex-wrap:wrap;align-items:center">
<div class="search-box" style="max-width:300px">
<input type="text" id="overseasSearchInput" class="form-input" placeholder="搜索编码/名称..." autocomplete="off"
onkeydown="if(event.key==='Enter') overseasDoSearch()" style="padding-left:32px;padding-right:8px;height:32px;font-size:13px">
<svg style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#999" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
<input type="text" id="overseasSupplierFilter" class="form-input" placeholder="供应商..." style="width:auto;min-width:120px;height:32px;font-size:13px" onkeydown="if(event.key==='Enter') overseasApplyFilters()">
<select id="overseasStatusFilter" class="form-input" style="width:auto;min-width:120px;height:32px;font-size:13px" onchange="overseasApplyFilters()">
<option value="">全部状态</option>
</select>
<select id="overseasOrdererFilter" class="form-input" style="width:auto;min-width:100px;height:32px;font-size:13px" onchange="overseasApplyFilters()">
<option value="">全部下单人</option>
</select>
<button class="btn btn-sm btn-primary" onclick="overseasDoSearch()">查询</button>
<button class="btn btn-sm btn-outline" onclick="overseasResetFilters()">重置</button>
<span id="overseasTotalInfo" style="font-size:13px;color:var(--text-light);margin-left:auto"></span>
</div>
</div>
<div id="overseasContent" class="page-flex">
<div class="card">
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
<h3>🌍 海外采购下单明细</h3>
<div>
<button class="btn btn-sm btn-outline" onclick="overseasBatchGenerateContract()" style="color:#52c41a;border-color:#52c41a">📄 批量生成合同</button>
<button class="btn btn-sm btn-primary" onclick="overseasAddRow()">+ 新增</button>
<button class="btn btn-sm btn-outline" onclick="overseasExport()">导出</button>
<button class="btn btn-sm btn-outline" onclick="openOverseasFrozenModal()">🔒 冻结列</button>
<button class="btn btn-sm btn-outline" onclick="openOverseasColModal()">⚙ 字段设置</button>
</div>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="overseasOrdersTable" style="font-size:12px;white-space:nowrap;min-width:5800px">
<thead style="position:sticky;top:0;z-index:10;background:var(--bg,#f5f5f5)"><tr id="overseasOrdersHeader">
<th style="position:sticky;left:0;z-index:3;background:var(--bg,#f5f5f5);min-width:40px;text-align:center"><input type="checkbox" id="overseasSelectAll" onchange="overseasToggleAll(this)"></th>
<th style="position:sticky;left:40px;z-index:3;background:var(--bg,#f5f5f5);min-width:50px">序号</th>`;
// 动态生成海外表头列
(function() {
var cols = OVERSEAS_COLUMNS;
var order = window._overseasDragOrder || cols.map(function(c) { return c.field; });
order.forEach(function(field) {
var col = cols.find(function(c) { return c.field === field; });
if (!col) return;
if (!window.overseasColVisible[field]) return;
var fieldAttr = col.field;
var thStyle = '';
if (fieldAttr === '产品编码') thStyle = 'min-width:120px;position:sticky;left:90px;z-index:3;background:var(--bg,#f5f5f5)';
else if (fieldAttr === '货品名称') thStyle = 'min-width:200px;position:sticky;left:210px;z-index:3;background:var(--bg,#f5f5f5)';
else if (fieldAttr === '日期') thStyle = 'width:85px;min-width:85px;position:sticky;left:410px;z-index:3;background:var(--bg,#f5f5f5)';
else if (fieldAttr === '计划下单时间' || fieldAttr === '新旧品') thStyle = 'width:1%;white-space:nowrap';
else if (fieldAttr === '单价') thStyle = 'min-width:70px';
else if (fieldAttr === '是否审核价格') thStyle = 'min-width:80px';
else if (fieldAttr === '数量') thStyle = 'min-width:60px';
else if (fieldAttr === '金额') thStyle = 'min-width:80px';
else if (fieldAttr === '供应商') thStyle = 'min-width:100px';
else if (fieldAttr === '仓库') thStyle = 'min-width:80px';
else if (fieldAttr === '是否已做采购单') thStyle = 'min-width:90px';
else if (fieldAttr === '是否做入库') thStyle = 'min-width:70px';
else if (fieldAttr === '采购员') thStyle = 'min-width:150px';
else if (fieldAttr === '下单人') thStyle = 'min-width:70px';
else if (fieldAttr === '进度') thStyle = 'min-width:100px';
else if (fieldAttr === '下单备注') thStyle = 'min-width:150px;white-space:normal';
else if (fieldAttr === '采购开发') thStyle = 'min-width:100px';
else if (fieldAttr === '旺店通生产单号') thStyle = 'min-width:120px';
else if (fieldAttr === '是否已录入到旺店通') thStyle = 'min-width:90px';
else if (fieldAttr === '实际到货日期' || fieldAttr === '预计到货日期') thStyle = 'min-width:100px';
else if (fieldAttr === '未到货数') thStyle = 'min-width:80px';
else if (fieldAttr === '状态') thStyle = 'min-width:70px';
else { thStyle = ''; }
container.querySelector('#overseasOrdersHeader').innerHTML += '<th data-field="' + fieldAttr + '" style="' + thStyle + '">' + col.label + ' <span class="order-filt-icon" onclick="event.stopPropagation();openOrderFilterMenu(\'overseas\',\'' + fieldAttr + '\',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>';
});
})();
container.querySelector('#overseasOrdersHeader').innerHTML += '<th style="min-width:70px">做货周期</th>';
container.querySelector('#overseasOrdersHeader').innerHTML += '<th style="width:1%;white-space:nowrap;padding:12px 8px;position:sticky;right:0;top:0;z-index:13;background:var(--bg,#f5f5f5);border-left:1px solid var(--border);text-align:center">操作</th>';
container.querySelector('#overseasOrdersTable').innerHTML += '</thead><tbody id="overseasOrdersTbody"><tr><td colspan="28" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>';
container.querySelector('#overseasContent').innerHTML += '</div></div><div id="overseasOrdersPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div></div>';
// 海外冻结列弹窗 + 字段设置弹窗
container.innerHTML += `
<div class="modal-overlay" id="overseasFrozenModal" style="display:none">
<div class="modal" style="width:400px">
<div class="modal-header">
<h3>🔒 冻结列设置</h3>
<span class="modal-close" onclick="closeOverseasFrozenModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:60vh;overflow-y:auto;padding:12px 20px" id="overseasFrozenModalBody">
</div>
<div class="modal-footer">
<button class="btn" onclick="closeOverseasFrozenModal()">取消</button>
<button class="btn btn-primary" onclick="confirmOverseasFrozen()">确定</button>
</div>
</div>
</div>
<!-- 字段设置弹窗 -->
<div class="modal-overlay" id="overseasColModal" style="display:none">
<div class="modal" style="width:720px">
<div class="modal-header">
<h3>字段设置</h3>
<span class="modal-close" onclick="closeOrderColModal('overseas')">&times;</span>
</div>
<div class="modal-body" style="max-height:65vh;overflow-y:auto;padding:16px 20px">
<!-- 方案管理 -->
<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="font-weight:600;font-size:14px;white-space:nowrap">📁 方案管理</span>
<input type="text" id="overseasSchemeNameInput" class="form-input" placeholder="输入方案名称..." style="flex:1;min-width:0">
<button class="btn btn-primary btn-sm" onclick="saveOrderScheme('overseas')">💾 保存方案</button>
</div>
<div id="overseasSchemeList" style="display:flex;flex-wrap:wrap;gap:6px;min-height:28px">
<span style="color:var(--text-light);font-size:13px">暂无保存方案</span>
</div>
</div>
<div class="col-toggle-all" style="padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:8px">
<label class="cb-all"><input type="checkbox" id="overseas-cb-all" checked onchange="toggleOrderColAll('overseas',this.checked)"> <strong>全选</strong></label>
</div>
<input type="text" id="overseasColSearchInput" class="form-input" placeholder="搜索字段名称..." oninput="filterOrderColCheckboxes('overseas')" style="margin-bottom:8px;width:100%;box-sizing:border-box">
<div id="overseasColCheckboxes"><div id="overseasColGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px 12px;padding:4px 0"></div></div>
</div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:12px;color:var(--text-light)">拖拽字段名可调整列顺序</span>
<div>
<button class="btn" onclick="closeOrderColModal('overseas')">取消</button>
<button class="btn btn-primary" onclick="confirmOrderCol('overseas')">确定</button>
</div>
</div>
</div>
</div>`;
// 初始化字段设置checkbox
var ocGrid = document.getElementById('overseasColGrid');
if (ocGrid) {
window._overseasDragOrder = OVERSEAS_COLUMNS.map(function(c) { return c.field; });
renderOrderColCheckboxes('overseas');
}
overseasLoadFilters();
overseasLoadOrders(1);
loadDefaultOrderScheme('overseas');
// 后台预拉取所有列的筛选值
prefetchOrderColumnValues('overseas');
}
function overseasLoadFilters() {
fetch('/api/overseas/orders/filters')
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var ss = document.getElementById('overseasStatusFilter');
var so = document.getElementById('overseasOrdererFilter');
(res.data.statuses || []).forEach(function(s) {
var opt = document.createElement('option');
opt.value = s; opt.textContent = s;
ss.appendChild(opt);
});
(res.data.orderers || []).forEach(function(s) {
var opt = document.createElement('option');
opt.value = s; opt.textContent = s;
so.appendChild(opt);
});
});
}
function overseasDoSearch() {
overseasApplyFilters();
}
function overseasApplyFilters() {
overseasSearch = document.getElementById('overseasSearchInput').value.trim();
overseasFilterSupplier = document.getElementById('overseasSupplierFilter').value.trim();
overseasFilterStatus = document.getElementById('overseasStatusFilter').value;
overseasFilterOrderer = document.getElementById('overseasOrdererFilter').value;
overseasLoadOrders(1);
}
function overseasResetFilters() {
document.getElementById('overseasSearchInput').value = '';
document.getElementById('overseasSupplierFilter').value = '';
document.getElementById('overseasStatusFilter').value = '';
document.getElementById('overseasOrdererFilter').value = '';
overseasSearch = '';
overseasFilterSupplier = '';
overseasFilterStatus = '';
overseasFilterOrderer = '';
overseasOrderFilter = {};
overseasOrderFilterActive = {};
_syncOrderFilterIcons('overseas');
overseasLoadOrders(1);
}
function overseasLoadOrders(page) { overseasOrderPage = page;
var params = '?page=' + page + '&size=' + OVERSEAS_PAGE_SIZE;
if (overseasSearch) params += '&search=' + encodeURIComponent(overseasSearch);
if (overseasFilterStatus) params += '&status=' + encodeURIComponent(overseasFilterStatus);
if (overseasFilterSupplier) params += '&supplier=' + encodeURIComponent(overseasFilterSupplier);
if (overseasFilterOrderer) params += '&orderer=' + encodeURIComponent(overseasFilterOrderer);
var overseasFilterStr = '';
if (Object.keys(overseasOrderFilter).length > 0) {
overseasFilterStr = encodeURIComponent(JSON.stringify(overseasOrderFilter));
params += '&order_filter=' + overseasFilterStr;
}
fetch('/api/overseas/orders/list' + params)
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var data = res.data;
var tbody = document.getElementById('overseasOrdersTbody');
var totalInfo = document.getElementById('overseasTotalInfo');
if (totalInfo) totalInfo.textContent = '共 ' + data.total + ' 条';
if (!data.rows || data.rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="28" style="text-align:center;padding:60px;color:var(--text-light)">暂无数据</td></tr>';
} else {
var startIdx = (page - 1) * OVERSEAS_PAGE_SIZE;
var html = '';
data.rows.forEach(function(r, i) {
var rowNum = startIdx + i + 1;
html += '<tr>';
html += '<td style="position:sticky;left:0;z-index:2;background:var(--card-bg,#fff);text-align:center"><input type="checkbox" class="overseas-checkbox" data-id="' + r.id + '"></td>';
html += '<td style="position:sticky;left:40px;z-index:2;background:var(--card-bg,#fff);text-align:center;color:#999">' + rowNum + '</td>';
// 按列顺序和可见性渲染单元格
var order = window._overseasDragOrder || OVERSEAS_COLUMNS.map(function(c) { return c.field; });
order.forEach(function(field) {
if (!window.overseasColVisible[field]) return;
var cellHtml = '';
if (field === '产品编码') {
cellHtml = '<td style="position:sticky;left:90px;z-index:2;background:var(--card-bg,#fff)"><code style="cursor:pointer;color:#1890ff" onclick="showSkuInfoModal(this,\'海外订单详情\')">' + escHtml(r['产品编码'] || '') + '</code></td>';
} else if (field === '货品名称') {
cellHtml = '<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;position:sticky;left:210px;z-index:2;background:var(--card-bg,#fff)" title="' + escHtml(r['货品名称'] || '') + '">' + escHtml(r['货品名称'] || '') + '</td>';
} else if (field === '日期') {
cellHtml = '<td style="position:sticky;left:410px;z-index:2;background:var(--card-bg,#fff)"><span class="date-text">' + escHtml(window._padDateStr(r['日期'] || '')) + '</span> <span class="date-edit-btn" onclick="event.stopPropagation();editDateField(this.parentNode,' + r.id + ',\'日期\',\'/api/overseas/orders/update\')" style="cursor:pointer;color:#1890ff;font-size:12px">📅</span></td>';
} else if (field === '计划下单时间') {
var planTime = window._padDateStr(r['计划下单时间'] || '');
if (planTime.length > 16) planTime = planTime.slice(0, 16);
cellHtml = '<td>' + escHtml(planTime) + '</td>';
} else if (field === '单价') {
cellHtml = '<td style="padding:2px 4px"><input type="text" class="price-inline" value="' + (r['单价'] ? parseFloat(r['单价']) : '') + '" onclick="this.select()" oninput="recalcAmount(this)" data-pid="' + r.id + '" data-pfield="单价" data-papi="/api/overseas/orders/update" style="width:70px;border:1px solid #d9d9d9;border-radius:3px;padding:2px 4px;font-size:12px;text-align:right;background:var(--bg,#fff);color:var(--text,#333)"></td>';
} else if (field === '是否审核价格') {
cellHtml = '<td>' + escHtml(r['是否审核价格'] || '') + '</td>';
} else if (field === '数量') {
cellHtml = '<td class="qty-cell"><strong>' + (parseInt(r['数量']) || 0).toLocaleString() + '</strong></td>';
} else if (field === '金额') {
var priceVal = parseFloat(r['单价']) || 0;
var qtyVal = parseInt(r['数量']) || 0;
var amtVal = priceVal * qtyVal;
cellHtml = '<td class="amt-cell">' + (amtVal ? '¥' + amtVal.toLocaleString() : '') + '</td>';
} else if (field === '供应商') {
cellHtml = '<td>' + escHtml(r['供应商'] || '') + '</td>';
} else if (field === '仓库') {
var orderId = r.id || '';
var whVal = r['仓库'] || '';
cellHtml = '<td><input type="text" class="form-input" style="width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px;min-width:80px" value="' + escHtml(whVal) + '" data-id="' + orderId + '" data-field="仓库" onkeydown="if(event.key===\'Enter\'){event.preventDefault();var inp=this;var v=inp.value;if(confirm(\'确认保存仓库为 \'+v+\'?\')){fetch(\'/api/domestic/orders/update\',{method:\'POST\',headers:{\'Content-Type\':\'application/json\'},body:JSON.stringify({id:inp.dataset.id,field:\'仓库\',value:v})}).then(function(r){return r.json()}).then(function(res){if(res.code===0){inp.blur();inp.style.borderColor=\'#52c41a\';setTimeout(function(){inp.style.borderColor=\'\'},800)}else{alert(res.msg||\'保存失败\')}}).catch(function(){alert(\'网络错误\')})}}"></td>';
} else if (field === '新旧品') {
cellHtml = '<td>' + escHtml(r['新旧品'] || '') + '</td>';
} else if (field === '是否已做采购单') {
cellHtml = '<td>' + escHtml(r['是否已做采购单'] || '') + '</td>';
} else if (field === '是否做入库') {
cellHtml = '<td>' + escHtml(r['是否做入库'] || '') + '</td>';
} else if (field === '采购员') {
cellHtml = '<td style="font-size:11px">' + renderNameBadges(r['采购员']) + '</td>';
} else if (field === '下单人') {
cellHtml = '<td>' + renderPersonBadges(r['下单人']) + '</td>';
} else if (field === '进度') {
cellHtml = '<td>' + escHtml(r['进度'] || '') + '</td>';
} else if (field === '下单备注') {
cellHtml = '<td title="' + escHtml(r['下单备注'] || '') + '" style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(r['下单备注'] || '-') + '</td>';
} else if (field === '采购开发') {
cellHtml = '<td>' + renderNameBadges(r['采购开发']) + '</td>';
} else if (field === '旺店通生产单号') {
cellHtml = '<td><code>' + escHtml(r['旺店通生产单号'] || '') + '</code></td>';
} else if (field === '是否已录入到旺店通') {
cellHtml = '<td>' + escHtml(r['是否已录入到旺店通'] || '') + '</td>';
} else if (field === '实际到货日期') {
cellHtml = '<td>' + escHtml(r['实际到货日期'] || '') + '</td>';
} else if (field === '预计到货日期') {
cellHtml = '<td>' + escHtml(r['预计到货日期'] || '') + '</td>';
} else if (field === '实际到货数量') {
cellHtml = '<td>' + ((r['实际到货数量'] || 0) > 0 ? parseInt(r['实际到货数量']).toLocaleString() : '0') + '</td>';
} else if (field === '未到货数') {
cellHtml = '<td><strong>' + (parseInt(r['未到货数']) || 0).toLocaleString() + '</strong></td>';
} else if (field === '状态') {
cellHtml = '<td><span class="badge">' + escHtml(r['状态'] || '') + '</span></td>';
} else {
cellHtml = '<td>' + escHtml(r[field] || '') + '</td>';
}
html += cellHtml;
});
// 做货周期 = 实际到货日期 - 跟单下单日期
var manuDaysOv = '';
if (r['实际到货日期'] && r['日期']) {
var d1 = new Date(r['实际到货日期'].replace(/-/g, '/'));
var d2 = new Date(r['日期'].replace(/-/g, '/'));
if (!isNaN(d1) && !isNaN(d2)) {
var diff = Math.round((d1 - d2) / 86400000);
if (diff >= 0) manuDaysOv = diff + '天';
}
}
html += '<td style="text-align:center;font-weight:600">' + manuDaysOv + '</td>';
html += '<td style="position:sticky;right:0;z-index:9;background:var(--card-bg,#fff);border-left:1px solid var(--border);white-space:nowrap;padding:10px 8px">';
html += '<button class="btn btn-sm btn-outline" onclick="showOrderDetail(\'' + encodeURIComponent(JSON.stringify(r)).replace(/'/g, '%27') + '\',\'海外订单详情\')" title="详情" style="font-size:11px;color:#1890ff">详情</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="generateContractForOrders(\'overseas\',[' + r.id + '])" title="生成合同" style="font-size:11px;color:#52c41a;border-color:#52c41a">生成合同</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="overseasDeleteRow(' + r.id +')" title="删除" style="font-size:11px;color:#f5222d;border-color:#f5222d">删除</button>';
html += '</td></tr>';
});
tbody.innerHTML = html;
}
window._applyTableFieldPerms && window._applyTableFieldPerms('overseasOrdersTable', 'orders_overseas');
_syncOrderFilterIcons('overseas');
restoreColumnWidths('overseasOrdersTable', 'overseasColWidths');
applyOverseasFrozenPositions();
enableColumnResize('overseasOrdersTable', 'overseasColWidths');
var pel = document.getElementById('overseasOrdersPagination');
if (pel) {
if (typeof renderPaginationWithSize === 'function') {
renderPaginationWithSize(pel, page, data.total || 0, OVERSEAS_PAGE_SIZE, 'overseasLoadOrders', 'OVERSEAS_PAGE_SIZE');
}
}
});
}
window.overseasEditRow = function(id) {
var newVal = prompt('输入新值(格式: 字段名=值),例如: 状态=已完结');
if (!newVal) return;
var parts = newVal.split('=');
if (parts.length < 2) { alert('格式错误,请用: 字段名=值'); return; }
var field = parts[0].trim();
var value = parts.slice(1).join('=').trim();
fetch('/api/overseas/orders/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, field: field, value: value})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) { overseasLoadOrders(overseasOrderPage); }
else { alert(res.msg || '更新失败'); }
});
};
window.overseasDeleteRow = function(id) {
if (!confirm('确认删除此记录?')) return;
fetch('/api/overseas/orders/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('overseas');
overseasLoadOrders(overseasOrderPage);
}
else { alert(res.msg || '删除失败'); }
});
};
window.overseasAddRow = function() {
var code = prompt('请输入产品编码(必填):');
if (!code) return;
var name = prompt('请输入货品名称:');
fetch('/api/overseas/orders/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({'产品编码': code, '货品名称': name || ''})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('overseas');
overseasLoadOrders(1);
}
else { alert(res.msg || '添加失败'); }
});
};
window.overseasExport = function() {
var tbody = document.getElementById('overseasOrdersTbody');
if (!tbody) return;
var lines = [];
var headers = ['#','货品编码','货品名称','跟单下单日期','计划下单时间','单价','审核价格','数量','金额','供应商','仓库','新旧品','已做采购单','已入库','采购组别','下单人','进度','下单备注','采购开发','旺店通单号','录入旺店通','实际到货日期','预计到货日期','未到货数','状态'];
lines.push(headers.join(','));
var rows = tbody.querySelectorAll('tr');
rows.forEach(function(tr) {
var cols = tr.querySelectorAll('td');
var row = [];
cols.forEach(function(td) {
var txt = td.textContent.trim().replace(/,/g, ',').replace(/"/g, '""');
row.push('"' + txt + '"');
});
lines.push(row.join(','));
});
if (lines.length <= 1) return;
var blob = new Blob([lines.join('\n')], {type: 'text/csv;charset=utf-8'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '海外订单_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
};
window.overseasToggleAll = function(master) {
var cbs = document.querySelectorAll('.overseas-checkbox');
cbs.forEach(function(cb) { cb.checked = master.checked; });
};
window.overseasBatchGenerateContract = function() {
var cbs = document.querySelectorAll('.overseas-checkbox:checked');
var ids = [];
cbs.forEach(function(cb) { ids.push(parseInt(cb.getAttribute('data-id'))); });
if (ids.length === 0) { alert('请先勾选要生成合同的订单'); return; }
generateContractForOrders('overseas', ids);
};
// ──────────── 包材订单管理 ────────────
var PACKAGING_PAGE_SIZE = 100;
var packagingOrderPage = 1;
var packagingSearch = '';
var packagingFilterStatus = '';
var packagingFilterSupplier = '';
var packagingFilterOrderer = '';
var packagingOrderFilter = {};
var packagingOrderFilterActive = {};
var packagingOrderFilterMenu = null;
var packagingOrderFilterField = '';
var packagingFrozen = localStorage.getItem('packagingFrozen') !== 'false';
var packagingFrozenFields = (function(){
var arr;
try { arr = JSON.parse(localStorage.getItem('packagingFrozenFields')) || [0,1,2]; }
catch(e){ arr = [0,1,2]; }
return arr.filter(function(v) { return v < 3; });
})();
// ─── 冻结列切换函数 ───
window.openDomesticFrozenModal = function() {
// 从 localStorage 加载当前冻结列设置
try { domesticFrozenFields = JSON.parse(localStorage.getItem('domesticFrozenFields')) || [0,1,2]; }
catch(e){ domesticFrozenFields = [0,1,2]; }
var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结
domesticFrozenFields = domesticFrozenFields.filter(function(v) { return v < labels.length; });
var html = '<div style="padding:8px 0">';
html += '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer">';
var allChecked = domesticFrozenFields.length === labels.length;
html += '<input type="checkbox" id="domesticFrozenCbAll" ' + (allChecked ? 'checked' : '') + ' onchange="domesticFrozenToggleAll(this.checked)"> <strong>全选/取消</strong>';
html += '</label><div style="border-top:1px solid var(--border);padding-top:8px;margin-top:4px">';
labels.forEach(function(label, idx) {
var checked = domesticFrozenFields.indexOf(idx) >= 0;
html += '<label style="display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer">';
html += '<input type="checkbox" class="domestic-frozen-cb" value="' + idx + '"' + (checked ? ' checked' : '') + ' onchange="domesticFrozenCbChange()">';
html += '<span>' + label + '</span></label>';
});
html += '</div></div>';
document.getElementById('domesticFrozenModalBody').innerHTML = html;
document.getElementById('domesticFrozenModal').style.display = 'flex';
};
window.domesticFrozenCbChange = function() {
var checked = [];
document.querySelectorAll('.domestic-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); });
domesticFrozenFields = checked.sort(function(a,b){return a-b;});
var allCb = document.getElementById('domesticFrozenCbAll');
if (allCb) allCb.checked = checked.length === document.querySelectorAll('.domestic-frozen-cb').length;
};
window.domesticFrozenToggleAll = function(checked) {
document.querySelectorAll('.domestic-frozen-cb').forEach(function(cb) { cb.checked = checked; });
domesticFrozenCbChange();
};
function closeDomesticFrozenModal() {
document.getElementById('domesticFrozenModal').style.display = 'none';
}
function confirmDomesticFrozen() {
domesticFrozen = domesticFrozenFields.length > 0;
localStorage.setItem('domesticFrozen', domesticFrozen);
localStorage.setItem('domesticFrozenFields', JSON.stringify(domesticFrozenFields));
closeDomesticFrozenModal();
domesticLoadOrders(1);
}
// ─── 海外冻结列弹窗 ───
window.openOverseasFrozenModal = function() {
try { overseasFrozenFields = JSON.parse(localStorage.getItem('overseasFrozenFields')) || [0,1,2]; }
catch(e){ overseasFrozenFields = [0,1,2]; }
var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结
overseasFrozenFields = overseasFrozenFields.filter(function(v) { return v < labels.length; });
var html = '<div style="padding:8px 0">';
html += '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer">';
var allChecked = overseasFrozenFields.length === labels.length;
html += '<input type="checkbox" id="overseasFrozenCbAll" ' + (allChecked ? 'checked' : '') + ' onchange="overseasFrozenToggleAll(this.checked)"> <strong>全选/取消</strong>';
html += '</label><div style="border-top:1px solid var(--border);padding-top:8px;margin-top:4px">';
labels.forEach(function(label, idx) {
var checked = overseasFrozenFields.indexOf(idx) >= 0;
html += '<label style="display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer">';
html += '<input type="checkbox" class="overseas-frozen-cb" value="' + idx + '"' + (checked ? ' checked' : '') + ' onchange="overseasFrozenCbChange()">';
html += '<span>' + label + '</span></label>';
});
html += '</div></div>';
document.getElementById('overseasFrozenModalBody').innerHTML = html;
document.getElementById('overseasFrozenModal').style.display = 'flex';
};
window.overseasFrozenCbChange = function() {
var checked = [];
document.querySelectorAll('.overseas-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); });
overseasFrozenFields = checked.sort(function(a,b){return a-b;});
var allCb = document.getElementById('overseasFrozenCbAll');
if (allCb) allCb.checked = checked.length === document.querySelectorAll('.overseas-frozen-cb').length;
};
window.overseasFrozenToggleAll = function(checked) {
document.querySelectorAll('.overseas-frozen-cb').forEach(function(cb) { cb.checked = checked; });
overseasFrozenCbChange();
};
function closeOverseasFrozenModal() {
document.getElementById('overseasFrozenModal').style.display = 'none';
}
function confirmOverseasFrozen() {
overseasFrozen = overseasFrozenFields.length > 0;
localStorage.setItem('overseasFrozen', overseasFrozen);
localStorage.setItem('overseasFrozenFields', JSON.stringify(overseasFrozenFields));
closeOverseasFrozenModal();
overseasLoadOrders(1);
}
// ─── 包材冻结列弹窗 ───
window.openPackagingFrozenModal = function() {
try { packagingFrozenFields = JSON.parse(localStorage.getItem('packagingFrozenFields')) || [0,1,2]; }
catch(e){ packagingFrozenFields = [0,1,2]; }
var labels = ['序号','货品编码','货品名称']; // 仅这三列可冻结
packagingFrozenFields = packagingFrozenFields.filter(function(v) { return v < labels.length; });
var html = '<div style="padding:8px 0">';
html += '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer">';
var allChecked = packagingFrozenFields.length === labels.length;
html += '<input type="checkbox" id="packagingFrozenCbAll" ' + (allChecked ? 'checked' : '') + ' onchange="packagingFrozenToggleAll(this.checked)"> <strong>全选/取消</strong>';
html += '</label><div style="border-top:1px solid var(--border);padding-top:8px;margin-top:4px">';
labels.forEach(function(label, idx) {
var checked = packagingFrozenFields.indexOf(idx) >= 0;
html += '<label style="display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer">';
html += '<input type="checkbox" class="packaging-frozen-cb" value="' + idx + '"' + (checked ? ' checked' : '') + ' onchange="packagingFrozenCbChange()">';
html += '<span>' + label + '</span></label>';
});
html += '</div></div>';
document.getElementById('packagingFrozenModalBody').innerHTML = html;
document.getElementById('packagingFrozenModal').style.display = 'flex';
};
window.packagingFrozenCbChange = function() {
var checked = [];
document.querySelectorAll('.packaging-frozen-cb:checked').forEach(function(cb) { checked.push(parseInt(cb.value)); });
packagingFrozenFields = checked.sort(function(a,b){return a-b;});
var allCb = document.getElementById('packagingFrozenCbAll');
if (allCb) allCb.checked = checked.length === document.querySelectorAll('.packaging-frozen-cb').length;
};
window.packagingFrozenToggleAll = function(checked) {
document.querySelectorAll('.packaging-frozen-cb').forEach(function(cb) { cb.checked = checked; });
packagingFrozenCbChange();
};
function closePackagingFrozenModal() {
document.getElementById('packagingFrozenModal').style.display = 'none';
}
function confirmPackagingFrozen() {
packagingFrozen = packagingFrozenFields.length > 0;
localStorage.setItem('packagingFrozen', packagingFrozen);
localStorage.setItem('packagingFrozenFields', JSON.stringify(packagingFrozenFields));
closePackagingFrozenModal();
packagingLoadOrders(1);
}
// ─── 包材冻结列位置 ───
function applyPackagingFrozenPositions() {
// 包装表格: [checkbox(0), 序号(1), 货品编码(2), 货品名称(3), 日期(4), ...]
var frozenColMap = function(idx) { return idx + 1; };
var table = document.getElementById('packagingOrdersTable');
if (!table) return;
var headerRow = table.querySelector('thead tr');
if (!headerRow) return;
var scope = headerRow.querySelectorAll('th');
var tbody = table.querySelector('tbody');
// checkbox(0) 和 序号(1) 始终固定
var frozenSet = {0: true, 1: true};
if (packagingFrozen && packagingFrozenFields) {
for (var fi = 0; fi < packagingFrozenFields.length; fi++) {
frozenSet[frozenColMap(packagingFrozenFields[fi])] = true;
}
}
// 处理 th:清除非冻结列的 sticky,设置冻结列
for (var i = 0; i < scope.length; i++) {
// 操作列保留 sticky:right
if (scope[i].style.position === 'sticky' && scope[i].style.right !== '') continue;
if (frozenSet[i]) {
scope[i].style.position = 'sticky';
scope[i].style.top = '0';
scope[i].style.zIndex = '3';
} else {
scope[i].style.position = '';
scope[i].style.left = '';
scope[i].style.zIndex = '';
}
}
scope[0].style.left = '0px'; // checkbox 始终 left:0
// 处理 td
if (tbody) {
for (var ri = 0; ri < tbody.rows.length; ri++) {
var tds = tbody.rows[ri].cells;
for (var i = 0; i < tds.length; i++) {
if (tds[i].style.position === 'sticky' && tds[i].style.right !== '') continue;
if (frozenSet[i]) {
tds[i].style.position = 'sticky';
tds[i].style.top = '0';
tds[i].style.zIndex = '2';
} else {
tds[i].style.position = '';
tds[i].style.left = '';
tds[i].style.zIndex = '';
}
}
tds[0].style.left = '0px';
tds[0].style.background = 'var(--card-bg,#fff)';
}
}
// 测量冻结列宽度并设置 left 偏移
var widths = [];
var acc = 0;
// 获取所有冻结列的宽度(按 col index 顺序)
var frozenIdxList = [];
for (var i = 0; i <= 10; i++) {
if (frozenSet[i]) frozenIdxList.push(i);
}
frozenIdxList.forEach(function(idx) {
var w = idx < scope.length ? scope[idx].offsetWidth : 0;
widths.push(w);
});
// 为每个冻结的 th 设置 left
for (var fi = 1; fi < frozenIdxList.length; fi++) {
var idx = frozenIdxList[fi];
if (idx >= scope.length) break;
acc += widths[fi - 1];
scope[idx].style.left = acc + 'px';
}
// 为 td 设置 left
if (tbody && frozenIdxList.length > 1) {
for (var ri = 0; ri < tbody.rows.length; ri++) {
var tds = tbody.rows[ri].cells;
var acc2 = widths[0]; // checkbox width
for (var fi = 1; fi < frozenIdxList.length; fi++) {
var idx = frozenIdxList[fi];
if (idx >= tds.length) break;
tds[idx].style.left = acc2 + 'px';
tds[idx].style.background = 'var(--card-bg,#fff)';
acc2 += widths[fi];
}
}
}
// 一次性测量所有冻结列宽度(勾选框排首位)
var widths = [];
var acc = 0;
var cbW = scope[0].offsetWidth;
widths.push(cbW);
acc += cbW;
for (var fi = 0; fi < packagingFrozenFields.length; fi++) {
var idx = frozenColMap(packagingFrozenFields[fi]);
if (idx >= scope.length) break;
var th = scope[idx];
var w = th.offsetWidth;
widths.push(w);
var leftVal = acc + 'px';
th.style.position = 'sticky';
th.style.top = '0';
th.style.left = leftVal;
th.style.zIndex = fi < 2 ? '3' : '11';
th.style.background = 'var(--bg,#f5f5f5)';
acc += w;
}
// 更新 td
if (tbody && widths.length > 0) {
var rows = tbody.rows;
for (var ri = 0; ri < rows.length; ri++) {
var tr = rows[ri];
var tds = tr.cells;
var acc2 = 0;
tds[0].style.position = 'sticky';
tds[0].style.left = acc2 + 'px';
tds[0].style.zIndex = '2';
tds[0].style.background = 'var(--card-bg,#fff)';
acc2 += widths[0];
for (var fi = 0; fi < packagingFrozenFields.length; fi++) {
var idx = frozenColMap(packagingFrozenFields[fi]);
if (idx >= tds.length) break;
tds[idx].style.position = 'sticky';
tds[idx].style.left = acc2 + 'px';
tds[idx].style.zIndex = '2';
tds[idx].style.background = 'var(--card-bg,#fff)';
acc2 += widths[fi + 1];
}
}
}
}
function loadPackagingOrdersPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>包材订单管理</h1>
<div style="display:flex;gap:10px;margin-top:8px;flex-wrap:wrap;align-items:center">
<div class="search-box" style="max-width:300px">
<input type="text" id="packagingSearchInput" class="form-input" placeholder="搜索编码/名称..." autocomplete="off"
onkeydown="if(event.key==='Enter') packagingDoSearch()" style="padding-left:32px;padding-right:8px;height:32px;font-size:13px">
<svg style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#999" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
<input type="text" id="packagingSupplierFilter" class="form-input" placeholder="供应商..." style="width:auto;min-width:120px;height:32px;font-size:13px" onkeydown="if(event.key==='Enter') packagingApplyFilters()">
<select id="packagingStatusFilter" class="form-input" style="width:auto;min-width:120px;height:32px;font-size:13px" onchange="packagingApplyFilters()">
<option value="">全部状态</option>
</select>
<select id="packagingOrdererFilter" class="form-input" style="width:auto;min-width:100px;height:32px;font-size:13px" onchange="packagingApplyFilters()">
<option value="">全部下单人</option>
</select>
<button class="btn btn-sm btn-primary" onclick="packagingDoSearch()">查询</button>
<button class="btn btn-sm btn-outline" onclick="packagingResetFilters()">重置</button>
<span id="packagingTotalInfo" style="font-size:13px;color:var(--text-light);margin-left:auto"></span>
</div>
</div>
<div id="packagingContent" class="page-flex">
<div class="card">
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
<h3>📦 包材采购下单明细</h3>
<div>
<button class="btn btn-sm btn-outline" onclick="packagingBatchGenerateContract()" style="color:#52c41a;border-color:#52c41a">📄 批量生成合同</button>
<button class="btn btn-sm btn-primary" onclick="packagingAddRow()">+ 新增</button>
<button class="btn btn-sm btn-outline" onclick="packagingExport()">导出</button>
<button class="btn btn-sm btn-outline" onclick="openPackagingFrozenModal()">🔒 冻结列</button>
<button class="btn btn-sm btn-outline" onclick="openPackagingColModal()">⚙ 字段设置</button>
</div>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="packagingOrdersTable" style="font-size:12px;white-space:nowrap;min-width:2400px">
<thead style="position:sticky;top:0;z-index:10;background:var(--bg,#f5f5f5)"><tr>
<th style="position:sticky;left:0;z-index:3;background:var(--bg,#f5f5f5);min-width:40px;text-align:center"><input type="checkbox" id="packagingSelectAll" onchange="packagingToggleAll(this)"></th>
<th style="position:sticky;left:40px;z-index:3;background:var(--bg,#f5f5f5);min-width:50px">序号</th>`;
// 动态生成包材表头列
(function() {
var cols = PACKAGING_COLUMNS;
var order = window._packagingDragOrder || cols.map(function(c) { return c.field; });
order.forEach(function(field) {
var col = cols.find(function(c) { return c.field === field; });
if (!col) return;
if (!window.packagingColVisible[field]) return;
var fieldAttr = col.field;
var thStyle = '';
if (fieldAttr === '日期') thStyle = 'position:sticky;left:90px;z-index:3;background:var(--bg,#f5f5f5);min-width:100px';
else if (fieldAttr === '编码') thStyle = 'min-width:140px';
else if (fieldAttr === '名称') thStyle = 'min-width:250px';
else if (fieldAttr === '单价') thStyle = 'min-width:70px';
else if (fieldAttr === '数量') thStyle = 'min-width:60px';
else if (fieldAttr === '合计') thStyle = 'min-width:80px';
else if (fieldAttr === '供应商') thStyle = 'min-width:100px';
else if (fieldAttr === '下单备注') thStyle = 'min-width:120px';
else if (fieldAttr === '仓库到货') thStyle = 'min-width:100px';
else if (fieldAttr === '进度') thStyle = 'min-width:160px';
else if (fieldAttr === '是否已做采购单') thStyle = 'min-width:90px';
else if (fieldAttr === '是否已做入库') thStyle = 'min-width:70px';
else if (fieldAttr === '采购下单人') thStyle = 'min-width:80px';
else if (fieldAttr === '旺店通编号') thStyle = 'min-width:120px';
else if (fieldAttr === '是否已录入旺店通') thStyle = 'min-width:90px';
else if (fieldAttr === '到货日期') thStyle = 'min-width:100px';
else if (fieldAttr === '状态') thStyle = 'min-width:70px';
else if (fieldAttr === '做货天数') thStyle = 'min-width:70px';
else if (fieldAttr === '包材开发') thStyle = 'min-width:100px';
else { thStyle = ''; }
container.querySelector('#packagingOrdersTable thead tr').innerHTML += '<th style="' + thStyle + '" data-field="' + fieldAttr + '">' + col.label + ' <span class="order-filt-icon" onclick="event.stopPropagation();openOrderFilterMenu(\'packaging\',\'' + fieldAttr + '\',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>';
});
})();
container.querySelector('#packagingOrdersTable thead tr').innerHTML += '<th style="width:1%;white-space:nowrap;padding:12px 8px;position:sticky;right:0;top:0;z-index:13;background:var(--bg,#f5f5f5);border-left:1px solid var(--border);text-align:center">操作</th>';
container.querySelector('#packagingOrdersTable').innerHTML += '</thead><tbody id="packagingOrdersTbody"><tr><td colspan="22" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>';
container.querySelector('#packagingContent').innerHTML += '</div></div><div id="packagingOrdersPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div></div>';
// 包材冻结列弹窗 + 字段设置弹窗
container.innerHTML += `
<div class="modal-overlay" id="packagingFrozenModal" style="display:none">
<div class="modal" style="width:400px">
<div class="modal-header">
<h3>🔒 冻结列设置</h3>
<span class="modal-close" onclick="closePackagingFrozenModal()">&times;</span>
</div>
<div class="modal-body" style="max-height:60vh;overflow-y:auto;padding:12px 20px" id="packagingFrozenModalBody">
</div>
<div class="modal-footer">
<button class="btn" onclick="closePackagingFrozenModal()">取消</button>
<button class="btn btn-primary" onclick="confirmPackagingFrozen()">确定</button>
</div>
</div>
</div>
<!-- 字段设置弹窗 -->
<div class="modal-overlay" id="packagingColModal" style="display:none">
<div class="modal" style="width:720px">
<div class="modal-header">
<h3>字段设置</h3>
<span class="modal-close" onclick="closeOrderColModal('packaging')">&times;</span>
</div>
<div class="modal-body" style="max-height:65vh;overflow-y:auto;padding:16px 20px">
<!-- 方案管理 -->
<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="font-weight:600;font-size:14px;white-space:nowrap">📁 方案管理</span>
<input type="text" id="packagingSchemeNameInput" class="form-input" placeholder="输入方案名称..." style="flex:1;min-width:0">
<button class="btn btn-primary btn-sm" onclick="saveOrderScheme('packaging')">💾 保存方案</button>
</div>
<div id="packagingSchemeList" style="display:flex;flex-wrap:wrap;gap:6px;min-height:28px">
<span style="color:var(--text-light);font-size:13px">暂无保存方案</span>
</div>
</div>
<div class="col-toggle-all" style="padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:8px">
<label class="cb-all"><input type="checkbox" id="packaging-cb-all" checked onchange="toggleOrderColAll('packaging',this.checked)"> <strong>全选</strong></label>
</div>
<input type="text" id="packagingColSearchInput" class="form-input" placeholder="搜索字段名称..." oninput="filterOrderColCheckboxes('packaging')" style="margin-bottom:8px;width:100%;box-sizing:border-box">
<div id="packagingColCheckboxes"><div id="packagingColGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px 12px;padding:4px 0"></div></div>
</div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:12px;color:var(--text-light)">拖拽字段名可调整列顺序</span>
<div>
<button class="btn" onclick="closeOrderColModal('packaging')">取消</button>
<button class="btn btn-primary" onclick="confirmOrderCol('packaging')">确定</button>
</div>
</div>
</div>
</div>`;
// 初始化字段设置checkbox
var pcGrid = document.getElementById('packagingColGrid');
if (pcGrid) {
window._packagingDragOrder = PACKAGING_COLUMNS.map(function(c) { return c.field; });
renderOrderColCheckboxes('packaging');
}
packagingLoadFilters();
packagingLoadOrders(1);
loadDefaultOrderScheme('packaging');
// 后台预拉取所有列的筛选值
prefetchOrderColumnValues('packaging');
}
function packagingLoadFilters() {
fetch('/api/packaging/orders/filters')
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var ss = document.getElementById('packagingStatusFilter');
var so = document.getElementById('packagingOrdererFilter');
(res.data.statuses || []).forEach(function(s) {
var opt = document.createElement('option');
opt.value = s; opt.textContent = s;
ss.appendChild(opt);
});
(res.data.orderers || []).forEach(function(s) {
var opt = document.createElement('option');
opt.value = s; opt.textContent = s;
so.appendChild(opt);
});
});
}
function packagingDoSearch() {
packagingApplyFilters();
}
function packagingApplyFilters() {
packagingSearch = document.getElementById('packagingSearchInput').value.trim();
packagingFilterSupplier = document.getElementById('packagingSupplierFilter').value.trim();
packagingFilterStatus = document.getElementById('packagingStatusFilter').value;
packagingFilterOrderer = document.getElementById('packagingOrdererFilter').value;
packagingLoadOrders(1);
}
function packagingResetFilters() {
document.getElementById('packagingSearchInput').value = '';
document.getElementById('packagingSupplierFilter').value = '';
document.getElementById('packagingStatusFilter').value = '';
document.getElementById('packagingOrdererFilter').value = '';
packagingSearch = ''; packagingFilterSupplier = ''; packagingFilterStatus = ''; packagingFilterOrderer = '';
packagingOrderFilter = {};
packagingOrderFilterActive = {};
_syncOrderFilterIcons('packaging');
packagingLoadOrders(1);
}
function packagingLoadOrders(page) { packagingOrderPage = page;
var params = '?page=' + page + '&size=' + PACKAGING_PAGE_SIZE;
if (packagingSearch) params += '&search=' + encodeURIComponent(packagingSearch);
if (packagingFilterStatus) params += '&status=' + encodeURIComponent(packagingFilterStatus);
if (packagingFilterSupplier) params += '&supplier=' + encodeURIComponent(packagingFilterSupplier);
if (packagingFilterOrderer) params += '&orderer=' + encodeURIComponent(packagingFilterOrderer);
var packagingFilterStr = '';
if (Object.keys(packagingOrderFilter).length > 0) {
packagingFilterStr = encodeURIComponent(JSON.stringify(packagingOrderFilter));
params += '&order_filter=' + packagingFilterStr;
}
fetch('/api/packaging/orders/list' + params)
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var data = res.data;
var tbody = document.getElementById('packagingOrdersTbody');
if (document.getElementById('packagingTotalInfo'))
document.getElementById('packagingTotalInfo').textContent = '共 ' + data.total + ' 条';
if (!data.rows || data.rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="22" style="text-align:center;padding:60px;color:var(--text-light)">暂无数据</td></tr>';
} else {
var startIdx = (page - 1) * PACKAGING_PAGE_SIZE;
var html = '';
data.rows.forEach(function(r, i) {
var rowNum = startIdx + i + 1;
html += '<tr>';
html += '<td style="position:sticky;left:0;z-index:2;background:var(--card-bg,#fff);text-align:center"><input type="checkbox" class="packaging-checkbox" data-id="' + r.id + '"></td>';
html += '<td style="position:sticky;left:40px;z-index:2;background:var(--card-bg,#fff);text-align:center;color:#999">' + rowNum + '</td>';
// 按列顺序和可见性渲染单元格
var order = window._packagingDragOrder || PACKAGING_COLUMNS.map(function(c) { return c.field; });
order.forEach(function(field) {
if (!window.packagingColVisible[field]) return;
var cellHtml = '';
if (field === '日期') {
cellHtml = '<td style="position:sticky;left:90px;z-index:2;background:var(--card-bg,#fff)">' + escHtml(r['日期'] || '') + '</td>';
} else if (field === '编码') {
cellHtml = '<td><code style="cursor:pointer;color:#1890ff" onclick="showSkuInfoModal(this,\'包材订单详情\')">' + escHtml(r['编码'] || '') + '</code></td>';
} else if (field === '名称') {
cellHtml = '<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(r['名称'] || '') + '">' + escHtml(r['名称'] || '') + '</td>';
} else if (field === '单价') {
cellHtml = '<td style="padding:2px 4px"><input type="text" class="price-inline" value="' + (r['单价'] ? parseFloat(r['单价']).toFixed(2) : '') + '" onclick="this.select()" oninput="recalcAmount(this)" data-pid="' + r.id + '" data-pfield="单价" data-papi="/api/packaging/orders/update" style="width:70px;border:1px solid #d9d9d9;border-radius:3px;padding:2px 4px;font-size:12px;text-align:right;background:var(--bg,#fff);color:var(--text,#333)"></td>';
} else if (field === '数量') {
cellHtml = '<td class="qty-cell"><strong>' + (parseInt(r['数量']) || 0).toLocaleString() + '</strong></td>';
} else if (field === '金额') {
var priceVal = parseFloat(r['单价']) || 0;
var qtyVal = parseInt(r['数量']) || 0;
var amtVal = priceVal * qtyVal;
cellHtml = '<td class="amt-cell">' + (amtVal ? '¥' + amtVal.toLocaleString() : '') + '</td>';
} else if (field === '供应商') {
cellHtml = '<td>' + escHtml(r['供应商'] || '') + '</td>';
} else if (field === '下单备注') {
cellHtml = '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(r['下单备注'] || '') + '">' + escHtml(r['下单备注'] || '') + '</td>';
} else if (field === '仓库到货') {
var orderId2 = r.id || '';
var whArrVal = r['仓库到货'] || '';
cellHtml = '<td><input type="text" class="form-input" style="width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px;min-width:80px" value="' + escHtml(whArrVal) + '" data-id="' + orderId2 + '" data-field="仓库到货" onkeydown="if(event.key===\'Enter\'){event.preventDefault();var inp=this;var v=inp.value;if(confirm(\'确认保存仓库到货为 \'+v+\'?\')){fetch(\'/api/packaging/orders/update\',{method:\'POST\',headers:{\'Content-Type\':\'application/json\'},body:JSON.stringify({id:inp.dataset.id,field:\'仓库到货\',value:v})}).then(function(r){return r.json()}).then(function(res){if(res.code===0){inp.blur();inp.style.borderColor=\'#52c41a\';setTimeout(function(){inp.style.borderColor=\'\'},800)}else{alert(res.msg||\'保存失败\')}}).catch(function(){alert(\'网络错误\')})}}"></td>';
} else if (field === '进度') {
cellHtml = '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(r['进度'] || '') + '">' + escHtml(r['进度'] || '') + '</td>';
} else if (field === '是否已做采购单') {
cellHtml = '<td>' + escHtml(r['是否已做采购单'] || '') + '</td>';
} else if (field === '是否已做入库') {
cellHtml = '<td>' + escHtml(r['是否已做入库'] || '') + '</td>';
} else if (field === '采购下单人') {
cellHtml = '<td>' + renderPersonBadges(r['采购下单人']) + '</td>';
} else if (field === '旺店通编号') {
cellHtml = '<td><code>' + escHtml(r['旺店通编号'] || '') + '</code></td>';
} else if (field === '是否已录入旺店通') {
cellHtml = '<td>' + escHtml(r['是否已录入旺店通'] || '') + '</td>';
} else if (field === '到货日期') {
cellHtml = '<td>' + escHtml(r['到货日期'] || '') + '</td>';
} else if (field === '状态') {
cellHtml = '<td><span class="badge">' + escHtml(r['状态'] || '未完结') + '</span></td>';
} else if (field === '做货天数') {
cellHtml = '<td>' + escHtml(r['做货天数'] || '') + '</td>';
} else if (field === '实际到货数量') {
cellHtml = '<td>' + ((r['实际到货数量'] || 0) > 0 ? parseInt(r['实际到货数量']).toLocaleString() : '0') + '</td>';
} else if (field === '未到货数') {
cellHtml = '<td><strong>' + (parseInt(r['未到货数']) || 0).toLocaleString() + '</strong></td>';
} else if (field === '包材开发') {
cellHtml = '<td>' + renderPersonBadges(r['包材开发']) + '</td>';
} else {
cellHtml = '<td>' + escHtml(r[field] || '') + '</td>';
}
html += cellHtml;
});
html += '<td style="position:sticky;right:0;z-index:2;background:var(--card-bg,#fff);border-left:1px solid var(--border);white-space:nowrap;padding:10px 8px">';
html += '<button class="btn btn-sm btn-outline" onclick="showOrderDetail(\'' + encodeURIComponent(JSON.stringify(r)).replace(/'/g, '%27') + '\',\'包材订单详情\')" title="详情" style="font-size:11px;color:#1890ff">详情</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="generateContractForOrders(\'packaging\',[' + r.id + '])" title="生成合同" style="font-size:11px;color:#52c41a;border-color:#52c41a">生成合同</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="packagingDeleteRow(' + r.id +')" title="删除" style="font-size:11px;color:#f5222d;border-color:#f5222d">删除</button>';
html += '</td></tr>';
});
tbody.innerHTML = html;
}
window._applyTableFieldPerms && window._applyTableFieldPerms('packagingOrdersTable', 'orders_packaging');
_syncOrderFilterIcons('packaging');
restoreColumnWidths('packagingOrdersTable', 'packagingColWidths');
applyPackagingFrozenPositions();
enableColumnResize('packagingOrdersTable', 'packagingColWidths');
var pel = document.getElementById('packagingOrdersPagination');
if (pel) {
if (typeof renderPaginationWithSize === 'function') {
renderPaginationWithSize(pel, page, data.total || 0, PACKAGING_PAGE_SIZE, 'packagingLoadOrders', 'PACKAGING_PAGE_SIZE');
}
}
});
}
window.packagingEditRow = function(id) {
var newVal = prompt('输入新值(格式: 字段名=值),例如: 状态=已完结');
if (!newVal) return;
var parts = newVal.split('=');
if (parts.length < 2) { alert('格式错误'); return; }
var field = parts[0].trim();
var value = parts.slice(1).join('=').trim();
fetch('/api/packaging/orders/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, field: field, value: value})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) { packagingLoadOrders(packagingOrderPage); }
else { alert(res.msg || '更新失败'); }
});
};
window.packagingDeleteRow = function(id) {
if (!confirm('确认删除?')) return;
fetch('/api/packaging/orders/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('packaging');
packagingLoadOrders(packagingOrderPage);
}
else { alert(res.msg || '删除失败'); }
});
};
window.packagingAddRow = function() {
var code = prompt('请输入编码(必填):');
if (!code) return;
var name = prompt('请输入名称:');
fetch('/api/packaging/orders/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({编码: code, 名称: name || ''})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('packaging');
packagingLoadOrders(1);
}
else { alert(res.msg || '添加失败'); }
});
};
window.packagingExport = function() {
var tbody = document.getElementById('packagingOrdersTbody');
if (!tbody) return;
var lines = [];
var headers = ['#','日期','编码','名称','单价','数量','金额','供应商','下单备注','仓库到货','进度','已做采购单','已入库','采购下单人','旺店通编号','录入旺店通','到货日期','状态','做货天数','包材开发'];
lines.push(headers.join(','));
tbody.querySelectorAll('tr').forEach(function(tr) {
var row = [];
tr.querySelectorAll('td').forEach(function(td) {
var txt = td.textContent.trim().replace(/,/g, ',').replace(/"/g, '""');
row.push('"' + txt + '"');
});
lines.push(row.join(','));
});
if (lines.length <= 1) return;
var blob = new Blob([lines.join('\n')], {type: 'text/csv;charset=utf-8'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '包材订单_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
};
window.packagingToggleAll = function(master) {
var cbs = document.querySelectorAll('.packaging-checkbox');
cbs.forEach(function(cb) { cb.checked = master.checked; });
};
window.packagingBatchGenerateContract = function() {
var cbs = document.querySelectorAll('.packaging-checkbox:checked');
var ids = [];
cbs.forEach(function(cb) { ids.push(parseInt(cb.getAttribute('data-id'))); });
if (ids.length === 0) { alert('请先勾选要生成合同的订单'); return; }
generateContractForOrders('packaging', ids);
};
// ──────────── 成品提货表 ────────────
var finishedOrderFilter = {}; // 表头筛选状态
var finishedFilterMenu = null; // 当前筛选弹窗
var finishedFilterField = ''; // 当前筛选字段
var FINISHED_FILTER_COLS = [
{field:'品类', label:'品类', type:'text'},
{field:'货品编号', label:'货品编号', type:'text'},
{field:'货品名称', label:'货品名称', type:'text'},
{field:'供应商', label:'供应商', type:'text'},
{field:'可用库存', label:'可用库存', type:'number'},
{field:'昨日销量', label:'昨日销量', type:'number'},
{field:'3天均销', label:'3天均销', type:'number'},
{field:'7天均销', label:'7天均销', type:'number'},
{field:'15天均销', label:'15天均销', type:'number'},
{field:'支撑天数', label:'支撑天数', type:'number'},
{field:'采购总数量', label:'采购总数量', type:'number'},
{field:'生产在途', label:'生产在途', type:'number'},
{field:'采购在途', label:'采购在途', type:'number'},
{field:'国内下单表进度', label:'国内下单表进度', type:'text'},
{field:'海外下单表进度', label:'海外下单表进度', type:'text'},
{field:'产品进度', label:'产品进度', type:'text'},
{field:'包材库存', label:'包材库存', type:'number'},
{field:'采购跟单', label:'采购跟单', type:'text'},
{field:'采购计划', label:'采购计划', type:'text'},
{field:'备注', label:'备注', type:'text'},
];
var FINISHED_PAGE_SIZE = 100;
var finishedPage = 1;
var finishedSearch = '';
var finishedCategory = '';
var finishedSupplier = '';
function loadFinishedProductsPage(container) {
container.innerHTML = `
<div class="page-header">
<h1>成品提货表</h1>
<div style="display:flex;gap:10px;margin-top:8px;flex-wrap:wrap;align-items:center">
<div class="search-box" style="max-width:300px">
<input type="text" id="finishedSearchInput" class="form-input" placeholder="搜索货品编号/名称/供应商..."
onkeydown="if(event.key==='Enter') finishedDoSearch()" style="padding-left:32px;padding-right:8px;height:32px;font-size:13px">
<svg style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#999" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
<select id="finishedCategoryFilter" class="form-input" style="width:auto;min-width:100px;height:32px;font-size:13px" onchange="finishedApplyFilters()">
<option value="">全部品类</option>
</select>
<select id="finishedSupplierFilter" class="form-input" style="width:auto;min-width:120px;height:32px;font-size:13px" onchange="finishedApplyFilters()">
<option value="">全部供应商</option>
</select>
<button class="btn btn-sm btn-primary" onclick="finishedDoSearch()">查询</button>
<button class="btn btn-sm btn-outline" onclick="finishedResetFilters()">重置</button>
<span id="finishedTotalInfo" style="font-size:13px;color:var(--text-light);margin-left:auto"></span>
</div>
</div>
<div id="finishedContent" class="page-flex">
<div class="card">
<div class="card-header"><h3>📋 成品提货明细</h3></div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="finishedOrdersTable" style="font-size:12px;white-space:nowrap;min-width:3000px">
<thead><tr>
<th style="position:sticky;left:0;z-index:3;background:var(--bg,#f5f5f5);min-width:50px">#</th>
<th style="position:sticky;left:50px;z-index:3;background:var(--bg,#f5f5f5);min-width:60px">序号</th>
<th style="min-width:60px">品类 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('品类',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:120px">货品编号 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('货品编号',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:250px">货品名称 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('货品名称',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:100px">供应商 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('供应商',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:70px">可用库存 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('可用库存',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:60px">昨日销量 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('昨日销量',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:65px">3天均销 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('3天均销',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:65px">7天均销 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('7天均销',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:65px">15天均销 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('15天均销',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:60px">支撑天数 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('支撑天数',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:80px">采购总数量 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('采购总数量',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:70px">生产在途 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('生产在途',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:70px">采购在途 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('采购在途',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:130px">国内下单表进度 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('国内下单表进度',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:130px">海外下单表进度 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('海外下单表进度',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:130px">产品进度 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('产品进度',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:70px">包材库存 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('包材库存',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:150px">采购跟单 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('采购跟单',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:100px">采购计划 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('采购计划',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:200px">备注 <span class="fn-filt-icon" onclick="event.stopPropagation();openFinishedFilterMenu('备注',event)" style="cursor:pointer;color:var(--text-light);font-size:20px;margin-left:2px;user-select:none" title="筛选">▾</span></th>
<th style="min-width:80px;position:sticky;right:0;z-index:3;background:var(--bg,#f5f5f5);border-left:1px solid var(--border)">操作</th>
</tr></thead>
<tbody id="finishedOrdersTbody"><tr><td colspan="23" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>
</table>
</div></div>
<div id="finishedOrdersPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div>
</div>
</div>
`;
finishedLoadFilters();
finishedLoadOrders(1);
// 后台预拉取成品提货表所有列的筛选值
prefetchOrderColumnValues('finished');
}
function finishedLoadFilters() {
fetch('/api/finished/filters')
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var sc = document.getElementById('finishedCategoryFilter');
var ss = document.getElementById('finishedSupplierFilter');
(res.data.categories || []).forEach(function(s) {
var opt = document.createElement('option'); opt.value = s; opt.textContent = s;
sc.appendChild(opt);
});
(res.data.suppliers || []).forEach(function(s) {
var opt = document.createElement('option'); opt.value = s; opt.textContent = s;
ss.appendChild(opt);
});
});
}
function finishedDoSearch() {
finishedSearch = document.getElementById('finishedSearchInput').value.trim();
finishedLoadOrders(1);
}
function finishedApplyFilters() {
finishedCategory = document.getElementById('finishedCategoryFilter').value;
finishedSupplier = document.getElementById('finishedSupplierFilter').value;
finishedLoadOrders(1);
}
function finishedResetFilters() {
document.getElementById('finishedSearchInput').value = '';
document.getElementById('finishedCategoryFilter').value = '';
document.getElementById('finishedSupplierFilter').value = '';
finishedSearch = ''; finishedCategory = ''; finishedSupplier = '';
finishedOrderFilter = {};
_syncFinishedFilterIcons();
finishedLoadOrders(1);
}
function finishedLoadOrders(page) { finishedPage = page;
var params = '?page=' + page + '&size=' + FINISHED_PAGE_SIZE;
if (finishedSearch) params += '&search=' + encodeURIComponent(finishedSearch);
if (finishedCategory) params += '&category=' + encodeURIComponent(finishedCategory);
if (finishedSupplier) params += '&supplier=' + encodeURIComponent(finishedSupplier);
var finishedFilterStr = '';
if (Object.keys(finishedOrderFilter).length > 0) {
finishedFilterStr = encodeURIComponent(JSON.stringify(finishedOrderFilter));
params += '&order_filter=' + finishedFilterStr;
}
fetch('/api/finished/list' + params)
.then(function(r){return r.json()})
.then(function(res){
if (res.code !== 0) return;
var data = res.data;
var tbody = document.getElementById('finishedOrdersTbody');
var totalInfo = document.getElementById('finishedTotalInfo');
if (totalInfo) totalInfo.textContent = '共 ' + data.total + ' 条';
if (!data.rows || data.rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="23" style="text-align:center;padding:60px;color:var(--text-light)">暂无数据</td></tr>';
} else {
var startIdx = (page - 1) * FINISHED_PAGE_SIZE;
var html = '';
data.rows.forEach(function(r, i) {
var rowNum = startIdx + i + 1;
html += '<tr>';
html += '<td style="position:sticky;left:0;z-index:2;background:var(--card-bg,#fff);text-align:center;color:#999">' + rowNum + '</td>';
html += '<td style="position:sticky;left:50px;z-index:2;background:var(--card-bg,#fff)">' + escHtml(r['序号'] || '') + '</td>';
html += '<td>' + escHtml(r['品类'] || '') + '</td>';
html += '<td><code style="cursor:pointer;color:#1890ff" onclick="showSkuInfoModal(this,\'成品提货详情\')">' + escHtml(r['货品编号'] || '') + '</code></td>';
html += '<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(r['货品名称'] || '') + '">' + escHtml(r['货品名称'] || '') + '</td>';
html += '<td>' + escHtml(r['供应商'] || '') + '</td>';
html += '<td><strong>' + (parseInt(r['可用库存']) || 0).toLocaleString() + '</strong></td>';
html += '<td>' + escHtml(r['昨日销量'] || '') + '</td>';
html += '<td>' + escHtml(r['3天均销量'] || '') + '</td>';
html += '<td>' + escHtml(r['7天均销量'] || '') + '</td>';
html += '<td>' + escHtml(r['15天均销量'] || '') + '</td>';
html += '<td>' + escHtml(r['支撑天数'] || '') + '</td>';
html += '<td>' + (parseInt(r['采购总数量']) || 0).toLocaleString() + '</td>';
html += '<td>' + (parseInt(r['生产在途']) || 0).toLocaleString() + '</td>';
html += '<td>' + (parseInt(r['采购在途']) || 0).toLocaleString() + '</td>';
html += '<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis">' + escHtml(r['国内下单表进度'] || '') + '</td>';
html += '<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis">' + escHtml(r['海外下单表进度'] || '') + '</td>';
html += '<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis">' + escHtml(r['产品进度'] || '') + '</td>';
html += '<td>' + (parseInt(r['包材库存']) || 0).toLocaleString() + '</td>';
html += '<td>' + renderNameBadges(r['采购跟单']) + '</td>';
html += '<td>' + renderNameBadges(r['采购计划']) + '</td>';
html += '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(r['备注'] || '') + '">' + escHtml(r['备注'] || '') + '</td>';
html += '<td style="position:sticky;right:0;z-index:2;background:var(--card-bg,#fff);border-left:1px solid var(--border)">';
html += '<button class="btn btn-sm btn-outline" onclick="showOrderDetail(\'' + encodeURIComponent(JSON.stringify(r)).replace(/'/g, '%27') + '\',\'成品提货详情\')" title="详情" style="font-size:11px;color:#1890ff">详情</button> ';
html += '<button class="btn btn-sm btn-outline" onclick="finishedDeleteRow(' + r.id +')" title="删除" style="font-size:11px;color:#f5222d;border-color:#f5222d">删除</button>';
html += '</td></tr>';
});
tbody.innerHTML = html;
}
window._applyTableFieldPerms && window._applyTableFieldPerms('finishedOrdersTable', 'orders_finished');
var pel = document.getElementById('finishedOrdersPagination');
if (pel) {
if (typeof renderPaginationWithSize === 'function') {
renderPaginationWithSize(pel, page, data.total || 0, FINISHED_PAGE_SIZE, 'finishedLoadOrders', 'FINISHED_PAGE_SIZE');
}
}
});
}
window.finishedDeleteRow = function(id) {
if (!confirm('确认删除此记录?')) return;
fetch('/api/finished/orders/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
invalidateOrderColumnCache('finished');
finishedLoadOrders(finishedPage);
}
else { alert(res.msg || '删除失败'); }
});
};
// ─── 成品提货表头筛选 ───
function _finishedGetColType(field) {
for (var i = 0; i < FINISHED_FILTER_COLS.length; i++) {
if (FINISHED_FILTER_COLS[i].field === field) return FINISHED_FILTER_COLS[i].type;
}
return 'text';
}
function openFinishedFilterMenu(field, event) {
event = event || window.event;
event.stopPropagation();
if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; }
finishedFilterField = field;
var th = event.currentTarget;
while (th && th.tagName !== 'TH') th = th.parentNode;
var rect = th.getBoundingClientRect();
var label = field;
var colType = _finishedGetColType(field);
if (colType === 'number') {
_buildFinishedNumericFilter(field, label, rect);
} else {
var filterActive = finishedOrderFilter && Object.keys(finishedOrderFilter).length > 0;
var hasOtherFilters = filterActive && !finishedOrderFilter[field];
var hasSearchCond = !!(document.getElementById('finishedSearchInput')?.value.trim() ||
document.getElementById('finishedCategoryFilter')?.value ||
document.getElementById('finishedSupplierFilter')?.value);
var cacheKey = 'finished:' + field;
var cached = !hasOtherFilters && !hasSearchCond && window._orderColumnCache && window._orderColumnCache[cacheKey];
if (cached) {
_buildFinishedListFilter(field, label, rect, cached);
} else if (hasOtherFilters || hasSearchCond) {
// 有筛选条件:从当前表格DOM提取列值(联动筛选)
_buildFinishedFilterShell(field, label, rect);
var vals = [];
var seen = {};
var tbody = document.getElementById('finishedOrdersTbody');
if (tbody) {
var fCols = FINISHED_FILTER_COLS;
var colIdx = fCols.findIndex(function(c) { return c.field === field; });
if (colIdx >= 0) {
var tdIdx = colIdx + 2; // +checkbox +序号
tbody.querySelectorAll('tr').forEach(function(tr) {
var td = tr.cells[tdIdx];
if (td) {
var txt = td.textContent.trim();
if (txt && txt !== '-' && !seen[txt]) { seen[txt] = 1; vals.push(txt); }
}
});
}
}
vals.sort();
if (vals.length === 0) {
var fp = '';
if (finishedOrderFilter && Object.keys(finishedOrderFilter).length > 0) {
fp = '&order_filter=' + encodeURIComponent(JSON.stringify(finishedOrderFilter));
}
fetch('/api/finished/column-values?field=' + encodeURIComponent(field) + fp)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) { _populateFinishedFilterList(field, res.data); }
});
} else {
_populateFinishedFilterList(field, vals);
}
} else {
_buildFinishedFilterShell(field, label, rect);
var filterParam = '';
if (finishedOrderFilter && Object.keys(finishedOrderFilter).length > 0) {
filterParam = '&order_filter=' + encodeURIComponent(JSON.stringify(finishedOrderFilter));
}
fetch('/api/finished/column-values?field=' + encodeURIComponent(field) + filterParam)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) {
if (!hasOtherFilters) {
window._orderColumnCache = window._orderColumnCache || {};
window._orderColumnCache[cacheKey] = res.data;
}
_populateFinishedFilterList(field, res.data);
}
});
}
}
if (finishedFilterMenu) {
var handler = function(e) {
if (finishedFilterMenu && !finishedFilterMenu.contains(e.target) && e.target !== th && !th.contains(e.target)) {
finishedFilterMenu.remove(); finishedFilterMenu = null;
document.removeEventListener('click', handler);
}
};
setTimeout(function() { document.addEventListener('click', handler); }, 10);
setTimeout(function() {
var inp = document.getElementById('finishedFilterSearch');
if (inp) inp.focus();
}, 150);
}
}
function _buildFinishedFilterShell(field, label, rect) {
var menu = document.createElement('div');
menu.id = 'finishedFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="finishedFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="finishedFilterSearch(this.value)" onkeydown="if(event.key===\'Enter\')applyFinishedFilter()"></div>';
html += '<div id="finishedFilterList" style="overflow-y:auto;flex:1;max-height:200px;text-align:center;padding:20px;color:#999">加载中...</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyFinishedFilter()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="clearFinishedHeaderFilter()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
finishedFilterMenu = menu;
}
function _buildFinishedListFilter(field, label, rect, values) {
var menu = document.createElement('div');
menu.id = 'finishedFilterMenu';
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var sel = finishedOrderFilter[field] || {};
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="finishedFilterSearch" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="finishedFilterSearch(this.value)" onkeydown="if(event.key===\'Enter\')applyFinishedFilter()"></div>';
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="finishedFilterSelectAll" checked onchange="finishedFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="clearFinishedHeaderFilter()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
html += '<div id="finishedFilterList" style="overflow-y:auto;flex:1;max-height:200px;border-top:1px solid #f0f0f0;padding-top:4px">';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="finished-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="finishedFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
html += '</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyFinishedFilter()" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="clearFinishedHeaderFilter()" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
finishedFilterMenu = menu;
}
function _buildFinishedNumericFilter(field, label, rect) {
var menu = document.createElement('div');
menu.id = 'finishedFilterMenu';
var current = finishedOrderFilter[field] || {};
var cond = current.cond || '';
var val = current.val || '';
var html = '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><select id="finishedNumCond" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px">';
html += '<option value="">-- 选择条件 --</option>';
html += '<option value="eq"' + (cond === 'eq' ? ' selected' : '') + '>等于</option>';
html += '<option value="neq"' + (cond === 'neq' ? ' selected' : '') + '>不等于</option>';
html += '<option value="gt"' + (cond === 'gt' ? ' selected' : '') + '>大于</option>';
html += '<option value="gte"' + (cond === 'gte' ? ' selected' : '') + '>大于等于</option>';
html += '<option value="lt"' + (cond === 'lt' ? ' selected' : '') + '>小于</option>';
html += '<option value="lte"' + (cond === 'lte' ? ' selected' : '') + '>小于等于</option>';
html += '</select></div>';
html += '<div style="margin-bottom:6px"><input type="text" id="finishedNumVal" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="输入数值..." value="' + escHtml(val) + '" autocomplete="off" onkeydown="if(event.key===\'Enter\')applyFinishedNumFilter()"></div>';
html += '<div style="border-top:1px solid #f0f0f0;padding-top:6px;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm" onclick="clearFinishedHeaderFilter()" style="font-size:12px;padding:4px 10px">清除</button>';
html += '<button class="btn btn-sm btn-primary" onclick="applyFinishedNumFilter()" style="font-size:12px;padding:4px 12px">确定</button></div>';
menu.innerHTML = html;
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:200px;font-size:13px';
document.body.appendChild(menu);
finishedFilterMenu = menu;
}
function _populateFinishedFilterList(field, values) {
var listEl = document.getElementById('finishedFilterList');
if (!listEl) return;
var sel = finishedOrderFilter[field] || {};
var html = '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="finishedFilterSelectAll" checked onchange="finishedFilterToggleAll(this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="clearFinishedHeaderFilter()" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="finished-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="finishedFilterCbChange()"> ' + escHtml(v);
html += '</label>';
});
listEl.innerHTML = html;
}
function finishedFilterSearch(q) {
var items = document.querySelectorAll('#finishedFilterList label');
q = (q || '').toLowerCase();
items.forEach(function(item) {
item.style.display = item.textContent.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
});
}
function finishedFilterToggleAll(checked) {
document.querySelectorAll('#finishedFilterList .finished-filter-cb').forEach(function(cb) { cb.checked = checked; });
}
function finishedFilterCbChange() {
var all = document.querySelectorAll('#finishedFilterList .finished-filter-cb');
var checked = document.querySelectorAll('#finishedFilterList .finished-filter-cb:checked');
var sa = document.getElementById('finishedFilterSelectAll');
if (sa) sa.checked = checked.length === all.length;
}
function applyFinishedFilter() {
var field = finishedFilterField;
var searchInput = document.getElementById('finishedFilterSearch');
var searchText = searchInput ? searchInput.value.trim() : '';
if (searchText) {
finishedOrderFilter[field] = {type: 'search', value: searchText};
} else {
var cbs = document.querySelectorAll('#finishedFilterList .finished-filter-cb:checked');
var checked = [];
cbs.forEach(function(cb) { checked.push(cb.value); });
var allCbs = document.querySelectorAll('#finishedFilterList .finished-filter-cb');
if (checked.length === 0 || checked.length === allCbs.length) {
delete finishedOrderFilter[field];
} else {
finishedOrderFilter[field] = {type: 'list', values: checked};
}
}
if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; }
_syncFinishedFilterIcons();
finishedLoadOrders(1);
}
function applyFinishedNumFilter() {
var field = finishedFilterField;
var cond = document.getElementById('finishedNumCond')?.value || '';
var val = document.getElementById('finishedNumVal')?.value || '';
if (!cond || !val) { alert('请选择条件并输入数值'); return; }
finishedOrderFilter[field] = {type: 'num', cond: cond, val: val};
if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; }
_syncFinishedFilterIcons();
finishedLoadOrders(1);
}
function clearFinishedHeaderFilter() {
delete finishedOrderFilter[finishedFilterField];
if (finishedFilterMenu) { finishedFilterMenu.remove(); finishedFilterMenu = null; }
_syncFinishedFilterIcons();
finishedLoadOrders(1);
}
function _syncFinishedFilterIcons() {
var icons = document.querySelectorAll('#finishedOrdersTable .fn-filt-icon');
icons.forEach(function(icon) {
var field = icon.parentNode.textContent.trim().replace(/▾/, '').trim();
var hasFilter = finishedOrderFilter[field];
icon.style.color = hasFilter ? '#1890ff' : 'var(--text-light)';
icon.style.fontWeight = hasFilter ? 'bold' : 'normal';
});
}
// ─── 下单备注可编辑通用函数 ───
// ─── 订单表头列筛选功能 ───
// 根据类型获取筛选状态对象
function _orderFilterState(type) {
if (type === 'domestic') return domesticOrderFilter;
if (type === 'overseas') return overseasOrderFilter;
if (type === 'packaging') return packagingOrderFilter;
return {};
}
// 检查订单表是否有搜索栏筛选条件(使用JS变量而非DOM值)
function _orderHasSearch(type) {
if (type === 'domestic') {
return !!(domesticSearch || domesticFilterStatus || domesticFilterSupplier || domesticFilterOrderer);
}
if (type === 'overseas') {
return !!(overseasSearch || overseasFilterStatus || overseasFilterSupplier || overseasFilterOrderer);
}
if (type === 'packaging') {
return !!(packagingSearch || packagingFilterStatus || packagingFilterSupplier || packagingFilterOrderer);
}
return false;
}
// 从当前表格DOM中提取指定列的所有唯一值(通过data-field属性精确定位列)
function _extractColumnValuesFromDOM(type, field) {
var tableId = type === 'domestic' ? 'domesticOrdersTable' :
type === 'overseas' ? 'overseasOrdersTable' : 'packagingOrdersTable';
var tbodyId = type === 'domestic' ? 'domesticOrdersTbody' :
type === 'overseas' ? 'overseasOrdersTbody' : 'packagingOrdersTbody';
var table = document.getElementById(tableId);
if (!table) return [];
// 从表头找到该字段的列索引
var headerRow = table.querySelector('thead tr');
if (!headerRow) return [];
var colIdx = -1;
headerRow.querySelectorAll('th').forEach(function(th, idx) {
if (th.getAttribute('data-field') === field) {
colIdx = idx;
}
});
if (colIdx < 0) return [];
// 从每行提取该列的值
var vals = [];
var seen = {};
var tbody = document.getElementById(tbodyId);
if (tbody) {
tbody.querySelectorAll('tr').forEach(function(tr) {
var td = tr.cells[colIdx];
if (td) {
var txt = td.textContent.trim();
if (txt && txt !== '-' && !seen[txt]) { seen[txt] = 1; vals.push(txt); }
}
});
}
vals.sort();
return vals;
}
function _orderFilterActive(type) {
if (type === 'domestic') return domesticOrderFilterActive;
if (type === 'overseas') return overseasOrderFilterActive;
if (type === 'packaging') return packagingOrderFilterActive;
return {};
}
function _orderFilterMenu(type) {
if (type === 'domestic') return domesticOrderFilterMenu;
if (type === 'overseas') return overseasOrderFilterMenu;
if (type === 'packaging') return packagingOrderFilterMenu;
return null;
}
function _setOrderFilterMenu(type, v) {
if (type === 'domestic') domesticOrderFilterMenu = v;
else if (type === 'overseas') overseasOrderFilterMenu = v;
else if (type === 'packaging') packagingOrderFilterMenu = v;
}
function _orderFilterField(type) {
if (type === 'domestic') return domesticOrderFilterField;
if (type === 'overseas') return overseasOrderFilterField;
if (type === 'packaging') return packagingOrderFilterField;
return '';
}
function _setOrderFilterField(type, v) {
if (type === 'domestic') domesticOrderFilterField = v;
else if (type === 'overseas') overseasOrderFilterField = v;
else if (type === 'packaging') packagingOrderFilterField = v;
}
function _getOrderColumns(type) {
if (type === 'domestic') return DOMESTIC_COLUMNS;
if (type === 'overseas') return OVERSEAS_COLUMNS;
if (type === 'packaging') return PACKAGING_COLUMNS;
if (type === 'finished') return FINISHED_FILTER_COLS;
return [];
}
function _getOrderColLabel(type, field) {
var cols = _getOrderColumns(type);
for (var i = 0; i < cols.length; i++) {
if (cols[i].field === field) return cols[i].label || field;
}
return field;
}
function _getOrderColType(type, field) {
var cols = _getOrderColumns(type);
for (var i = 0; i < cols.length; i++) {
if (cols[i].field === field) {
if (cols[i].type) return cols[i].type;
// 根据字段名自动判断数字列
var numFields = ['数量','金额','单价','实际到货数量','未到货数','做货天数','待提数量'];
if (numFields.indexOf(field) >= 0) return 'number';
return 'text';
}
}
return 'text';
}
// 打开列筛选菜单
function openOrderFilterMenu(type, field, event) {
event = event || window.event;
event.stopPropagation();
// 关闭已有的
var existing = _orderFilterMenu(type);
if (existing) { existing.remove(); _setOrderFilterMenu(type, null); }
_setOrderFilterField(type, field);
var th = event.currentTarget.parentNode;
var rect = th.getBoundingClientRect();
var label = _getOrderColLabel(type, field);
var colType = _getOrderColType(type, field);
if (colType === 'number') {
_buildOrderNumericFilter(type, field, label, rect);
} else {
// 检查缓存:优先从当前表格DOM提取(联动搜索条件),否则用API/缓存
var filterActive = _orderFilterActive(type);
var hasOtherFilters = filterActive && Object.keys(filterActive).length > 0;
var hasSearchCond = _orderHasSearch(type);
var cacheKey = type + ':' + field;
var cached = !hasOtherFilters && !hasSearchCond && window._orderColumnCache && window._orderColumnCache[cacheKey];
if (cached) {
// 命中缓存(无任何筛选条件),直接显示
_buildOrderFilterList(type, field, label, rect, cached);
} else if (hasOtherFilters || hasSearchCond) {
// 有筛选条件:从当前表格DOM提取列值(联动筛选)
_buildOrderFilterShell(type, field, label, rect);
var vals = _extractColumnValuesFromDOM(type, field);
if (vals.length === 0) {
// DOM提取失败则fallback到API
var apiMap2 = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'};
var fp2 = '';
if (hasOtherFilters) {
fp2 = '&order_filter=' + encodeURIComponent(JSON.stringify(filterActive));
}
fetch(apiMap2[type] + '?field=' + encodeURIComponent(field) + fp2)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) { _populateOrderFilterList(type, field, res.data); }
});
} else {
_populateOrderFilterList(type, field, vals);
}
} else {
// 未命中缓存:显示外壳 + 异步加载
_buildOrderFilterShell(type, field, label, rect);
var apiMap = {'domestic':'/api/domestic/orders/column-values','overseas':'/api/overseas/orders/column-values','packaging':'/api/packaging/orders/column-values'};
var filterParam = '';
if (hasOtherFilters) {
filterParam = '&order_filter=' + encodeURIComponent(JSON.stringify(filterActive));
}
fetch(apiMap[type] + '?field=' + encodeURIComponent(field) + filterParam)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0 && res.data) {
// 无联动筛选时缓存结果
if (!hasOtherFilters) {
window._orderColumnCache = window._orderColumnCache || {};
window._orderColumnCache[cacheKey] = res.data;
}
_populateOrderFilterList(type, field, res.data);
}
});
}
}
// 点击外部关闭
var menu = _orderFilterMenu(type);
if (menu) {
var handler = function(e) {
if (!menu.contains(e.target) && e.target !== th && !th.contains(e.target)) {
menu.remove();
_setOrderFilterMenu(type, null);
document.removeEventListener('click', handler);
}
};
setTimeout(function() { document.addEventListener('click', handler); }, 10);
// 搜索自动聚焦
setTimeout(function() {
var inp = document.getElementById('orderFilterSearch_' + type);
if (inp) inp.focus();
}, 150);
}
}
// 构建列表筛选菜单
function _buildOrderFilterList(type, field, label, rect, values) {
var menu = document.createElement('div');
menu.id = 'orderFilterMenu_' + type;
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var filterState = _orderFilterState(type);
// 加锁防止快速多次点击
if (window._orderFilterLock) return;
window._orderFilterLock = true;
setTimeout(function() { window._orderFilterLock = false; }, 300);
if (!filterState[field]) filterState[field] = {};
var sel = filterState[field];
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="orderFilterSearch_' + type + '" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="orderFilterSearchList(\'' + type + '\',this.value)" onkeydown="if(event.key===\'Enter\')applyOrderListFilter(\'' + type + '\')"></div>';
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="orderFilterSelectAll_' + type + '" checked onchange="orderFilterToggleAll(\'' + type + '\',this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="orderFilterClear(\'' + type + '\')" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
html += '<div id="orderFilterList_' + type + '" style="overflow-y:auto;flex:1;max-height:200px;border-top:1px solid #f0f0f0;padding-top:4px">';
values.forEach(function(v) {
var key = escHtml(v).replace(/'/g, "\\'");
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="order-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="orderFilterCbChange(\'' + type + '\')"> ' + escHtml(v);
html += '</label>';
});
html += '</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyOrderListFilter(\'' + type + '\')" style="font-size:12px;padding:4px 12px">确定</button><button class="btn btn-sm" onclick="orderFilterResetAll(\'' + type + '\')" style="font-size:12px;padding:4px 10px;margin-left:6px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
_setOrderFilterMenu(type, menu);
}
// 构建订单筛选菜单外壳(用于异步加载的字段)
function _buildOrderFilterShell(type, field, label, rect) {
var existing = _orderFilterMenu(type);
if (existing) { existing.remove(); _setOrderFilterMenu(type, null); }
_setOrderFilterField(type, field);
var menu = document.createElement('div');
menu.id = 'orderFilterMenu_' + type;
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:220px;max-width:320px;max-height:400px;font-size:13px;display:flex;flex-direction:column';
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px"><input type="text" id="orderFilterSearch_' + type + '" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="搜索..." autocomplete="off" oninput="orderFilterSearchList(\'' + type + '\',this.value)" onkeydown="if(event.key===\'Enter\')applyOrderListFilter(\'' + type + '\')"></div>';
html += '<div id="orderFilterList_' + type + '" style="overflow-y:auto;flex:1;max-height:200px;text-align:center;padding:20px;color:#999">加载中...</div>';
html += '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm btn-primary" onclick="applyOrderListFilter(\'' + type + '\')" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="orderFilterResetAll(\'' + type + '\')" style="font-size:12px;padding:4px 10px">重置</button></div>';
menu.innerHTML = html;
document.body.appendChild(menu);
_setOrderFilterMenu(type, menu);
}
// 填充订单筛选列表(从API返回的数据)
function _populateOrderFilterList(type, field, values) {
var listEl = document.getElementById('orderFilterList_' + type);
if (!listEl) return;
var filterState = _orderFilterState(type);
if (!filterState[field]) filterState[field] = {};
var sel = filterState[field];
var html = '';
html += '<div style="margin-bottom:4px;display:flex;gap:8px;font-size:12px">';
html += '<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="orderFilterSelectAll_' + type + '" checked onchange="orderFilterToggleAll(\'' + type + '\',this.checked)"> <strong>全选</strong></label>';
html += '<a href="javascript:;" onclick="orderFilterClear(\'' + type + '\')" style="color:var(--text-light);font-size:12px">清除筛选</a></div>';
values.forEach(function(v) {
var checked = (sel[v] !== false) ? 'checked' : '';
html += '<label style="display:flex;align-items:center;gap:4px;padding:3px 2px;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">';
html += '<input type="checkbox" class="order-filter-cb" value="' + escHtml(v) + '" ' + checked + ' onchange="orderFilterCbChange(\'' + type + '\')"> ' + escHtml(v);
html += '</label>';
});
listEl.innerHTML = html;
// 搜索自动聚焦
setTimeout(function() {
var inp = document.getElementById('orderFilterSearch_' + type);
if (inp) inp.focus();
}, 150);
}
// 构建数字筛选菜单
function _buildOrderNumericFilter(type, field, label, rect) {
var menu = document.createElement('div');
menu.id = 'orderFilterMenu_' + type;
var filterState = _orderFilterState(type);
var current = filterState[field] || {};
var cond = current.cond || '';
var val = current.val || '';
var html = '';
html += '<div style="font-weight:600;margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #f0f0f0">' + escHtml(label) + '</div>';
html += '<div style="margin-bottom:6px">';
html += '<select id="orderNumCond_' + type + '" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px">';
html += '<option value="">-- 选择条件 --</option>';
html += '<option value="eq"' + (cond === 'eq' ? ' selected' : '') + '>等于</option>';
html += '<option value="neq"' + (cond === 'neq' ? ' selected' : '') + '>不等于</option>';
html += '<option value="gt"' + (cond === 'gt' ? ' selected' : '') + '>大于</option>';
html += '<option value="gte"' + (cond === 'gte' ? ' selected' : '') + '>大于等于</option>';
html += '<option value="lt"' + (cond === 'lt' ? ' selected' : '') + '>小于</option>';
html += '<option value="lte"' + (cond === 'lte' ? ' selected' : '') + '>小于等于</option>';
html += '<option value="range"' + (cond === 'range' ? ' selected' : '') + '>介于(逗号分隔)</option>';
html += '</select>';
html += '</div>';
html += '<div style="margin-bottom:6px">';
html += '<input type="text" id="orderNumVal_' + type + '" class="form-input" style="width:100%;box-sizing:border-box;font-size:12px" placeholder="输入数值..." value="' + escHtml(val) + '" autocomplete="off" onkeydown="if(event.key===\'Enter\')applyOrderNumFilter(\'' + type + '\')">';
html += '</div>';
html += '<div style="border-top:1px solid #f0f0f0;padding-top:6px;display:flex;justify-content:flex-end;gap:6px">';
html += '<button class="btn btn-sm" onclick="orderFilterClear(\'' + type + '\')" style="font-size:12px;padding:4px 10px">清除</button>';
html += '<button class="btn btn-sm btn-primary" onclick="applyOrderNumFilter(\'' + type + '\')" style="font-size:12px;padding:4px 12px">确定</button>';
html += '<button class="btn btn-sm" onclick="orderFilterResetAll(\'' + type + '\')" style="font-size:12px;padding:4px 10px;margin-left:6px">重置</button>';
html += '</div>';
menu.innerHTML = html;
menu.style.cssText = 'position:fixed;top:' + (rect.bottom + 4) + 'px;left:' + Math.max(rect.left, 10) + 'px;z-index:10001;background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:10px 12px;min-width:200px;font-size:13px';
document.body.appendChild(menu);
_setOrderFilterMenu(type, menu);
}
function closeOrderFilterMenu(type) {
var menu = _orderFilterMenu(type);
if (menu) { menu.remove(); _setOrderFilterMenu(type, null); }
}
// 应用列表筛选
function applyOrderListFilter(type) {
var field = _orderFilterField(type);
var searchInput = document.getElementById('orderFilterSearch_' + type);
var searchText = searchInput ? searchInput.value.trim() : '';
var filterState = _orderFilterState(type);
if (searchText) {
// 搜索框有内容 → 文本模糊匹配
filterState[field] = {type: 'search', value: searchText};
} else {
// 搜索框为空 → 用复选框选择
var cbs = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb:checked');
var checked = [];
cbs.forEach(function(cb) { checked.push(cb.value); });
var allCbs = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb');
if (checked.length === 0 || checked.length === allCbs.length) {
delete filterState[field];
} else {
filterState[field] = {type: 'list', values: checked};
}
}
closeOrderFilterMenu(type);
_syncOrderFilterIcons(type);
// 重新加载列表
if (type === 'domestic') domesticLoadOrders(1);
else if (type === 'overseas') overseasLoadOrders(1);
else if (type === 'packaging') packagingLoadOrders(1);
}
// 应用数字筛选
function applyOrderNumFilter(type) {
var field = _orderFilterField(type);
var cond = document.getElementById('orderNumCond_' + type)?.value || '';
var val = document.getElementById('orderNumVal_' + type)?.value || '';
if (!cond || !val) {
alert('请选择条件并输入数值');
return;
}
var filterState = _orderFilterState(type);
filterState[field] = {type: 'num', cond: cond, val: val};
closeOrderFilterMenu(type);
_syncOrderFilterIcons(type);
if (type === 'domestic') domesticLoadOrders(1);
else if (type === 'overseas') overseasLoadOrders(1);
else if (type === 'packaging') packagingLoadOrders(1);
}
function orderFilterSearchList(type, q) {
var items = document.querySelectorAll('#orderFilterList_' + type + ' label');
q = (q || '').toLowerCase();
items.forEach(function(label) {
var txt = label.textContent.toLowerCase();
label.style.display = (!q || txt.indexOf(q) >= 0) ? '' : 'none';
});
}
function orderFilterToggleAll(type, checked) {
document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb').forEach(function(cb) { cb.checked = checked; });
}
function orderFilterCbChange(type) {
var all = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb');
var checked = document.querySelectorAll('#orderFilterList_' + type + ' .order-filter-cb:checked');
var cb = document.getElementById('orderFilterSelectAll_' + type);
if (cb) {
cb.checked = all.length === checked.length;
cb.indeterminate = checked.length > 0 && checked.length < all.length;
}
}
// 清除某一列的筛选
function orderFilterClear(type) {
var field = _orderFilterField(type);
var filterState = _orderFilterState(type);
delete filterState[field];
closeOrderFilterMenu(type);
_syncOrderFilterIcons(type);
if (type === 'domestic') domesticLoadOrders(1);
else if (type === 'overseas') overseasLoadOrders(1);
else if (type === 'packaging') packagingLoadOrders(1);
}
// 重置所有筛选
function orderFilterResetAll(type) {
var filterState = _orderFilterState(type);
Object.keys(filterState).forEach(function(k) { delete filterState[k]; });
closeOrderFilterMenu(type);
_syncOrderFilterIcons(type);
if (type === 'domestic') domesticLoadOrders(1);
else if (type === 'overseas') overseasLoadOrders(1);
else if (type === 'packaging') packagingLoadOrders(1);
}
// 同步筛选图标高亮
function _syncOrderFilterIcons(type) {
var filterState = _orderFilterState(type);
var active = {};
Object.keys(filterState).forEach(function(f) {
if (filterState[f]) active[f] = true;
});
var activeObj = _orderFilterActive(type);
Object.keys(activeObj).forEach(function(k) { delete activeObj[k]; });
Object.keys(active).forEach(function(k) { activeObj[k] = true; });
// 更新表头图标颜色
var tableId = type + 'OrdersTable';
var table = document.getElementById(tableId);
if (!table) return;
var ths = table.querySelectorAll('thead th[data-field]');
ths.forEach(function(th) {
var field = th.getAttribute('data-field');
var icon = th.querySelector('.order-filt-icon');
if (icon) {
if (active[field]) {
icon.style.color = '#1890ff';
icon.style.fontWeight = 'bold';
icon.textContent = '▼';
} else {
icon.style.color = 'var(--text-light)';
icon.style.fontWeight = 'normal';
icon.textContent = '▾';
}
}
});
}
// ─── 字段设置功能 ───
const DOMESTIC_COLUMNS = [
{field:'编码',label:'货品编码',defShow:true},
{field:'名称',label:'货品名称',defShow:true},
{field:'日期',label:'跟单下单日期',defShow:true},
{field:'计划下单时间',label:'计划下单时间',defShow:true},
{field:'新旧品',label:'新旧品',defShow:true},
{field:'下单备注',label:'下单备注',defShow:true},
{field:'单价',label:'单价',defShow:true,type:'number'},
{field:'是否审核价格',label:'审核价格',defShow:true},
{field:'数量',label:'数量',defShow:true,type:'number'},
{field:'金额',label:'金额',defShow:true,type:'number'},
{field:'供货商',label:'供应商',defShow:true},
{field:'仓库',label:'仓库',defShow:true},
{field:'金蝶采购单',label:'金蝶采购单',defShow:true},
{field:'采购员',label:'采购员',defShow:true},
{field:'下单人',label:'下单人',defShow:true},
{field:'进度',label:'进度',defShow:true},
{field:'旺店通生产单号',label:'旺店通单号',defShow:true},
{field:'是否已录入旺店通',label:'录入旺店通',defShow:true},
{field:'采购开发',label:'采购开发',defShow:true},
{field:'实际到货日期',label:'实际到货日期',defShow:true},
{field:'实际到货数量',label:'实际到货数量',defShow:true,type:'number'},
{field:'未到货数',label:'未到货数',defShow:true,type:'number'},
{field:'预计到货日期',label:'预计到货日期',defShow:true},
{field:'金蝶入库',label:'金蝶入库',defShow:true},
{field:'状态',label:'状态',defShow:true},
]
const OVERSEAS_COLUMNS = [
{field:'产品编码',label:'货品编码',defShow:true},
{field:'货品名称',label:'货品名称',defShow:true},
{field:'日期',label:'跟单下单日期',defShow:true},
{field:'计划下单时间',label:'计划下单时间',defShow:true},
{field:'单价',label:'单价',defShow:true,type:'number'},
{field:'是否审核价格',label:'审核价格',defShow:true},
{field:'数量',label:'数量',defShow:true,type:'number'},
{field:'金额',label:'金额',defShow:true,type:'number'},
{field:'供应商',label:'供应商',defShow:true},
{field:'仓库',label:'仓库',defShow:true},
{field:'新旧品',label:'新旧品',defShow:true},
{field:'是否已做采购单',label:'已做采购单',defShow:true},
{field:'是否做入库',label:'已入库',defShow:true},
{field:'采购员',label:'采购员',defShow:true},
{field:'下单人',label:'下单人',defShow:true},
{field:'进度',label:'进度',defShow:true},
{field:'下单备注',label:'下单备注',defShow:true},
{field:'采购开发',label:'采购开发',defShow:true},
{field:'旺店通生产单号',label:'旺店通单号',defShow:true},
{field:'是否已录入到旺店通',label:'录入旺店通',defShow:true},
{field:'实际到货日期',label:'实际到货日期',defShow:true},
{field:'实际到货数量',label:'实际到货数量',defShow:true,type:'number'},
{field:'预计到货日期',label:'预计到货日期',defShow:true},
{field:'未到货数',label:'未到货数',defShow:true,type:'number'},
{field:'状态',label:'状态',defShow:true},
];
const PACKAGING_COLUMNS = [
{field:'编码',label:'货品编码',defShow:true},
{field:'名称',label:'货品名称',defShow:true},
{field:'日期',label:'日期',defShow:true},
{field:'单价',label:'单价',defShow:true,type:'number'},
{field:'数量',label:'数量',defShow:true,type:'number'},
{field:'金额',label:'金额',defShow:true,type:'number'},
{field:'供应商',label:'供应商',defShow:true},
{field:'下单备注',label:'下单备注',defShow:true},
{field:'仓库到货',label:'仓库到货',defShow:true},
{field:'进度',label:'进度',defShow:true},
{field:'是否已做采购单',label:'已做采购单',defShow:true},
{field:'是否已做入库',label:'已入库',defShow:true},
{field:'采购下单人',label:'采购下单人',defShow:true},
{field:'旺店通编号',label:'旺店通编号',defShow:true},
{field:'是否已录入旺店通',label:'录入旺店通',defShow:true},
{field:'到货日期',label:'到货日期',defShow:true},
{field:'状态',label:'状态',defShow:true},
{field:'做货天数',label:'做货天数',defShow:true,type:'number'},
{field:'包材开发',label:'包材开发',defShow:true}
];
// ─── 通用字段设置系统(支持拖拽排序 + 方案管理) ───
// 按类型获取列数据
function getOrderColumns(type) {
if (type === 'domestic') return DOMESTIC_COLUMNS;
if (type === 'overseas') return OVERSEAS_COLUMNS;
if (type === 'packaging') return PACKAGING_COLUMNS;
return [];
}
// 获取可见性映射(兼容localStorage)
function getOrderColVisible(type) {
var key = type + 'ColVisible';
var stored = localStorage.getItem(key);
if (stored) {
try { var m = JSON.parse(stored); return m; } catch(e) {}
}
var cols = getOrderColumns(type);
var m = {};
cols.forEach(function(c) { m[c.field] = c.defShow; });
return m;
}
function saveOrderColVisible(type, m) {
localStorage.setItem(type + 'ColVisible', JSON.stringify(m));
}
// 初始化各页面状态
function initOrderColState(type) {
var cols = getOrderColumns(type);
var visVar = type + 'ColVisible';
var pendingVar = '_' + type + 'PendingVisible';
var dragVar = '_' + type + 'DragOrder';
if (!window[visVar]) {
window[visVar] = getOrderColVisible(type);
}
if (!window[pendingVar]) {
window[pendingVar] = {};
cols.forEach(function(c) { window[pendingVar][c.field] = window[visVar][c.field]; });
}
if (!window[dragVar]) {
window[dragVar] = cols.map(function(c) { return c.field; });
}
}
// 确保所有页面已初始化
initOrderColState('domestic');
initOrderColState('overseas');
initOrderColState('packaging');
// ─── 拖拽checkbox渲染 ───
function renderOrderColCheckboxes(type) {
var grid = document.getElementById(type + 'ColGrid');
if (!grid) return;
var cols = getOrderColumns(type);
var dragVar = '_' + type + 'DragOrder';
var pendingVar = '_' + type + 'PendingVisible';
var order = window[dragVar] || cols.map(function(c) { return c.field; });
grid.innerHTML = '';
order.forEach(function(field) {
var col = cols.find(function(c) { return c.field === field; });
if (!col) return;
var checked = window[pendingVar][field] ? 'checked' : '';
var label = document.createElement('label');
label.draggable = true;
label.dataset.field = field;
label.style.cssText = 'padding:4px 6px;border-radius:4px;cursor:grab;user-select:none;display:flex;align-items:center;gap:4px';
label.innerHTML = '<span style="color:#bbb;font-size:12px;cursor:grab">⠿</span><input type="checkbox" id="' + type + '-cb-' + field + '" ' + checked + ' onchange="toggleOrderCol(\'' + type + '\',\'' + field + '\')"> ' + col.label;
label.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', field);
this.style.opacity = '0.4';
});
label.addEventListener('dragend', function(e) {
this.style.opacity = '1';
document.querySelectorAll('#' + type + 'ColGrid label').forEach(function(el) { el.style.border = ''; });
});
label.addEventListener('dragover', function(e) {
e.preventDefault();
document.querySelectorAll('#' + type + 'ColGrid label').forEach(function(el) { el.style.border = ''; });
this.style.border = '2px dashed var(--primary)';
});
label.addEventListener('dragleave', function(e) {
this.style.border = '';
});
label.addEventListener('drop', function(e) {
e.preventDefault();
document.querySelectorAll('#' + type + 'ColGrid label').forEach(function(el) { el.style.border = ''; });
var fromField = e.dataTransfer.getData('text/plain');
var toField = this.dataset.field;
if (fromField === toField) return;
var order = window[dragVar];
var fromIdx = order.indexOf(fromField);
var toIdx = order.indexOf(toField);
if (fromIdx === -1 || toIdx === -1) return;
order.splice(fromIdx, 1);
order.splice(toIdx, 0, fromField);
renderOrderColCheckboxes(type);
});
grid.appendChild(label);
});
}
// ─── 模态弹窗 ───
function openOrderColModal(type) {
var cols = getOrderColumns(type);
var dragVar = '_' + type + 'DragOrder';
var pendingVar = '_' + type + 'PendingVisible';
var visVar = type + 'ColVisible';
// 初始化拖拽顺序
window[dragVar] = cols.map(function(c) { return c.field; });
// 把当前生效状态复制到临时状态
cols.forEach(function(c) {
window[pendingVar][c.field] = window[visVar][c.field];
});
// 渲染grid
var grid = document.getElementById(type + 'ColGrid');
if (grid) renderOrderColCheckboxes(type);
// 全选checkbox同步
var allCb = document.getElementById(type + '-cb-all');
if (allCb) {
allCb.checked = cols.every(function(c) { return window[visVar][c.field]; });
}
// 重置搜索
var searchInput = document.getElementById(type + 'ColSearchInput');
if (searchInput) searchInput.value = '';
filterOrderColCheckboxes(type);
document.getElementById(type + 'ColModal').style.display = 'flex';
refreshOrderSchemeList(type);
}
// ─── 通用辅助函数 ───
function toggleOrderCol(type, field) {
var pendingVar = '_' + type + 'PendingVisible';
window[pendingVar][field] = !window[pendingVar][field];
var show = window[pendingVar][field];
var cb = document.getElementById(type + '-cb-' + field);
if (cb) cb.checked = show;
}
function filterOrderColCheckboxes(type) {
var keyword = (document.getElementById(type + 'ColSearchInput')?.value || '').trim();
var labels = document.querySelectorAll('#' + type + 'ColCheckboxes label');
labels.forEach(function(label) {
if (!keyword) {
label.style.display = '';
} else {
var text = label.textContent || '';
label.style.display = text.includes(keyword) ? '' : 'none';
}
});
}
function toggleOrderColAll(type, checked) {
var cols = getOrderColumns(type);
var pendingVar = '_' + type + 'PendingVisible';
cols.forEach(function(c) {
window[pendingVar][c.field] = checked;
var cb = document.getElementById(type + '-cb-' + c.field);
if (cb) cb.checked = checked;
});
}
function closeOrderColModal(type) {
document.getElementById(type + 'ColModal').style.display = 'none';
}
// ─── 确认/应用字段设置 ───
function confirmOrderCol(type) {
var cols = getOrderColumns(type);
var dragVar = '_' + type + 'DragOrder';
var pendingVar = '_' + type + 'PendingVisible';
var visVar = type + 'ColVisible';
// 应用拖拽后的新顺序
var newOrder = window[dragVar];
if (newOrder) {
var reordered = [];
newOrder.forEach(function(field) {
var col = cols.find(function(c) { return c.field === field; });
if (col) reordered.push(col);
});
cols.forEach(function(c) {
if (newOrder.indexOf(c.field) === -1) reordered.push(c);
});
// 修改原数组(按引用)
cols.length = 0;
reordered.forEach(function(c) { cols.push(c); });
}
// 更新可见性
cols.forEach(function(c) {
window[visVar][c.field] = window[pendingVar][c.field];
});
saveOrderColVisible(type, window[visVar]);
document.getElementById(type + 'ColModal').style.display = 'none';
// 全量刷新页面,确保表头与表体列顺序一致(同计划预测 confirmPlanCol 做法)
var pageMap = { 'domestic': 'orders_domestic', 'overseas': 'orders_overseas', 'packaging': 'orders_packaging' };
var pageKey = pageMap[type];
if (pageKey && window._pageCache) {
delete window._pageCache[pageKey];
loadPage(pageKey);
}
}
// ─── 方案管理 ───
// 构建当前列配置JSON
function buildOrderColumnsJSON(type) {
var cols = getOrderColumns(type);
var dragVar = '_' + type + 'DragOrder';
var pendingVar = '_' + type + 'PendingVisible';
var order = window[dragVar] || cols.map(function(c) { return c.field; });
var result = [];
order.forEach(function(field) {
var colDef = cols.find(function(c) { return c.field === field; });
if (colDef) {
result.push({
field: colDef.field,
label: colDef.label,
visible: window[pendingVar][field] !== undefined ? window[pendingVar][field] : true
});
}
});
cols.forEach(function(c) {
var found = result.some(function(col) { return col.field === c.field; });
if (!found) {
result.push({
field: c.field,
label: c.label,
visible: window[pendingVar][c.field] !== undefined ? window[pendingVar][c.field] : c.defShow
});
}
});
return JSON.stringify(result);
}
// 保存方案
window.saveOrderScheme = function(type) {
var nameInput = document.getElementById(type + 'SchemeNameInput');
if (!nameInput) return;
var schemeName = nameInput.value.trim();
if (!schemeName) {
alert('请输入方案名称');
nameInput.focus();
return;
}
var columnsJson = buildOrderColumnsJSON(type);
fetch('/api/planning/scheme/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
scheme_name: schemeName,
columns_json: columnsJson,
page: type,
is_default: 0
})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
nameInput.value = '';
refreshOrderSchemeList(type);
alert('方案已保存');
} else {
alert(res.msg || '保存失败');
}
})
.catch(function() { alert('保存失败,请检查网络'); });
};
// 加载方案
window.loadOrderScheme = function(type, schemeId) {
if (schemeId === undefined || schemeId === null) return;
var cols = getOrderColumns(type);
var pendingVar = '_' + type + 'PendingVisible';
var dragVar = '_' + type + 'DragOrder';
fetch('/api/planning/schemes?page=' + type)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var schemes = res.data || [];
var target = null;
for (var i = 0; i < schemes.length; i++) {
if (schemes[i].id == schemeId) {
target = schemes[i];
break;
}
}
if (!target) {
alert('方案不存在或已被删除');
refreshOrderSchemeList(type);
return;
}
var parsed;
try {
parsed = JSON.parse(target.columns_json);
} catch(e) {
alert('方案数据异常');
return;
}
if (!parsed || !Array.isArray(parsed) || parsed.length === 0) {
alert('方案数据为空');
return;
}
var newOrder = [];
var pendingVisible = {};
parsed.forEach(function(c) {
if (c && c.field) {
newOrder.push(c.field);
pendingVisible[c.field] = c.visible !== false;
}
});
cols.forEach(function(c) {
if (pendingVisible[c.field] === undefined) {
newOrder.push(c.field);
pendingVisible[c.field] = false;
}
});
window[dragVar] = newOrder.slice();
Object.keys(pendingVisible).forEach(function(field) {
window[pendingVar][field] = pendingVisible[field];
});
var grid = document.getElementById(type + 'ColGrid');
if (grid) renderOrderColCheckboxes(type);
var allCb = document.getElementById(type + '-cb-all');
if (allCb) {
var allChecked = cols.every(function(c) { return window[pendingVar][c.field]; });
allCb.checked = allChecked;
}
refreshOrderSchemeList(type);
})
.catch(function(err) {
console.error('loadOrderScheme error:', err);
});
};
// 删除方案
window.deleteOrderScheme = function(type, schemeId) {
if (!confirm('确定要删除此方案吗?')) return;
fetch('/api/planning/scheme/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: schemeId})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
refreshOrderSchemeList(type);
} else {
alert(res.msg || '删除失败');
}
});
};
// 设为默认方案
window.setDefaultOrderScheme = function(type, schemeId) {
fetch('/api/planning/scheme/set_default', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: schemeId, page: type})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
loadOrderScheme(type, schemeId);
refreshOrderSchemeList(type);
} else {
alert(res.msg || '设置失败');
}
})
.catch(function(err) {
console.error('setDefaultOrderScheme error:', err);
});
};
// 刷新方案列表UI
function refreshOrderSchemeList(type) {
var container = document.getElementById(type + 'SchemeList');
if (!container) return;
fetch('/api/planning/schemes?page=' + type)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var schemes = res.data || [];
// 只显示当前页面的方案
var pageSchemes = [];
schemes.forEach(function(s) {
if (s.page === type) pageSchemes.push(s);
});
if (pageSchemes.length === 0) {
container.innerHTML = '<span style="color:var(--text-light);font-size:13px">暂无保存方案</span>';
return;
}
var html = '';
pageSchemes.forEach(function(s) {
var isDefault = s.is_default == 1;
html += '<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;font-size:13px;background:' + (isDefault ? '#e6f7ff' : '#f5f5f5') + ';border:1px solid ' + (isDefault ? '#1890ff' : '#e8e8e8') + '">';
html += '<span style="cursor:pointer" onclick="loadOrderScheme(\'' + type + '\',' + s.id + ')">' + escHtml(s.scheme_name) + '</span>';
if (isDefault) {
html += '<span style="color:#1890ff;font-size:11px">★默认</span>';
}
html += '<span style="color:#bbb;font-size:14px;cursor:pointer;margin-left:2px" onclick="setDefaultOrderScheme(\'' + type + '\',' + s.id + ')" title="设为默认">📌</span>';
html += '<span style="color:#bbb;font-size:14px;cursor:pointer" onclick="deleteOrderScheme(\'' + type + '\',' + s.id + ')" title="删除">✕</span>';
html += '</span>';
});
container.innerHTML = html;
});
}
// 加载默认方案(页面初始化时调用)
function loadDefaultOrderScheme(type) {
fetch('/api/planning/schemes?page=' + type)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var schemes = res.data || [];
var defaultScheme = null;
schemes.forEach(function(s) {
if (s.is_default == 1 && s.page === type) defaultScheme = s;
});
if (defaultScheme) {
loadOrderScheme(type, defaultScheme.id);
}
});
}
// ─── 保持向后兼容 ───
function getDomesticVisible() { return getOrderColVisible('domestic'); }
function getOverseasVisible() { return getOrderColVisible('overseas'); }
function getPackagingVisible() { return getOrderColVisible('packaging'); }
function saveDomesticVisible(m) { saveOrderColVisible('domestic', m); }
function saveOverseasVisible(m) { saveOrderColVisible('overseas', m); }
function savePackagingVisible(m) { saveOrderColVisible('packaging', m); }
window.openDomesticColModal = function() { openOrderColModal('domestic'); };
window.openOverseasColModal = function() { openOrderColModal('overseas'); };
window.openPackagingColModal = function() { openOrderColModal('packaging'); };
window.editRemark = function(td, id, field, apiEndpoint) {
var textSpan = td.querySelector('.remark-text');
var current = textSpan ? textSpan.textContent.trim() : td.textContent.trim();
var input = document.createElement('input');
input.type = 'text';
input.className = 'remark-input';
input.value = current;
input.style.cssText = 'width:100%;border:1px solid #1890ff;padding:2px 6px;font-size:12px;border-radius:3px;box-sizing:border-box';
td.innerHTML = '';
td.appendChild(input);
input.focus();
function revert() {
if (td.querySelector('input')) {
td.innerHTML = '<span class="remark-text">' + escHtml(current) + '</span> <span class="remark-edit-btn" onclick="event.stopPropagation();editRemark(this.parentNode,' + id + ',\'' + field + '\',\'' + apiEndpoint + '\')" style="cursor:pointer;color:#1890ff;font-size:12px">✏️</span>';
}
}
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 = '<span class="remark-text">' + escHtml(newVal) + '</span> <span class="remark-edit-btn" onclick="event.stopPropagation();editRemark(this.parentNode,' + id + ',\'' + field + '\',\'' + apiEndpoint + '\')" style="cursor:pointer;color:#1890ff;font-size:12px">✏️</span>';
} else {
alert('保存失败: ' + (res.msg || ''));
revert();
}
})
.catch(function(){
alert('网络错误');
revert();
});
} else {
revert();
}
}
input.onkeydown = function(e) {
if (e.key === 'Enter') { e.preventDefault(); doSave(); }
else if (e.key === 'Escape') { revert(); }
};
input.onblur = function() {
setTimeout(function() {
if (td.querySelector('input')) revert();
}, 250);
};
};
// ─── 单价保存函数 ───
// ─── 供应商模糊搜索联动 ───
// ─── 供应商模糊搜索下拉(按简称匹配)───
window.domesticSupSearch = function(input) {
var dd = document.getElementById('domesticSupDD');
if (!dd) {
dd = document.createElement('div');
dd.id = 'domesticSupDD';
dd.style.cssText = 'position:fixed;z-index:99999;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12);min-width:200px';
document.body.appendChild(dd);
}
var val = input.value.trim().toLowerCase();
if (!val || !window._supplierList) { dd.style.display = 'none'; return; }
var matched = [];
window._supplierList.forEach(function(s) {
var sn = (s.short_name || '').toLowerCase();
if (sn.indexOf(val) >= 0) matched.push(s);
});
if (matched.length === 0) { dd.style.display = 'none'; return; }
var rect = input.getBoundingClientRect();
dd.style.left = rect.left + 'px';
dd.style.top = (rect.bottom + 2) + 'px';
dd.style.minWidth = Math.max(rect.width, 200) + 'px';
var html = '';
matched.slice(0, 20).forEach(function(s) {
var name = escHtml(s.supplier_name || '');
var sn = escHtml(s.short_name || '');
var code = escHtml(s.supplier_code || '');
html += '<div style="padding:6px 10px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0" onmouseenter="this.style.background=\'#e6f7ff\'" onmouseleave="this.style.background=\'\'" onclick="domesticSupSelect(\'' + input.getAttribute('data-pid') + '\',\'' + escJs(name) + '\')">';
html += '<span style="font-weight:600">' + name + '</span>';
html += ' <code style="font-size:11px;color:#999;margin-left:4px">' + sn + '</code>';
html += ' <code style="font-size:11px;color:#bbb;margin-left:4px">' + code + '</code>';
html += '</div>';
});
dd.innerHTML = html;
dd.style.display = 'block';
};
window.domesticSupSelect = function(pid, name) {
var input = document.querySelector('.supplier-inline[data-pid="' + pid + '"]');
if (input) {
input.value = name;
input.focus();
supplierSave(input);
}
var dd = document.getElementById('domesticSupDD');
if (dd) dd.style.display = 'none';
};
window.supplierSave = function(input) {
var oldVal = input.defaultValue;
var newVal = input.value.trim();
if (newVal === oldVal) return;
var id = input.getAttribute('data-pid');
var field = input.getAttribute('data-pfield');
var api = input.getAttribute('data-papi');
if (confirm('是否保存修改?将供应商修改为:' + newVal)) {
fetch(api, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: parseInt(id), field: field, value: newVal || ''})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
input.defaultValue = newVal;
if (window.domesticLoadOrders) domesticLoadOrders(domesticOrderPage || 1);
} else {
alert('保存失败: ' + (res.msg || ''));
input.value = oldVal;
}
})
.catch(function(){ alert('网络错误'); input.value = oldVal; });
} else {
input.value = oldVal;
}
};
// 单价修改时自动重算同行金额
function recalcAmount(input) {
var row = input.closest('tr');
if (!row) return;
var price = parseFloat(input.value) || 0;
var qty = 0;
var amtCell = null;
Array.from(row.cells).forEach(function(cell) {
if (cell.classList.contains('qty-cell')) {
qty = parseInt(cell.textContent.replace(/[^0-9-]/g,'')) || 0;
}
if (cell.classList.contains('amt-cell')) {
amtCell = cell;
}
});
if (amtCell) {
var amount = price * qty;
amtCell.textContent = amount ? '¥' + amount.toLocaleString() : '';
amtCell.dataset.amount = amount;
}
}
window.priceSave = function(input) {
var oldVal = input.defaultValue;
var newVal = input.value.trim();
if (newVal === oldVal) return;
var id = input.getAttribute('data-pid');
var field = input.getAttribute('data-pfield');
var api = input.getAttribute('data-papi');
if (confirm('是否保存修改?')) {
fetch(api, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: parseInt(id), field: field, value: newVal || '0'})
})
.then(function(r){return r.json()})
.then(function(res){
if (res.code === 0) {
input.defaultValue = newVal;
// 根据API路径判定刷新哪个列表
if (api && api.indexOf('overseas') >= 0 && window.overseasLoadOrders) {
overseasLoadOrders(overseasOrderPage || 1);
} else if (api && api.indexOf('packaging') >= 0 && window.packagingLoadOrders) {
packagingLoadOrders(packagingOrderPage || 1);
} else if (window.domesticLoadOrders) {
domesticLoadOrders(domesticOrderPage || 1);
}
} else {
alert('保存失败: ' + (res.msg || ''));
input.value = oldVal;
}
})
.catch(function(){ alert('网络错误'); input.value = oldVal; });
} else {
input.value = oldVal;
}
};
// 表格事件委托:单价输入框
if (!window._priceDelegated) {
window._priceDelegated = true;
document.addEventListener('focusin', function(e) {
if (e.target.classList.contains('price-inline')) e.target.style.borderColor = '#1890ff';
});
document.addEventListener('focusout', function(e) {
if (e.target.classList.contains('price-inline')) {
e.target.style.borderColor = '#d9d9d9';
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.classList.contains('price-inline')) {
e.preventDefault();
priceSave(e.target);
}
if (e.key === 'Enter' && e.target.classList.contains('supplier-inline')) {
e.preventDefault();
supplierSave(e.target);
}
});
document.addEventListener('input', function(e) {
if (e.target.classList.contains('supplier-inline')) {
domesticSupSearch(e.target);
}
});
document.addEventListener('focusout', function(e) {
if (e.target.classList.contains('supplier-inline')) {
setTimeout(function() {
var dd = document.getElementById('domesticSupDD');
if (dd) dd.style.display = 'none';
}, 200);
}
});
}
// ─── 日期编辑函数 ───
window.editDateField = function(td, id, field, apiEndpoint) {
var textSpan = td.querySelector('.date-text');
var current = textSpan ? textSpan.textContent.trim() : td.textContent.trim();
var dateOnly = current.split(' ')[0];
var input = document.createElement('input');
input.type = 'date';
input.className = 'date-input';
input.value = dateOnly || '';
input.style.cssText = 'width:100%;border:1px solid #1890ff;padding:2px 4px;font-size:12px;border-radius:3px;box-sizing:border-box';
td.innerHTML = '';
td.appendChild(input);
input.focus();
// 主动弹出日期选择器(标记pickering防止blur误杀)
input._picking = true;
if (input.showPicker) input.showPicker();
setTimeout(function() { input._picking = false; }, 500);
// 点击外部关闭编辑器
function outsideClick(e) {
if (!td.contains(e.target)) {
document.removeEventListener('click', outsideClick, true);
if (!saved) { saved = true; restoreView(current); }
}
}
setTimeout(function() { document.addEventListener('click', outsideClick, true); }, 100);
function makeIconTd(val) {
var v = val || '';
return '<span class="date-text">' + escHtml(v) + '</span> <span class="date-edit-btn" data-did="' + id + '" data-dfield="' + field + '" data-dapi="' + apiEndpoint + '" style="cursor:pointer;color:#1890ff;font-size:12px">📅</span>';
}
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 = `
<div class="page-header">
<h1>订单管理</h1>
<p class="page-desc">已生成的采购订单明细</p>
</div>
<div id="ordersContent">
<div class="card">
<div class="card-header"><h3>📋 采购订单明细</h3></div>
<div class="card-body" style="padding:0;overflow-x:auto">
<table class="data-table" id="ordersTable">
<thead><tr>
<th>序号</th><th>下单时间</th><th>下单单号</th><th>货品编号</th><th>货品名称</th><th>单价</th><th>数量</th><th>总金额</th><th>供应商名称</th><th>货期</th><th>状态</th><th>操作</th>
</tr></thead>
<tbody id="ordersTbody"><tr><td colspan="12" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>
</table>
</div>
<div id="ordersPagination" class="card-footer" style="padding:8px 16px;border-top:1px solid var(--border)"></div>
</div>
</div>
<!-- 订单详情弹窗 -->
<div class="modal-overlay" id="orderDetailModal" style="display:none">
<div class="modal" style="width:900px">
<div class="modal-header">
<h3 id="orderDetailTitle">订单详情</h3>
<span class="modal-close" onclick="document.getElementById('orderDetailModal').style.display='none'">&times;</span>
</div>
<div class="modal-body" style="max-height:70vh;overflow-y:auto;padding:16px 20px">
<div id="orderDetailContent"></div>
</div>
<div class="modal-footer">
<button class="btn" onclick="document.getElementById('orderDetailModal').style.display='none'">关闭</button>
</div>
</div>
</div>
<!-- 订单编辑弹窗 -->
<div class="modal-overlay" id="orderEditModal" style="display:none">
<div class="modal" style="width:500px">
<div class="modal-header">
<h3>编辑订单明细</h3>
<span class="modal-close" onclick="document.getElementById('orderEditModal').style.display='none'">&times;</span>
</div>
<div class="modal-body">
<input type="hidden" id="editItemId">
<div class="form-group">
<label>货品编号</label>
<input type="text" id="editItemCode" class="form-input" readonly>
</div>
<div class="form-group">
<label>货品名称</label>
<input type="text" id="editItemName" class="form-input" readonly>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>单价</label>
<input type="number" id="editUnitPrice" class="form-input" step="0.01">
</div>
<div class="form-group" style="flex:1">
<label>数量</label>
<input type="number" id="editOrderQty" class="form-input">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="document.getElementById('orderEditModal').style.display='none'">取消</button>
<button class="btn btn-primary" onclick="saveOrderItem()">保存</button>
</div>
</div>
</div>
`;
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 = '<tr><td colspan="12" style="text-align:center;padding:60px;color:var(--text-light)">暂无采购订单明细</td></tr>';
} 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 '<tr>' +
'<td>' + (startIdx + i + 1) + '</td>' +
'<td>' + escHtml(o.order_time || '-') + '</td>' +
'<td><code>' + escHtml(o.order_no) + '</code></td>' +
'<td><code>' + escHtml(o.product_code) + '</code></td>' +
'<td>' + escHtml(o.product_name || '-') + '</td>' +
'<td>¥' + (parseFloat(o.unit_price) || 0).toFixed(2) + '</td>' +
'<td><strong>' + (parseInt(o.order_qty) || 0).toLocaleString() + '</strong></td>' +
'<td><strong>¥' + (parseFloat(o.amount || amount) || 0).toLocaleString() + '</strong></td>' +
'<td>' + escHtml(o.supplier_name) + '</td>' +
'<td>' + (o.lead_time || '-') + '天</td>' +
'<td>' + statLabel + '</td>' +
'<td style="white-space:nowrap">' +
'<button class="btn btn-sm btn-outline" onclick="viewOrder(' + o.order_id + ')" title="详情">详情</button> ' +
'<button class="btn btn-sm btn-outline" onclick="editOrderItem(' + o.item_id + ',\'" + escHtml(o.product_code) + "\',\'" + escHtml(o.product_name) + "\',' + (parseFloat(o.unit_price) || 0) + ',' + (parseInt(o.order_qty) || 0) + ')" title="编辑">编辑</button> ' +
(isPending ? '<button class="btn btn-sm btn-outline" onclick="submitOrder(' + o.order_id + ')" title="提交">提交</button> ' : '') +
'<button class="btn btn-sm btn-outline-danger" onclick="deleteOrder(' + o.order_id + ')" title="删除">删除</button>' +
'</td>' +
'</tr>';
}).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 = '<div class="page-loading"><div class="loading-spinner"></div></div>';
fetch('/api/purchase/orders/detail?id=' + orderId)
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { content.innerHTML = '<div style="text-align:center;padding:40px;color:red">' + (res.msg || '加载失败') + '</div>'; return; }
var order = res.data.order;
var items = res.data.items || [];
var statusMap = {'pending':'🟡 待确认','confirmed':'🟢 已确认'};
var html = '<div class="card" style="margin-bottom:15px"><div class="card-body" style="padding:12px 16px">';
html += '<div style="display:flex;flex-wrap:wrap;gap:20px;font-size:14px">';
html += '<span><strong>订单号:</strong><code>' + escHtml(order.order_no) + '</code></span>';
html += '<span><strong>供应商:</strong>' + escHtml(order.supplier_name) + '</span>';
html += '<span><strong>总数量:</strong>' + (order.total_qty || 0).toLocaleString() + '</span>';
html += '<span><strong>总金额:</strong>¥' + (parseFloat(order.total_amount) || 0).toLocaleString() + '</span>';
html += '<span><strong>状态:</strong>' + (statusMap[order.status] || order.status) + '</span>';
html += '<span><strong>创建人:</strong>' + escHtml(order.created_by || '-') + '</span>';
html += '<span><strong>创建时间:</strong>' + (order.created_at || '-') + '</span>';
html += '</div></div></div>';
if (items.length > 0) {
html += '<table class="data-table" style="font-size:13px"><thead><tr>';
html += '<th>货品编号</th><th>货品名称</th><th>规格</th><th>建议下单</th><th>下单数量</th><th>单价</th><th>金额</th><th>货期</th><th class="col-action" style="width:120px;min-width:120px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th></tr></thead><tbody>';
items.forEach(function(item) {
html += '<tr><td><code>' + escHtml(item.product_code) + '</code></td><td>' + escHtml(item.product_name) + '</td><td>' + escHtml(item.specification || '-') + '</td><td>' + (item.suggested_qty || 0) + '</td><td><strong>' + (item.order_qty || 0).toLocaleString() + '</strong></td><td>¥' + parseFloat(item.unit_price || 0).toFixed(2) + '</td><td>¥' + (parseFloat(item.amount) || 0).toLocaleString() + '</td><td>' + (item.lead_time || '-') + '天</td></tr>';
});
html += '</tbody></table>';
}
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 = `
<div class="page-header">
<h1>物料清单管理</h1>
<p class="page-desc">产品物料清单 — 以货品编号分页,点击产品行展开物料明细</p>
</div>
<div id="bomContent" class="page-flex">
<div class="card">
<div class="card-header">
<h3>📋 物料清单</h3>
<div style="display:flex;gap:6px;align-items:center;flex:1;justify-content:flex-end">
<input type="text" id="bomSupplierFilter" class="form-input" style="width:130px" placeholder="🏢 供应商筛选" autocomplete="off" onkeydown="bomSearchKeydown(event)">
<select id="bomMaintFilter" class="form-input" style="width:80px" onchange="bomSearch()">
<option value="">全部</option>
<option value="有BOM">有BOM</option>
<option value="无BOM">无BOM</option>
</select>
<input type="text" id="bomSearchInput" class="form-input" style="width:180px" placeholder="🔍 搜索编号/名称" autocomplete="off" onkeydown="bomSearchKeydown(event)">
<button class="btn btn-sm btn-primary" onclick="bomSearch()" style="padding:3px 10px">🔍 搜索</button>
<button class="btn btn-sm" onclick="bomReset()" style="padding:3px 10px;color:#888;border-color:#d9d9d9">↺ 重置</button>
<button class="btn btn-sm" style="background:#52c41a;color:#fff;border-color:#52c41a;padding:3px 10px" onclick="openBomAddModal()">+ 新增</button>
<button class="btn btn-sm" style="background:#fa8c16;color:#fff;border-color:#fa8c16;padding:3px 10px" onclick="openBomImportModal()">📥 导入</button>
<button class="btn btn-sm" style="background:#722ed1;color:#fff;border-color:#722ed1;padding:3px 10px" onclick="exportBom()">📤 导出</button>
</div>
</div>
<div class="card-body">
<div class="table-scroll">
<table class="data-table" id="bomTable">
<thead style="position:sticky;top:0;z-index:4;background:var(--bg,#fff)"><tr>
<th style="width:40px"></th>
<th>物料编号</th>
<th>物料名称</th>
<th style="width:50px;text-align:center">用量</th>
<th>类型</th>
<th>成本价</th>
<th>供应商</th>
<th>可用库存</th>
<th>生产在途</th>
<th>采购在途</th>
<th style="width:50px;text-align:center">维护</th>
<th class="col-action" style="width:120px;min-width:120px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th>
</tr></thead>
<tbody id="bomTbody"><tr><td colspan="13" style="text-align:center;padding:40px;color:var(--text-light)">加载中...</td></tr></tbody>
</table>
</div></div>
<div id="bomPagination" class="card-footer pagination-sticky" style="padding:8px 16px;border-top:1px solid var(--border);text-align:center"></div>
</div>
</div>
`;
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 = '<tr><td colspan="13" style="text-align:center;padding:60px;color:var(--text-light)">暂无数据,请先导入BOM文件</td></tr>';
} else {
// Group by product_code (already batched by product)
var groups = {};
var groupOrder = [];
data.rows.forEach(function(r) {
var key = r.product_code;
if (!groups[key]) {
groups[key] = { product_code: r.product_code, product_name: r.product_name, product_cost_price: r.product_cost_price, product_supplier: r.product_supplier, materials: [] };
groupOrder.push(key);
}
groups[key].materials.push(r);
});
var html = '';
groupOrder.forEach(function(key) {
var g = groups[key];
var isExpanded = bomExpanded[key] === true;
// Product header row
// 成品成本价 = 绑定的包材成本价 × 用量 之和
var totalMatCost = 0;
g.materials.forEach(function(m) {
var mc = parseFloat(m.material_cost_price) || 0;
var mq = parseFloat(m.quantity) || 0;
totalMatCost += mc * mq;
});
var productCost = totalMatCost > 0 ? '¥' + totalMatCost.toFixed(2) : '-';
var productSupplier = g.product_supplier || '-';
var foldIcon = isExpanded ? '▼' : '▶';
// 成品库存(从第一行取,所有行同产品数据一致)
var prodAvail = g.materials[0] ? (parseInt(g.materials[0].product_available_qty) || 0) : 0;
var prodProdTr = g.materials[0] ? (parseInt(g.materials[0].product_production_in_transit) || 0) : 0;
var prodPurchTr = g.materials[0] ? (parseInt(g.materials[0].product_purchase_in_transit) || 0) : 0;
var prodInvClass = prodAvail > 0 ? '' : ' style="color:#ff4d4f;font-weight:600"';
// 判断是否有实际包材
var hasRealMats = g.materials.some(function(r) { return r.material_code && r.material_code.trim(); });
var maintLabel = hasRealMats ? '<span style="color:#52c41a;font-weight:600">有BOM</span>' : '<span style="color:#ff4d4f;font-weight:600">无BOM</span>';
html += '<tr class="bom-product-row" style="cursor:pointer;background:var(--bg-hover);font-weight:600">' +
'<td style="text-align:center;cursor:pointer" onclick="toggleBomMaterial(\'' + escJs(key) + '\')">' + foldIcon + '</td>' +
'<td><code>' + escHtml(g.product_code) + '</code></td>' +
'<td>' + escHtml(g.product_name) + '</td>' +
'<td style="text-align:center">-</td>' +
'<td><span class="tag tag-blue">成品</span></td>' +
'<td>' + productCost + '</td>' +
'<td>' + escHtml(productSupplier) + '</td>' +
'<td' + prodInvClass + '>' + prodAvail.toLocaleString() + '</td>' +
'<td>' + prodProdTr.toLocaleString() + '</td>' +
'<td>' + prodPurchTr.toLocaleString() + '</td>' +
'<td style="text-align:center">' + maintLabel + '</td>' +
'<td class="col-action" style="position:sticky;right:0;z-index:2;background:var(--bg,#fff);border-left:1px solid var(--border);white-space:nowrap">' +
'<button class="btn btn-sm btn-outline" onclick="showBomProductDetail(\'' + escJs(key) + '\')" title="展开详情">详情</button> ' +
'<button class="btn btn-sm btn-outline" onclick="showBomEdit(\'' + escJs(key) + '\')" title="编辑BOM">编辑</button> ' +
'<button class="btn btn-sm btn-outline-danger" onclick="deleteBomProduct(\'' + escJs(key) + '\')" style="color:#f5222d;border-color:#f5222d">删除</button>' +
'</td>' +
'</tr>';
// 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 += '<tr class="bom-material-row" style="background:#fafbfc">' +
'<td colspan="11" style="text-align:center;padding:20px;color:#bbb;font-size:13px">暂无包材数据,请点击编辑维护</td></tr>';
} 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 += '<tr class="bom-material-row" style="background:#fafbfc">' +
'<td></td>' +
'<td><code>' + escHtml(r.material_code) + '</code></td>' +
'<td>' + escHtml(r.material_name) + '</td>' +
'<td style="text-align:center">' + qty + '</td>' +
'<td><span class="tag tag-orange">包材</span></td>' +
'<td>' + matCost + '</td>' +
'<td>' + escHtml(matSupplier) + '</td>' +
'<td>' + (avail || 0) + '</td>' +
'<td>' + (prodTr || 0) + '</td>' +
'<td>' + (purchTr || 0) + '</td>' +
'<td></td>' +
'</tr>';
});
}
}
});
tbody.innerHTML = html;
}
var pel = document.getElementById('bomPagination');
if (pel) {
renderPaginationWithSize(pel, page, data.total || 0, BOM_PAGE_SIZE, 'loadBom', 'BOM_PAGE_SIZE');
}
});
}
window.toggleBomMaterial = function(productCode) {
bomExpanded[productCode] = !bomExpanded[productCode];
loadBom(bomCurrentPage);
};
// 绑定回车搜索
window.bomSearchKeydown = function(evt) {
if (evt.key === 'Enter') bomSearch();
};
window.bomSearch = function() { bomCurrentPage = 1; loadBom(1); };
// 重置所有筛选条件
window.bomReset = function() {
var si = document.getElementById('bomSearchInput');
var sf = document.getElementById('bomSupplierFilter');
var mf = document.getElementById('bomMaintFilter');
if (si) si.value = '';
if (sf) sf.value = '';
if (mf) mf.value = '';
bomCurrentPage = 1;
loadBom(1);
};
window.bomMaintFilterChange = function() { bomCurrentPage = 1; loadBom(1); };
// ─── 物料清单编辑弹窗 ───
window.showBomEdit = function(productCode) {
if (!productCode) return;
// 缓存弹窗打开时的旧物料数据,用于检测编号变更
window._oldBomMaterials = null;
fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(productCode))
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { alert(res.msg || '获取BOM数据失败'); return; }
var materials = (res.data && res.data.bom_materials) || [];
// 缓存旧物料(深拷贝,用于后续检测编号变化)
window._oldBomMaterials = JSON.parse(JSON.stringify(materials));
var old = document.getElementById('bomEditModal');
if (old) old.remove();
var overlay = document.createElement('div');
overlay.id = 'bomEditModal';
overlay.className = 'modal-overlay';
overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center';
overlay.onclick = function(e) { if (e.target === overlay) {} };
// Build rows
var bodyHtml = '';
if (materials.length === 0) {
bodyHtml = window._bomEditRowHtml(1);
} else {
materials.forEach(function(m, i) {
bodyHtml += window._bomEditRowHtml(i + 1, m);
});
}
var productName = (res.data && res.data.product && res.data.product.product_name) || productCode;
overlay.innerHTML = [
'<div class="modal" style="width:95vw;max-width:900px">',
'<div class="modal-header">',
'<h3>✏️ 编辑物料清单 — <strong>' + escHtml(productName) + '</strong></h3>',
'<span class="modal-close" onclick="document.getElementById(\'bomEditModal\').remove()" style="margin-left:auto;font-size:22px;cursor:pointer;color:#999">✕</span>',
'</div>',
'<div class="modal-body" style="padding:10px 16px;overflow:auto;max-height:65vh">',
'<table class="data-table" style="width:100%;font-size:12px">',
'<thead><tr><th style="width:30px">#</th><th>物料编号</th><th>物料名称</th><th style="width:60px;text-align:center">用量</th><th style="width:80px;text-align:center">成本价</th><th>供应商</th><th style="width:30px"></th></tr></thead>',
'<tbody id="bomEditTbody">' + bodyHtml + '</tbody>',
'</table>',
'<div style="margin-top:6px"><button class="btn btn-sm" onclick="addBomEditRow()">+ 添加一行</button></div>',
'</div>',
'<div class="modal-footer" style="display:flex;justify-content:space-between">',
'<button class="btn" onclick="document.getElementById(\'bomEditModal\').remove()">取消</button>',
'<button class="btn btn-primary" onclick="submitBomEdit(\'' + escJs(productCode) + '\')">💾 保存</button>',
'</div>',
'</div>'
].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 = '<tr class="bom-bind-row">';
html += '<td>' + idx + '</td>';
html += '<td><input class="form-input bom-mat-code" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + code + '" placeholder="必填" autocomplete="off"></td>';
html += '<td><input class="form-input bom-mat-name" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + name + '" placeholder="必填" autocomplete="off"></td>';
html += '<td><input class="form-input bom-mat-qty" type="number" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;text-align:center" value="' + qty + '" min="0.01" step="0.01" autocomplete="off"></td>';
html += '<td><input class="form-input bom-mat-cost" type="number" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;text-align:center" value="' + cost + '" min="0" step="0.01" autocomplete="off"></td>';
html += '<td style="position:relative"><input class="form-input bom-sup-name" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + supName + '" placeholder="输入关键词" autocomplete="off">';
html += '<div class="bom-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></td>';
html += '<td style="width:30px;text-align:center"><span style="cursor:pointer;color:#999;font-size:14px" onclick="removeBomBindRow(this)">✕</span></td>';
html += '</tr>';
return html;
};
window.addBomEditRow = function() {
var tbody = document.getElementById('bomEditTbody');
if (!tbody) return;
var idx = tbody.querySelectorAll('.bom-bind-row').length + 1;
tbody.insertAdjacentHTML('beforeend', window._bomEditRowHtml(idx));
window._bindBomSupEvents(tbody.querySelectorAll('.bom-bind-row:last-child .bom-sup-name'));
};
window.submitBomEdit = function(productCode) {
var tbody = document.getElementById('bomEditTbody');
if (!tbody) return;
var materials = [];
[].forEach.call(tbody.querySelectorAll('.bom-bind-row'), function(row) {
var code = (row.querySelector('.bom-mat-code')?.value || '').trim();
var name = (row.querySelector('.bom-mat-name')?.value || '').trim();
var supName = (row.querySelector('.bom-sup-name')?.value || '').trim();
var qty = parseFloat(row.querySelector('.bom-mat-qty')?.value) || 1;
var cost = parseFloat(row.querySelector('.bom-mat-cost')?.value) || 0;
if (code) { materials.push({material_code: code, material_name: name, quantity: qty, cost_price: cost, supplier_name: supName}); }
});
if (materials.length === 0) { alert('请填写至少一行物料信息'); return; }
// 检测物料编号变更,同步到产品表
var codeChanges = {};
if (window._oldBomMaterials && window._oldBomMaterials.length > 0) {
window._oldBomMaterials.forEach(function(oldMat) {
var oldCode = oldMat.material_code;
if (!oldCode) return;
// 在当前提交的物料列表中找相同编号
var matched = materials.some(function(newMat) { return newMat.material_code === oldCode; });
if (!matched) {
// 旧编号不存在了,尝试按物料名称匹配
var oldName = oldMat.material_name;
materials.forEach(function(newMat) {
if (newMat.material_code !== oldCode && newMat.material_name === oldName) {
codeChanges[oldCode] = newMat.material_code;
}
});
}
});
// 如果按名称没匹配到,再尝试按顺序
if (Object.keys(codeChanges).length === 0 && window._oldBomMaterials.length === materials.length) {
window._oldBomMaterials.forEach(function(oldMat, idx) {
var oldCode = oldMat.material_code;
var newCode = materials[idx].material_code;
if (oldCode && newCode && oldCode !== newCode) {
codeChanges[oldCode] = newCode;
}
});
}
window._oldBomMaterials = null;
}
var btn = document.querySelector('.btn-primary[onclick*="submitBomEdit"]');
if (btn) { btn.disabled = true; btn.textContent = '保存中...'; }
// 先删除旧的,再新增
fetch('/api/bom/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode})
})
.then(function(r) { return r.json(); })
.then(function(delRes) {
if (delRes.code !== 0) { throw new Error(delRes.msg || '删除旧数据失败'); }
return fetch('/api/bom/create_with_materials', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, materials: materials})
});
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (btn) { btn.disabled = false; btn.textContent = '💾 保存'; }
if (res.code === 0) {
// 同步物料编号变更到产品表
var codeChangeEntries = Object.keys(codeChanges);
if (codeChangeEntries.length > 0) {
(function syncCode(idx) {
if (idx >= codeChangeEntries.length) {
alert('✅ ' + res.msg + ';' + codeChangeEntries.length + '个物料编号已同步到产品信息');
document.getElementById('bomEditModal').remove();
loadBom(bomCurrentPage);
return;
}
var oldCode = codeChangeEntries[idx];
var newCode = codeChanges[oldCode];
fetch('/api/product/batch_update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: oldCode, fields: {product_code: newCode}})
})
.then(function(r) { return r.json(); })
.then(function(sr) {
if (sr.code !== 0) {
alert('物料编号 [' + oldCode + '→' + newCode + '] 同步失败: ' + (sr.msg || ''));
}
syncCode(idx + 1);
})
.catch(function() {
alert('物料编号 [' + oldCode + '→' + newCode + '] 同步失败: 网络错误');
syncCode(idx + 1);
});
})(0);
} else {
alert('✅ ' + res.msg);
document.getElementById('bomEditModal').remove();
loadBom(bomCurrentPage);
}
} else {
alert('❌ ' + (res.msg || '保存失败'));
}
})
.catch(function(err) {
if (btn) { btn.disabled = false; btn.textContent = '💾 保存'; }
alert('❌ ' + (err.message || '操作失败'));
});
};
// ─── 删除成品BOM(含所有物料行) ───
window.deleteBomProduct = function(productCode) {
if (!productCode) return;
if (!confirm('确认删除 [' + productCode + '] 的物料清单数据吗?此操作不可恢复!')) return;
fetch('/api/bom/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ ' + (res.msg || '删除成功'));
loadBom(bomCurrentPage);
} else {
alert('❌ ' + (res.msg || '删除失败'));
}
})
.catch(function() { alert('❌ 网络错误'); });
};
// ─── BOM新增弹窗 ───
window.openBomAddModal = function() {
var old = document.getElementById('bomAddModal');
if (old) old.remove();
var overlay = document.createElement('div');
overlay.id = 'bomAddModal';
overlay.className = 'modal-overlay';
overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center';
overlay.onclick = function(e) { if (e.target === overlay) {} };
overlay.innerHTML = [
'<div class="modal" style="width:95vw;max-width:900px">',
'<div class="modal-header">',
'<h3>✏️ 新建物料清单</h3>',
'<span style="font-size:13px;color:#888;font-weight:normal;margin-left:10px">成品 + 包材一次性添加</span>',
'<span class="modal-close" onclick="closeBomAddModal()" style="margin-left:auto;font-size:22px;cursor:pointer;color:#999">✕</span>',
'</div>',
'<div class="modal-body" style="padding:12px 16px;max-height:70vh;overflow-y:auto">',
'<!-- 成品选择区 -->',
'<div class="pd-card" style="margin-bottom:10px">',
'<div class="pd-card-header"><span>🏭 成品信息</span></div>',
'<div class="pd-card-body" style="padding:10px 14px">',
'<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">',
'<div style="flex:1;min-width:150px"><label style="font-size:12px;color:#888">成品编号 *</label>',
'<input id="bomNewProdCode" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" autocomplete="off"></div>',
'<div style="flex:1;min-width:150px"><label style="font-size:12px;color:#888">成品名称</label>',
'<input id="bomNewProdName" class="form-input" style="width:100%;box-sizing:border-box" placeholder="必填" autocomplete="off"></div>',
'<div style="flex:1;min-width:150px"><label style="font-size:12px;color:#888">供应商</label>',
'<div style="position:relative;width:100%" class="bom-sup-wrap"><input id="bomNewProdSup" class="form-input bom-sup-name" style="width:100%;box-sizing:border-box" placeholder="输入关键词" autocomplete="off">',
'<div class="bom-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:99999;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div></div>',
'</div>',
'</div>',
'</div>',
'<!-- 包材列表区 -->',
'<div class="pd-card">',
'<div class="pd-card-header" style="display:flex;justify-content:space-between;align-items:center">',
'<span>📦 包材列表 <span style="font-size:12px;color:#888;font-weight:normal">(用量按实际填写,支持多条)</span></span>',
'<button class="btn btn-sm" onclick="bomAddNewRow()" style="padding:2px 10px">+ 添加一行</button>',
'</div>',
'<div class="pd-card-body" style="padding:0;overflow-x:auto">',
'<table class="data-table" style="font-size:12px;width:100%">',
'<thead><tr><th style="width:30px">#</th><th>物料编号 *</th><th>物料名称</th><th style="width:80px;text-align:center">用量 *</th><th>供应商</th><th style="width:80px;text-align:center">成本价</th><th style="width:70px;text-align:center">货品类型</th><th style="width:30px"></th></tr></thead>',
'<tbody id="bomNewMatTbody">',
window._bomAddMatRowHtml(1),
'</tbody>',
'</table>',
'</div>',
'</div>',
'</div>',
'<div class="modal-footer" style="display:flex;justify-content:space-between">',
'<button class="btn" onclick="closeBomAddModal()">取消</button>',
'<button class="btn btn-primary" onclick="submitBomAdd()">💾 保存 BOM</button>',
'</div>',
'</div>'
].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 '<tr class="bom-add-row">' +
'<td style="text-align:center">' + idx + '</td>' +
'<td><input class="form-input bom-add-mat-code" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + code + '" placeholder="必填" autocomplete="off"></td>' +
'<td><input class="form-input bom-add-mat-name" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + name + '" placeholder="自动匹配" autocomplete="off"></td>' +
'<td><input class="form-input bom-add-mat-qty" type="number" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;text-align:center" value="' + qty + '" min="0.01" step="0.01" autocomplete="off"></td>' +
'<td style="position:relative"><input class="form-input bom-sup-name" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px" value="' + sup + '" placeholder="输入关键词" autocomplete="off"><div class="bom-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></td>' +
'<td><input class="form-input bom-add-mat-cost" type="number" style="width:100%;box-sizing:border-box;padding:4px 6px;font-size:12px;text-align:center" value="' + cost + '" min="0" step="0.01" autocomplete="off"></td>' +
'<td style="text-align:center"><select class="form-input bom-add-mat-type" style="width:100%;box-sizing:border-box;padding:4px 4px;font-size:12px"><option value="包材"' + (ptype === '包材' ? ' selected' : '') + '>包材</option><option value="成品"' + (ptype === '成品' ? ' selected' : '') + '>成品</option><option value="原材料"' + (ptype === '原材料' ? ' selected' : '') + '>原材料</option></select></td>' +
'<td style="text-align:center"><span style="cursor:pointer;color:#999;font-size:14px" onclick="bomAddRemoveRow(this)">✕</span></td>' +
'</tr>';
};
// 新增弹窗 - 添加行
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 = [
'<div class="modal" style="width:420px">',
'<div class="modal-header"><h3>导入BOM</h3><span class="modal-close" onclick="document.getElementById(\'bomImportModal\').remove()">&times;</span></div>',
'<div class="modal-body" style="padding:20px;text-align:center">',
'<p style="font-size:13px;color:#888;margin-bottom:14px">上传CSV文件导入BOM数据(全量替换)</p>',
'<p style="font-size:12px;color:#999;margin-bottom:14px">CSV格式:货品编号,货品名称,物料编号,物料名称,用量</p>',
'<div style="border:2px dashed #e8e8e8;border-radius:8px;padding:20px;cursor:pointer"',
'onmouseenter="this.style.borderColor=\'var(--primary)\';" onmouseleave="this.style.borderColor=\'#e8e8e8\';"',
'onclick="document.getElementById(\'bomImportFile\').click()">',
'<div style="font-size:36px;margin-bottom:8px">📂</div>',
'<div style="font-size:14px;color:#333">点击选择 CSV 文件</div>',
'</div>',
'<input type="file" id="bomImportFile" accept=".csv" style="display:none" onchange="previewBomImportFile()">',
'<div id="bomImportPreview" style="margin-top:10px;font-size:13px;color:#888"></div>',
'</div>',
'<div class="modal-footer">',
'<button class="btn" onclick="document.getElementById(\'bomImportModal\').remove()">取消</button>',
'<button class="btn btn-primary" id="bomImportBtn" disabled onclick="submitBomImport()">开始导入</button>',
'</div>',
'</div>'
].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 '<span style="font-weight:500;color:' + gc + '">' + escHtml(val) + '</span>';
}
if (field === '库存健康指数') {
var v = parseFloat(val);
if (isNaN(v) || v === 999) return '<span class="tag tag-grey">999</span>';
if (v >= 1.5) return '<span class="tag tag-green">' + v.toFixed(1) + '</span>';
if (v >= 0.8) return '<span class="tag tag-orange">' + v.toFixed(1) + '</span>';
return '<span class="tag tag-red">' + v.toFixed(1) + '</span>';
}
if (field === '风险把控') {
var rc = {'正常':'tag-green','新品':'tag-blue','呆滞品':'tag-orange','需启动清库存':'tag-red'};
return '<span class="tag ' + (rc[val] || 'tag-grey') + '">' + escHtml(val) + '</span>';
}
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 '<span class="tag ' + c + '">' + escHtml(val) + '</span>';
}
if (field === '日销量预测') {
if (val === '近期无销售') return '近期无销售';
return '<span style="color:#1890ff">' + escHtml(val) + '</span>';
}
if (field === '建议下单数量' || field === '实际下单数量') {
var v = parseInt(val);
if (isNaN(v) || v === 0) return '0';
return '<span style="color:var(--primary)">' + v.toLocaleString() + '</span>';
}
if (field === '建议回货数量') {
var v = parseFloat(val);
if (isNaN(v) || v === 0) return '0';
return '<span style="color:#cf1322">' + v.toFixed(1) + '</span>';
}
if (field === '库存可支撑天数' || field === '总支撑天数' || field === '加工厂库存支撑天数' ||
field === '库存工厂可支撑天数' || field === '库存工厂待生产可支撑天数') {
var v = parseFloat(val);
if (isNaN(v)) return '-';
if (v === 999) return '999';
return v.toFixed(1);
}
if (typeof val === 'number') {
return val.toLocaleString();
}
return escHtml(val);
}
// 占位:下单功能(待完善)
// ─── 正式下单数量编辑 ───
function editFormalQty(productCode) {
if (window._canEditField && !window._canEditField('planning', '正式下单数量')) return;
var valSpan = document.getElementById('fqv-' + productCode);
if (!valSpan) return;
var currentVal = parseInt(valSpan.dataset.val) || 0;
var code = productCode;
// 替换为输入框
var input = document.createElement('input');
input.type = 'number';
input.className = 'form-input';
input.style.width = '80px';
input.value = currentVal;
input.dataset.original = currentVal;
valSpan.replaceWith(input);
input.focus();
input.select();
// Enter 确认
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
// 已有确认弹窗打开,忽略重复回车
if (input.dataset.confirming) return;
var newVal = parseInt(input.value);
if (isNaN(newVal) || newVal < 0) newVal = 0;
if (newVal === parseInt(input.dataset.original)) {
replaceBack(code, newVal);
return;
}
// 标记正在确认,阻止 blur 恢复原值
input.dataset.confirming = '1';
showCenteredConfirm('修改确认', '确认将正式下单数量修改为 <strong>' + newVal + '</strong>?').then(function(confirmed) {
delete input.dataset.confirming;
if (confirmed) {
// 保存到后端 - 只更新数量,不创建待下单
fetch('/api/purchase/update_formal_qty', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code, formal_order_qty: newVal})
});
replaceBack(code, newVal);
} else {
// 取消 → 恢复原值
replaceBack(code, parseInt(input.dataset.original));
}
});
} else if (e.key === 'Escape') {
replaceBack(code, parseInt(input.dataset.original));
}
});
// 没有回车确认就失焦 → 恢复原值
input.addEventListener('blur', function() {
if (input.dataset.confirming) return;
setTimeout(function() {
replaceBack(code, parseInt(input.dataset.original));
}, 200);
});
}
function replaceBack(code, val) {
var input = document.querySelector('.formal-qty-wrap input');
if (!input) return;
var wrap = input.closest('.formal-qty-wrap');
if (!wrap) return;
var newSpan = document.createElement('span');
newSpan.className = 'formal-qty-val';
newSpan.id = 'fqv-' + code;
newSpan.dataset.code = code;
newSpan.dataset.val = val;
newSpan.textContent = val;
input.replaceWith(newSpan);
}
function planBatchOrder() {
var checkedCodes = Object.keys(window._planSelectedProductCodes);
if (checkedCodes.length === 0) {
alert('请先勾选要下单的商品');
return;
}
if (!confirm('确认将选中的 ' + checkedCodes.length + ' 个商品加入待下单列表?')) return;
// 清除待下单页面缓存
if (window._pageCache) delete window._pageCache['purchase'];
fetch('/api/purchase/batch_add_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_codes: checkedCodes})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
var d = res.data || {};
var msg = [];
if (d.added > 0) msg.push('✅ ' + d.added + ' 个已加入待下单列表');
if (d.skipped > 0) msg.push('⚠️ ' + d.skipped + ' 个已在列表中,已跳过');
alert(msg.join('\n') || '操作完成');
} else {
alert('❌ ' + (res.msg || '操作失败'));
}
})
.catch(function() {
alert('❌ 网络错误');
});
}
function showBatchResult(success, skipped) {
var msg = [];
if (success > 0) msg.push('✅ ' + success + ' 个已加入待下单列表');
if (skipped > 0) msg.push('⚠️ ' + skipped + ' 个已在列表中,已跳过');
alert(msg.join('\n') || '操作完成');
}
// 进度详情弹窗
window.showProgressDetail = function(el) {
var progressText = el.getAttribute('data-progress') || '-';
showCenteredAlert('进度详情', escHtml(progressText));
};
// 通用居中确认弹窗(返回Promise,resolve(true)=确认, resolve(false)=取消)
window.showCenteredAlert = function(title, message) {
var modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto';
modal.innerHTML = '<div style="background:#fff;border-radius:12px;width:440px;max-width:90vw;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px;overflow:hidden">' +
'<div style="padding:24px 24px 12px">' +
'<h3 style="margin:0 0 12px;font-size:17px">' + escHtml(title) + '</h3>' +
'<p style="margin:0;font-size:14px;color:#555;line-height:1.6">' + message + '</p>' +
'</div>' +
'<div style="padding:12px 24px 20px;display:flex;justify-content:flex-end;gap:10px">' +
'<button class="btn btn-primary" id="alertOkBtn">确认</button>' +
'</div></div>';
document.body.appendChild(modal);
document.getElementById('alertOkBtn').onclick = function() { modal.remove(); };
// 点击遮罩层不关闭弹窗(仅通过按钮关闭)
modal.addEventListener('click', function(e) { if (e.target === modal) {} });
};
// 覆盖原生 alert,全部走自定义弹窗,避免浏览器"不允许再次提示"限制
(function() {
var origAlert = window.alert;
window.alert = function(msg) {
if (typeof window.showCenteredAlert === 'function') {
window.showCenteredAlert('提示', String(msg));
} else {
origAlert(msg);
}
};
})();
window.showCenteredConfirm = function(title, message) {
return new Promise(function(resolve) {
var modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:10000;display:flex;align-items:center;justify-content:center;overflow-y:auto';
modal.innerHTML = '<div style="background:#fff;border-radius:12px;width:440px;max-width:90vw;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px;overflow:hidden">' +
'<div style="padding:24px 24px 12px">' +
'<h3 style="margin:0 0 12px;font-size:17px">' + escHtml(title) + '</h3>' +
'<p style="margin:0;font-size:14px;color:#555;line-height:1.6">' + message + '</p>' +
'</div>' +
'<div style="padding:12px 24px 20px;display:flex;justify-content:flex-end;gap:10px">' +
'<button class="btn" id="confirmCancelBtn">取消</button>' +
'<button class="btn btn-primary" id="confirmOkBtn">确认</button>' +
'</div></div>';
document.body.appendChild(modal);
document.getElementById('confirmCancelBtn').onclick = function() { modal.remove(); resolve(false); };
document.getElementById('confirmOkBtn').onclick = function() { modal.remove(); resolve(true); };
// 点击遮罩层不关闭弹窗(仅通过按钮关闭)
modal.addEventListener('click', function(e) { if (e.target === modal) {} });
});
};
// 从计划预测中移除产品
window.planDeleteProduct = function(productCode) {
if (!productCode) return;
showCenteredConfirm('确认移除',
'确认将 <strong>[' + escHtml(productCode) + ']</strong> 从计划预测中移除?<br>移除后该产品不再出现在计划预测列表中。'
).then(function(confirmed) {
if (!confirmed) return;
return showCenteredConfirm('⚠️ 二次确认',
'确定要移除 <strong>[' + escHtml(productCode) + ']</strong> 吗?');
}).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 = '<div style="background:#fff;border-radius:12px;width:580px;max-width:90vw;max-height:85vh;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' +
'<div style="padding:24px;text-align:center;color:#999">加载BOM包材数据...</div></div>';
document.body.appendChild(modal);
// 异步加载BOM数据
fetch('/api/product/detail_with_bom?code=' + encodeURIComponent(pc))
.then(function(r) { return r.json(); })
.then(function(res) {
var bomMaterials = (res.code === 0 && res.data && res.data.bom_materials) ? res.data.bom_materials : [];
renderPlanConfirmDialog(modal, data, bomMaterials);
})
.catch(function() {
// 加载失败也继续,无BOM显示
renderPlanConfirmDialog(modal, data, []);
});
}
function renderPlanConfirmDialog(modal, data, bomMaterials) {
var formalQty = data.formal_order_qty || 0;
// 检查BOM弹窗中是否已保存成品实际下单数量和备注
var savedProductQty = 0;
var savedRemark = '';
if (window._pendingBomData && window._pendingBomData[data.product_code]) {
window._pendingBomData[data.product_code].forEach(function(m) {
if (m.material_code === '成品_' + data.product_code) {
if (m.actual_quantity >= 0) savedProductQty = m.actual_quantity;
if (m.remark) savedRemark = m.remark;
}
});
}
var displayQty = savedProductQty >= 0 ? savedProductQty : formalQty;
// 过滤掉内料,只显示非内料之外包材数据
bomMaterials = bomMaterials.filter(function(b) {
return !(b.material_name && b.material_name.indexOf('内料') !== -1);
});
var hasBom = bomMaterials.length > 0;
// 商品基础信息:2列布局
var confirmHtml = '<div style="padding:20px 24px 8px">';
confirmHtml += '<h3 style="margin-bottom:16px;font-size:17px">确认加入待下单列表?</h3>';
confirmHtml += '<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden">';
confirmHtml += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
var fieldTriples = [
[['货品编号', data.product_code], ['供应商', data.supplier_name], ['采购跟单', data.procurement_follower]],
[['货品名称', data.product_name], ['货期(天)', data.lead_time], ['采购计划', data.procurement_planner]],
[['产品类型', data.product_type], ['物流(天)', data.logistics_time], ['实际下单数量', displayQty]],
[['产品分级', data.product_grade], ['总支撑天数', data.total_support_days], ['下单备注', savedRemark || data.remark || '']]
];
fieldTriples.forEach(function(triple) {
confirmHtml += '<tr>';
for (var t = 0; t < 3; t++) {
var item = triple[t];
if (item) {
confirmHtml += '<td style="padding:7px 10px;border:1px solid #e8e8e8;background:#f5f5f5;color:#999;font-weight:500;white-space:nowrap;width:90px">' + item[0] + '</td>';
confirmHtml += '<td style="padding:7px 10px;border:1px solid #e8e8e8;font-weight:600;width:auto">' + escHtml(String(item[1] || '-')) + '</td>';
} else {
confirmHtml += '<td style="padding:7px 10px;border:1px solid #e8e8e8;background:#f5f5f5;color:#999;font-weight:500;white-space:nowrap;width:90px"></td>';
confirmHtml += '<td style="padding:7px 10px;border:1px solid #e8e8e8"></td>';
}
}
confirmHtml += '</tr>';
});
confirmHtml += '</table></div>';
// BOM包材信息
if (hasBom) {
confirmHtml += '<div style="margin-top:12px;border:1px solid var(--border);border-radius:8px;overflow:hidden">';
confirmHtml += '<div style="background:#fafafa;padding:8px 12px;font-weight:600;font-size:13px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">';
confirmHtml += '<span>📦 包材下单数据</span>';
confirmHtml += '<span style="font-size:12px;color:#999;font-weight:400">共 ' + bomMaterials.length + ' 项</span>';
confirmHtml += '</div>';
confirmHtml += '<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">';
// 获取之前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 += '<thead><tr style="background:#555;color:#fff">';
confirmHtml += '<th style="' + thBase + '">物料编号</th>';
confirmHtml += '<th style="' + thBase + '">物料名称</th>';
confirmHtml += '<th style="' + thTight + '">用量</th>';
confirmHtml += '<th style="' + thTight + '">建议下单数量</th>';
confirmHtml += '<th style="' + thTight + '">实际下单数量</th>';
confirmHtml += '<th style="' + thTight + '">库存</th>';
confirmHtml += '<th style="' + thTight + '">采购在途</th>';
confirmHtml += '<th style="' + thTight + '">生产在途</th>';
confirmHtml += '<th style="' + thTight + '">是否修改稿件</th>';
confirmHtml += '<th style="' + thBase + '">下单备注</th>';
confirmHtml += '</tr></thead><tbody>';
var tdCellBase = 'padding:7px 8px;' + cellBorder + ';text-align:center';
var tdTight = 'padding:7px 3px;' + cellBorder + ';text-align:center';
bomMaterials.forEach(function(b, i) {
var orderQty = Math.ceil(formalQty * (b.bom_qty || 0));
var savedActual = (b.material_code in savedBomMap) ? savedBomMap[b.material_code] : orderQty;
var bg2 = i % 2 === 0 ? '#fff' : '#f5f5f5';
confirmHtml += '<tr style="background:' + bg2 + '">';
confirmHtml += '<td style="' + tdCellBase + ';font-weight:600"><code style="font-weight:700;font-size:13px;color:#333">' + escHtml(b.material_code || '') + '</code></td>';
confirmHtml += '<td style="' + tdCellBase + ';font-weight:600;color:#333">' + escHtml(b.material_name || '') + '</td>';
confirmHtml += '<td style="' + tdTight + '">' + (b.bom_qty || 0) + '</td>';
confirmHtml += '<td style="' + tdTight + ';font-weight:600;color:#1890ff">' + orderQty.toLocaleString() + '</td>';
confirmHtml += '<td style="' + tdTight + ';font-weight:600;color:#d4380d">' + (savedActual > 0 ? savedActual.toLocaleString() : '0') + '</td>';
confirmHtml += '<td style="' + tdTight + '">' + (b.available_qty || 0).toLocaleString() + '</td>';
confirmHtml += '<td style="' + tdTight + '">' + (b.purchase_in_transit || 0).toLocaleString() + '</td>';
confirmHtml += '<td style="' + tdTight + '">' + (b.production_in_transit || 0).toLocaleString() + '</td>';
var manuscriptVal = savedManuscriptMap[b.material_code] || '';
var manuscriptDisplay = manuscriptVal === '是' ? '<span style="color:#d4380d;font-weight:600">是</span>' : '<span style="color:#999">否</span>';
confirmHtml += '<td style="' + tdTight + ';font-weight:600">' + manuscriptDisplay + '</td>';
confirmHtml += '<td style="' + tdCellBase + ';min-width:240px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(savedRemarkMap[b.material_code] || '') + '">' + escHtml(savedRemarkMap[b.material_code] || '') + '</td>';
confirmHtml += '</tr>';
});
confirmHtml += '</tbody></table></div></div>';
}
confirmHtml += '</div>';
confirmHtml += '<div style="padding:16px 24px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px">';
confirmHtml += '<button class="btn" id="planOrderCancelBtn">取消</button>';
confirmHtml += '<button class="btn btn-primary" id="planOrderConfirmBtn">✅ 确认加入</button></div>';
modal.innerHTML = '<div style="background:#fff;border-radius:12px;width:' + (hasBom ? '1400px' : '520px') + ';max-width:95vw;max-height:85vh;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' +
confirmHtml + '</div>';
document.getElementById('planOrderCancelBtn').onclick = function() { modal.remove(); };
document.getElementById('planOrderConfirmBtn').onclick = function() {
modal.remove();
if (window._pageCache) delete window._pageCache['purchase'];
var orderData = JSON.parse(JSON.stringify(data));
if (window._pendingBomData && window._pendingBomData[data.product_code]) {
orderData.bom_items = JSON.stringify(window._pendingBomData[data.product_code]);
// 从已保存的BOM数据中获取成品实际下单数量和备注
window._pendingBomData[data.product_code].forEach(function(m) {
if (m.material_code === '成品_' + data.product_code) {
if (m.actual_quantity >= 0) orderData.formal_order_qty = m.actual_quantity;
if (m.remark) orderData.remark = m.remark;
}
});
}
fetch('/api/purchase/add_pending', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(orderData)
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
showCenteredAlert('操作成功', '✅ 已加入待下单列表');
} else {
showCenteredAlert('操作失败', '❌ ' + (res.msg || '操作失败'));
}
})
.catch(function() {
showCenteredAlert('网络错误', '❌ 网络错误');
});
};
};
window.showProductDetail = function(productCode) {
if (!productCode) return;
const old = document.getElementById('productDetailModal');
if (old) old.remove();
const modal = document.createElement('div');
modal.id = 'productDetailModal';
modal.className = 'pd-overlay';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="pd-modal pd-modal-wide" style="width:1140px">
<div class="pd-header">
<span class="pd-title">产品详情 \u2014 加载中...</span>
<button class="pd-close-btn" onclick="closeProductDetail()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="pd-body" id="pdBody">
<div class="pd-loading"><div class="loading-spinner"></div><span>加载中...</span></div>
</div>
</div>
`;
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 = '<div class="pd-error">' + (planRes.msg || '\u67e5\u8be2\u5931\u8d25') + '</div>';
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 <strong>' + escHtml(p.product_name || productCode) + '</strong>';
}
// \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 += '<div class="pd-row pd-row-2col">' +
'<span class="pd-label">' + escHtml(label) + '</span>' +
'<span class="pd-value">' + valHtml + '</span>' +
'</div>';
}
// \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 += '<div class="pd-row pd-row-4col">' +
'<span class="pd-label">' + escHtml(label) + '</span>' +
'<span class="pd-value">' + valHtml + '</span>' +
'</div>';
}
// 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 '<tr>' +
'<td style="text-align:center"><code>' + escHtml(m.material_code) + '</code></td>' +
'<td style="text-align:center">' + escHtml(m.material_name) + '</td>' +
'<td style="text-align:center">' + m.available_qty.toLocaleString() + '</td>' +
'<td style="text-align:center">' + m.production_in_transit.toLocaleString() + '</td>' +
'<td style="text-align:center">' + m.purchase_in_transit.toLocaleString() + '</td>' +
'<td style="text-align:center">' + m.material_cost.toFixed(4) + '</td>' +
'</tr>';
}).join('');
}
body.innerHTML = `
<!-- \u57fa\u7840\u4fe1\u606f\u5361\u7247\uff082\u5217\uff09 -->
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
<span>\u57fa\u7840\u4fe1\u606f</span>
</div>
<div class="pd-card-body">
<div class="pd-fields pd-fields-2col">
${basicHtml}
</div>
</div>
</div>
<!-- \u9500\u91cf\u4fe1\u606f\u5361\u7247\uff084\u5217\uff09 -->
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>
</svg>
<span>\u9500\u91cf\u4fe1\u606f</span>
<div style="margin-left:12px;display:flex;gap:4px;font-size:12px" id="salesTabBar">
<span class="sales-tab sales-tab-active" data-tab="default" onclick="switchSalesTab('default')" style="cursor:pointer;padding:2px 8px;border-radius:4px;background:#e6f7ff;color:#1890ff;font-weight:600">默认数据</span>
<span class="sales-tab" data-tab="cloud" onclick="switchSalesTab('cloud')" style="cursor:pointer;padding:2px 8px;border-radius:4px;color:#888">云仓数据</span>
<span class="sales-tab" data-tab="external" onclick="switchSalesTab('external')" style="cursor:pointer;padding:2px 8px;border-radius:4px;color:#888">外仓数据</span>
</div>
</div>
<div class="pd-card-body">
<div id="salesTabDefault" class="pd-fields pd-fields-4col">
${salesHtml}
</div>
<div id="salesTabCloud" style="display:none">
<div style="text-align:center;padding:24px;color:var(--text-light)">云仓数据加载中...</div>
</div>
<div id="salesTabExternal" style="display:none">
<div style="text-align:center;padding:24px;color:var(--text-light)">外仓数据加载中...</div>
</div>
</div>
</div>
<!-- BOM\u7269\u6599\u6e05\u5355 -->
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<span>BOM\u7269\u6599\u6e05\u5355</span>
${order && order.total_order_qty ? '<span style="margin-left:12px;font-size:13px;color:var(--text-light);font-weight:normal">\u6210\u54c1\u4e0b\u5355\u91cf: <strong>' + (order.total_order_qty || 0).toLocaleString() + '</strong> \u4ef6\uff08\u5efa\u8bae: ' + (order.total_suggested_qty || 0).toLocaleString() + '\uff09</span>' : ''}
</div>
<div class="pd-card-body" style="padding:0;overflow-x:auto">
${bomMaterials.length > 0
? '<table class="data-table" style="font-size:12px"><thead><tr><th style="text-align:center">\u7269\u6599\u7f16\u53f7</th><th style="text-align:center">\u7269\u6599\u540d\u79f0</th><th style="text-align:center">\u53ef\u7528\u5e93\u5b58</th><th style="text-align:center">\u751f\u4ea7\u5728\u9014</th><th style="text-align:center">\u91c7\u8d2d\u5728\u9014</th><th style="text-align:center">\u5355\u4ef7</th></tr></thead><tbody>' + bomHtml + '</tbody></table>'
: '<div style="padding:24px;text-align:center;color:var(--text-light)">\u6682\u65e0BOM\u7269\u6599\u6e05\u5355</div>'
}
</div>
</div>
<!-- \u8ba2\u5355\u5728\u9014 -->
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
<span>\u8ba2\u5355\u5728\u9014</span>
<span id="orderInTransitLoading" style="margin-left:12px;font-size:13px;color:#9ca3af;font-weight:normal">\u52a0\u8f7d\u4e2d...</span>
</div>
<div class="pd-card-body" style="padding:0;overflow-x:auto" id="orderInTransitBody">
<div style="text-align:center;padding:24px;color:#9ca3af">\u67e5\u8be2\u4e2d...</div>
</div>
</div>
<!-- \u9500\u91cf\u8d8b\u52bf -->
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>
</svg>
<span>\u9500\u91cf\u8d8b\u52bf</span>
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;font-size:12px" id="salesDateRangeBar">
<span style="color:#8c8c8c">\u65e5\u671f\u8303\u56f4</span>
<input type="date" id="salesDateStart" class="form-input" style="width:130px;font-size:12px;padding:2px 6px;height:26px" onchange="updateSalesChartRange()">
<span style="color:#d9d9d9">\u2014</span>
<input type="date" id="salesDateEnd" class="form-input" style="width:130px;font-size:12px;padding:2px 6px;height:26px" onchange="updateSalesChartRange()">
<button class="btn btn-sm" onclick="resetSalesChartRange()" style="font-size:11px;padding:2px 8px">\u91cd\u7f6e</button>
<span style="color:#d9d9d9;margin:0 4px">|</span>
<span style="cursor:pointer;color:#1890ff;font-weight:600" id="chartTypeBar" onclick="switchSalesChartType('bar')">\u67f1\u72b6\u56fe</span>
<span style="color:#d9d9d9;margin:0 4px">/</span>
<span style="cursor:pointer;color:#8c8c8c" id="chartTypeLine" onclick="switchSalesChartType('line')">\u6298\u7ebf\u56fe</span>
</div>
</div>
<div class="pd-card-body">
<div style="height:280px;position:relative">
<canvas id="salesTrendChart"></canvas>
</div>
${sales.length === 0 ? '<div class="pd-no-data">\u6682\u65e0\u9500\u91cf\u6570\u636e</div>' : ''}
</div>
</div>
<div class="pd-footer">
<span>\u521b\u5efa\u65f6\u95f4\uff1a${escHtml(p.created_at || '-')}</span>
<span class="pd-footer-sep">|</span>
<span>\u5bfc\u5165\u65f6\u95f4\uff1a${escHtml(p.import_date || '-')}</span>
</div>
`;
// 使用 detail_with_bom 返回的 orders_in_transit 数据显示订单在途(国内/海外/包材订单)
var transitOrders = bomRes.data && bomRes.data.orders_in_transit || [];
document.getElementById('orderInTransitLoading').textContent = '';
var container = document.getElementById('orderInTransitBody');
if (container) {
if (transitOrders.length === 0) {
container.innerHTML = '<div style="padding:20px;text-align:center;color:#bbb;font-size:13px">暂无在途订单</div>';
} else {
var totalQty = 0;
var html = '<table style="width:100%;border-collapse:collapse;font-size:13px">';
html += '<thead><tr style="background:#f5f5f5">' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">下单时间</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">货品编码</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">货品名称</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">下单数量</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">未到数量</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">下单进度</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">预计到货时间</th>' +
'</tr></thead><tbody>';
transitOrders.forEach(function(o) {
totalQty += parseInt(o['下单数量']) || 0;
html += '<tr>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center">' + (o['下单时间'] || '-') + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center"><code style="font-size:12px">' + (o['编码'] || '') + '</code></td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center;max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(o['货品名称'] || '') + '">' + (o['货品名称'] || '-') + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center;font-weight:600">' + (parseInt(o['下单数量']) || 0).toLocaleString() + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center;font-weight:600;color:' + ((parseInt(o['未到数量']) || 0) > 0 ? '#f5222d' : '#52c41a') + '">' + (parseInt(o['未到数量']) || 0).toLocaleString() + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center">' + (o['下单进度'] || '-') + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center">' + (o['预计到货时间'] || '-') + '</td>' +
'</tr>';
});
html += '</tbody></table>';
html += '<div style="padding:10px 20px;background:#fafafa;border:1px solid #d9d9d9;border-top:none;font-size:13px;text-align:center">' +
'共 <strong>' + transitOrders.length + '</strong> 笔在途订单,' +
'合计数量: <strong>' + totalQty.toLocaleString() + '</strong>' +
'</div>';
container.innerHTML = html;
}
}
// \u9500\u91cf\u56fe\u8868
if (sales.length > 0) {
window._salesData = sales;
window._currentSalesChartType = 'bar';
setTimeout(function() {
var startEl = document.getElementById('salesDateStart');
var endEl = document.getElementById('salesDateEnd');
if (startEl) startEl.value = sales[0].date;
if (endEl) endEl.value = sales[sales.length - 1].date;
renderSalesChartByRange('bar');
}, 100);
}
});
};
// ─── BOM管理模式下的简化详情弹窗(仅基础信息 + BOM物料清单)───
window.showBomProductDetail = function(productCode) {
if (!productCode) return;
const old = document.getElementById('productDetailModal');
if (old) old.remove();
const modal = document.createElement('div');
modal.id = 'productDetailModal';
modal.className = 'pd-overlay';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="pd-modal pd-modal-wide" style="width:900px">
<div class="pd-header">
<span class="pd-title">产品详情 \u2014 加载中...</span>
<button class="pd-close-btn" onclick="closeProductDetail()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="pd-body" id="pdBody">
<div class="pd-loading"><div class="loading-spinner"></div><span>加载中...</span></div>
</div>
</div>
`;
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 = '<div class="pd-error">' + (planRes.msg || '\u67e5\u8be2\u5931\u8d25') + '</div>';
return;
}
var p = planRes.data;
var titleEl = modal.querySelector('.pd-title');
if (titleEl) {
titleEl.innerHTML = '\u4ea7\u54c1\u8be6\u60c5 \u2014 <strong>' + escHtml(p.product_name || productCode) + '</strong>';
}
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 += '<div class="pd-row pd-row-2col">' +
'<span class="pd-label">' + escHtml(label) + '</span>' +
'<span class="pd-value">' + valHtml + '</span>' +
'</div>';
}
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 '<tr>' +
'<td style="text-align:center"><code>' + escHtml(m.material_code) + '</code></td>' +
'<td style="text-align:center">' + escHtml(m.material_name) + '</td>' +
'<td style="text-align:center">' + m.available_qty.toLocaleString() + '</td>' +
'<td style="text-align:center">' + m.production_in_transit.toLocaleString() + '</td>' +
'<td style="text-align:center">' + m.purchase_in_transit.toLocaleString() + '</td>' +
'<td style="text-align:center">' + m.material_cost.toFixed(4) + '</td>' +
'</tr>';
}).join('');
}
body.innerHTML = `
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
<span>\u57fa\u7840\u4fe1\u606f</span>
</div>
<div class="pd-card-body">
<div class="pd-fields pd-fields-2col">
${basicHtml}
</div>
</div>
</div>
<div class="pd-card">
<div class="pd-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<span>BOM\u7269\u6599\u6e05\u5355</span>
${order && order.total_order_qty ? '<span style="margin-left:12px;font-size:13px;color:var(--text-light);font-weight:normal">\u6210\u54c1\u4e0b\u5355\u91cf: <strong>' + (order.total_order_qty || 0).toLocaleString() + '</strong> \u4ef6\uff08\u5efa\u8bae: ' + (order.total_suggested_qty || 0).toLocaleString() + '\uff09</span>' : ''}
</div>
<div class="pd-card-body" style="padding:0;overflow-x:auto">
${bomMaterials.length > 0
? '<table class="data-table" style="font-size:12px"><thead><tr><th style="text-align:center">\u7269\u6599\u7f16\u53f7</th><th style="text-align:center">\u7269\u6599\u540d\u79f0</th><th style="text-align:center">\u53ef\u7528\u5e93\u5b58</th><th style="text-align:center">\u751f\u4ea7\u5728\u9014</th><th style="text-align:center">\u91c7\u8d2d\u5728\u9014</th><th style="text-align:center">\u5355\u4ef7</th></tr></thead><tbody>' + bomHtml + '</tbody></table>'
: '<div style="padding:24px;text-align:center;color:var(--text-light)">\u6682\u65e0BOM\u7269\u6599\u6e05\u5355</div>'
}
</div>
</div>
`;
});
};
// 切换销量信息tab页
window.switchSalesTab = function(tab) {
var tabs = ['default', 'cloud', 'external'];
tabs.forEach(function(t) {
var tabEl = document.querySelector('.sales-tab[data-tab="' + t + '"]');
var contentEl = document.getElementById('salesTab' + t.charAt(0).toUpperCase() + t.slice(1));
if (tab === t) {
if (tabEl) { tabEl.style.background = '#e6f7ff'; tabEl.style.color = '#1890ff'; tabEl.style.fontWeight = '600'; }
if (contentEl) contentEl.style.display = '';
} else {
if (tabEl) { tabEl.style.background = ''; tabEl.style.color = '#888'; tabEl.style.fontWeight = ''; }
if (contentEl) contentEl.style.display = 'none';
}
});
};
// ─── 编辑弹窗(动态跟随列显隐)───
// 列字段名 → 可编辑字段映射
const PLAN_EDITABLE_FIELDS = {
'cost_price': {label:'成本价', field:'cost_price', type:'number'},
'product_type': {label:'产品类型', field:'product_type', type:'select', options:['成品','包材','现货','赠品','清库存','其他']},
'产品种类': {label:'产品种类', field:'product_type', type:'text'},
'品牌': {label:'品牌', field:'brand', type:'text'},
'specification': {label:'规格', field:'specification', type:'text'},
'一级类目': {label:'一级类目', field:'category1', type:'text'},
'二级类目': {label:'二级类目', field:'category2', type:'text'},
'三级类目': {label:'三级类目', field:'category3', type:'text'},
'供应商名称': {label:'供应商名称', field:'supplier_name_match', type:'text'},
'供应商编码': {label:'供应商编码', field:'supplier_code', type:'text'},
'生产货期': {label:'货期', field:'lead_time', type:'number'},
'物流时间': {label:'物流时间', field:'logistics_time', type:'number'},
'采购跟单': {label:'采购跟单', field:'procurement_follower', type:'text'},
'采购计划': {label:'采购计划', field:'procurement_planner', type:'text'},
'开发专员': {label:'开发专员', field:'developer', type:'text'},
'归属平台': {label:'归属平台', field:'platform', type:'text'},
'运营专员': {label:'运营专员', field:'operator', type:'text'}
};
// 产品信息管理 - 删除产品
window.deleteProduct = function(productCode) {
if (!productCode) return;
// 弹窗二次确认
var existing = document.getElementById('prodDeleteConfirmOverlay');
if (existing) existing.remove();
var overlay = document.createElement('div');
overlay.id = 'prodDeleteConfirmOverlay';
overlay.className = 'modal-overlay';
overlay.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99999;align-items:center;justify-content:center';
overlay.onclick = function(e) { if (e.target === overlay) {} };
var modalBodyHtml = [
'<p style="margin:0 0 8px">确认删除产品 <strong>' + escHtml(productCode) + '</strong> 吗?</p>',
'<p style="margin:0;color:#f5222d;font-size:13px">删除后将移入回收站,可在回收站中恢复</p>'
];
if (window.prodActiveTab !== '成品' && window.prodActiveTab !== undefined) {
modalBodyHtml.push('<p style="margin:0;margin-top:12px;padding-top:12px;border-top:1px dashed #eee;color:#fa8c16;font-size:12px;line-height:1.8;text-align:left">⚠️ 若该产品被其他成品引用,对应的物料清单记录将被同时删除</p>');
}
if (window.prodActiveTab === '成品' || window.prodActiveTab === undefined) {
modalBodyHtml.push('<p style="margin:0;margin-top:12px;padding-top:12px;border-top:1px dashed #eee;color:#fa8c16;font-size:12px;line-height:1.8;text-align:left">⚠️ 删除后将级联删除:<br>• 物料清单(BOM)中的关联记录<br>• 关联的包材产品<br>• 计划预测中的关联数据</p>');
}
overlay.innerHTML = [
'<div class="modal" style="width:380px;text-align:center;padding:0">',
'<div class="modal-header" style="border-bottom:1px solid #f0f0f0;padding:14px 20px;display:flex;align-items:center;gap:8px">',
'<span style="font-size:22px;color:#f5222d">&#9888;</span>',
'<h3 style="margin:0;font-size:15px;color:#f5222d">确认删除</h3>',
'</div>',
'<div class="modal-body" style="padding:24px 20px;font-size:14px;color:#333;line-height:1.6">',
modalBodyHtml.join(''),
'</div>',
'<div class="modal-footer" style="border-top:1px solid #f0f0f0;padding:10px 20px;display:flex;justify-content:center;gap:10px">',
'<button class="btn" onclick="document.getElementById(\'prodDeleteConfirmOverlay\').remove()">取消</button>',
'<button class="btn btn-danger" style="background:#f5222d;color:#fff;border-color:#f5222d" onclick="confirmDeleteProduct(\'' + escHtml(productCode) + '\')">确认删除</button>',
'</div>',
'</div>'
].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 = [
'<div class="modal" style="width:90vw;max-width:1200px;max-height:90vh;display:flex;flex-direction:column;padding:0">',
'<div class="modal-header" style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #f0f0f0;flex-shrink:0">',
'<span style="font-size:20px">🗑️</span>',
'<h3 style="margin:0;font-size:16px">回收站</h3>',
'<span style="font-size:12px;color:#999">已删除的商品暂存于此</span>',
'<span class="modal-close" onclick="closeRecycleBin()" style="margin-left:auto;font-size:22px;cursor:pointer;color:#999">&times;</span>',
'</div>',
'<div class="modal-body" style="padding:12px 20px;flex:1;overflow:hidden;display:flex;flex-direction:column">',
'<div style="display:flex;gap:8px;margin-bottom:10px;flex-shrink:0">',
'<input type="text" id="rbCode" class="form-input" style="width:160px" placeholder="货品编号" autocomplete="off" onkeydown="if(event.key===\'Enter\')searchRecycleBin(1)">',
'<input type="text" id="rbName" class="form-input" style="width:180px" placeholder="货品名称" autocomplete="off" onkeydown="if(event.key===\'Enter\')searchRecycleBin(1)">',
'<button class="btn btn-primary" onclick="searchRecycleBin(1)">查询</button>',
'<button class="btn" onclick="resetRecycleSearch()">重置</button>',
'<span class="data-count" id="rbCount" style="font-size:13px;color:#666"></span>',
' <span style="margin-left:auto;display:flex;gap:4px">',
' <button class="btn btn-sm" onclick="rbBatchRestore()" style="color:#1890ff;border-color:#1890ff">批量恢复</button>',
' <button class="btn btn-sm" onclick="rbBatchDelete()" style="color:#f5222d;border-color:#f5222d">批量删除</button>',
' </span>',
'</div>',
'<div style="overflow:auto;flex:1;position:relative">',
'<table class="data-table" style="width:100%;font-size:12px;border-collapse:separate">',
'<thead id="rbThead"></thead>',
'<tbody id="rbTbody"><tr><td colspan="99" style="text-align:center;padding:40px;color:#999">加载中...</td></tr></tbody>',
'</table>',
'</div>',
'<div id="rbPagination" class="pagination-controls" style="padding:8px 0 0;flex-shrink:0"></div>',
'</div>',
'</div>'
].join('');
document.body.appendChild(overlay);
loadRecycleBin(1);
};
window.loadRecycleBin = function(page) {
_recycleCurrentPage = page;
// 构建全字段表头
var thead = document.getElementById('rbThead');
if (thead) {
var thHtml = '<tr><th style="width:30px"><input type="checkbox" id="rbCbAll" onchange="rbToggleAll(this.checked)"></th><th style="width:40px">#</th>';
PROD_COLUMNS.forEach(function(c) {
if (c.field === 'cost_price') return; // 成本价不显示在回收站
thHtml += '<th class="col-rb-' + c.field + '" style="white-space:nowrap">' + c.label + '</th>';
});
thHtml += '<th style="width:130px;white-space:nowrap">删除时间</th>';
thHtml += '<th style="width:150px;white-space:nowrap;position:sticky;right:0;z-index:5;background:var(--bg,#fff);border-left:1px solid var(--border)">操作</th></tr>';
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 = '<tr><td colspan="99" style="text-align:center;padding:50px 20px">'
+ '<div style="font-size:36px;margin-bottom:8px">🗑️</div>'
+ '<div style="font-size:14px;color:#999">回收站为空</div></td></tr>';
} 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 += '<tr>';
html += '<td style="text-align:center"><input type="checkbox" class="rb-row-cb" value="' + escHtml(r.product_code) + '"></td>';
html += '<td>' + ((page - 1) * _recyclePageSize + i + 1) + '</td>';
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 = '<span class="tag ' + cls + '">' + escHtml(raw || '-') + '</span>';
} else {
val = escHtml(val);
}
html += '<td class="col-rb-' + c.field + '">' + val + '</td>';
});
html += '<td style="color:#999;font-size:12px;white-space:nowrap">' + (r.deleted_at ? r.deleted_at.substring(0, 16) : '-') + '</td>';
html += '<td style="position:sticky;right:0;background:var(--bg,#fff);border-left:1px solid var(--border);z-index:2;white-space:nowrap">';
html += '<button class="btn btn-sm btn-primary" onclick="restoreProduct(\'' + escHtml(r.product_code) + '\')" style="font-size:11px;margin-right:4px">↩ 恢复</button>';
html += '<button class="btn btn-sm btn-outline-danger" onclick="permanentDeleteProduct(\'' + escHtml(r.product_code) + '\')" style="font-size:11px;color:#f5222d;border-color:#f5222d">彻底删除</button></td>';
html += '</tr>';
});
tbody.innerHTML = html;
}
// 分页
var totalPages = Math.max(1, Math.ceil(data.total / _recyclePageSize));
var ph = '';
if (page > 1) ph += '<button class="btn btn-sm" onclick="loadRecycleBin(1)">首页</button><button class="btn btn-sm" onclick="loadRecycleBin(' + (page - 1) + ')">上一页</button>';
ph += '<span style="font-size:13px;color:#666;margin:0 8px">共 ' + data.total + ' 条,第 ' + page + '/' + totalPages + ' 页</span>';
if (page < totalPages) ph += '<button class="btn btn-sm" onclick="loadRecycleBin(' + (page + 1) + ')">下一页</button><button class="btn btn-sm" onclick="loadRecycleBin(' + totalPages + ')">末页</button>';
document.getElementById('rbPagination').innerHTML = ph;
document.getElementById('rbCount').textContent = '共 ' + data.total + ' 条';
});
};
window.searchRecycleBin = function(page) {
loadRecycleBin(page || 1);
};
window.resetRecycleSearch = function() {
var c = document.getElementById('rbCode'); if (c) c.value = '';
var n = document.getElementById('rbName'); if (n) n.value = '';
loadRecycleBin(1);
};
window.restoreProduct = function(productCode) {
if (!confirm('确认恢复产品 ' + productCode + ' 吗?')) return;
fetch('/api/product/restore', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
loadRecycleBin(_recycleCurrentPage);
window._recycleChanged = true;
} else {
alert(res.msg || '恢复失败');
}
});
};
window.permanentDeleteProduct = function(productCode) {
if (!confirm('确认彻底删除产品 ' + productCode + ' 吗?\n此操作不可恢复!')) return;
fetch('/api/product/permanent_delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
loadRecycleBin(_recycleCurrentPage);
window._recycleChanged = true;
} else {
alert(res.msg || '删除失败');
}
});
};
// ─── 回收站全选/批量操作 ───
window.rbToggleAll = function(checked) {
[].forEach.call(document.querySelectorAll('.rb-row-cb'), function(cb) { cb.checked = checked; });
};
window.getSelectedRbCodes = function() {
var codes = [];
[].forEach.call(document.querySelectorAll('.rb-row-cb:checked'), function(cb) { codes.push(cb.value); });
return codes;
};
window.rbBatchRestore = function() {
var codes = window.getSelectedRbCodes();
if (codes.length === 0) { alert('请先勾选需要恢复的商品'); return; }
if (!confirm('确认批量恢复 ' + codes.length + ' 个商品吗?')) return;
var done = 0;
codes.forEach(function(code) {
fetch('/api/product/restore', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) done++;
if (done === codes.length || done + (codes.length - codes.indexOf(code) - 1) === codes.length) {
// 已有写入保护,取最后一次处理结果
}
});
});
// 延迟刷新列表
setTimeout(function() {
loadRecycleBin(_recycleCurrentPage);
window._recycleChanged = true;
}, 500);
};
window.rbBatchDelete = function() {
var codes = window.getSelectedRbCodes();
if (codes.length === 0) { alert('请先勾选需要删除的商品'); return; }
if (!confirm('确认彻底删除 ' + codes.length + ' 个商品吗?\n此操作不可恢复!')) return;
var done = 0;
codes.forEach(function(code) {
fetch('/api/product/permanent_delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: code})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) done++;
});
});
setTimeout(function() {
loadRecycleBin(_recycleCurrentPage);
window._recycleChanged = true;
}, 500);
};
window.closeRecycleBin = function() {
var modal = document.getElementById('recycleBinModal');
if (modal) modal.remove();
// 如果回收站有改动(恢复/删除),刷新主列表
if (window._recycleChanged) {
window._recycleChanged = false;
loadProducts(prodCurrentPage || 1);
}
};
// 产品信息管理 - 查看详情
window.viewProductDetail = function(productCode) {
if (!productCode) return;
var row = window._prodRowCache && window._prodRowCache[productCode];
if (!row) {
showProductDetailEdit(productCode);
return;
}
var old = document.getElementById('productViewModal');
if (old) old.remove();
var isPackaging = prodActiveTab === '非成品';
var packHide = ['品牌','采购跟单','采购计划','平台','运营','开发专员'];
var fields = [
{label:'货品编号', val:row.product_code},
{label:'货品名称', val:row.product_name},
{label:'品牌', val:row.brand},
{label:'货品类型', val:row.product_type},
{label:'供应商编码', val:row.supplier_code},
{label:'供应商名称', val:row.supplier_name_match},
{label:'成本价', val:row.cost_price},
{label:'货期(天)', val:row.lead_time},
{label:'物流时间(天)', val:row.logistics_time},
{label:'最低起订量', val:row.min_order_qty != null ? parseInt(row.min_order_qty) : '-'},
{label:'采购跟单', val:row.procurement_follower},
{label:'采购计划', val:row.procurement_planner},
{label:'平台', val:row.platform},
{label:'运营', val:row.operator},
{label:'开发专员', val:row.developer},
{label:'创建时间', val:(row.created_at||'').toString().slice(0,10)},
{label:'导入日期', val:row.import_date},
];
if (isPackaging) fields = fields.filter(function(f) { return packHide.indexOf(f.label) < 0; });
var html = '<table style="width:auto;border-collapse:collapse;font-size:14px">';
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)) : '<span style="color:#ccc">-</span>';
html += '<tr style="background:' + bg + '">';
html += '<td style="padding:8px 8px 8px 16px;white-space:nowrap;font-size:12px;color:#666;text-align:right;border-bottom:1px solid #f0f0f0;width:1px;background:#f2f2f2;font-weight:500" nowrap>' + left.label + '</td>';
html += '<td style="padding:8px 24px 8px 4px;white-space:nowrap;font-weight:600;color:#222;border-bottom:1px solid #f0f0f0;border-right:1px solid #e0e0e0;width:auto" nowrap>' + lv + '</td>';
if (right) {
var rv = right.val != null && right.val !== '' ? escHtml(String(right.val)) : '<span style="color:#ccc">-</span>';
html += '<td style="padding:8px 8px 8px 12px;white-space:nowrap;font-size:12px;color:#666;text-align:right;border-bottom:1px solid #f0f0f0;width:1px;background:#f2f2f2;font-weight:500" nowrap>' + right.label + '</td>';
html += '<td style="padding:8px 16px 8px 4px;white-space:nowrap;font-weight:600;color:#222;border-bottom:1px solid #f0f0f0;width:auto" nowrap>' + rv + '</td>';
} else {
html += '<td style="width:1px"></td><td></td>';
}
html += '</tr>';
}
html += '</table>';
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 = '<div class="pd-modal pd-modal-wide" style="min-width:700px;width:auto;max-width:92vw;max-height:85vh;overflow-y:auto;border-radius:10px">' +
'<div class="pd-header" style="padding:14px 18px;border-bottom:1px solid #eee"><span class="pd-title" style="font-size:16px;font-weight:700">📋 产品详情</span>' +
'<span style="font-size:12px;color:#999;margin:0 6px">——</span>' +
'<span style="font-size:14px;font-weight:600;color:#333">' + escHtml(row.product_name || productCode) + '</span>' +
'<button class="pd-close-btn" onclick="this.closest(\'.pd-overlay\').remove()" style="float:right;background:none;border:none;cursor:pointer;font-size:20px;color:#999;padding:0">&times;</button></div>' +
'<div class="pd-body" style="padding:14px 18px">' + html + '</div></div>';
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 += '<div style="padding:6px 10px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0" onmouseenter="this.style.background=\'#e6f7ff\'" onmouseleave="this.style.background=\'\'" onclick="selectAddSupplier(\'' + escJs(s.supplier_code) + '\',\'' + escJs(s.supplier_name) + '\')">';
html += '<span style="font-weight:600">' + escHtml(s.supplier_name) + '</span>';
if (s.supplier_code) html += ' <code style="font-size:11px;color:#999;margin-left:6px">' + escHtml(s.supplier_code) + '</code>';
html += '</div>';
});
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 += '<div style="padding:5px 10px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0" onmouseenter="this.style.background=\'#f5f5f5\'" onmouseleave="this.style.background=\'\'" onclick="selectFollower(\'' + escJs(v) + '\')">' + escHtml(v) + '</div>';
});
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 = '<div class="pd-modal pd-modal-wide" style="max-width:650px;max-height:85vh;overflow-y:auto">' +
'<div class="pd-header"><span class="pd-title">✏️ 编辑产品信息</span>' +
'<button class="pd-close-btn" onclick="document.getElementById(\'productEditModal\').remove()">' +
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>' +
'<div class="pd-body" id="pdEditBody">' + _buildProductEditForm(row) + '</div></div>';
document.body.appendChild(modal);
};
// 构建产品编辑表单
function _buildProductEditForm(row) {
var isNonProd = prodActiveTab === '非成品';
var hideFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer'];
var fields = [
{key:'product_code', label:'货品编号', val:row.product_code, type:'text'},
{key:'product_name', label:'货品名称', val:row.product_name, type:'text'},
{key:'brand', label:'品牌', val:row.brand, type:'text'},
{key:'product_type', label:'货品类型', val:row.product_type, type:'select', opts:['成品','现货','清库存','赠品','原材料','包材','其他']},
{key:'supplier_code', label:'供应商编码', val:row.supplier_code, type:'text', ro:true},
{key:'supplier_name_match', label:'供应商名称', val:row.supplier_name_match, type:'text', supSearch:true},
{key:'cost_price', label:'成本价', val:row.cost_price, type:'number'},
{key:'lead_time', label:'货期(天)', val:row.lead_time, type:'number'},
{key:'logistics_time', label:'物流时间(天)', val:row.logistics_time, type:'number', max:999},
{key:'min_order_qty', label:'最低起订量', val:row.min_order_qty != null ? parseInt(row.min_order_qty) : 0, type:'number'},
{key:'procurement_follower', label:'采购跟单', val:row.procurement_follower, type:'text'},
{key:'procurement_planner', label:'采购计划', val:row.procurement_planner, type:'text'},
{key:'platform', label:'平台', val:row.platform, type:'text'},
{key:'operator', label:'运营', val:row.operator, type:'text'},
{key:'developer', label:'开发专员', val:row.developer, type:'text'},
{key:'created_at', label:'创建时间', val:row.created_at ? String(row.created_at).slice(0,10) : '', type:'date'},
];
if (isNonProd) fields = fields.filter(function(f) { return hideFields.indexOf(f.key) < 0; });
var html = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px 16px">';
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 += '<option value="' + o + '"' + sel + '>' + o + '</option>';
});
html += '<div><label style="font-size:12px;color:#888">' + f.label + '</label><select id="pe_' + f.key + '" class="form-input" style="width:100%;box-sizing:border-box">' + opts + '</select></div>';
} else if (f.supSearch) {
html += '<div style="position:relative"><label style="font-size:12px;color:#888">' + f.label + '</label><input id="pe_' + f.key + '" type="text" class="form-input" style="width:100%;box-sizing:border-box" value="' + val + '"' + roAttr + ' autocomplete="off" oninput="editSupSearch(this)" onfocus="editSupSearch(this)"><div class="pe-sup-dd" style="position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid #d9d9d9;border-radius:4px;max-height:160px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.12)"></div></div>';
} else {
html += '<div><label style="font-size:12px;color:#888">' + f.label + '</label><input id="pe_' + f.key + '" type="' + f.type + '" class="form-input" style="width:100%;box-sizing:border-box" value="' + val + '"' + roAttr + ' autocomplete="off"></div>';
}
});
html += '</div>';
html += '<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px;padding-top:10px;border-top:1px solid var(--border,#eee)">' +
'<button class="btn" onclick="document.getElementById(\'productEditModal\').remove()">取消</button>' +
'<button class="btn btn-primary" onclick="saveQuickEditProduct(\'' + escHtml(row.product_code) + '\')">保存</button></div>';
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 = '<span>' + escHtml(s.supplier_name) + '</span>' + (s.supplier_code ? ' <code style="font-size:10px;color:#999">' + escHtml(s.supplier_code) + '</code>' : '');
dd.appendChild(item);
});
};
window.saveQuickEditProduct = function(productCode) {
var isNonProd = prodActiveTab === '非成品';
var skipFields = ['brand','procurement_follower','procurement_planner','platform','operator','developer','created_at'];
var fields = {};
var newCodeEl = document.getElementById('pe_product_code');
var newCode = newCodeEl ? newCodeEl.value.trim() : productCode;
if (newCode && newCode !== productCode) {
fields['product_code'] = newCode;
}
var fieldNames = ['product_name','brand','product_type','supplier_code','supplier_name_match',
'cost_price','lead_time','logistics_time','min_order_qty',
'procurement_follower','procurement_planner',
'platform','operator','developer','created_at'];
fieldNames.forEach(function(f) {
if (isNonProd && skipFields.indexOf(f) >= 0) return;
var el = document.getElementById('pe_' + f);
if (el) {
var val = el.value;
if (f === 'lead_time' || f === 'logistics_time' || f === 'min_order_qty') val = parseInt(val) || 0;
if (f === 'cost_price') val = parseFloat(val) || 0;
fields[f] = val;
}
});
fetch('/api/product/batch_update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, fields: fields})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
document.getElementById('productEditModal').remove();
if (newCode && newCode !== productCode) {
// 编号已变更,更新缓存
if (window._prodRowCache && window._prodRowCache[productCode]) {
var oldRow = window._prodRowCache[productCode];
oldRow.product_code = newCode;
window._prodRowCache[newCode] = oldRow;
delete window._prodRowCache[productCode];
}
}
loadProducts(prodCurrentPage);
} else {
alert(res.msg || '保存失败');
}
});
};
window.showProductDetailEdit = function(productCode) {
if (!productCode) return;
// 关闭已有弹窗
var oldEdit = document.getElementById('productEditModal');
if (oldEdit) oldEdit.remove();
var modal = document.createElement('div');
modal.id = 'productEditModal';
modal.className = 'pd-overlay';
modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center';
modal.innerHTML = '<div class="pd-modal pd-modal-wide" style="max-width:650px;max-height:85vh;overflow-y:auto">' +
'<div class="pd-header"><span class="pd-title">✏️ 编辑产品信息</span>' +
'<button class="pd-close-btn" onclick="document.getElementById(\'productEditModal\').remove()">' +
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>' +
'<div class="pd-body" id="pdEditBody"><div class="pd-loading">加载中...</div></div></div>';
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 = '<div class="pd-error">' + (res.msg || '查询失败') + '</div>';
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 = '<div style="padding:16px 20px">';
html += '<div style="margin-bottom:16px;border:1px solid var(--border);border-radius:8px;overflow:hidden">';
html += '<div style="background:#f5f5f5;padding:8px 14px;font-weight:600;font-size:14px;border-bottom:1px solid var(--border)">基础信息</div>';
html += '<div style="padding:10px 14px;display:grid;grid-template-columns:1fr 1fr;gap:8px 16px">';
html += '<div style="display:flex;flex-direction:column;gap:2px">';
html += '<label for="edit_product_code" style="font-size:12px;color:var(--text-secondary);font-weight:500">货品编号</label>';
html += '<input type="text" id="edit_product_code" class="form-input" value="' + escHtml(productCode) + '" style="width:100%;box-sizing:border-box;font-weight:600">';
html += '</div>';
html += '<div style="display:flex;flex-direction:column;gap:2px">';
html += '<label style="font-size:12px;color:var(--text-secondary);font-weight:500">货品名称</label>';
html += '<div style="padding:6px 10px;background:#f5f5f5;border:1px solid #d9d9d9;border-radius:6px;font-size:14px;font-weight:600">' + escHtml(p.product_name || '-') + '</div>';
html += '</div></div></div>';
if (editFields.length > 0) {
html += '<div style="margin-bottom:16px;border:1px solid var(--border);border-radius:8px;overflow:hidden">';
html += '<div style="background:#f5f5f5;padding:8px 14px;font-weight:600;font-size:14px;border-bottom:1px solid var(--border)">可编辑字段</div>';
html += '<div style="padding:10px 14px;display:grid;grid-template-columns:1fr 1fr;gap:8px 16px">';
editFields.forEach(function(ef) {
var f = ef.cfg;
var inputId = 'edit_' + f.field;
html += '<div style="display:flex;flex-direction:column;gap:2px">';
html += '<label for="' + inputId + '" style="font-size:12px;color:var(--text-secondary);font-weight:500">' + f.label + '</label>';
var val = (ef.val !== null && ef.val !== undefined) ? ef.val : '';
if (f.type === 'select' && f.options) {
html += '<select id="' + inputId + '" class="form-input" style="width:100%;box-sizing:border-box">';
f.options.forEach(function(o) {
var sel = (String(val) === String(o)) ? ' selected' : '';
html += '<option value="' + o + '"' + sel + '>' + o + '</option>';
});
html += '</select>';
} else if (f.type === 'number') {
html += '<input type="number" step="0.0001" id="' + inputId + '" class="form-input" value="' + (val || 0) + '" style="width:100%;box-sizing:border-box">';
} else {
html += '<input type="text" id="' + inputId + '" class="form-input" value="' + escHtml(String(val)) + '" style="width:100%;box-sizing:border-box">';
}
html += '</div>';
});
html += '</div></div>';
}
html += '<div style="text-align:right;padding-top:8px">';
html += '<button class="btn" onclick="document.getElementById(\'productEditModal\').remove()" style="margin-right:8px">取消</button>';
if (editFields.length > 0) {
html += '<button class="btn btn-primary" onclick="confirmProductEdit(\'' + escHtml(productCode) + '\')">💾 保存</button>';
}
html += '</div></div>';
document.getElementById('pdEditBody').innerHTML = html;
});
};
window.confirmProductEdit = function(productCode) {
var fields = {};
var changed = false;
// 读取货品编号
var newCodeInput = document.getElementById('edit_product_code');
var newCode = newCodeInput ? newCodeInput.value.trim() : productCode;
if (newCode !== productCode && newCode) {
fields['product_code'] = newCode;
changed = true;
}
// 查找所有以 edit_ 开头的 input 元素(排除 product_code,因为已单独处理)
document.querySelectorAll('#productEditModal input[id^="edit_"]').forEach(function(el) {
var fieldName = el.id.replace('edit_', '');
if (fieldName === 'product_code') return;
var val = el.value.trim();
if (el.type === 'number') val = parseFloat(val) || 0;
fields[fieldName] = val;
changed = true;
});
if (!changed) {
var nbEl = document.getElementById('editNoChangeTip');
if (!nbEl) {
nbEl = document.createElement('div');
nbEl.id = 'editNoChangeTip';
nbEl.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;padding:24px 32px;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:10001;font-size:15px;color:var(--text-secondary)';
nbEl.textContent = '没有可保存的字段';
document.body.appendChild(nbEl);
setTimeout(function() { nbEl.remove(); }, 1500);
}
return;
}
var fieldLabels = {
'cost_price':'成本价', 'product_type':'产品类型', 'brand':'品牌', 'specification':'规格',
'category1':'一级类目', 'category2':'二级类目', 'category3':'三级类目',
'supplier_name_match':'供应商名称', 'supplier_code':'供应商编码',
'lead_time':'货期(天)', 'logistics_time':'物流时间(天)',
'procurement_follower':'采购跟单', 'procurement_planner':'采购计划',
'developer':'开发专员', 'operator':'运营专员', 'platform':'归属平台'
};
// 自定义确认弹窗(替代原生 confirm)
var oldConfirm = document.getElementById('editConfirmModal');
if (oldConfirm) oldConfirm.remove();
var confirmHtml = '';
confirmHtml += '<div style="padding:28px 24px 8px">';
confirmHtml += '<h3 style="margin-bottom:24px;font-size:17px">确认保存以下修改?</h3>';
Object.keys(fields).forEach(function(f) {
confirmHtml += '<div style="display:flex;padding:14px 0;border-bottom:1px solid #f0f0f0;font-size:14px;line-height:1.6">';
confirmHtml += '<span style="flex:0 0 120px;color:var(--text-secondary);font-weight:500">' + (fieldLabels[f] || f) + '</span>';
confirmHtml += '<span style="flex:1;color:var(--text);font-weight:600">' + fields[f] + '</span>';
confirmHtml += '</div>';
});
confirmHtml += '</div>';
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 = '<div style="background:#fff;border-radius:12px;width:520px;max-width:90vw;max-height:80vh;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' +
confirmHtml +
'<div style="padding:16px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px">' +
'<button class="btn" onclick="this.closest(\'#editConfirmModal\').remove()">取消</button>' +
'<button class="btn btn-primary" id="editConfirmSaveBtn">确认保存</button></div></div>';
document.body.appendChild(confirmModal);
// 绑定确认按钮
document.getElementById('editConfirmSaveBtn').onclick = function() {
confirmModal.remove();
doSaveProductEdit(productCode, fields);
};
};
window.doSaveProductEdit = function(productCode, fields) {
if (typeof loadPlanPage !== 'function' && typeof planCurrentPage === 'undefined') {
planCurrentPage = 1;
}
// 如果货品编号变更,使用新编号刷新页面
var refreshCode = fields['product_code'] || productCode;
fetch('/api/product/batch_update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({product_code: productCode, fields: fields})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ 保存成功');
var editModal = document.getElementById('productEditModal');
if (editModal) editModal.remove();
if (typeof loadPlanPage === 'function') loadPlanPage(planCurrentPage || 1);
} else {
alert('❌ ' + (res.msg || '保存失败'));
}
})
.catch(function() { alert('❌ 网络错误'); });
};
window.showProductDetailFull = function(productCode) {
if (!productCode) return;
const old = document.getElementById('productDetailFullModal');
if (old) old.remove();
const modal = document.createElement('div');
modal.id = 'productDetailFullModal';
modal.className = 'pd-overlay';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="pd-modal pd-modal-wide" style="width:1200px">
<div class="pd-header">
<span class="pd-title">完整产品详情 — <code style="background:#e6f7ff;padding:2px 8px;border-radius:4px;font-size:13px;color:#096dd9">${escHtml(productCode)}</code></span>
<button class="pd-close-btn" onclick="closeProductDetailFull()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="pd-body" id="pdFullBody">
<div class="pd-loading"><div class="loading-spinner"></div><span>加载中...</span></div>
</div>
</div>
`;
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 = '<div class="pd-error">' + (res.msg || '查询失败') + '</div>';
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 '<tr>' +
'<td><code>' + escHtml(m.material_code) + '</code></td>' +
'<td>' + escHtml(m.material_name) + '</td>' +
'<td>' + m.bom_qty + '</td>' +
'<td>' + (m.required_qty || 0).toLocaleString() + '</td>' +
'<td>' + m.available_qty.toLocaleString() + '</td>' +
'<td>' + m.production_in_transit.toLocaleString() + '</td>' +
'<td>' + m.purchase_in_transit.toLocaleString() + '</td>' +
'<td>¥' + m.material_cost.toFixed(4) + '</td>' +
'<td>¥' + m.total_cost.toLocaleString() + '</td>' +
'<td class="' + shortageClass + '">' + (shortage > 0 ? '+' : '') + shortage.toLocaleString() + '</td>' +
'</tr>';
}).join('');
var summaryHtml = '<tfoot><tr style="font-weight:600;background:var(--bg-hover)">' +
'<td colspan="3">合计</td>' +
'<td>' + bomMaterials.reduce(function(s,m){return s+m.required_qty;},0).toLocaleString() + '</td>' +
'<td>' + bomMaterials.reduce(function(s,m){return s+m.available_qty;},0).toLocaleString() + '</td>' +
'<td>' + bomMaterials.reduce(function(s,m){return s+m.production_in_transit;},0).toLocaleString() + '</td>' +
'<td>' + bomMaterials.reduce(function(s,m){return s+m.purchase_in_transit;},0).toLocaleString() + '</td>' +
'<td></td>' +
'<td>¥' + totalMatCost.toLocaleString() + '</td>' +
'<td></td>' +
'</tr></tfoot>';
}
body.innerHTML = '';
// BOM物料清单
if (bomMaterials.length > 0) {
body.innerHTML += '<div class="pd-card">' +
'<div class="pd-card-header">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>' +
'</svg>' +
'<span>BOM物料清单</span>' +
'<span style="margin-left:12px;font-size:13px;color:var(--text-light);font-weight:normal">' +
'成品下单量: <strong>' + (order.total_order_qty || 0).toLocaleString() + '</strong> 件' +
'(建议: ' + (order.total_suggested_qty || 0).toLocaleString() + ')' +
'</span>' +
'</div>' +
'<div class="pd-card-body" style="padding:0;overflow-x:auto">' +
'<table class="data-table" style="font-size:12px">' +
'<thead><tr>' +
'<th>物料编号</th><th>物料名称</th><th>BOM用量</th><th>需用量</th>' +
'<th>可用库存</th><th>生产在途</th><th>采购在途</th>' +
'<th class="col-action" style="width:120px;min-width:120px;position:sticky;right:0;background:var(--bg,#fff);z-index:3;border-left:1px solid var(--border)">操作</th>' +
'<th>单价</th><th>总成本</th><th>缺口</th>' +
'</tr></thead>' +
'<tbody>' + bomHtml + '</tbody>' +
summaryHtml +
'</table>' +
'</div>' +
'</div>';
} else {
body.innerHTML += '<div class="pd-card"><div class="pd-card-body" style="padding:24px;text-align:center;color:var(--text-light)">暂无BOM物料清单</div></div>';
}
// 订单在途
body.innerHTML += '<div class="pd-card"><div class="pd-card-header">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/>' +
'</svg>' +
'<span>订单在途</span>' +
'<span id="orderInTransitLoading" style="margin-left:12px;font-size:13px;color:#9ca3af;font-weight:normal">加载中...</span>' +
'</div>' +
'<div class="pd-card-body" style="padding:0;overflow-x:auto" id="orderInTransitBody">' +
'<div style="text-align:center;padding:24px;color:#9ca3af">查询中...</div>' +
'</div></div>';
// 使用 detail_with_bom 返回的 orders_in_transit 数据显示订单在途(国内/海外/包材订单)
var transitOrders = bomRes.data && bomRes.data.orders_in_transit || [];
document.getElementById('orderInTransitLoading').textContent = '';
var container = document.getElementById('orderInTransitBody');
if (container) {
if (transitOrders.length === 0) {
container.innerHTML = '<div style="padding:20px;text-align:center;color:#bbb;font-size:13px">暂无在途订单</div>';
} else {
var totalQty = 0;
var html = '<table style="width:100%;border-collapse:collapse;font-size:13px">';
html += '<thead><tr style="background:#f5f5f5">' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">下单时间</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">货品编码</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">货品名称</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">下单数量</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">未到数量</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">下单进度</th>' +
'<th style="padding:8px 10px;border:1px solid #d9d9d9;text-align:center;font-weight:600;color:#999">预计到货时间</th>' +
'</tr></thead><tbody>';
transitOrders.forEach(function(o) {
totalQty += parseInt(o['下单数量']) || 0;
html += '<tr>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center">' + (o['下单时间'] || '-') + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center"><code style="font-size:12px">' + (o['编码'] || '') + '</code></td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center;max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' + escHtml(o['货品名称'] || '') + '">' + (o['货品名称'] || '-') + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center;font-weight:600">' + (parseInt(o['下单数量']) || 0).toLocaleString() + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center;font-weight:600;color:' + ((parseInt(o['未到数量']) || 0) > 0 ? '#f5222d' : '#52c41a') + '">' + (parseInt(o['未到数量']) || 0).toLocaleString() + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center">' + (o['下单进度'] || '-') + '</td>' +
'<td style="padding:6px 8px;border:1px solid #e8e8e8;text-align:center">' + (o['预计到货时间'] || '-') + '</td>' +
'</tr>';
});
html += '</tbody></table>';
html += '<div style="padding:10px 20px;background:#fafafa;border:1px solid #d9d9d9;border-top:none;font-size:13px;text-align:center">' +
'共 <strong>' + transitOrders.length + '</strong> 笔在途订单,' +
'合计数量: <strong>' + totalQty.toLocaleString() + '</strong>' +
'</div>';
container.innerHTML = html;
container.innerHTML = html;
}
}
// 销量趋势
body.innerHTML += '<div class="pd-card">' +
'<div class="pd-card-header">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>' +
'</svg>' +
'<span>销量趋势</span>' +
'<div style="margin-left:auto;display:flex;align-items:center;gap:8px;font-size:12px" id="salesDateRangeBar">' +
'<span style="color:#8c8c8c">日期范围</span>' +
'<input type="date" id="salesDateStart" class="form-input" style="width:130px;font-size:12px;padding:2px 6px;height:26px" onchange="updateSalesChartRange()">' +
'<span style="color:#d9d9d9">—</span>' +
'<input type="date" id="salesDateEnd" class="form-input" style="width:130px;font-size:12px;padding:2px 6px;height:26px" onchange="updateSalesChartRange()">' +
'<button class="btn btn-sm" onclick="resetSalesChartRange()" style="font-size:11px;padding:2px 8px">重置</button>' +
'<span style="color:#d9d9d9;margin:0 4px">|</span>' +
'<span style="cursor:pointer;color:#1890ff;font-weight:600" id="chartTypeBar" onclick="switchSalesChartType(&#39;bar&#39;)">柱状图</span>' +
'<span style="color:#d9d9d9;margin:0 4px">/</span>' +
'<span style="cursor:pointer;color:#8c8c8c" id="chartTypeLine" onclick="switchSalesChartType(&#39;line&#39;)">折线图</span>' +
'</div>' +
'</div>' +
'<div class="pd-card-body">' +
'<div style="height:280px;position:relative">' +
'<canvas id="salesTrendChart"></canvas>' +
'</div>' +
(sales.length === 0 ? '<div class="pd-no-data">暂无销量数据</div>' : '') +
'</div>' +
'</div>';
// 底部信息
body.innerHTML += '<div class="pd-footer">' +
'<span>创建时间:' + (p.created_at || '-') + '</span>' +
'<span class="pd-footer-sep">|</span>' +
'<span>导入时间:' + (p.import_date || '-') + '</span>' +
'</div>';
if (sales.length > 0) {
// 设置日期范围输入框的默认值(数据的起止日期)
setTimeout(function() {
if (sales.length > 0) {
var startEl = document.getElementById('salesDateStart');
var endEl = document.getElementById('salesDateEnd');
if (startEl) startEl.value = sales[0].date;
if (endEl) endEl.value = sales[sales.length - 1].date;
}
renderSalesChartByRange('bar');
}, 100);
}
});
};
window.closeProductDetailFull = function() {
const modal = document.getElementById('productDetailFullModal');
if (modal) modal.remove();
if (window._salesChart) {
window._salesChart.destroy();
window._salesChart = null;
}
};
window.switchSalesChartType = function(type) {
if (!window._salesData) return;
window._currentSalesChartType = type;
var barEl = document.getElementById('chartTypeBar');
var lineEl = document.getElementById('chartTypeLine');
if (barEl && lineEl) {
if (type === 'bar') {
barEl.style.color = '#1890ff'; barEl.style.fontWeight = '600';
lineEl.style.color = '#8c8c8c'; lineEl.style.fontWeight = 'normal';
} else {
barEl.style.color = '#8c8c8c'; barEl.style.fontWeight = 'normal';
lineEl.style.color = '#1890ff'; lineEl.style.fontWeight = '600';
}
}
renderSalesChartByRange(type);
};
function getSalesFilteredData() {
var allSales = window._salesData || [];
if (allSales.length === 0) return [];
var startEl = document.getElementById('salesDateStart');
var endEl = document.getElementById('salesDateEnd');
var startDate = startEl ? startEl.value : '';
var endDate = endEl ? endEl.value : '';
if (!startDate && !endDate) return allSales;
return allSales.filter(function(s) {
var d = s.date;
if (startDate && d < startDate) return false;
if (endDate && d > endDate) return false;
return true;
});
}
function updateSalesChartRange() {
renderSalesChartByRange(window._currentSalesChartType || 'bar');
}
function resetSalesChartRange() {
var startEl = document.getElementById('salesDateStart');
var endEl = document.getElementById('salesDateEnd');
if (startEl) startEl.value = '';
if (endEl) endEl.value = '';
renderSalesChartByRange(window._currentSalesChartType || 'bar');
}
function renderSalesChartByRange(chartType) {
chartType = chartType || 'bar';
var filtered = getSalesFilteredData();
renderSalesChart(filtered, chartType);
}
function renderSalesChart(sales, chartType) {
chartType = chartType || 'bar';
const canvas = document.getElementById('salesTrendChart');
if (!canvas) return;
if (window._salesChart) {
window._salesChart.destroy();
}
const labels = sales.map(function(s) { return s.date; });
const data = sales.map(function(s) { return s.qty; });
const barColor = 'rgba(24, 144, 255, 0.6)';
const lineColor = '#096dd9';
var datasets = [];
if (chartType === 'bar') {
datasets.push({
label: ' ',
data: data,
backgroundColor: barColor,
borderColor: barColor,
borderWidth: 1,
borderRadius: 2,
order: 1,
yAxisID: 'y'
});
} else {
datasets.push({
label: ' ',
data: data,
borderColor: lineColor,
backgroundColor: 'rgba(9, 109, 217, 0.1)',
borderWidth: 2,
pointBackgroundColor: lineColor,
pointRadius: 4,
pointHoverRadius: 6,
tension: 0.3,
fill: true,
yAxisID: 'y'
});
}
// 自定义插件:在柱状/折线图上显示数值
var valueDisplayPlugin = {
id: 'valueDisplay',
afterDraw: function(chart) {
var ctx = chart.ctx;
var chartArea = chart.chartArea;
var meta = chart.getDatasetMeta(0);
if (!meta || !meta.data || meta.data.length === 0) return;
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.font = '11px sans-serif';
var maxVal = Math.max.apply(null, data);
meta.data.forEach(function(element, idx) {
var val = data[idx];
if (val === null || val === undefined) return;
var x = element.x;
var y = element.y;
ctx.fillStyle = '#333';
ctx.fillText(val.toLocaleString(), x, y - (chartType === 'line' ? 8 : 4));
});
ctx.restore();
}
};
window._salesChart = new Chart(canvas, {
type: chartType === 'bar' ? 'bar' : 'line',
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
title: function(items) { return ''; },
label: function(context) {
if (context.dataset.label === '趋势线') return null;
const idx = context.dataIndex;
const date = labels[idx] || '';
const val = context.raw || 0;
return '日期: ' + date + ' 销量: ' + val.toLocaleString();
}
}
}
},
scales: {
x: {
grid: { display: false },
ticks: { display: false }
},
y: {
beginAtZero: true,
grid: { color: 'rgba(0,0,0,0.06)' },
ticks: { font: { size: 11 } }
}
}
},
plugins: [valueDisplayPlugin]
});
}
window.closeProductDetail = function() {
const modal = document.getElementById('productDetailModal');
if (modal) modal.remove();
};
// 通用分页+页码选择器
function changePageSize(newSize, loadFn) {
window._pageSize = parseInt(newSize);
eval(loadFn)(1);
}
function renderPaginationWithSize(el, currentPage, total, pageSize, loadFnName, sizeVarName) {
if (!el) return;
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1 && total <= pageSize) {
el.innerHTML = '<div class="pagination-inner"><span class="page-info">共 ' + total + ' 条</span></div>';
return;
}
let html = '<div class="pagination-inner">';
html += '<button class="btn btn-sm" onclick="' + loadFnName + '(1)" ' + (currentPage <= 1 ? 'disabled' : '') + '>&#9664;&#9664;</button>';
html += '<button class="btn btn-sm" onclick="' + loadFnName + '(' + (currentPage - 1) + ')" ' + (currentPage <= 1 ? 'disabled' : '') + '>&#9664;</button>';
const start = Math.max(1, currentPage - 2);
const end = Math.min(totalPages, currentPage + 2);
for (let i = start; i <= end; i++) {
html += '<button class="btn btn-sm ' + (i === currentPage ? 'btn-primary' : '') + '" onclick="' + loadFnName + '(' + i + ')">' + i + '</button>';
}
html += '<button class="btn btn-sm" onclick="' + loadFnName + '(' + (currentPage + 1) + ')" ' + (currentPage >= totalPages ? 'disabled' : '') + '>&#9654;</button>';
html += '<button class="btn btn-sm" onclick="' + loadFnName + '(' + totalPages + ')" ' + (currentPage >= totalPages ? 'disabled' : '') + '>&#9654;&#9654;</button>';
html += '<span class="page-info">' + currentPage + '/' + totalPages + '页 共' + total + '条</span>';
html += '<span class="goto-label" style="margin-left:10px">跳转</span>';
html += '<input class="goto-input" type="number" min="1" max="' + totalPages + '" value="' + currentPage + '" ';
html += 'onkeydown="if(event.key===\'Enter\'){var v=parseInt(this.value);if(v>=1&&v<=' + totalPages + ')' + loadFnName + '(v);}" ';
html += 'onblur="if(this.value===\'\'||parseInt(this.value)<1||parseInt(this.value)>' + totalPages + ')this.value=' + currentPage + ';" ';
html += 'onfocus="this.select()">';
html += '<span class="goto-label">页</span>';
html += '<span class="page-size-label" style="margin-left:10px">每页</span> ';
html += '<select class="page-size-select" onchange="' + sizeVarName + '=parseInt(this.value);' + loadFnName + '(1);">';
[15,20,50,100,200].forEach(function(s) {
html += '<option value="' + s + '"' + (s == pageSize ? ' selected' : '') + '>' + s + '</option>';
});
html += '</select> <span class="page-size-label">条</span>';
html += '</div>';
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 = '<div style="padding:20px">';
html += '<p style="margin-bottom:16px;color:var(--text-secondary);font-size:13px">编码: <strong>' + escHtml(code) + '</strong></p>';
html += '<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden;padding:14px;display:grid;grid-template-columns:1fr 1fr;gap:10px 16px">';
var fields = [
{label:'供应商名称', id:'es_name', val:s.supplier_name, type:'text'},
{label:'做货类型', id:'es_ptype', val:s.production_type, type:'text'},
{label:'简称', id:'es_sname', val:s.short_name, type:'text'},
{label:'合同简称', id:'es_csname', val:s.contract_short_name, type:'text'},
{label:'供应商分组', id:'es_group', val:s.supplier_group, type:'text'},
{label:'审核人', id:'es_reviewer', val:s.reviewer, type:'text'},
{label:'审核日期', id:'es_rdate', val:s.review_date || '', type:'text'},
{label:'数据状态', id:'es_dstatus', val:s.data_status, type:'select', options:['已审核','未审核']},
{label:'禁用状态', id:'es_bstatus', val:s.disabled_status, type:'select', options:['否','是']}
];
fields.forEach(function(f) {
html += '<div style="display:flex;flex-direction:column;gap:3px"><label style="font-size:12px;color:var(--text-secondary)">' + f.label + '</label>';
if (f.type === 'select') {
var opts = (f.options || []).map(function(o) {
var sel = (String(f.val || '') === o) ? ' selected' : '';
return '<option value="' + escHtml(o) + '"' + sel + '>' + (o || '') + '</option>';
}).join('');
html += '<select id="' + f.id + '" class="form-input" style="width:100%;box-sizing:border-box">' + opts + '</select></div>';
} else {
html += '<input type="text" id="' + f.id + '" class="form-input" value="' + escHtml(String(f.val || '')) + '"></div>';
}
});
html += '</div>';
html += '<div style="text-align:right;margin-top:16px">';
html += '<button class="btn" onclick="document.getElementById(\'editSupplierModal\').remove()">取消</button>';
html += '<button class="btn btn-primary" onclick="confirmSupplierEdit(\'' + escHtml(code) + '\')" style="margin-left:8px">💾 保存</button></div></div>';
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 = '<div style="background:#fff;border-radius:12px;width:500px;max-width:90vw;box-shadow:0 8px 24px rgba(0,0,0,0.15);margin:20px">' +
'<div class="pd-header"><span class="pd-title">✏️ 编辑供应商</span>' +
'<button class="pd-close-btn" onclick="this.closest(\'#editSupplierModal\').remove()">×</button></div>' + html + '</div>';
document.body.appendChild(modal);
});
};
window.confirmSupplierEdit = function(code) {
window._editingSupplierCode = code;
var overlay = document.createElement('div');
overlay.className = 'sup-confirm-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center';
overlay.innerHTML = '<div style="background:#fff;border-radius:12px;padding:24px;width:380px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,0.18)"><h3 style="margin:0 0 12px">💾 确认保存</h3><p style="color:#666;font-size:14px;margin:0 0 20px">确认保存供应商修改?</p><div style="display:flex;justify-content:center;gap:12px"><button class="btn" onclick="this.closest(\'.sup-confirm-overlay\').remove()">取消</button><button class="btn btn-primary" onclick="doSaveSupplier()">确认保存</button></div></div>';
document.body.appendChild(overlay);
};
window.doSaveSupplier = function() {
var overlays = document.querySelectorAll('.sup-confirm-overlay');
overlays.forEach(function(o) { o.remove(); });
var supplierCode = window._editingSupplierCode || '';
window._editingSupplierCode = null;
if (!supplierCode) return;
var fields = {
supplier_name: document.getElementById('es_name')?.value?.trim() || '',
production_type: document.getElementById('es_ptype')?.value?.trim() || '',
short_name: document.getElementById('es_sname')?.value?.trim() || '',
contract_short_name: document.getElementById('es_csname')?.value?.trim() || '',
supplier_group: document.getElementById('es_group')?.value?.trim() || '',
reviewer: document.getElementById('es_reviewer')?.value?.trim() || '',
review_date: document.getElementById('es_rdate')?.value?.trim() || '',
data_status: document.getElementById('es_dstatus')?.value?.trim() || '',
disabled_status: document.getElementById('es_bstatus')?.value?.trim() || ''
};
fetch('/api/supplier/update', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({supplier_code: supplierCode, fields: fields})
}).then(function(r) { return r.json(); }).then(function(res) {
if (res.code === 0) { alert('✅ 保存成功'); document.getElementById('editSupplierModal').remove(); loadSuppliers(supCurrentPage); }
else { alert('❌ ' + (res.msg || '保存失败')); }
}).catch(function() { alert('❌ 网络错误'); });
};
// ─── 新增供应商 ───
window.addSupplier = function() {
var oldEl = document.getElementById('addSupplierModal');
if (oldEl) oldEl.remove();
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'addSupplierModal';
overlay.style.display = 'flex';
overlay.onclick = function(e) { if (e.target === overlay) {} };
var html = '<div class="modal" style="width:520px;max-height:90vh;overflow-y:auto">';
html += '<div class="modal-header"><h3>新增供应商</h3><span class="modal-close" onclick="this.closest(\'#addSupplierModal\').remove()">&times;</span></div>';
html += '<div class="modal-body" style="padding:16px 20px">';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px"><span style="color:#f5222d">*</span> 供应商编码</label><input id="newSupCode" class="form-input" style="width:100%;box-sizing:border-box" placeholder="请输入供应商编码" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px"><span style="color:#f5222d">*</span> 供应商名称</label><input id="newSupName" class="form-input" style="width:100%;box-sizing:border-box" placeholder="请输入供应商名称" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">做货类型</label><input id="newSupProdType" class="form-input" style="width:100%;box-sizing:border-box" placeholder="如: 成品/包材/原料" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">简称</label><input id="newSupShortName" class="form-input" style="width:100%;box-sizing:border-box" placeholder="供应商简称" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">合同简称</label><input id="newSupContractShortName" class="form-input" style="width:100%;box-sizing:border-box" placeholder="合同简称" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">供应商分组</label><input id="newSupGroup" class="form-input" style="width:100%;box-sizing:border-box" placeholder="供应商分组" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">审核人</label><input id="newSupReviewer" class="form-input" style="width:100%;box-sizing:border-box" placeholder="审核人" autocomplete="off"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">审核日期</label><input id="newSupReviewDate" type="date" class="form-input" style="width:100%;box-sizing:border-box"></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">数据状态</label><select id="newSupDataStatus" class="form-input" style="width:100%;box-sizing:border-box"><option value="已审核">已审核</option><option value="未审核">未审核</option></select></div>';
html += '<div style="margin-bottom:12px"><label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:4px">禁用状态</label><select id="newSupDisabledStatus" class="form-input" style="width:100%;box-sizing:border-box"><option value="否">否</option><option value="是">是</option></select></div>';
html += '</div>';
html += '<div class="modal-footer"><button class="btn" onclick="this.closest(\'#addSupplierModal\').remove()">取消</button><button class="btn btn-primary" onclick="saveNewSupplier()">保存</button></div>';
html += '</div>';
overlay.innerHTML = html;
document.body.appendChild(overlay);
};
window.saveNewSupplier = function() {
var code = document.getElementById('newSupCode').value.trim();
var name = document.getElementById('newSupName').value.trim();
if (!code || !name) { alert('供应商编码和名称不能为空'); return; }
var data = {
supplier_code: code,
supplier_name: name,
production_type: document.getElementById('newSupProdType').value.trim(),
short_name: document.getElementById('newSupShortName').value.trim(),
contract_short_name: document.getElementById('newSupContractShortName').value.trim(),
supplier_group: document.getElementById('newSupGroup').value.trim(),
reviewer: document.getElementById('newSupReviewer').value.trim(),
review_date: document.getElementById('newSupReviewDate').value || '',
data_status: document.getElementById('newSupDataStatus').value.trim(),
disabled_status: document.getElementById('newSupDisabledStatus').value.trim()
};
fetch('/api/supplier/create', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)})
.then(function(r) { return r.json(); }).then(function(res) {
if (res.code === 0) { alert('新增成功'); document.getElementById('addSupplierModal').remove(); loadSuppliers(supCurrentPage || 1); }
else { alert(res.msg || '新增失败'); }
});
};
// ─── 删除供应商 ───
window.deleteSupplier = function(code) {
if (!code) return;
// 自定义确认弹窗
var overlay = document.createElement('div');
overlay.className = 'sup-confirm-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center';
overlay.setAttribute('data-sup-code', code);
overlay.innerHTML = '<div style="background:#fff;border-radius:12px;padding:24px;width:380px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,0.18)"><h3 style="margin:0 0 12px">🗑️ 确认删除</h3><p style="color:#666;font-size:14px;margin:0 0 20px">确认删除供应商 <strong>' + escHtml(code) + '</strong> 吗?<br>此操作不可恢复!</p><div style="display:flex;justify-content:center;gap:12px"><button class="btn" onclick="this.closest(\'.sup-confirm-overlay\').remove()">取消</button><button class="btn" style="background:#f5222d;color:#fff;border:none;padding:6px 20px;border-radius:6px;font-size:13px;cursor:pointer" onclick="confirmSupDelete(this)">确认删除</button></div></div>';
document.body.appendChild(overlay);
};
window.confirmSupDelete = function(btn) {
var overlay = btn.closest('.sup-confirm-overlay');
var code = overlay.getAttribute('data-sup-code');
overlay.remove();
doDeleteSupplier(code);
};
window.doDeleteSupplier = function(code) {
var overlays = document.querySelectorAll('.sup-confirm-overlay');
overlays.forEach(function(o) { o.remove(); });
fetch('/api/supplier/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({supplier_code: code})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
alert('✅ ' + (res.msg || '删除成功'));
loadSuppliers(supCurrentPage);
} else {
alert('❌ ' + (res.msg || '删除失败'));
}
})
.catch(function() { alert('❌ 网络错误'); });
};