feat: 添加盒子起始号建议功能,优化盒子创建和更新界面
This commit is contained in:
150
app.py
150
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/<int:box_id>")
|
||||
def view_box(box_id: int):
|
||||
box = Box.query.get_or_404(box_id)
|
||||
|
||||
@@ -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,
|
||||
|
||||
25
templates/error.html
Normal file
25
templates/error.html
Normal 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>
|
||||
@@ -16,7 +16,7 @@
|
||||
<h2>容器列表</h2>
|
||||
|
||||
{% 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">
|
||||
<h3>{{ meta.label }}</h3>
|
||||
<span class="group-desc">{{ meta.default_desc }}</span>
|
||||
@@ -28,7 +28,9 @@
|
||||
<input type="text" name="slot_prefix" placeholder="前缀(如A/B/C)">
|
||||
<input type="number" name="start_number" min="0" value="1" 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>
|
||||
<span class="hint suggest-preview"></span>
|
||||
</form>
|
||||
|
||||
<section class="box-list">
|
||||
@@ -67,7 +69,9 @@
|
||||
<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="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>
|
||||
<span class="hint suggest-preview"></span>
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
@@ -78,5 +82,61 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
</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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user