19464 lines
1.0 MiB
19464 lines
1.0 MiB
// 蝶雅采购智能体平台 - 前端逻辑
|
||
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()">×</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'">×</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, '"'); }
|
||
function escHtml(str) {
|
||
if (str === null || str === undefined) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function truncate(str, n) {
|
||
if (!str) return '';
|
||
return str.length > n ? str.substring(0, n) + '...' : str;
|
||
}
|
||
|
||
// 等待Chart.js加载完成
|
||
function waitChartJS() {
|
||
return new Promise(function(resolve) {
|
||
if (typeof Chart !== 'undefined') { resolve(); return; }
|
||
var check = setInterval(function() {
|
||
if (typeof Chart !== 'undefined') {
|
||
clearInterval(check);
|
||
resolve();
|
||
}
|
||
}, 100);
|
||
setTimeout(function() { clearInterval(check); resolve(); }, 10000);
|
||
});
|
||
}
|
||
|
||
// ─── 测试问题(意见反馈)───
|
||
function loadFeedbackPage(container) {
|
||
renderFeedbackPage(container);
|
||
}
|
||
|
||
function renderFeedbackPage(container) {
|
||
container.innerHTML = `
|
||
<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('fbFileInput').click()" ondrop="handleFbDrop(event)" ondragover="event.preventDefault();this.classList.add('dragover')" ondragleave="this.classList.remove('dragover')">📎 点击选择文件,或将文件拖拽到此处</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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
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()">×</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()">×</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">×</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()">×</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()">×</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' : '') + '>◀◀</button>';
|
||
ph += '<button class="btn btn-sm" onclick="fetchWarehouses(' + (whCurrentPage - 1) + ')" ' + (whCurrentPage <= 1 ? 'disabled' : '') + '>◀</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' : '') + '>▶</button>';
|
||
ph += '<button class="btn btn-sm" onclick="fetchWarehouses(' + totalPages + ')" ' + (whCurrentPage >= totalPages ? 'disabled' : '') + '>▶▶</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\'">×</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()">×</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()">×</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()">×</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()">×</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()">×</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,'"') + '"' : '') + '>' + 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()">×</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">×</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, '"') + '">📊 ' + 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()">×</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')">×</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()">×</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')">×</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()">×</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')">×</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'">×</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'">×</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()">×</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">⚠</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">×</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">×</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('bar')">柱状图</span>' +
|
||
'<span style="color:#d9d9d9;margin:0 4px">/</span>' +
|
||
'<span style="cursor:pointer;color:#8c8c8c" id="chartTypeLine" onclick="switchSalesChartType('line')">折线图</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' : '') + '>◀◀</button>';
|
||
html += '<button class="btn btn-sm" onclick="' + loadFnName + '(' + (currentPage - 1) + ')" ' + (currentPage <= 1 ? 'disabled' : '') + '>◀</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' : '') + '>▶</button>';
|
||
html += '<button class="btn btn-sm" onclick="' + loadFnName + '(' + totalPages + ')" ' + (currentPage >= totalPages ? 'disabled' : '') + '>▶▶</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()">×</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('❌ 网络错误'); });
|
||
};
|
||
|
||
|