Files
inventory/templates/search.html

316 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 slim">
<div>
<h1>快速搜索</h1>
<p>支持自然语言搜索,自动映射到料号、名称、规格和备注组合查询</p>
</div>
<nav class="hero-actions">
<a class="btn btn-light" href="{{ url_for('index') }}">返回首页</a>
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
{% include '_account_menu.html' %}
</nav>
</header>
<main class="container">
{% if error %}
<p class="alert">{{ error }}</p>
{% endif %}
{% if notice %}
<p class="notice">{{ notice }}</p>
{% endif %}
<section class="panel">
<form id="search-form" method="get" action="{{ url_for('search_page') }}" class="search-row">
<input id="search-input" type="search" name="q" placeholder="如 3.3V 稳压芯片、0805 常用电阻、USB 相关器件" value="{{ keyword }}" aria-label="搜索关键字">
<select id="fuzziness-select" name="fuzziness" aria-label="匹配宽松度">
{% for key, profile in fuzziness_profiles.items() %}
<option value="{{ key }}" {% if fuzziness == key %}selected{% endif %}>{{ profile.label }}</option>
{% endfor %}
</select>
<label class="priority-label">优先级:
<select id="priority-select" name="priority" aria-label="搜索优先级">
<option value="name" {% if priority == 'name' %}selected{% endif %}>名称优先</option>
<option value="part_no" {% if priority == 'part_no' %}selected{% endif %}>料号优先</option>
</select>
</label>
<!-- AI 开关:用户可选择是否启用 AI 解析,状态保存在 localStorage -->
<input type="hidden" id="use-ai-hidden" name="use_ai" value="1">
<label class="ai-toggle">
<input type="checkbox" id="use-ai-toggle" checked>
启用 AI 解析
</label>
<!-- 最低展示分数控制(本地保存) -->
<label class="min-score-label">最低展示分数:
<input id="min-score-input" type="number" name="min_score" step="0.1" min="0" value="{{ min_display_score }}" style="width:80px">
</label>
<button class="btn" type="submit">搜索</button>
</form>
<div class="search-examples">
<button class="chip" type="button" data-example="3.3V 稳压芯片">3.3V 稳压芯片</button>
<button class="chip" type="button" data-example="0805 常用电阻">0805 常用电阻</button>
<button class="chip" type="button" data-example="USB 相关器件">USB 相关器件</button>
</div>
<p class="hint">出库只需要输入数量,系统会自动扣减库存并记录统计。</p>
<p class="hint">当前宽松度: {{ fuzziness_profiles[fuzziness].label }}(严格更精准,宽松更容易召回)</p>
</section>
{% if search_plan %}
<section class="panel search-analysis">
<div class="group-title-row">
<h2>搜索解析</h2>
<div class="actions">
<span class="hint">模式: {{ 'AI解析' if search_plan.mode == 'ai' else '规则解析' }}</span>
<button class="btn btn-light" type="button" id="show-search-trace">查看AI过程</button>
</div>
</div>
<p class="hint">{{ search_plan.summary }}</p>
{% if search_parse_notice %}
<p class="notice">{{ search_parse_notice }}</p>
{% endif %}
<div class="search-map">
<div>
<strong>料号</strong>
<p>{{ ' / '.join(search_plan.field_map.part_no) if search_plan.field_map.part_no else '-' }}</p>
</div>
<div>
<strong>名称</strong>
<p>{{ ' / '.join(search_plan.field_map.name) if search_plan.field_map.name else '-' }}</p>
</div>
<div>
<strong>规格</strong>
<p>{{ ' / '.join(search_plan.field_map.specification) if search_plan.field_map.specification else '-' }}</p>
</div>
<div>
<strong>备注</strong>
<p>{{ ' / '.join(search_plan.field_map.note) if search_plan.field_map.note else '-' }}</p>
</div>
</div>
</section>
{% endif %}
<section class="panel">
<h2>搜索结果</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>料号</th>
<th>名称</th>
<th>规格</th>
<th>库存</th>
<th>位置</th>
<th>匹配说明</th>
<th>跳转</th>
<th>出库</th>
</tr>
</thead>
<tbody>
{% for row in results %}
{% set c = row.component %}
<tr>
<td>{{ c.part_no }}</td>
<td>{{ c.name }}</td>
<td>{{ c.specification or '-' }}</td>
<td>{{ c.quantity }}</td>
<td>{{ row.box_name }} / {{ row.slot_code }}</td>
<td>
<div>{{ row.match_summary }}</div>
<div class="hint">综合分: {{ '%.1f'|format(row.match_score) }}</div>
{% if row.matched_terms %}
<div class="match-tags">
{% for term in row.matched_terms %}
<span class="tag">{{ term }}</span>
{% endfor %}
</div>
{% endif %}
{% if row.fuzzy_matches %}
<details class="fuzzy-details">
<summary>模糊命中详情</summary>
<ul>
{% for item in row.fuzzy_matches %}
<li>{{ item.term }} ({{ item.score }})</li>
{% endfor %}
</ul>
</details>
{% endif %}
</td>
<td><a class="btn btn-light" href="{{ row.edit_url }}">进入位置</a></td>
<td>
<form method="post" action="{{ url_for('quick_outbound', component_id=c.id) }}" class="search-outbound-form">
<input type="hidden" name="q" value="{{ keyword }}">
<input type="hidden" name="fuzziness" value="{{ fuzziness }}">
<input type="number" name="amount" min="1" step="1" placeholder="数量" required class="outbound-amount">
<button class="btn" type="submit">出库</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="8">{% if keyword %}未找到匹配元件{% else %}先输入关键字进行搜索{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<div class="modal-backdrop" id="search-trace-modal" hidden>
<div class="modal-card panel" role="dialog" aria-modal="true" aria-labelledby="search-trace-title">
<div class="group-title-row">
<h2 id="search-trace-title">AI 搜索工作过程</h2>
<button class="btn btn-light" type="button" id="close-search-trace">关闭</button>
</div>
{% if search_trace %}
<ol class="trace-steps">
<li>收到自然语言输入: {{ search_trace.query }}</li>
<li>规则拆分候选字段,生成 fallback 计划</li>
<li>{% if search_trace.used_ai %}调用 AI 解析并返回字段映射{% else %}未调用 AI参数未配置{% endif %}</li>
<li>{% if search_trace.used_fallback %}最终回退规则计划{% else %}最终采用 AI 计划{% endif %}</li>
<li>对每条库存记录执行多字段模糊评分并排序</li>
<li>当前宽松度: {{ search_trace.fuzziness_label if search_trace.fuzziness_label else '-' }}</li>
</ol>
{% if search_trace.ai_error %}
<p class="alert">AI 错误: {{ search_trace.ai_error }}</p>
{% endif %}
<h3>最终计划</h3>
<pre class="trace-block">{{ search_plan|tojson(indent=2) }}</pre>
<h3>规则兜底计划</h3>
<pre class="trace-block">{{ search_trace.fallback_plan|tojson(indent=2) }}</pre>
{% if search_trace.ai_raw %}
<h3>AI 原始返回</h3>
<pre class="trace-block">{{ search_trace.ai_raw }}</pre>
{% endif %}
{% else %}
<p class="hint">当前没有可展示的过程数据。</p>
{% endif %}
</div>
</div>
</main>
<script>
(function () {
var searchInput = document.getElementById('search-input');
var searchForm = document.getElementById('search-form');
if (searchInput && searchForm) {
searchInput.focus();
searchInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
searchForm.requestSubmit();
}
});
}
// AI 开关行为:从 localStorage 读取用户偏好并在提交时保存
var useAiToggle = document.getElementById('use-ai-toggle');
var useAiHidden = document.getElementById('use-ai-hidden');
var minScoreInput = document.getElementById('min-score-input');
var prioritySelect = document.getElementById('priority-select');
try {
var saved = localStorage.getItem('use_ai_enabled');
if (saved !== null) {
var enabled = saved === '1' || saved === 'true';
if (useAiToggle) useAiToggle.checked = enabled;
if (useAiHidden) useAiHidden.value = enabled ? '1' : '0';
}
var savedScore = localStorage.getItem('search_min_score');
if (savedScore !== null && minScoreInput) {
minScoreInput.value = savedScore;
}
var savedPriority = localStorage.getItem('search_priority');
if (savedPriority !== null && prioritySelect) {
try { prioritySelect.value = savedPriority; } catch (e) {}
}
} catch (e) {
// ignore localStorage errors
}
if (searchForm) {
searchForm.addEventListener('submit', function () {
var enabled = !!(useAiToggle && useAiToggle.checked);
if (useAiHidden) useAiHidden.value = enabled ? '1' : '0';
try {
if (minScoreInput) {
localStorage.setItem('search_min_score', String(minScoreInput.value));
}
if (prioritySelect) {
localStorage.setItem('search_priority', String(prioritySelect.value));
}
} catch (e) {}
try {
localStorage.setItem('use_ai_enabled', enabled ? '1' : '0');
} catch (e) {}
});
}
document.querySelectorAll('[data-example]').forEach(function (button) {
button.addEventListener('click', function () {
if (!searchInput || !searchForm) {
return;
}
searchInput.value = button.getAttribute('data-example') || '';
searchForm.requestSubmit();
});
});
document.querySelectorAll('.outbound-amount').forEach(function (input) {
input.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
var form = input.closest('form');
if (form) {
form.requestSubmit();
}
}
});
});
var traceOpenBtn = document.getElementById('show-search-trace');
var traceCloseBtn = document.getElementById('close-search-trace');
var traceModal = document.getElementById('search-trace-modal');
function closeTraceModal() {
if (!traceModal) {
return;
}
traceModal.hidden = true;
document.body.classList.remove('modal-open');
}
if (traceOpenBtn && traceModal) {
traceOpenBtn.addEventListener('click', function () {
traceModal.hidden = false;
document.body.classList.add('modal-open');
});
}
if (traceCloseBtn) {
traceCloseBtn.addEventListener('click', closeTraceModal);
}
if (traceModal) {
traceModal.addEventListener('click', function (event) {
if (event.target === traceModal) {
closeTraceModal();
}
});
}
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
closeTraceModal();
}
});
})();
</script>
</body>
</html>