feat:增强框类型管理和搜索功能

- 引入基于 JSON 的框类型覆盖,允许动态更新标签、描述和前缀。
- 增加了一种可调节容量的定制盒型。
- 实现了应用和保存盒子类型覆盖的函数。
- 更新仪表盘,显示按箱型分组的库存低库存商品。
- 创建了一个新的搜索页面,方便快速访问具有增强搜索功能的组件。
- 用搜索页面取代扫描页面,将出站功能直接集成到搜索结果中。
- 改进的界面元素,提升导航和用户体验,包括新增按钮和样式。
- 移除过时的 scanner.js 文件并将其功能集成到搜索页面。
- 更新了各种模板,以反映新的搜索和框类型管理功能。
This commit is contained in:
2026-03-11 16:01:11 +08:00
parent 0a54bfd5aa
commit 6f4a8d82f3
12 changed files with 524 additions and 276 deletions

View File

@@ -14,9 +14,10 @@
</div>
<nav class="hero-actions">
<a class="btn btn-light" href="{{ url_for('index') }}">返回首页</a>
<a class="btn btn-light" href="{{ url_for('type_page', box_type=box.box_type) }}">返回上一级容器</a>
<a class="btn btn-light" href="{{ url_for('search_page') }}">快速搜索</a>
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
<a class="btn btn-light" href="{{ url_for('export_box_labels_csv', box_id=box.id) }}">导出打标CSV</a>
<a class="btn" href="{{ url_for('scan_page') }}">扫码/搜索</a>
</nav>
</header>

View File

@@ -13,6 +13,7 @@
<p>步骤: 填写核心字段 -> 检查数量 -> 保存</p>
</div>
<div class="hero-actions">
<a class="btn btn-light" href="{{ url_for('search_page', q=search_query) if search_query else url_for('search_page') }}">返回快速搜索</a>
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
<a class="btn btn-light" href="{{ url_for('view_box', box_id=box.id) }}">返回宫格</a>
</div>
@@ -26,6 +27,7 @@
<div class="entry-shell">
<section class="entry-main">
<form class="panel form-grid" method="post">
<input type="hidden" name="q" value="{{ search_query or '' }}">
<label>
料号 *
<input type="text" name="part_no" required value="{{ component.part_no if component else '' }}" aria-label="料号" placeholder="如 STM32F103C8T6">

View File

