import os from flask import Flask, redirect, render_template, request, url_for from flask_sqlalchemy import SQLAlchemy BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_DIR = os.path.join(BASE_DIR, "data") os.makedirs(DB_DIR, exist_ok=True) DB_PATH = os.path.join(DB_DIR, "inventory.db") app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db = SQLAlchemy(app) 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) class Component(db.Model): __tablename__ = "components" id = db.Column(db.Integer, primary_key=True) box_id = db.Column(db.Integer, db.ForeignKey("boxes.id"), nullable=False) slot_index = db.Column(db.Integer, nullable=False) part_no = db.Column(db.String(100), nullable=False) name = db.Column(db.String(120), nullable=False) specification = db.Column(db.String(120), nullable=True) quantity = db.Column(db.Integer, nullable=False, default=0) location = db.Column(db.String(120), nullable=True) note = db.Column(db.Text, nullable=True) box = db.relationship("Box", backref=db.backref("components", lazy=True)) __table_args__ = ( db.UniqueConstraint("box_id", "slot_index", name="uq_box_slot"), ) def ensure_default_box() -> None: if not Box.query.first(): default_box = Box(name="默认大盒", description="每盒 28 个格子") db.session.add(default_box) db.session.commit() def slot_data_for_box(box_id: int): 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): slots.append({"slot": slot, "component": slot_map.get(slot)}) return slots @app.route("/") def index(): boxes = Box.query.order_by(Box.id.asc()).all() box_cards = [] 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) @app.route("/box/") 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) @app.route("/edit//", methods=["GET", "POST"]) def edit_component(box_id: int, slot: int): if slot < 1 or slot > 28: 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": action = request.form.get("action", "save") if action == "delete": if component: db.session.delete(component) db.session.commit() return redirect(url_for("view_box", box_id=box.id)) part_no = request.form.get("part_no", "").strip() name = request.form.get("name", "").strip() specification = request.form.get("specification", "").strip() quantity_raw = request.form.get("quantity", "0").strip() location = request.form.get("location", "").strip() note = request.form.get("note", "").strip() if not part_no or not name: error = "料号和名称不能为空" return render_template( "edit.html", box=box, slot=slot, component=component, error=error, ) try: quantity = int(quantity_raw) if quantity < 0: raise ValueError except ValueError: error = "数量必须是大于等于 0 的整数" return render_template( "edit.html", box=box, slot=slot, component=component, error=error, ) if component is None: component = Component(box_id=box.id, slot_index=slot) db.session.add(component) component.part_no = part_no component.name = name component.specification = specification or None component.quantity = quantity component.location = location or None component.note = note or None db.session.commit() return redirect(url_for("view_box", box_id=box.id)) return render_template("edit.html", box=box, slot=slot, component=component) @app.route("/scan") def scan_page(): keyword = request.args.get("q", "").strip() results = [] if keyword: results = ( Component.query.filter( db.or_( Component.part_no.ilike(f"%{keyword}%"), Component.name.ilike(f"%{keyword}%"), ) ) .order_by(Component.part_no.asc()) .all() ) return render_template("scan.html", keyword=keyword, results=results) @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 return { "ok": True, "data": { "id": item.id, "part_no": item.part_no, "name": item.name, "quantity": item.quantity, "box_id": item.box_id, "slot_index": item.slot_index, }, } def bootstrap() -> None: with app.app_context(): db.create_all() ensure_default_box() if __name__ == "__main__": bootstrap() app.run(host="0.0.0.0", port=5000, debug=True)