feat: 添加 AI 补货建议功能,优化相关设置和界面

This commit is contained in:
2026-03-11 18:44:59 +08:00
parent 6f4a8d82f3
commit f7a82528e7
7 changed files with 819 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI参数设置</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="hero slim">
<div>
<h1>AI参数设置</h1>
<p>在此修改硅基流动 API 和补货建议参数</p>
</div>
<div class="hero-actions">
<a class="btn btn-light" href="{{ url_for('types_page') }}">返回仓库概览</a>
</div>
</header>
<main class="container">
{% if error %}
<p class="alert">{{ error }}</p>
{% endif %}
{% if notice %}
<p class="notice">{{ notice }}</p>
{% endif %}
<section class="panel">
<form class="form-grid" method="post">
<label>
API URL *
<input type="text" name="api_url" required value="{{ settings.api_url }}" placeholder="https://api.siliconflow.cn/v1/chat/completions">
</label>
<label>
模型名称 *
<input type="text" name="model" required value="{{ settings.model }}" placeholder="Qwen/Qwen2.5-7B-Instruct">
</label>
<label class="full">
API Key *
<input type="text" name="api_key" required value="{{ settings.api_key }}" placeholder="sk-...">
</label>
<label>
超时(秒)
<input type="number" name="timeout" min="5" value="{{ settings.timeout }}">
</label>
<label>
低库存阈值
<input type="number" name="restock_threshold" min="0" value="{{ settings.restock_threshold }}">
</label>
<label>
建议条目上限
<input type="number" name="restock_limit" min="1" value="{{ settings.restock_limit }}">
</label>
<div class="actions full">
<button class="btn" type="submit">保存参数</button>
<a class="btn btn-light" href="{{ url_for('types_page') }}">取消</a>
</div>
</form>
</section>
</main>
</body>
</html>

View File

@@ -27,6 +27,9 @@
<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>
@@ -80,6 +83,167 @@
</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>