@@ -13,17 +13,16 @@
<p>{% if separate_mode %}当前为独立分类界面,减少长列表翻找成本{% else %}极简中性灰布局,聚焦数量/分类/变动核心信息{% endif %}</p>
</div>
<div class="hero-actions">
<a class="btn btn-light" href="{{ url_for('types_page') }}">分类总</a>
<a class="btn btn-light" href="{{ url_for('types_page') }}">仓库概</a>
<a class="btn btn-light" href="{{ url_for('search_page') }}">快速搜索</a>
<a class="btn btn-light" href="#quick-add">新增库存</a>
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
<a class="btn" href="{{ url_for('scan_page') }}">扫码/搜索</a>
</div>
</header>
<main class="container">
<div class="layout-shell">
<aside class="catalog-sidebar">
<button class="sidebar-toggle btn btn-light" type="button" aria-expanded="false" aria-controls="side-low-stock">低库存面板</button>
<section class="panel" id="sidebar-nav-panel">
<h2>容器导航</h2>
<div class="card-actions icon-links" aria-label="快捷功能">
@@ -36,9 +35,6 @@
<a class="icon-link" href="#quick-add" title="跳转新增库存" aria-label="跳转新增库存">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>
</a>
<a class="icon-link" href="#side-low-stock" title="跳转低库存面板" aria-label="跳转低库存面板">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="5" width="6" height="6" rx="2"/><rect x="13" y="5" width="6" height="6" rx="2"/><rect x="5" y="13" width="6" height="6" rx="2"/><rect x="13" y="13" width="6" height="6" rx="2"/></svg>
</a>
</div>
<nav class="catalog-nav" id="catalog-nav-links">
{% for key, meta in box_types.items() %}
@@ -46,45 +42,6 @@
{% endfor %}
</nav>
</section>
<section class="panel side-metrics">
<h2>关键指标</h2>
<div class="side-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">近7天净变动</p>
<p class="metric-value">{% if stats.period_net_change_7d > 0 %}+{% endif %}{{ stats.period_net_change_7d }}</p>
</article>
<article class="metric-card">
<p class="metric-title">待补货元件</p>
<p class="metric-value">{{ stats.low_stock_count }}</p>
</article>
</div>
</section>
<section class="panel side-low-stock" id="side-low-stock">
<h2>低库存元器件</h2>
<ul class="side-low-stock-list">
{% for item in low_stock_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>
</section>
</aside>
<section class="catalog-content">
@@ -109,6 +66,9 @@
{% if separate_mode %}<input type="hidden" name="return_to_type" value="{{ current_box_type }}">{% endif %}
<input type="text" name="name" placeholder="基础名称(自动拼范围)" required aria-label="基础名称">
<input type="text" name="slot_prefix" placeholder="前缀(如A/B/C)">
{% if key == 'custom' %}
<input type="number" name="slot_capacity" min="1" value="{{ meta.default_capacity }}" placeholder="格数" required>
{% endif %}
<input type="number" name="start_number" min="0" value="1" placeholder="起始序号">
<input type="text" name="description" placeholder="备注(可选)">
<button class="btn btn-light suggest-start-btn" type="button" data-box-type="{{ key }}">建议起始号</button>
@@ -127,6 +87,9 @@
{% if item.box.box_type == 'bag' %}
<p>编号前缀: {{ item.box.slot_prefix }} | 袋装清单不使用范围</p>
<p>已记录: {{ item.used_count }} 项</p>
{% elif item.box.box_type == 'custom' %}
<p>格数: {{ item.box.slot_capacity }} | 编号前缀: {{ item.box.slot_prefix }} | 范围: {{ item.slot_range }}</p>
<p>已启用: {{ item.used_count }}/{{ item.box.slot_capacity }}</p>
{% else %}
<p>编号前缀: {{ item.box.slot_prefix }} | 范围: {{ item.slot_range }}</p>
<p>已启用: {{ item.used_count }}/{{ item.box.slot_capacity }}</p>
@@ -162,6 +125,9 @@
{% if separate_mode %}<input type="hidden" name="return_to_type" value="{{ current_box_type }}">{% endif %}
<input type="text" name="name" value="{{ item.base_name }}" required>
<input type="text" name="slot_prefix" value="{{ item.box.slot_prefix }}" required>
{% if item.box.box_type == 'custom' %}
<input type="number" name="slot_capacity" min="1" value="{{ item.box.slot_capacity }}" required>
{% endif %}
<input type="number" name="start_number" min="0" value="{{ item.box.start_number }}" required>
<input type="text" name="description" value="{{ item.box.description or '' }}">
<button class="btn btn-light suggest-start-btn" type="button" data-box-id="{{ item.box.id }}" data-box-type="{{ item.box.box_type }}">建议起始号</button>
@@ -182,53 +148,6 @@
<script>
(function () {
function bindSidebarCompactMode() {
var sidebar = document.querySelector('.catalog-sidebar');
var toggleBtn = document.querySelector('.sidebar-toggle');
if (!sidebar) {
return;
}
var storageKey = 'inventorySidebarManualExpand';
var manualExpand = false;
try {
manualExpand = localStorage.getItem(storageKey) === '1';
} catch (e) {
manualExpand = false;
}
function updateToggleState() {
if (!toggleBtn) {
return;
}
toggleBtn.setAttribute('aria-expanded', manualExpand ? 'true' : 'false');
toggleBtn.textContent = manualExpand ? '收起低库存面板' : '展开低库存面板';
}
function syncCompactState() {
var shouldCompact = window.innerWidth > 980 && window.scrollY > 220;
sidebar.classList.toggle('compact', shouldCompact);
sidebar.classList.toggle('manual-expand', shouldCompact && manualExpand);
updateToggleState();
}
if (toggleBtn) {
toggleBtn.addEventListener('click', function () {
manualExpand = !manualExpand;
try {
localStorage.setItem(storageKey, manualExpand ? '1' : '0');
} catch (e) {
// ignore storage errors
}
syncCompactState();
});
}
window.addEventListener('scroll', syncCompactState, { passive: true });
window.addEventListener('resize', syncCompactState);
syncCompactState();
}
function showToast(message) {
var stack = document.querySelector('.toast-stack');
if (!stack) {
@@ -273,6 +192,8 @@
var boxType = btn.dataset.boxType || 'small_28';
var boxId = btn.dataset.boxId || '';
var prefix = prefixInput ? prefixInput.value.trim() : '';
var slotCapacityInput = form.querySelector('input[name="slot_capacity"]');
var slotCapacity = slotCapacityInput ? slotCapacityInput.value.trim() : '';
var params = new URLSearchParams();
params.set('box_type', boxType);
@@ -282,6 +203,9 @@
if (boxId) {
params.set('box_id', boxId);
}
if (slotCapacity) {
params.set('slot_capacity', slotCapacity);
}
fetch('{{ url_for('suggest_start') }}?' + params.toString())
.then(function (resp) { return resp.json(); })
@@ -297,8 +221,6 @@
});
});
});
bindSidebarCompactMode();
})();
</script>
</body>

