427 lines
18 KiB
HTML
427 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>仓库概览</title>
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||
</head>
|
||
<body>
|
||
<header class="hero">
|
||
<div>
|
||
<h1>仓库概览</h1>
|
||
<p>先看关键指标与待补货,再进入对应分类处理</p>
|
||
</div>
|
||
<div class="hero-actions">
|
||
<a class="btn" href="{{ url_for('type_page', box_type='custom') }}#quick-add">添加容器</a>
|
||
<a class="btn btn-light" href="{{ url_for('search_page') }}">快速搜索</a>
|
||
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
|
||
{% include '_account_menu.html' %}
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
{% if error %}
|
||
<p class="alert">{{ error }}</p>
|
||
{% endif %}
|
||
{% if notice %}
|
||
<p class="notice">{{ notice }}</p>
|
||
{% endif %}
|
||
|
||
<div class="overview-layout">
|
||
<section class="overview-main">
|
||
|
||
<section class="metrics-grid">
|
||
<article class="metric-card">
|
||
<p class="metric-title">容器总数</p>
|
||
<p class="metric-value">{{ stats.box_count }}</p>
|
||
</article>
|
||
<article class="metric-card">
|
||
<p class="metric-title">启用元件</p>
|
||
<p class="metric-value">{{ stats.active_items }}</p>
|
||
</article>
|
||
<article class="metric-card">
|
||
<p class="metric-title">待补货元件</p>
|
||
<p class="metric-value">{{ stats.low_stock_count }}</p>
|
||
</article>
|
||
<article class="metric-card">
|
||
<p class="metric-title">近7天净变动</p>
|
||
<p class="metric-value">{% if stats.period_net_change_7d > 0 %}+{% endif %}{{ stats.period_net_change_7d }}</p>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="metrics-grid">
|
||
{% for item in type_cards %}
|
||
<article class="metric-card type-card">
|
||
<div class="type-card-head">
|
||
<p class="metric-title">{{ item.label }}</p>
|
||
<a class="type-card-more" href="{{ url_for('edit_container_type', box_type=item.key) }}" aria-label="编辑容器属性" title="编辑容器属性">...</a>
|
||
</div>
|
||
<p class="metric-value">{{ item.count }}</p>
|
||
<p class="hint">{{ item.desc }}</p>
|
||
<p class="hint">容器 {{ item.count }} 个 | 启用元件 {{ item.item_count }} 种 | 总库存 {{ item.quantity }}</p>
|
||
<a class="btn" href="{{ item.url }}">进入分类</a>
|
||
</article>
|
||
{% endfor %}
|
||
</section>
|
||
|
||
<section class="panel side-low-stock" id="overview-low-stock">
|
||
<h2>低库存元器件</h2>
|
||
{% for group in low_stock_groups %}
|
||
<h3>{{ group['label'] }}({{ group['items']|length }})</h3>
|
||
<ul class="side-low-stock-list">
|
||
{% for item in group['items'] %}
|
||
<li>
|
||
<div>
|
||
<strong>{{ item.name }}</strong>
|
||
<p class="hint">{{ item.part_no }} | {{ item.box_name }} / {{ item.slot_code }} | 数量 {{ item.quantity }}</p>
|
||
</div>
|
||
<a class="btn btn-light" href="{{ item.edit_url }}">编辑</a>
|
||
</li>
|
||
{% else %}
|
||
<li class="muted">当前分类没有低库存元器件。</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endfor %}
|
||
</section>
|
||
|
||
</section>
|
||
|
||
<aside class="overview-sidebar">
|
||
<section class="panel ai-panel" id="ai-panel">
|
||
<div class="ai-panel-head">
|
||
<h2>AI补货建议</h2>
|
||
<a class="btn btn-light" href="{{ url_for('ai_settings_page') }}">参数</a>
|
||
</div>
|
||
<p class="hint">根据低库存与近30天出库数据,生成可执行补货建议。</p>
|
||
<button class="btn" id="ai-restock-btn" type="button">生成建议</button>
|
||
<p class="hint" id="ai-panel-status"></p>
|
||
<p class="hint" id="ai-panel-warning"></p>
|
||
<div id="ai-plan-groups" class="ai-plan-groups"></div>
|
||
<details class="box-overview" id="ai-raw-wrap" hidden>
|
||
<summary>查看原始 AI 文本</summary>
|
||
<pre id="ai-panel-content" class="ai-panel-content"></pre>
|
||
</details>
|
||
|
||
<hr class="ai-divider">
|
||
|
||
<div class="ai-panel-head">
|
||
<h2>AI重复物料巡检</h2>
|
||
</div>
|
||
<p class="hint">扫描疑似同料号、同参数、同立创编号记录,生成人工复核清单。</p>
|
||
<div class="actions">
|
||
<button class="btn" id="ai-duplicate-btn" type="button">开始巡检</button>
|
||
<button class="btn btn-light" id="ai-duplicate-export-current-btn" type="button">导出当前显示</button>
|
||
<button class="btn btn-light" id="ai-duplicate-export-all-btn" type="button">导出全量</button>
|
||
</div>
|
||
<p class="hint" id="ai-duplicate-status"></p>
|
||
<p class="hint" id="ai-duplicate-warning"></p>
|
||
<div id="ai-duplicate-groups" class="ai-plan-groups"></div>
|
||
</section>
|
||
</aside>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<script>
|
||
(function () {
|
||
var aiBtn = document.getElementById('ai-restock-btn');
|
||
var statusNode = document.getElementById('ai-panel-status');
|
||
var warningNode = document.getElementById('ai-panel-warning');
|
||
var contentNode = document.getElementById('ai-panel-content');
|
||
var planGroups = document.getElementById('ai-plan-groups');
|
||
var rawWrap = document.getElementById('ai-raw-wrap');
|
||
|
||
function clearPlan() {
|
||
if (planGroups) {
|
||
planGroups.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderPlan(plan) {
|
||
if (!planGroups) {
|
||
return;
|
||
}
|
||
clearPlan();
|
||
|
||
var groups = [
|
||
{ key: 'urgent', title: '紧急补货' },
|
||
{ key: 'this_week', title: '本周建议补货' },
|
||
{ key: 'defer', title: '暂缓补货' }
|
||
];
|
||
|
||
groups.forEach(function (groupMeta) {
|
||
var rows = (plan && plan[groupMeta.key]) || [];
|
||
var wrap = document.createElement('section');
|
||
wrap.className = 'ai-plan-group';
|
||
|
||
var title = document.createElement('h3');
|
||
title.textContent = groupMeta.title + '(' + rows.length + ')';
|
||
wrap.appendChild(title);
|
||
|
||
var list = document.createElement('ul');
|
||
list.className = 'side-low-stock-list';
|
||
if (!rows.length) {
|
||
var empty = document.createElement('li');
|
||
empty.className = 'muted';
|
||
empty.textContent = '暂无条目';
|
||
list.appendChild(empty);
|
||
} else {
|
||
rows.forEach(function (item) {
|
||
var li = document.createElement('li');
|
||
var content = document.createElement('div');
|
||
|
||
var strong = document.createElement('strong');
|
||
strong.textContent = (item.name || '未命名元件') + ' (' + (item.part_no || '-') + ')';
|
||
content.appendChild(strong);
|
||
|
||
var qty = document.createElement('p');
|
||
qty.className = 'hint';
|
||
qty.textContent = '建议补货: ' + (item.suggest_qty || '待确认');
|
||
content.appendChild(qty);
|
||
|
||
var reason = document.createElement('p');
|
||
reason.className = 'hint';
|
||
reason.textContent = '理由: ' + (item.reason || '无');
|
||
content.appendChild(reason);
|
||
|
||
li.appendChild(content);
|
||
list.appendChild(li);
|
||
});
|
||
}
|
||
|
||
wrap.appendChild(list);
|
||
planGroups.appendChild(wrap);
|
||
});
|
||
}
|
||
|
||
if (!aiBtn) {
|
||
return;
|
||
}
|
||
|
||
aiBtn.addEventListener('click', function () {
|
||
aiBtn.disabled = true;
|
||
statusNode.textContent = '正在生成建议,请稍候...';
|
||
if (warningNode) {
|
||
warningNode.textContent = '';
|
||
}
|
||
clearPlan();
|
||
if (contentNode) {
|
||
contentNode.textContent = '';
|
||
}
|
||
if (rawWrap) {
|
||
rawWrap.hidden = true;
|
||
}
|
||
|
||
fetch('{{ url_for('ai_restock_plan') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: '{}'
|
||
})
|
||
.then(function (resp) {
|
||
return resp.json().then(function (data) {
|
||
return { ok: resp.ok, data: data };
|
||
});
|
||
})
|
||
.then(function (result) {
|
||
var data = result.data || {};
|
||
if (!result.ok || !data.ok) {
|
||
statusNode.textContent = '生成失败';
|
||
if (warningNode) {
|
||
warningNode.textContent = data.message || 'AI服务暂时不可用';
|
||
}
|
||
if (data.plan) {
|
||
renderPlan(data.plan);
|
||
}
|
||
return;
|
||
}
|
||
statusNode.textContent = (data.plan && data.plan.summary) || '建议已生成';
|
||
if (warningNode) {
|
||
warningNode.textContent = data.parse_warning || '';
|
||
}
|
||
renderPlan(data.plan || {});
|
||
if (contentNode && data.suggestion) {
|
||
contentNode.textContent = data.suggestion;
|
||
if (rawWrap) {
|
||
rawWrap.hidden = false;
|
||
}
|
||
}
|
||
})
|
||
.catch(function () {
|
||
statusNode.textContent = '生成失败';
|
||
if (warningNode) {
|
||
warningNode.textContent = '请求失败,请稍后重试';
|
||
}
|
||
})
|
||
.finally(function () {
|
||
aiBtn.disabled = false;
|
||
});
|
||
});
|
||
})();
|
||
|
||
(function () {
|
||
var auditBtn = document.getElementById('ai-duplicate-btn');
|
||
var exportCurrentBtn = document.getElementById('ai-duplicate-export-current-btn');
|
||
var exportAllBtn = document.getElementById('ai-duplicate-export-all-btn');
|
||
var statusNode = document.getElementById('ai-duplicate-status');
|
||
var warningNode = document.getElementById('ai-duplicate-warning');
|
||
var groupsWrap = document.getElementById('ai-duplicate-groups');
|
||
var latestAuditGroups = [];
|
||
|
||
if (!auditBtn || !exportCurrentBtn || !exportAllBtn || !statusNode || !groupsWrap) {
|
||
return;
|
||
}
|
||
|
||
function clearGroups() {
|
||
latestAuditGroups = [];
|
||
groupsWrap.innerHTML = '';
|
||
}
|
||
|
||
function renderAuditGroups(groups) {
|
||
clearGroups();
|
||
if (!Array.isArray(groups) || !groups.length) {
|
||
var empty = document.createElement('p');
|
||
empty.className = 'muted';
|
||
empty.textContent = '未发现疑似重复物料';
|
||
groupsWrap.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
latestAuditGroups = groups.slice();
|
||
|
||
groups.forEach(function (group) {
|
||
var section = document.createElement('section');
|
||
section.className = 'ai-plan-group ai-audit-group';
|
||
|
||
var title = document.createElement('h3');
|
||
title.textContent = (group.reason || '疑似重复') + ' | ' + (group.key || '-') + '(' + (group.member_count || 0) + ')';
|
||
section.appendChild(title);
|
||
|
||
var suggestion = document.createElement('p');
|
||
suggestion.className = 'hint';
|
||
suggestion.textContent = '建议: ' + (group.suggestion || '请人工复核');
|
||
section.appendChild(suggestion);
|
||
|
||
var list = document.createElement('ul');
|
||
list.className = 'side-low-stock-list';
|
||
|
||
(group.members || []).forEach(function (member) {
|
||
var li = document.createElement('li');
|
||
|
||
var content = document.createElement('div');
|
||
var strong = document.createElement('strong');
|
||
strong.textContent = (member.name || '未命名元件') + ' (' + (member.part_no || '-') + ')';
|
||
content.appendChild(strong);
|
||
|
||
var spec = document.createElement('p');
|
||
spec.className = 'hint';
|
||
spec.textContent = '规格: ' + (member.specification || '-');
|
||
content.appendChild(spec);
|
||
|
||
var pos = document.createElement('p');
|
||
pos.className = 'hint';
|
||
pos.textContent = (member.box_name || '-') + ' / ' + (member.slot_code || '-') + ' | 数量 ' + (member.quantity || 0);
|
||
content.appendChild(pos);
|
||
|
||
if (member.lcsc_code) {
|
||
var lcsc = document.createElement('p');
|
||
lcsc.className = 'hint';
|
||
lcsc.textContent = '立创编号: ' + member.lcsc_code;
|
||
content.appendChild(lcsc);
|
||
}
|
||
|
||
li.appendChild(content);
|
||
|
||
if (member.edit_url) {
|
||
var editBtn = document.createElement('a');
|
||
editBtn.className = 'btn btn-light';
|
||
editBtn.href = member.edit_url;
|
||
editBtn.textContent = '编辑';
|
||
li.appendChild(editBtn);
|
||
}
|
||
|
||
list.appendChild(li);
|
||
});
|
||
|
||
section.appendChild(list);
|
||
groupsWrap.appendChild(section);
|
||
});
|
||
}
|
||
|
||
auditBtn.addEventListener('click', function () {
|
||
auditBtn.disabled = true;
|
||
statusNode.textContent = '正在巡检重复物料,请稍候...';
|
||
if (warningNode) {
|
||
warningNode.textContent = '';
|
||
}
|
||
clearGroups();
|
||
|
||
fetch('{{ url_for('ai_duplicate_audit') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: '{}'
|
||
})
|
||
.then(function (resp) {
|
||
return resp.json().then(function (data) {
|
||
return { ok: resp.ok, data: data };
|
||
});
|
||
})
|
||
.then(function (result) {
|
||
var data = result.data || {};
|
||
if (!result.ok || !data.ok) {
|
||
statusNode.textContent = '巡检失败';
|
||
if (warningNode) {
|
||
warningNode.textContent = data.message || '服务暂时不可用';
|
||
}
|
||
return;
|
||
}
|
||
|
||
statusNode.textContent = data.summary || '巡检已完成';
|
||
if (warningNode) {
|
||
warningNode.textContent = data.parse_warning || '';
|
||
}
|
||
renderAuditGroups((data.data && data.data.groups) || []);
|
||
})
|
||
.catch(function () {
|
||
statusNode.textContent = '巡检失败';
|
||
if (warningNode) {
|
||
warningNode.textContent = '请求失败,请稍后重试';
|
||
}
|
||
})
|
||
.finally(function () {
|
||
auditBtn.disabled = false;
|
||
});
|
||
});
|
||
|
||
exportCurrentBtn.addEventListener('click', function () {
|
||
if (!latestAuditGroups.length) {
|
||
statusNode.textContent = '请先运行巡检,再导出当前显示结果';
|
||
if (warningNode) {
|
||
warningNode.textContent = '';
|
||
}
|
||
return;
|
||
}
|
||
|
||
var params = new URLSearchParams();
|
||
params.set('limit', String(latestAuditGroups.length));
|
||
latestAuditGroups.forEach(function (group) {
|
||
params.append('group_id', (group.type || '') + '::' + (group.key || ''));
|
||
});
|
||
|
||
var downloadUrl = '{{ url_for('export_duplicate_audit_csv') }}?' + params.toString();
|
||
window.location.href = downloadUrl;
|
||
});
|
||
|
||
exportAllBtn.addEventListener('click', function () {
|
||
var downloadUrl = '{{ url_for('export_duplicate_audit_csv') }}?limit=1000';
|
||
window.location.href = downloadUrl;
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|