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 = ""):
|
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)
|
||||||
|
|||||||
@@ -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
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>
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user