View File

@@ -1,73 +0,0 @@
<!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>
<div class="hero-actions">
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
<a class="btn btn-light" href="{{ url_for('index') }}">返回首页</a>
</div>
</header>
<main class="container">
<section class="panel">
<div class="card-actions" aria-hidden="true">
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7V4h3"/><path d="M20 7V4h-3"/><path d="M4 17v3h3"/><path d="M20 17v3h-3"/><path d="M8 12h8"/></svg>
</div>
<form method="get" action="{{ url_for('scan_page') }}" class="search-row" id="scan-search-form">
<input id="scan-input" type="search" name="q" placeholder="输入或扫码料号/名称" value="{{ keyword }}" aria-label="搜索关键字">
<button class="btn" type="submit">搜索</button>
</form>
<p class="hint">扫码枪通常会自动输入后回车,可直接触发搜索。</p>
</section>
<section class="panel">
<h2>搜索结果</h2>
{% if keyword and results %}
<div class="table-wrap">
<table>
<thead>
<tr>
<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.quantity }}</td>
<td>{{ row.box_name }} / {{ row.slot_code }}</td>
<td>{% if c.is_enabled %}启用{% else %}停用{% endif %}</td>
<td><a class="btn btn-light" href="{{ url_for('edit_component', box_id=c.box_id, slot=c.slot_index) }}">编辑</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif keyword %}
<p>未找到关键词 "{{ keyword }}" 的元件。</p>
{% else %}
<p>请输入关键词开始搜索。</p>
{% endif %}
</section>
</main>
<script src="{{ url_for('static', filename='js/scanner.js') }}"></script>
</body>
</html>

109
templates/search.html Normal file
View File

@@ -0,0 +1,109 @@
<!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>
</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="输入料号或名称" value="{{ keyword }}" aria-label="搜索关键字">
<button class="btn" type="submit">搜索</button>
</form>
<p class="hint">出库只需要输入数量,系统会自动扣减库存并记录统计。</p>
</section>
<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>
</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><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="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="7">{% if keyword %}未找到匹配元件{% else %}先输入关键字进行搜索{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</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();
}
});
}
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();
}
}
});
});
})();
</script>
</body>
</html>

View File

@@ -14,7 +14,7 @@
</div>
<nav class="hero-actions">
<a class="btn btn-light" href="{{ url_for('index') }}">返回首页</a>
<a class="btn" href="{{ url_for('scan_page') }}">扫码/搜索</a>
<a class="btn btn-light" href="{{ url_for('search_page') }}">快速搜索</a>
</nav>
</header>

47
templates/type_edit.html Normal file
View File

@@ -0,0 +1,47 @@
<!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>
<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 %}
<section class="panel">
<form class="form-grid" method="post">
<label>
容器名称 *
<input type="text" name="label" required value="{{ meta.label }}">
</label>
<label>
默认前缀 *
<input type="text" name="default_prefix" required value="{{ meta.default_prefix }}">
</label>
<label class="full">
默认描述
<input type="text" name="default_desc" value="{{ meta.default_desc }}">
</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

@@ -3,32 +3,83 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分类总</title>
<title>仓库概</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="hero">
<div>
<h1>分类总</h1>
<p>将容器拆分为独立界面,避免长页面翻找</p>
<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>
<a class="btn" href="{{ url_for('scan_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="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">
<p class="metric-title">{{ item.label }}</p>
<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>
</main>
</body>
</html>