feat:增强模板的用户界面和功能

- 在 scanner.js 中为用户操作添加了 toast 通知。
- 更新 box.html 以包含额外的导航选项和改进的布局。
- 增强 edit.html,提供更清晰的说明和改进表单的可访问性。
- 修改了 error.html,以提供有关输入错误的用户指导。
- 改进了 index.html,以优化导航并添加了关键指标显示。
- 增强了 scan.html,优化了搜索输入和操作按钮。
- 引入了 stats.html,用于详细的库存统计和趋势。
- 创建了 types.html,用于分类概述库存类型。
This commit is contained in:
2026-03-10 01:34:02 +08:00
parent 859f92cdf0
commit 0a54bfd5aa
11 changed files with 2255 additions and 258 deletions

View File

@@ -8,34 +8,106 @@
</head>
<body>
<header class="hero">
<h1>电子元件库存管理 v1.0</h1>
<a class="btn" href="{{ url_for('scan_page') }}">扫码/搜索</a>
<div>
<h1>{% if separate_mode %}{{ box_types[current_box_type].label }}{% else %}库存管理{% endif %}</h1>
<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="#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 panel">
<h2>容器导航</h2>
<nav class="catalog-nav">
<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="快捷功能">
<a class="icon-link" href="#sidebar-nav-panel" title="定位容器导航" aria-label="定位容器导航">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="6" width="16" height="12" rx="2"/><path d="M4 10h16"/><path d="M12 10v8"/></svg>
</a>
<a class="icon-link" href="{{ url_for('stats_page') }}" title="打开统计页" aria-label="打开统计页">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 18h16"/><path d="M6 14l4-4 4 2 4-5"/></svg>
</a>
<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() %}
<a href="#group-{{ key }}">{{ meta.label }} ({{ groups[key]|length }})</a>
<a href="{{ url_for('type_page', box_type=key) }}" class="{% if separate_mode and current_box_type == key %}active{% endif %}">{{ meta.label }} ({{ groups[key]|length }})</a>
{% 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">
<h2>容器列表</h2>
{% for key, meta in box_types.items() %}
{% if view_box_types is defined %}
{% set display_box_types = view_box_types %}
{% else %}
{% set display_box_types = box_types.keys() %}
{% endif %}
{% for key in display_box_types %}
{% set meta = box_types[key] %}
<section class="group-panel panel" id="group-{{ key }}">
<div class="group-title-row">
<h3>{{ meta.label }}</h3>
<span class="group-desc">{{ meta.default_desc }}</span>
</div>
<form class="new-box-form" method="post" action="{{ url_for('create_box') }}">
{% if key != 'bag' %}
<form class="new-box-form" method="post" action="{{ url_for('create_box') }}" {% if loop.first %}id="quick-add"{% endif %}>
<input type="hidden" name="box_type" value="{{ key }}">
<input type="text" name="name" placeholder="基础名称(自动拼范围)" required>
{% 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)">
<input type="number" name="start_number" min="0" value="1" placeholder="起始序号">
<input type="text" name="description" placeholder="备注(可选)">
@@ -43,20 +115,31 @@
<button class="btn" type="submit">新增盒子</button>
<span class="hint suggest-preview"></span>
</form>
{% else %}
<p class="hint">袋装清单为固定容器(大盒),不需要新增盒子。</p>
{% endif %}
<section class="box-list">
{% for item in groups[key] %}
<article class="box-card">
<h4>{{ item.box.name }}</h4>
<p>{{ item.box.description or '暂无描述' }}</p>
{% if item.box.box_type == 'bag' %}
<p>编号前缀: {{ item.box.slot_prefix }} | 袋装清单不使用范围</p>
<p>已记录: {{ item.used_count }} 项</p>
{% else %}
<p>编号前缀: {{ item.box.slot_prefix }} | 范围: {{ item.slot_range }}</p>
<p>已启用: {{ item.used_count }}/{{ item.box.slot_capacity }}</p>
{% endif %}
<div class="card-actions">
<a class="btn" href="{{ url_for('view_box', box_id=item.box.id) }}">进入列表</a>
<form method="post" action="{{ url_for('delete_box', box_id=item.box.id) }}" onsubmit="return confirm('确认删除盒子及内部全部记录吗?')">
{% if item.box.box_type != 'bag' %}
<form method="post" action="{{ url_for('delete_box', box_id=item.box.id) }}" onsubmit="return confirm('确认删除这个盒子及其内部记录吗?')">
{% if separate_mode %}<input type="hidden" name="return_to_type" value="{{ current_box_type }}">{% endif %}
<button class="btn btn-danger" type="submit">删除</button>
</form>
{% endif %}
</div>
<details class="box-overview">
@@ -76,6 +159,7 @@
<summary>设置(改名/前缀/起始号)</summary>
<p class="hint">输入基础名称后,系统会自动生成: 基础名称 + 编号范围。</p>
<form class="new-box-form compact" method="post" action="{{ url_for('update_box', box_id=item.box.id) }}">
{% 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>
<input type="number" name="start_number" min="0" value="{{ item.box.start_number }}" required>
@@ -98,6 +182,69 @@
<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) {
stack = document.createElement('div');
stack.className = 'toast-stack';
document.body.appendChild(stack);
}
var toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
stack.appendChild(toast);
setTimeout(function () {
toast.remove();
}, 1600);
}
function updateSuggest(form, payload) {
var startInput = form.querySelector('input[name="start_number"]');
var prefixInput = form.querySelector('input[name="slot_prefix"]');
@@ -112,6 +259,7 @@
if (preview) {
preview.textContent = '建议范围: ' + payload.preview_range;
}
showToast('已更新建议起始号');
}
document.querySelectorAll('.suggest-start-btn').forEach(function (btn) {
@@ -139,16 +287,18 @@
.then(function (resp) { return resp.json(); })
.then(function (data) {
if (!data.ok) {
alert(data.message || '建议起始号失败');
showToast(data.message || '建议起始号失败');
return;
}
updateSuggest(form, data);
})
.catch(function () {
alert('建议起始号失败,请稍后重试');
showToast('建议起始号失败,请稍后重试');
});
});
});
bindSidebarCompactMode();
})();
</script>
</body>