diff --git a/app.py b/app.py index 8b8a5f7..12a0172 100644 --- a/app.py +++ b/app.py @@ -269,6 +269,74 @@ def box_sort_key(box: Box): ) +def has_range_conflict( + *, + box_type: str, + prefix: str, + start_number: int, + slot_capacity: int, + exclude_box_id: int = None, +): + end_number = start_number + slot_capacity - 1 + query = Box.query.filter_by(box_type=box_type, slot_prefix=prefix) + if exclude_box_id is not None: + query = query.filter(Box.id != exclude_box_id) + + for other in query.all(): + other_start = other.start_number + other_end = other.start_number + other.slot_capacity - 1 + # Two ranges overlap unless one is strictly before the other. + if not (end_number < other_start or start_number > other_end): + return True, other + + return False, None + + +def suggest_next_start_number( + *, + box_type: str, + prefix: str, + slot_capacity: int, + exclude_box_id: int = None, +) -> int: + max_end = 0 + query = Box.query.filter_by(box_type=box_type, slot_prefix=prefix) + if exclude_box_id is not None: + query = query.filter(Box.id != exclude_box_id) + + for other in query.all(): + other_end = other.start_number + other.slot_capacity - 1 + if other_end > max_end: + max_end = other_end + + return max_end + 1 if max_end > 0 else 1 + + +def build_index_anchor(box_type: str = "") -> str: + if box_type in BOX_TYPES: + return f"group-{box_type}" + return "" + + +def bad_request(message: str, box_type: str = ""): + anchor = build_index_anchor(box_type) + if anchor: + back_url = f"{url_for('index')}#{anchor}" + else: + back_url = request.referrer or url_for("index") + + return ( + render_template( + "error.html", + status_code=400, + title="请求参数有误", + message=message, + back_url=back_url, + ), + 400, + ) + + def render_box_page(box: Box, error: str = "", notice: str = ""): slots = slot_data_for_box(box) bag_rows = bag_rows_for_box(box) if box.box_type == "bag" else [] @@ -314,17 +382,31 @@ def create_box(): slot_prefix = request.form.get("slot_prefix", "").strip().upper() if box_type not in BOX_TYPES: - return "无效盒子类型", 400 + return bad_request("无效盒子类型", box_type) if not base_name: - return "盒子名称不能为空", 400 + return bad_request("盒子名称不能为空", box_type) try: start_number = _parse_non_negative_int(request.form.get("start_number", "1"), 1) except ValueError: - return "起始序号必须是大于等于 0 的整数", 400 + return bad_request("起始序号必须是大于等于 0 的整数", box_type) meta = BOX_TYPES[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"], + ) + if conflict: + return bad_request( + "编号范围冲突: 与现有盒子 " + f"[{other_box.name}]" + " 重叠,请更换前缀或起始序号", + box_type, + ) + generated_name = compose_box_name( base_name=base_name, prefix=effective_prefix, @@ -355,14 +437,29 @@ def update_box(box_id: int): slot_prefix = request.form.get("slot_prefix", "").strip().upper() if not base_name: - return "盒子名称不能为空", 400 + return bad_request("盒子名称不能为空", box.box_type) try: start_number = _parse_non_negative_int(request.form.get("start_number", "1"), 1) except ValueError: - return "起始序号必须是大于等于 0 的整数", 400 + return bad_request("起始序号必须是大于等于 0 的整数", 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, + exclude_box_id=box.id, + ) + if conflict: + return bad_request( + "编号范围冲突: 与现有盒子 " + f"[{other_box.name}]" + " 重叠,请更换前缀或起始序号", + box.box_type, + ) + generated_name = compose_box_name( base_name=base_name, prefix=effective_prefix, @@ -387,6 +484,49 @@ def delete_box(box_id: int): return redirect(url_for("index")) +@app.route("/boxes/suggest-start") +def suggest_start(): + box_type = request.args.get("box_type", "small_28").strip() + if box_type not in BOX_TYPES: + return {"ok": False, "message": "无效盒子类型"}, 400 + + slot_prefix = request.args.get("slot_prefix", "").strip().upper() + effective_prefix = slot_prefix or BOX_TYPES[box_type]["default_prefix"] + + box_id_raw = request.args.get("box_id", "").strip() + exclude_box_id = None + slot_capacity = BOX_TYPES[box_type]["default_capacity"] + + if box_id_raw: + try: + box_id = int(box_id_raw) + except ValueError: + return {"ok": False, "message": "box_id 非法"}, 400 + + box = Box.query.get(box_id) + if not box: + return {"ok": False, "message": "盒子不存在"}, 404 + + box_type = box.box_type + slot_capacity = box.slot_capacity + exclude_box_id = box.id + + suggested = suggest_next_start_number( + box_type=box_type, + prefix=effective_prefix, + slot_capacity=slot_capacity, + exclude_box_id=exclude_box_id, + ) + end_number = suggested + slot_capacity - 1 + + return { + "ok": True, + "start_number": suggested, + "slot_prefix": effective_prefix, + "preview_range": f"{effective_prefix}{suggested}-{effective_prefix}{end_number}", + } + + @app.route("/box/") def view_box(box_id: int): box = Box.query.get_or_404(box_id) diff --git a/static/css/style.css b/static/css/style.css index 161d067..9bd950f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -101,13 +101,18 @@ body { .new-box-form { display: grid; - grid-template-columns: 1.2fr 0.8fr 0.7fr 1fr auto; + grid-template-columns: 1.2fr 0.8fr 0.7fr 1fr auto auto; gap: 8px; margin-bottom: 12px; } .new-box-form.compact { - grid-template-columns: 1.1fr 0.8fr 0.7fr 1fr auto; + grid-template-columns: 1.1fr 0.8fr 0.7fr 1fr auto auto; +} + +.new-box-form .suggest-preview { + grid-column: 1 / -1; + margin: 0; } .box-card, diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..ed37c4e --- /dev/null +++ b/templates/error.html @@ -0,0 +1,25 @@ + + + + + + {{ status_code }} - {{ title }} + + + +
+

{{ status_code }} - {{ title }}

+ 回到首页 +
+ +
+
+

{{ message }}

+
+ 返回上一页 + +
+
+
+ + diff --git a/templates/index.html b/templates/index.html index abfa45a..b17e683 100644 --- a/templates/index.html +++ b/templates/index.html @@ -16,7 +16,7 @@

容器列表

{% for key, meta in box_types.items() %} -
+

{{ meta.label }}

{{ meta.default_desc }} @@ -28,7 +28,9 @@ + +
@@ -67,7 +69,9 @@ + + @@ -78,5 +82,61 @@
{% endfor %} + +