250 lines
10 KiB
HTML
250 lines
10 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>
|
||
</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>
|
||
</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;
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|