From 6f4a8d82f343f37f32d5c16cdffb24140ac41417 Mon Sep 17 00:00:00 2001
From: wangbeihong
Date: Wed, 11 Mar 2026 16:01:11 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=BC=BA=E6=A1=86?=
=?UTF-8?q?=E7=B1=BB=E5=9E=8B=E7=AE=A1=E7=90=86=E5=92=8C=E6=90=9C=E7=B4=A2?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 引入基于 JSON 的框类型覆盖,允许动态更新标签、描述和前缀。
- 增加了一种可调节容量的定制盒型。
- 实现了应用和保存盒子类型覆盖的函数。
- 更新仪表盘,显示按箱型分组的库存低库存商品。
- 创建了一个新的搜索页面,方便快速访问具有增强搜索功能的组件。
- 用搜索页面取代扫描页面,将出站功能直接集成到搜索结果中。
- 改进的界面元素,提升导航和用户体验,包括新增按钮和样式。
- 移除过时的 scanner.js 文件并将其功能集成到搜索页面。
- 更新了各种模板,以反映新的搜索和框类型管理功能。
---
README.md | 30 ++---
app.py | 280 +++++++++++++++++++++++++++++++++------
static/css/style.css | 38 ++++++
static/js/scanner.js | 45 -------
templates/box.html | 3 +-
templates/edit.html | 2 +
templates/index.html | 110 +++------------
templates/scan.html | 73 ----------
templates/search.html | 109 +++++++++++++++
templates/stats.html | 2 +-
templates/type_edit.html | 47 +++++++
templates/types.html | 61 ++++++++-
12 files changed, 524 insertions(+), 276 deletions(-)
delete mode 100644 static/js/scanner.js
delete mode 100644 templates/scan.html
create mode 100644 templates/search.html
create mode 100644 templates/type_edit.html
diff --git a/README.md b/README.md
index eb15245..2dfe3b1 100644
--- a/README.md
+++ b/README.md
@@ -31,13 +31,10 @@ inventory/
│ ├── index.html
│ ├── box.html
│ ├── edit.html
-│ ├── scan.html
│ └── stats.html
└── static/
├── css/
│ └── style.css
- └── js/
- └── scanner.js
```
## 2. 本地运行
@@ -68,9 +65,9 @@ python app.py
### 3.1 首页 `/`
-- 首页已改为入口跳转到 `分类总览` 页面。
+- 首页已改为入口跳转到 `仓库概览` 页面。
-### 3.1.1 分类总览 `/types`
+### 3.1.1 仓库概览 `/types`
- 展示三类独立界面入口:`28格小盒大盒`、`14格中盒大盒`、`袋装清单`。
- 每类入口显示当前容器数量,点击进入单独分类页面。
@@ -87,24 +84,19 @@ python app.py
- `袋装清单` 仅使用编号前缀(如 `BAG`),不设置编号范围。
- `28格/14格` 支持快速入库:多行粘贴后自动分配空位。
- 支持按当前盒子导出打标 CSV(仅导出启用记录),可用于热敏打标机导入。
-- 打标 CSV 列名为中英双语格式(如 `料号(part_no)`、`打标文本(label_text)`),便于直接识别。
+- 打标 CSV 列名为中英双语格式(如 `料号(part_no)`、`备注(note)`),便于直接识别。
### 3.3 编辑页 `/edit//`
-- 编辑料号、名称、规格、数量、位置备注、备注。
-- 支持勾选启用,或通过按钮启用/停用。
+- 编辑料号、名称、规格、数量、备注。
+- 通过按钮启用/停用。
- 可删除当前格子记录。
-### 3.4 扫码/搜索 `/scan`
-
-- 可按料号或名称搜索。
-- 支持扫码枪输入后回车触发搜索。
-
-### 3.5 统计页 `/stats`
+### 3.4 统计页 `/stats`
- 独立统计页,仅展示核心指标:`库存总量 / 分类占比 / 变动趋势`。
- 支持 `7天` 与 `30天` 视图切换:`/stats?days=7`、`/stats?days=30`。
-- 支持分类筛选:`/stats?days=30&box_type=small_28`(可选值:`small_28`、`medium_14`、`bag`、`all`)。
+- 支持分类筛选:`/stats?days=30&box_type=small_28`(可选值:`small_28`、`medium_14`、`custom`、`bag`、`all`)。
- 趋势图基于库存变动日志实时计算,来源包括:新增、快速入库、启用/停用、删除。
- 说明:升级前的历史操作不会自动回溯写入日志,趋势从启用该版本后开始逐步真实化。
- 新增最近操作时间线(最新 20 条),便于追踪库存变化来源。
@@ -112,12 +104,18 @@ python app.py
- 支持趋势数据导出 CSV:`/stats/export?days=7&box_type=all`(包含 `daily_delta` 日增减列)。
- 支持清除统计日志(当前筛选或全部),仅影响统计与趋势,不影响库存数据本体。
+### 3.5 快速搜索与出库 `/search`
+
+- 支持按 `料号` 或 `名称` 搜索已启用元件。
+- 搜索结果可一键跳转到对应盒位编辑页。
+- 支持快速出库:只填写数量即可扣减库存,并写入统计日志。
+
## 4. 袋装批量新增格式
在袋装清单页面的批量输入框里,每行一条,可用英文逗号或 Tab 分隔:
```text
-料号, 名称, 数量, 规格, 位置备注, 备注
+料号, 名称, 数量, 规格, 备注
```
示例:
diff --git a/app.py b/app.py
index a5a5f5b..bf7adba 100644
--- a/app.py
+++ b/app.py
@@ -1,6 +1,8 @@
import os
import re
import csv
+import json
+from copy import deepcopy
from io import StringIO
from datetime import datetime, timedelta
@@ -19,9 +21,10 @@ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
LOW_STOCK_THRESHOLD = 5
+BOX_TYPES_OVERRIDE_PATH = os.path.join(DB_DIR, "box_types.json")
-BOX_TYPES = {
+DEFAULT_BOX_TYPES = {
"small_28": {
"label": "28格小盒大盒",
"default_capacity": 28,
@@ -34,6 +37,12 @@ BOX_TYPES = {
"default_desc": "14格中盒,内部摆放方向与28格不同",
"default_prefix": "B",
},
+ "custom": {
+ "label": "自定义容器",
+ "default_capacity": 20,
+ "default_desc": "可按实际盒型设置格数与编号前缀",
+ "default_prefix": "C",
+ },
"bag": {
"label": "袋装清单",
"default_capacity": 1,
@@ -42,6 +51,52 @@ BOX_TYPES = {
},
}
+BOX_TYPES = deepcopy(DEFAULT_BOX_TYPES)
+
+
+def _apply_box_type_overrides() -> None:
+ if not os.path.exists(BOX_TYPES_OVERRIDE_PATH):
+ return
+
+ try:
+ with open(BOX_TYPES_OVERRIDE_PATH, "r", encoding="utf-8") as f:
+ overrides = json.load(f)
+ except (OSError, json.JSONDecodeError):
+ return
+
+ if not isinstance(overrides, dict):
+ return
+
+ for key, value in overrides.items():
+ if key not in BOX_TYPES or not isinstance(value, dict):
+ continue
+
+ for field in ("label", "default_desc", "default_prefix"):
+ if field not in value:
+ continue
+ BOX_TYPES[key][field] = value[field]
+
+ # Keep bag list capacity fixed by domain rule.
+ BOX_TYPES["bag"]["default_capacity"] = 1
+
+
+def _save_box_type_overrides() -> None:
+ payload = {}
+ for key, defaults in DEFAULT_BOX_TYPES.items():
+ current = BOX_TYPES.get(key, defaults)
+ changed = {}
+ for field in ("label", "default_desc", "default_prefix"):
+ if current.get(field) != defaults.get(field):
+ changed[field] = current.get(field)
+ if changed:
+ payload[key] = changed
+
+ with open(BOX_TYPES_OVERRIDE_PATH, "w", encoding="utf-8") as f:
+ json.dump(payload, f, ensure_ascii=False, indent=2)
+
+
+_apply_box_type_overrides()
+
class Box(db.Model):
__tablename__ = "boxes"
@@ -512,6 +567,7 @@ def event_type_label(event_type: str) -> str:
labels = {
"quick_inbound_add": "快速入库新增",
"quick_inbound_merge": "快速入库合并",
+ "component_outbound": "快速出库",
"component_save": "编辑保存",
"component_enable": "启用元件",
"component_disable": "停用元件",
@@ -581,11 +637,14 @@ def build_dashboard_context():
low_stock_items = []
for c in sorted(low_stock_components, key=lambda item: (item.quantity, item.name or ""))[:12]:
box = box_by_id.get(c.box_id)
+ box_type_key = box.box_type if box and box.box_type in BOX_TYPES else "small_28"
low_stock_items.append(
{
"name": c.name,
"part_no": c.part_no,
"quantity": c.quantity,
+ "box_type": box_type_key,
+ "box_type_label": BOX_TYPES[box_type_key]["label"],
"box_name": box.name if box else f"盒 {c.box_id}",
"slot_code": slot_code_for_box(box, c.slot_index) if box else str(c.slot_index),
"edit_url": url_for("edit_component", box_id=c.box_id, slot=c.slot_index),
@@ -636,19 +695,82 @@ def index():
@app.route("/types")
def types_page():
dashboard = build_dashboard_context()
+ notice = request.args.get("notice", "").strip()
+ error = request.args.get("error", "").strip()
+ category_stats_map = {item["key"]: item for item in dashboard["category_stats"]}
+ low_stock_groups = []
+
+ for key, meta in BOX_TYPES.items():
+ grouped_items = [
+ item
+ for item in dashboard["low_stock_items"]
+ if item.get("box_type") == key
+ ]
+ low_stock_groups.append(
+ {
+ "key": key,
+ "label": meta["label"],
+ "items": grouped_items,
+ }
+ )
+
type_cards = []
for key, meta in BOX_TYPES.items():
+ category_item = category_stats_map.get(key, {})
type_cards.append(
{
"key": key,
"label": meta["label"],
"desc": meta["default_desc"],
"count": len(dashboard["groups"].get(key, [])),
+ "item_count": int(category_item.get("item_count", 0)),
+ "quantity": int(category_item.get("quantity", 0)),
"url": url_for("type_page", box_type=key),
}
)
- return render_template("types.html", type_cards=type_cards)
+ return render_template(
+ "types.html",
+ type_cards=type_cards,
+ stats=dashboard["stats"],
+ low_stock_groups=low_stock_groups,
+ notice=notice,
+ error=error,
+ )
+
+
+@app.route("/container-type//edit", methods=["GET", "POST"])
+def edit_container_type(box_type: str):
+ if box_type not in BOX_TYPES:
+ return bad_request("无效盒子类型", "")
+
+ meta = BOX_TYPES[box_type]
+ error = ""
+
+ if request.method == "POST":
+ label = request.form.get("label", "").strip()
+ default_desc = request.form.get("default_desc", "").strip()
+ default_prefix = request.form.get("default_prefix", "").strip().upper()
+
+ if not label:
+ error = "容器名称不能为空"
+ elif not default_prefix:
+ error = "默认前缀不能为空"
+
+ if not error:
+ meta["label"] = label
+ meta["default_desc"] = default_desc or DEFAULT_BOX_TYPES[box_type]["default_desc"]
+ meta["default_prefix"] = default_prefix
+
+ _save_box_type_overrides()
+ return redirect(url_for("types_page", notice="容器属性已更新"))
+
+ return render_template(
+ "type_edit.html",
+ box_type=box_type,
+ meta=meta,
+ error=error,
+ )
@app.route("/type/")
@@ -691,12 +813,24 @@ def create_box():
return bad_request("起始序号必须是大于等于 0 的整数", box_type)
meta = BOX_TYPES[box_type]
+ slot_capacity = meta["default_capacity"]
+ if box_type == "custom":
+ try:
+ slot_capacity = _parse_non_negative_int(
+ request.form.get("slot_capacity", str(meta["default_capacity"])),
+ meta["default_capacity"],
+ )
+ except ValueError:
+ return bad_request("格数必须是大于等于 1 的整数", box_type)
+ if slot_capacity < 1:
+ return bad_request("格数必须是大于等于 1 的整数", box_type)
+
effective_prefix = slot_prefix or meta["default_prefix"]
conflict, other_box = has_range_conflict(
box_type=box_type,
prefix=effective_prefix,
start_number=start_number,
- slot_capacity=meta["default_capacity"],
+ slot_capacity=slot_capacity,
)
if conflict:
return bad_request(
@@ -710,7 +844,7 @@ def create_box():
base_name=base_name,
prefix=effective_prefix,
start_number=start_number,
- slot_capacity=meta["default_capacity"],
+ slot_capacity=slot_capacity,
)
final_name = make_unique_box_name(generated_name)
@@ -718,7 +852,7 @@ def create_box():
name=final_name,
description=description or meta["default_desc"],
box_type=box_type,
- slot_capacity=meta["default_capacity"],
+ slot_capacity=slot_capacity,
slot_prefix=effective_prefix,
start_number=start_number,
)
@@ -745,12 +879,36 @@ def update_box(box_id: int):
except ValueError:
return bad_request("起始序号必须是大于等于 0 的整数", box.box_type)
+ slot_capacity = box.slot_capacity
+ if box.box_type == "custom":
+ try:
+ slot_capacity = _parse_non_negative_int(
+ request.form.get("slot_capacity", str(box.slot_capacity)),
+ box.slot_capacity,
+ )
+ except ValueError:
+ return bad_request("格数必须是大于等于 1 的整数", box.box_type)
+ if slot_capacity < 1:
+ return bad_request("格数必须是大于等于 1 的整数", box.box_type)
+
+ max_used_slot = (
+ db.session.query(db.func.max(Component.slot_index))
+ .filter_by(box_id=box.id)
+ .scalar()
+ or 0
+ )
+ if max_used_slot > slot_capacity:
+ return bad_request(
+ f"格数不能小于已使用位置 {max_used_slot}",
+ box.box_type,
+ )
+
effective_prefix = slot_prefix or BOX_TYPES[box.box_type]["default_prefix"]
conflict, other_box = has_range_conflict(
box_type=box.box_type,
prefix=effective_prefix,
start_number=start_number,
- slot_capacity=box.slot_capacity,
+ slot_capacity=slot_capacity,
exclude_box_id=box.id,
)
if conflict:
@@ -765,13 +923,14 @@ def update_box(box_id: int):
base_name=base_name,
prefix=effective_prefix,
start_number=start_number,
- slot_capacity=box.slot_capacity,
+ slot_capacity=slot_capacity,
)
box.name = make_unique_box_name(generated_name, exclude_box_id=box.id)
box.description = description or BOX_TYPES[box.box_type]["default_desc"]
box.slot_prefix = effective_prefix
box.start_number = start_number
+ box.slot_capacity = slot_capacity
db.session.commit()
return_to_type = parse_box_type_filter(request.form.get("return_to_type", ""))
target_type = return_to_type if return_to_type != "all" else box.box_type
@@ -813,6 +972,17 @@ def suggest_start():
exclude_box_id = None
slot_capacity = BOX_TYPES[box_type]["default_capacity"]
+ if box_type == "custom" and not box_id_raw:
+ try:
+ slot_capacity = _parse_non_negative_int(
+ request.args.get("slot_capacity", str(slot_capacity)),
+ slot_capacity,
+ )
+ except ValueError:
+ return {"ok": False, "message": "格数必须是大于等于 1 的整数"}, 400
+ if slot_capacity < 1:
+ return {"ok": False, "message": "格数必须是大于等于 1 的整数"}, 400
+
if box_id_raw:
try:
box_id = int(box_id_raw)
@@ -1195,10 +1365,13 @@ def edit_component(box_id: int, slot: int):
if slot < 1 or slot > box.slot_capacity:
return "无效的格子编号", 400
+ search_query = request.args.get("q", "").strip()
component = Component.query.filter_by(box_id=box.id, slot_index=slot).first()
if request.method == "POST":
action = request.form.get("action", "save")
+ search_query_post = request.form.get("q", "").strip()
+ search_query_effective = search_query_post or search_query
if action == "delete":
if component:
@@ -1211,6 +1384,8 @@ def edit_component(box_id: int, slot: int):
)
db.session.delete(component)
db.session.commit()
+ if search_query_effective:
+ return redirect(url_for("search_page", q=search_query_effective))
return redirect(url_for("view_box", box_id=box.id))
if action == "toggle_enable":
@@ -1225,7 +1400,7 @@ def edit_component(box_id: int, slot: int):
component=component,
)
db.session.commit()
- return redirect(url_for("edit_component", box_id=box.id, slot=slot))
+ return redirect(url_for("edit_component", box_id=box.id, slot=slot, q=search_query_post))
if action == "toggle_disable":
if component:
@@ -1239,7 +1414,7 @@ def edit_component(box_id: int, slot: int):
component=component,
)
db.session.commit()
- return redirect(url_for("edit_component", box_id=box.id, slot=slot))
+ return redirect(url_for("edit_component", box_id=box.id, slot=slot, q=search_query_post))
part_no = request.form.get("part_no", "").strip()
name = request.form.get("name", "").strip()
@@ -1256,6 +1431,7 @@ def edit_component(box_id: int, slot: int):
slot_code=slot_code_for_box(box, slot),
component=component,
error=error,
+ search_query=search_query_post,
)
try:
@@ -1269,6 +1445,7 @@ def edit_component(box_id: int, slot: int):
slot_code=slot_code_for_box(box, slot),
component=component,
error=error,
+ search_query=search_query_post,
)
old_enabled_qty = 0
@@ -1299,6 +1476,8 @@ def edit_component(box_id: int, slot: int):
)
db.session.commit()
+ if search_query_effective:
+ return redirect(url_for("search_page", q=search_query_effective))
return redirect(url_for("view_box", box_id=box.id))
return render_template(
@@ -1307,24 +1486,28 @@ def edit_component(box_id: int, slot: int):
slot=slot,
slot_code=slot_code_for_box(box, slot),
component=component,
+ search_query=search_query,
)
-@app.route("/scan")
-def scan_page():
+@app.route("/search")
+def search_page():
keyword = request.args.get("q", "").strip()
+ notice = request.args.get("notice", "").strip()
+ error = request.args.get("error", "").strip()
results = []
if keyword:
raw_results = (
Component.query.join(Box, Box.id == Component.box_id)
.filter(
+ Component.is_enabled.is_(True),
db.or_(
Component.part_no.ilike(f"%{keyword}%"),
Component.name.ilike(f"%{keyword}%"),
- )
+ ),
)
- .order_by(Component.part_no.asc())
+ .order_by(Component.part_no.asc(), Component.name.asc())
.all()
)
@@ -1335,10 +1518,52 @@ def scan_page():
"component": c,
"box_name": box.name if box else f"盒 {c.box_id}",
"slot_code": slot_code_for_box(box, c.slot_index) if box else str(c.slot_index),
+ "edit_url": url_for("edit_component", box_id=c.box_id, slot=c.slot_index, q=keyword),
}
)
- return render_template("scan.html", keyword=keyword, results=results)
+ return render_template(
+ "search.html",
+ keyword=keyword,
+ results=results,
+ notice=notice,
+ error=error,
+ )
+
+
+@app.route("/component//outbound", methods=["POST"])
+def quick_outbound(component_id: int):
+ component = Component.query.get_or_404(component_id)
+ keyword = request.form.get("q", "").strip()
+
+ try:
+ amount = _parse_non_negative_int(request.form.get("amount", "0"), 0)
+ except ValueError:
+ return redirect(url_for("search_page", q=keyword, error="出库数量必须是大于等于 0 的整数"))
+
+ if amount <= 0:
+ return redirect(url_for("search_page", q=keyword, error="出库数量必须大于 0"))
+
+ if not component.is_enabled:
+ return redirect(url_for("search_page", q=keyword, error="该元件已停用,不能出库"))
+
+ if amount > int(component.quantity or 0):
+ return redirect(url_for("search_page", q=keyword, error="出库数量超过当前库存"))
+
+ component.quantity = int(component.quantity or 0) - amount
+ box = Box.query.get(component.box_id)
+ log_inventory_event(
+ event_type="component_outbound",
+ delta=-amount,
+ box=box,
+ component=component,
+ part_no=component.part_no,
+ )
+ db.session.commit()
+
+ slot_code = slot_code_for_box(box, component.slot_index) if box else str(component.slot_index)
+ notice = f"出库成功: {component.part_no} -{amount}({slot_code})"
+ return redirect(url_for("search_page", q=keyword, notice=notice))
@app.route("/stats")
@@ -1580,33 +1805,6 @@ def clear_stats_logs():
return redirect(url_for("stats_page", days=days, box_type=box_type_filter, notice=notice))
-@app.route("/api/scan")
-def scan_api():
- code = request.args.get("code", "").strip()
- if not code:
- return {"ok": False, "message": "code 不能为空"}, 400
-
- item = Component.query.filter_by(part_no=code).first()
- if not item:
- return {"ok": False, "message": "未找到元件"}, 404
-
- box = Box.query.get(item.box_id)
- return {
- "ok": True,
- "data": {
- "id": item.id,
- "part_no": item.part_no,
- "name": item.name,
- "quantity": item.quantity,
- "box_id": item.box_id,
- "box_name": box.name if box else None,
- "slot_index": item.slot_index,
- "slot_code": slot_code_for_box(box, item.slot_index) if box else str(item.slot_index),
- "is_enabled": bool(item.is_enabled),
- },
- }
-
-
def bootstrap() -> None:
with app.app_context():
db.create_all()
diff --git a/static/css/style.css b/static/css/style.css
index c26dd10..1b4c7f8 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -421,6 +421,34 @@ body {
gap: var(--space-1);
}
+.type-card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-1);
+}
+
+.type-card-more {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 999px;
+ border: 1px solid var(--line);
+ background: var(--card-alt);
+ color: var(--muted);
+ text-decoration: none;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.type-card-more:hover {
+ color: var(--accent-press);
+ border-color: color-mix(in srgb, var(--accent) 60%, var(--line));
+ background: color-mix(in srgb, var(--card-alt) 76%, var(--accent) 24%);
+}
+
.catalog-content {
min-width: 0;
}
@@ -772,6 +800,16 @@ input[type="checkbox"] {
flex: 1;
}
+.search-outbound-form {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.search-outbound-form input[type="number"] {
+ width: 88px;
+}
+
.table-wrap {
overflow-x: auto;
}
diff --git a/static/js/scanner.js b/static/js/scanner.js
deleted file mode 100644
index 60f9f4f..0000000
--- a/static/js/scanner.js
+++ /dev/null
@@ -1,45 +0,0 @@
-(function () {
- const input = document.getElementById("scan-input");
- const form = document.getElementById("scan-search-form");
-
- function showToast(message) {
- let stack = document.querySelector(".toast-stack");
- if (!stack) {
- stack = document.createElement("div");
- stack.className = "toast-stack";
- document.body.appendChild(stack);
- }
-
- const toast = document.createElement("div");
- toast.className = "toast";
- toast.textContent = message;
- stack.appendChild(toast);
-
- setTimeout(function () {
- toast.remove();
- }, 1600);
- }
-
- if (!input || !form) {
- return;
- }
-
- // Keep focus for barcode scanners that type and send Enter immediately.
- window.addEventListener("load", function () {
- input.focus();
- });
-
- document.addEventListener("keydown", function (event) {
- if (event.key === "Escape") {
- input.value = "";
- input.focus();
- showToast("已清空搜索词");
- }
- });
-
- form.addEventListener("submit", function () {
- if (input.value.trim()) {
- showToast("正在搜索...");
- }
- });
-})();
diff --git a/templates/box.html b/templates/box.html
index 0de4106..99513b0 100644
--- a/templates/box.html
+++ b/templates/box.html
@@ -14,9 +14,10 @@
diff --git a/templates/edit.html b/templates/edit.html
index 2265553..78d3057 100644
--- a/templates/edit.html
+++ b/templates/edit.html
@@ -13,6 +13,7 @@
步骤: 填写核心字段 -> 检查数量 -> 保存
@@ -26,6 +27,7 @@
-
-
-
扫码 / 搜索
-
一步输入,直达元件位置与库存状态
-
-
-
-
-
-
-
-
- 扫码枪通常会自动输入后回车,可直接触发搜索。
-
-
-
- 搜索结果
- {% if keyword and results %}
-
-
-
-
- | 料号 |
- 名称 |
- 库存 |
- 位置 |
- 状态 |
- 操作 |
-
-
-
- {% for row in results %}
- {% set c = row.component %}
-
- | {{ c.part_no }} |
- {{ c.name }} |
- {{ c.quantity }} |
- {{ row.box_name }} / {{ row.slot_code }} |
- {% if c.is_enabled %}启用{% else %}停用{% endif %} |
- 编辑 |
-
- {% endfor %}
-
-
-
- {% elif keyword %}
- 未找到关键词 "{{ keyword }}" 的元件。
- {% else %}
- 请输入关键词开始搜索。
- {% endif %}
-
-
-
-
-
diff --git a/templates/scan.html b/templates/scan.html
deleted file mode 100644
index 5ab159e..0000000
--- a/templates/scan.html
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-