feat: 添加盒子起始号建议功能,优化盒子创建和更新界面

This commit is contained in:
2026-03-08 03:28:33 +08:00
parent 59bcd69bb1
commit 99d002e188
4 changed files with 238 additions and 8 deletions

150
app.py
View File

@@ -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 = ""): def render_box_page(box: Box, error: str = "", notice: str = ""):
slots = slot_data_for_box(box) slots = slot_data_for_box(box)
bag_rows = bag_rows_for_box(box) if box.box_type == "bag" else [] 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() slot_prefix = request.form.get("slot_prefix", "").strip().upper()
if box_type not in BOX_TYPES: if box_type not in BOX_TYPES:
return "无效盒子类型", 400 return bad_request("无效盒子类型", box_type)
if not base_name: if not base_name:
return "盒子名称不能为空", 400 return bad_request("盒子名称不能为空", box_type)
try: try:
start_number = _parse_non_negative_int(request.form.get("start_number", "1"), 1) start_number = _parse_non_negative_int(request.form.get("start_number", "1"), 1)
except ValueError: except ValueError:
return "起始序号必须是大于等于 0 的整数", 400 return bad_request("起始序号必须是大于等于 0 的整数", box_type)
meta = BOX_TYPES[box_type] meta = BOX_TYPES[box_type]
effective_prefix = slot_prefix or meta["default_prefix"] 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( generated_name = compose_box_name(
base_name=base_name, base_name=base_name,
prefix=effective_prefix, prefix=effective_prefix,
@@ -355,14 +437,29 @@ def update_box(box_id: int):
slot_prefix = request.form.get("slot_prefix", "").strip().upper() slot_prefix = request.form.get("slot_prefix", "").strip().upper()
if not base_name: if not base_name:
return "盒子名称不能为空", 400 return bad_request("盒子名称不能为空", box.box_type)
try: try:
start_number = _parse_non_negative_int(request.form.get("start_number", "1"), 1) start_number = _parse_non_negative_int(request.form.get("start_number", "1"), 1)
except ValueError: except ValueError:
return "起始序号必须是大于等于 0 的整数", 400 return bad_request("起始序号必须是大于等于 0 的整数", box.box_type)
effective_prefix = slot_prefix or BOX_TYPES[box.box_type]["default_prefix"] 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( generated_name = compose_box_name(
base_name=base_name, base_name=base_name,
prefix=effective_prefix, prefix=effective_prefix,
@@ -387,6 +484,49 @@ def delete_box(box_id: int):
return redirect(url_for("index")) 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/<int:box_id>") @app.route("/box/<int:box_id>")
def view_box(box_id: int): def view_box(box_id: int):
box = Box.query.get_or_404(box_id) box = Box.query.get_or_404(box_id)

View File

@@ -101,13 +101,18 @@ body {
.new-box-form { .new-box-form {
display: grid; 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; gap: 8px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.new-box-form.compact { .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, .box-card,

25
templates/error.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ status_code }} - {{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="hero slim">
<h1>{{ status_code }} - {{ title }}</h1>
<a class="btn btn-light" href="{{ url_for('index') }}">回到首页</a>
</header>
<main class="container">
<section class="panel">
<p class="alert">{{ message }}</p>
<div class="actions" style="margin-top: 10px;">
<a class="btn" href="{{ back_url }}">返回上一页</a>
<button class="btn btn-light" type="button" onclick="history.back()">浏览器返回</button>
</div>
</section>
</main>
</body>
</html>

View File

@@ -16,7 +16,7 @@
<h2>容器列表</h2> <h2>容器列表</h2>
{% for key, meta in box_types.items() %} {% for key, meta in box_types.items() %}
<section class="group-panel panel"> <section class="group-panel panel" id="group-{{ key }}">
<div class="group-title-row"> <div class="group-title-row">
<h3>{{ meta.label }}</h3> <h3>{{ meta.label }}</h3>
<span class="group-desc">{{ meta.default_desc }}</span> <span class="group-desc">{{ meta.default_desc }}</span>
@@ -28,7 +28,9 @@
<input type="text" name="slot_prefix" placeholder="前缀(如A/B/C)"> <input type="text" name="slot_prefix" placeholder="前缀(如A/B/C)">
<input type="number" name="start_number" min="0" value="1" placeholder="起始序号"> <input type="number" name="start_number" min="0" value="1" placeholder="起始序号">
<input type="text" name="description" placeholder="备注(可选)"> <input type="text" name="description" placeholder="备注(可选)">
<button class="btn btn-light suggest-start-btn" type="button" data-box-type="{{ key }}">建议起始号</button>
<button class="btn" type="submit">新增盒子</button> <button class="btn" type="submit">新增盒子</button>
<span class="hint suggest-preview"></span>
</form> </form>
<section class="box-list"> <section class="box-list">
@@ -67,7 +69,9 @@
<input type="text" name="slot_prefix" value="{{ item.box.slot_prefix }}" 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> <input type="number" name="start_number" min="0" value="{{ item.box.start_number }}" required>
<input type="text" name="description" value="{{ item.box.description or '' }}"> <input type="text" name="description" value="{{ item.box.description or '' }}">
<button class="btn btn-light suggest-start-btn" type="button" data-box-id="{{ item.box.id }}" data-box-type="{{ item.box.box_type }}">建议起始号</button>
<button class="btn" type="submit">保存设置</button> <button class="btn" type="submit">保存设置</button>
<span class="hint suggest-preview"></span>
</form> </form>
</details> </details>
</article> </article>
@@ -78,5 +82,61 @@
</section> </section>
{% endfor %} {% endfor %}
</main> </main>
<script>
(function () {
function updateSuggest(form, payload) {
var startInput = form.querySelector('input[name="start_number"]');
var prefixInput = form.querySelector('input[name="slot_prefix"]');
var preview = form.querySelector('.suggest-preview');
if (startInput) {
startInput.value = payload.start_number;
}
if (prefixInput && !prefixInput.value.trim()) {
prefixInput.value = payload.slot_prefix;
}
if (preview) {
preview.textContent = '建议范围: ' + payload.preview_range;
}
}
document.querySelectorAll('.suggest-start-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var form = btn.closest('form');
if (!form) {
return;
}
var prefixInput = form.querySelector('input[name="slot_prefix"]');
var boxType = btn.dataset.boxType || 'small_28';
var boxId = btn.dataset.boxId || '';
var prefix = prefixInput ? prefixInput.value.trim() : '';
var params = new URLSearchParams();
params.set('box_type', boxType);
if (prefix) {
params.set('slot_prefix', prefix);
}
if (boxId) {
params.set('box_id', boxId);
}
fetch('{{ url_for('suggest_start') }}?' + params.toString())
.then(function (resp) { return resp.json(); })
.then(function (data) {
if (!data.ok) {
alert(data.message || '建议起始号失败');
return;
}
updateSuggest(form, data);
})
.catch(function () {
alert('建议起始号失败,请稍后重试');
});
});
});
})();
</script>
</body> </body>
</html> </html>