feat: 完善文档和界面,增加盒子类型支持及批量新增功能

This commit is contained in:
2026-03-08 02:16:20 +08:00
parent fc38b1eb3d
commit 672336c578
6 changed files with 696 additions and 32 deletions

270
app.py
View File

@@ -1,4 +1,5 @@
import os
import re
from flask import Flask, redirect, render_template, request, url_for
from flask_sqlalchemy import SQLAlchemy
@@ -15,12 +16,33 @@ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
BOX_TYPES = {
"small_28": {
"label": "28格小盒大盒",
"default_capacity": 28,
"default_desc": "4连排小盒常见摆放为竖向7排",
},
"medium_14": {
"label": "14格中盒大盒",
"default_capacity": 14,
"default_desc": "装十四个中等盒子(尺寸不固定)",
},
"bag": {
"label": "袋装清单",
"default_capacity": 1,
"default_desc": "用于较小防静电袋存放",
},
}
class Box(db.Model):
__tablename__ = "boxes"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
description = db.Column(db.String(255), nullable=True)
box_type = db.Column(db.String(30), nullable=False, default="small_28")
slot_capacity = db.Column(db.Integer, nullable=False, default=28)
class Component(db.Model):
@@ -45,44 +67,250 @@ class Component(db.Model):
def ensure_default_box() -> None:
if not Box.query.first():
default_box = Box(name="默认大盒", description="每盒 28 个格子")
db.session.add(default_box)
db.session.commit()
defaults = [
("默认28格大盒", "small_28"),
("默认14格中盒", "medium_14"),
("默认袋装清单", "bag"),
]
for box_name, box_type in defaults:
exists = Box.query.filter_by(name=box_name).first()
if exists:
continue
meta = BOX_TYPES[box_type]
db.session.add(
Box(
name=box_name,
description=meta["default_desc"],
box_type=box_type,
slot_capacity=meta["default_capacity"],
)
)
db.session.commit()
def slot_data_for_box(box_id: int):
components = Component.query.filter_by(box_id=box_id).all()
def slot_data_for_box(box: Box):
components = Component.query.filter_by(box_id=box.id).all()
slot_map = {c.slot_index: c for c in components}
slots = []
for slot in range(1, 29):
for slot in range(1, box.slot_capacity + 1):
slots.append({"slot": slot, "component": slot_map.get(slot)})
return slots
def bag_items_for_box(box: Box):
return (
Component.query.filter_by(box_id=box.id)
.order_by(Component.slot_index.asc())
.all()
)
def _parse_non_negative_int(raw_value: str, default_value: int = 0) -> int:
raw = (raw_value or "").strip()
if raw == "":
return default_value
value = int(raw)
if value < 0:
raise ValueError
return value
def _add_bag_item(
box: Box,
part_no: str,
name: str,
quantity: int,
specification: str,
location: str,
note: str,
) -> None:
next_slot = (
db.session.query(db.func.max(Component.slot_index))
.filter(Component.box_id == box.id)
.scalar()
or 0
) + 1
item = Component(
box_id=box.id,
slot_index=next_slot,
part_no=part_no,
name=name,
quantity=quantity,
specification=specification or None,
location=location or None,
note=note or None,
)
db.session.add(item)
if next_slot > box.slot_capacity:
box.slot_capacity = next_slot
def render_box_page(box: Box, error: str = "", notice: str = ""):
slots = slot_data_for_box(box)
bag_items = bag_items_for_box(box) if box.box_type == "bag" else []
return render_template(
"box.html",
box=box,
slots=slots,
bag_items=bag_items,
box_types=BOX_TYPES,
error=error,
notice=notice,
)
def ensure_box_schema() -> None:
columns = {
row[1]
for row in db.session.execute(db.text("PRAGMA table_info(boxes)")).fetchall()
}
if "box_type" not in columns:
db.session.execute(
db.text(
"ALTER TABLE boxes ADD COLUMN box_type VARCHAR(30) NOT NULL DEFAULT 'small_28'"
)
)
if "slot_capacity" not in columns:
db.session.execute(
db.text(
"ALTER TABLE boxes ADD COLUMN slot_capacity INTEGER NOT NULL DEFAULT 28"
)
)
db.session.commit()
@app.route("/")
def index():
boxes = Box.query.order_by(Box.id.asc()).all()
box_cards = []
groups = {key: [] for key in BOX_TYPES.keys()}
for box in boxes:
used_count = Component.query.filter_by(box_id=box.id).count()
box_cards.append({"box": box, "used_count": used_count})
return render_template("index.html", box_cards=box_cards)
box_type = box.box_type if box.box_type in BOX_TYPES else "small_28"
groups[box_type].append({"box": box, "used_count": used_count})
return render_template("index.html", groups=groups, box_types=BOX_TYPES)
@app.route("/boxes/create", methods=["POST"])
def create_box():
box_type = request.form.get("box_type", "small_28").strip()
name = request.form.get("name", "").strip()
description = request.form.get("description", "").strip()
if box_type not in BOX_TYPES:
return "无效盒子类型", 400
if not name:
return "盒子名称不能为空", 400
if Box.query.filter_by(name=name).first():
return "盒子名称已存在,请更换", 400
meta = BOX_TYPES[box_type]
box = Box(
name=name,
description=description or meta["default_desc"],
box_type=box_type,
slot_capacity=meta["default_capacity"],
)
db.session.add(box)
db.session.commit()
return redirect(url_for("index"))
@app.route("/box/<int:box_id>")
def view_box(box_id: int):
box = Box.query.get_or_404(box_id)
slots = slot_data_for_box(box.id)
return render_template("box.html", box=box, slots=slots)
return render_box_page(box)
@app.route("/box/<int:box_id>/bags/add", methods=["POST"])
def add_bag_item(box_id: int):
box = Box.query.get_or_404(box_id)
if box.box_type != "bag":
return "当前盒子不是袋装清单", 400
part_no = request.form.get("part_no", "").strip()
name = request.form.get("name", "").strip()
specification = request.form.get("specification", "").strip()
location = request.form.get("location", "").strip()
note = request.form.get("note", "").strip()
if not part_no or not name:
return render_box_page(box, error="袋装新增失败: 料号和名称不能为空")
try:
quantity = _parse_non_negative_int(request.form.get("quantity", "0"), 0)
except ValueError:
return render_box_page(box, error="袋装新增失败: 数量必须是大于等于 0 的整数")
_add_bag_item(box, part_no, name, quantity, specification, location, note)
db.session.commit()
return render_box_page(box, notice="已新增 1 条袋装记录")
@app.route("/box/<int:box_id>/bags/batch", methods=["POST"])
def add_bag_items_batch(box_id: int):
box = Box.query.get_or_404(box_id)
if box.box_type != "bag":
return "当前盒子不是袋装清单", 400
raw_lines = request.form.get("lines", "")
lines = [line.strip() for line in raw_lines.splitlines() if line.strip()]
if not lines:
return render_box_page(box, error="批量新增失败: 请至少输入一行")
invalid_lines = []
added_count = 0
for line_no, line in enumerate(lines, start=1):
parts = [p.strip() for p in re.split(r"[,\t]", line)]
while len(parts) < 6:
parts.append("")
part_no = parts[0]
name = parts[1]
quantity_raw = parts[2]
specification = parts[3]
location = parts[4]
note = parts[5]
if not part_no or not name:
invalid_lines.append(f"{line_no}")
continue
try:
quantity = _parse_non_negative_int(quantity_raw, 0)
except ValueError:
invalid_lines.append(f"{line_no}")
continue
_add_bag_item(box, part_no, name, quantity, specification, location, note)
added_count += 1
if added_count:
db.session.commit()
if invalid_lines and added_count == 0:
return render_box_page(
box,
error="批量新增失败: 数据格式不正确(请检查: " + ", ".join(invalid_lines) + "",
)
if invalid_lines:
return render_box_page(
box,
notice=f"已新增 {added_count} 条,以下行被跳过: " + ", ".join(invalid_lines),
)
return render_box_page(box, notice=f"批量新增成功,共 {added_count}")
@app.route("/edit/<int:box_id>/<int:slot>", methods=["GET", "POST"])
def edit_component(box_id: int, slot: int):
if slot < 1 or slot > 28:
box = Box.query.get_or_404(box_id)
if slot < 1 or slot > box.slot_capacity:
return "无效的格子编号", 400
box = Box.query.get_or_404(box_id)
component = Component.query.filter_by(box_id=box.id, slot_index=slot).first()
if request.method == "POST":
@@ -186,9 +414,21 @@ def scan_api():
def bootstrap() -> None:
with app.app_context():
db.create_all()
ensure_box_schema()
db.session.execute(
db.text(
"UPDATE boxes SET box_type = 'small_28' WHERE box_type IS NULL OR box_type = ''"
)
)
db.session.execute(
db.text("UPDATE boxes SET slot_capacity = 28 WHERE slot_capacity IS NULL")
)
db.session.commit()
ensure_default_box()
bootstrap()
if __name__ == "__main__":
bootstrap()
app.run(host="0.0.0.0", port=5000, debug=True)