feat: 添加 AI 重复物料巡检功能,支持生成人工复核清单和导出 CSV
This commit is contained in:
10
README.md
10
README.md
@@ -359,11 +359,11 @@ cp /www/wwwroot/inventory/data/inventory.db /www/backup/inventory_$(date +%F).db
|
|||||||
|
|
||||||
### 第二阶段:提升库存数据质量
|
### 第二阶段:提升库存数据质量
|
||||||
|
|
||||||
- [ ] AI 重复物料巡检
|
- [x] AI 重复物料巡检
|
||||||
- [ ] 定期扫描库存,找出“疑似同料号”“疑似同参数”“疑似同立创编号”的记录
|
- [x] 定期扫描库存,找出“疑似同料号”“疑似同参数”“疑似同立创编号”的记录
|
||||||
- [ ] 输出“疑似重复物料清单”,由人工决定是否合并
|
- [x] 输出“疑似重复物料清单”,由人工决定是否合并
|
||||||
- [ ] 给出重复原因说明,例如名称近似、规格一致、备注含相同 LCSC 编号
|
- [x] 给出重复原因说明,例如名称近似、规格一致、备注含相同 LCSC 编号
|
||||||
- [ ] 增加“建议统一名称/规格写法”的辅助提示
|
- [x] 增加“建议统一名称/规格写法”的辅助提示
|
||||||
|
|
||||||
### 第三阶段:升级补货能力
|
### 第三阶段:升级补货能力
|
||||||
|
|
||||||
|
|||||||
326
app.py
326
app.py
@@ -1491,6 +1491,184 @@ def _build_restock_payload(*, limit: int = 20, threshold: int = LOW_STOCK_THRESH
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_part_no_key(part_no: str) -> str:
|
||||||
|
return (part_no or "").strip().upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_standard_text(values: list[str]) -> str:
|
||||||
|
"""从多个文本候选中挑选建议标准写法。
|
||||||
|
|
||||||
|
中文说明:优先选择出现次数最多的写法;若次数相同,选择长度更长的写法,
|
||||||
|
这样一般能保留更多有效信息。
|
||||||
|
"""
|
||||||
|
bucket = {}
|
||||||
|
for value in values:
|
||||||
|
text = (value or "").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
bucket[text] = bucket.get(text, 0) + 1
|
||||||
|
|
||||||
|
if not bucket:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
ordered = sorted(bucket.items(), key=lambda item: (-item[1], -len(item[0]), item[0]))
|
||||||
|
return ordered[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_duplicate_member(component: Component, box_by_id: dict[int, Box]) -> dict:
|
||||||
|
box = box_by_id.get(component.box_id)
|
||||||
|
lcsc_code = _extract_lcsc_code_from_text(component.note or "")
|
||||||
|
if not lcsc_code:
|
||||||
|
lcsc_code = _extract_lcsc_code_from_text(component.part_no or "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"component_id": component.id,
|
||||||
|
"part_no": component.part_no or "",
|
||||||
|
"name": component.name or "",
|
||||||
|
"specification": component.specification or "",
|
||||||
|
"quantity": int(component.quantity or 0),
|
||||||
|
"lcsc_code": lcsc_code,
|
||||||
|
"box_id": component.box_id,
|
||||||
|
"box_name": box.name if box else f"盒 {component.box_id}",
|
||||||
|
"slot_code": slot_code_for_box(box, component.slot_index) if box else str(component.slot_index),
|
||||||
|
"edit_url": url_for("edit_component", box_id=component.box_id, slot=component.slot_index),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_duplicate_audit_payload(limit_groups: int = 60) -> dict:
|
||||||
|
"""构建重复物料巡检结果。
|
||||||
|
|
||||||
|
中文说明:本巡检不自动合并库存,只输出“疑似重复”清单和原因,
|
||||||
|
由人工在编辑页确认后执行合并,保持现有安全策略。
|
||||||
|
"""
|
||||||
|
boxes = Box.query.all()
|
||||||
|
box_by_id = {box.id: box for box in boxes}
|
||||||
|
enabled_components = Component.query.filter_by(is_enabled=True).all()
|
||||||
|
|
||||||
|
by_part_no = {}
|
||||||
|
by_material = {}
|
||||||
|
by_lcsc = {}
|
||||||
|
|
||||||
|
for component in enabled_components:
|
||||||
|
part_no_key = _normalize_part_no_key(component.part_no)
|
||||||
|
if part_no_key:
|
||||||
|
by_part_no.setdefault(part_no_key, []).append(component)
|
||||||
|
|
||||||
|
material_key = _material_identity_key(component.name, component.specification)
|
||||||
|
if material_key:
|
||||||
|
by_material.setdefault(material_key, []).append(component)
|
||||||
|
|
||||||
|
lcsc_code = _extract_lcsc_code_from_text(component.note or "")
|
||||||
|
if not lcsc_code:
|
||||||
|
lcsc_code = _extract_lcsc_code_from_text(component.part_no or "")
|
||||||
|
if lcsc_code:
|
||||||
|
by_lcsc.setdefault(lcsc_code, []).append(component)
|
||||||
|
|
||||||
|
def make_groups(source: dict, group_type: str, reason_text: str) -> list[dict]:
|
||||||
|
groups = []
|
||||||
|
for key, members in source.items():
|
||||||
|
if len(members) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sorted_members = sorted(members, key=lambda c: (c.box_id, c.slot_index, c.id))
|
||||||
|
names = [(c.name or "").strip() for c in sorted_members]
|
||||||
|
specifications = [(c.specification or "").strip() for c in sorted_members]
|
||||||
|
|
||||||
|
unique_names = sorted({text for text in names if text})
|
||||||
|
unique_specifications = sorted({text for text in specifications if text})
|
||||||
|
standard_name = _pick_standard_text(names)
|
||||||
|
standard_specification = _pick_standard_text(specifications)
|
||||||
|
|
||||||
|
suggestion_lines = []
|
||||||
|
if len(unique_names) > 1 and standard_name:
|
||||||
|
suggestion_lines.append(f"建议统一名称为: {standard_name}")
|
||||||
|
if len(unique_specifications) > 1 and standard_specification:
|
||||||
|
suggestion_lines.append(f"建议统一规格为: {standard_specification}")
|
||||||
|
if not suggestion_lines:
|
||||||
|
suggestion_lines.append("命名和规格写法基本一致,可仅核对是否需要合并库存位置")
|
||||||
|
|
||||||
|
groups.append(
|
||||||
|
{
|
||||||
|
"type": group_type,
|
||||||
|
"key": key,
|
||||||
|
"reason": reason_text,
|
||||||
|
"member_count": len(sorted_members),
|
||||||
|
"unique_name_count": len(unique_names),
|
||||||
|
"unique_specification_count": len(unique_specifications),
|
||||||
|
"standard_name": standard_name,
|
||||||
|
"standard_specification": standard_specification,
|
||||||
|
"suggestion": ";".join(suggestion_lines),
|
||||||
|
"members": [_build_duplicate_member(component, box_by_id) for component in sorted_members],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
groups.sort(key=lambda row: (-int(row["member_count"]), row["key"]))
|
||||||
|
return groups[:limit_groups]
|
||||||
|
|
||||||
|
part_no_groups = make_groups(by_part_no, "part_no", "疑似同料号")
|
||||||
|
material_groups = make_groups(by_material, "material", "疑似同参数")
|
||||||
|
lcsc_groups = make_groups(by_lcsc, "lcsc", "疑似同立创编号")
|
||||||
|
|
||||||
|
all_groups = part_no_groups + material_groups + lcsc_groups
|
||||||
|
all_groups.sort(key=lambda row: (-int(row["member_count"]), row["type"], row["key"]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"total_enabled": len(enabled_components),
|
||||||
|
"total_groups": len(all_groups),
|
||||||
|
"part_no_groups": part_no_groups,
|
||||||
|
"material_groups": material_groups,
|
||||||
|
"lcsc_groups": lcsc_groups,
|
||||||
|
"groups": all_groups[:limit_groups],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_duplicate_audit_summary_with_ai(payload: dict, settings: dict) -> tuple[str, str]:
|
||||||
|
api_key = (settings.get("api_key") or "").strip()
|
||||||
|
api_url = (settings.get("api_url") or "").strip()
|
||||||
|
model = (settings.get("model") or "").strip()
|
||||||
|
if not api_key or not api_url or not model:
|
||||||
|
return "", "AI 参数未完整配置,摘要使用规则生成"
|
||||||
|
|
||||||
|
brief_payload = {
|
||||||
|
"generated_at": payload.get("generated_at"),
|
||||||
|
"total_enabled": payload.get("total_enabled", 0),
|
||||||
|
"total_groups": payload.get("total_groups", 0),
|
||||||
|
"part_no_groups": len(payload.get("part_no_groups", [])),
|
||||||
|
"material_groups": len(payload.get("material_groups", [])),
|
||||||
|
"lcsc_groups": len(payload.get("lcsc_groups", [])),
|
||||||
|
"top_groups": [
|
||||||
|
{
|
||||||
|
"reason": g.get("reason", ""),
|
||||||
|
"key": g.get("key", ""),
|
||||||
|
"member_count": g.get("member_count", 0),
|
||||||
|
"suggestion": g.get("suggestion", ""),
|
||||||
|
}
|
||||||
|
for g in payload.get("groups", [])[:8]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
system_prompt = (
|
||||||
|
"你是库存数据治理助手。"
|
||||||
|
"请输出简短中文总结(不超过120字),包含风险级别和处理优先顺序。"
|
||||||
|
"不要使用Markdown。"
|
||||||
|
)
|
||||||
|
user_prompt = "重复物料巡检结果(JSON):\n" + json.dumps(brief_payload, ensure_ascii=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
summary = _call_siliconflow_chat(
|
||||||
|
system_prompt,
|
||||||
|
user_prompt,
|
||||||
|
api_url=api_url,
|
||||||
|
model=model,
|
||||||
|
api_key=api_key,
|
||||||
|
timeout=int(settings.get("timeout", 30)),
|
||||||
|
)
|
||||||
|
return summary, ""
|
||||||
|
except Exception:
|
||||||
|
return "", "AI 摘要生成失败,已使用规则结果"
|
||||||
|
|
||||||
|
|
||||||
def _call_siliconflow_chat(
|
def _call_siliconflow_chat(
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
user_prompt: str,
|
user_prompt: str,
|
||||||
@@ -2849,6 +3027,154 @@ def ai_restock_plan():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/ai/duplicate-audit", methods=["POST"])
|
||||||
|
def ai_duplicate_audit():
|
||||||
|
"""AI 重复物料巡检接口。
|
||||||
|
|
||||||
|
中文说明:
|
||||||
|
1. 巡检同料号、同参数、同立创编号三类疑似重复;
|
||||||
|
2. 输出重复原因与标准化建议;
|
||||||
|
3. 不自动合并,仅提供人工核对清单。
|
||||||
|
"""
|
||||||
|
payload = _build_duplicate_audit_payload(limit_groups=60)
|
||||||
|
settings = _get_ai_settings()
|
||||||
|
|
||||||
|
summary = ""
|
||||||
|
parse_warning = ""
|
||||||
|
if payload.get("total_groups", 0) > 0:
|
||||||
|
summary, parse_warning = _build_duplicate_audit_summary_with_ai(payload, settings)
|
||||||
|
|
||||||
|
if not summary:
|
||||||
|
part_no_count = len(payload.get("part_no_groups", []))
|
||||||
|
material_count = len(payload.get("material_groups", []))
|
||||||
|
lcsc_count = len(payload.get("lcsc_groups", []))
|
||||||
|
summary = (
|
||||||
|
"巡检完成: "
|
||||||
|
f"同料号 {part_no_count} 组,"
|
||||||
|
f"同参数 {material_count} 组,"
|
||||||
|
f"同立创编号 {lcsc_count} 组。"
|
||||||
|
"请优先处理成员数较多的分组。"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"summary": summary,
|
||||||
|
"parse_warning": parse_warning,
|
||||||
|
"data": payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/ai/duplicate-audit/export")
|
||||||
|
def export_duplicate_audit_csv():
|
||||||
|
"""导出重复物料巡检结果 CSV。"""
|
||||||
|
limit_raw = (request.args.get("limit", "200") or "200").strip()
|
||||||
|
try:
|
||||||
|
limit_groups = int(limit_raw)
|
||||||
|
except ValueError:
|
||||||
|
limit_groups = 200
|
||||||
|
limit_groups = min(max(limit_groups, 1), 1000)
|
||||||
|
|
||||||
|
payload = _build_duplicate_audit_payload(limit_groups=limit_groups)
|
||||||
|
groups = payload.get("groups", [])
|
||||||
|
|
||||||
|
selected_group_ids = {
|
||||||
|
(group_id or "").strip()
|
||||||
|
for group_id in request.args.getlist("group_id")
|
||||||
|
if (group_id or "").strip()
|
||||||
|
}
|
||||||
|
if selected_group_ids:
|
||||||
|
groups = [
|
||||||
|
group
|
||||||
|
for group in groups
|
||||||
|
if f"{group.get('type', '')}::{group.get('key', '')}" in selected_group_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
output = StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"巡检时间(generated_at)",
|
||||||
|
"重复类型(type)",
|
||||||
|
"重复原因(reason)",
|
||||||
|
"分组标识(key)",
|
||||||
|
"分组成员数(member_count)",
|
||||||
|
"建议(suggestion)",
|
||||||
|
"标准名称建议(standard_name)",
|
||||||
|
"标准规格建议(standard_specification)",
|
||||||
|
"元件ID(component_id)",
|
||||||
|
"料号(part_no)",
|
||||||
|
"名称(name)",
|
||||||
|
"规格(specification)",
|
||||||
|
"数量(quantity)",
|
||||||
|
"立创编号(lcsc_code)",
|
||||||
|
"盒子名称(box_name)",
|
||||||
|
"位置编号(slot_code)",
|
||||||
|
"编辑链接(edit_url)",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_at = payload.get("generated_at", "")
|
||||||
|
for group in groups:
|
||||||
|
members = group.get("members", []) or [None]
|
||||||
|
for member in members:
|
||||||
|
if member is None:
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
generated_at,
|
||||||
|
group.get("type", ""),
|
||||||
|
group.get("reason", ""),
|
||||||
|
group.get("key", ""),
|
||||||
|
group.get("member_count", 0),
|
||||||
|
group.get("suggestion", ""),
|
||||||
|
group.get("standard_name", ""),
|
||||||
|
group.get("standard_specification", ""),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
generated_at,
|
||||||
|
group.get("type", ""),
|
||||||
|
group.get("reason", ""),
|
||||||
|
group.get("key", ""),
|
||||||
|
group.get("member_count", 0),
|
||||||
|
group.get("suggestion", ""),
|
||||||
|
group.get("standard_name", ""),
|
||||||
|
group.get("standard_specification", ""),
|
||||||
|
member.get("component_id", ""),
|
||||||
|
member.get("part_no", ""),
|
||||||
|
member.get("name", ""),
|
||||||
|
member.get("specification", ""),
|
||||||
|
member.get("quantity", 0),
|
||||||
|
member.get("lcsc_code", ""),
|
||||||
|
member.get("box_name", ""),
|
||||||
|
member.get("slot_code", ""),
|
||||||
|
member.get("edit_url", ""),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_content = "\ufeff" + output.getvalue()
|
||||||
|
output.close()
|
||||||
|
scope = "selected" if selected_group_ids else "all"
|
||||||
|
filename = f"duplicate_audit_{scope}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
csv_content,
|
||||||
|
mimetype="text/csv; charset=utf-8",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ai/settings", methods=["GET", "POST"])
|
@app.route("/ai/settings", methods=["GET", "POST"])
|
||||||
def ai_settings_page():
|
def ai_settings_page():
|
||||||
settings = _get_ai_settings()
|
settings = _get_ai_settings()
|
||||||
|
|||||||
@@ -370,6 +370,11 @@ body {
|
|||||||
.side-low-stock-list li {
|
.side-low-stock-list li {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
.ai-divider {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
}
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -488,6 +493,12 @@ body {
|
|||||||
font: 13px/1.55 Consolas, "Cascadia Mono", monospace;
|
font: 13px/1.55 Consolas, "Cascadia Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-divider {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ai-plan-groups {
|
.ai-plan-groups {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
|
|||||||
@@ -101,6 +101,21 @@
|
|||||||
<summary>查看原始 AI 文本</summary>
|
<summary>查看原始 AI 文本</summary>
|
||||||
<pre id="ai-panel-content" class="ai-panel-content"></pre>
|
<pre id="ai-panel-content" class="ai-panel-content"></pre>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<hr class="ai-divider">
|
||||||
|
|
||||||
|
<div class="ai-panel-head">
|
||||||
|
<h2>AI重复物料巡检</h2>
|
||||||
|
</div>
|
||||||
|
<p class="hint">扫描疑似同料号、同参数、同立创编号记录,生成人工复核清单。</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" id="ai-duplicate-btn" type="button">开始巡检</button>
|
||||||
|
<button class="btn btn-light" id="ai-duplicate-export-current-btn" type="button">导出当前显示</button>
|
||||||
|
<button class="btn btn-light" id="ai-duplicate-export-all-btn" type="button">导出全量</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint" id="ai-duplicate-status"></p>
|
||||||
|
<p class="hint" id="ai-duplicate-warning"></p>
|
||||||
|
<div id="ai-duplicate-groups" class="ai-plan-groups"></div>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,6 +259,167 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var auditBtn = document.getElementById('ai-duplicate-btn');
|
||||||
|
var exportCurrentBtn = document.getElementById('ai-duplicate-export-current-btn');
|
||||||
|
var exportAllBtn = document.getElementById('ai-duplicate-export-all-btn');
|
||||||
|
var statusNode = document.getElementById('ai-duplicate-status');
|
||||||
|
var warningNode = document.getElementById('ai-duplicate-warning');
|
||||||
|
var groupsWrap = document.getElementById('ai-duplicate-groups');
|
||||||
|
var latestAuditGroups = [];
|
||||||
|
|
||||||
|
if (!auditBtn || !exportCurrentBtn || !exportAllBtn || !statusNode || !groupsWrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGroups() {
|
||||||
|
latestAuditGroups = [];
|
||||||
|
groupsWrap.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAuditGroups(groups) {
|
||||||
|
clearGroups();
|
||||||
|
if (!Array.isArray(groups) || !groups.length) {
|
||||||
|
var empty = document.createElement('p');
|
||||||
|
empty.className = 'muted';
|
||||||
|
empty.textContent = '未发现疑似重复物料';
|
||||||
|
groupsWrap.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
latestAuditGroups = groups.slice();
|
||||||
|
|
||||||
|
groups.forEach(function (group) {
|
||||||
|
var section = document.createElement('section');
|
||||||
|
section.className = 'ai-plan-group ai-audit-group';
|
||||||
|
|
||||||
|
var title = document.createElement('h3');
|
||||||
|
title.textContent = (group.reason || '疑似重复') + ' | ' + (group.key || '-') + '(' + (group.member_count || 0) + ')';
|
||||||
|
section.appendChild(title);
|
||||||
|
|
||||||
|
var suggestion = document.createElement('p');
|
||||||
|
suggestion.className = 'hint';
|
||||||
|
suggestion.textContent = '建议: ' + (group.suggestion || '请人工复核');
|
||||||
|
section.appendChild(suggestion);
|
||||||
|
|
||||||
|
var list = document.createElement('ul');
|
||||||
|
list.className = 'side-low-stock-list';
|
||||||
|
|
||||||
|
(group.members || []).forEach(function (member) {
|
||||||
|
var li = document.createElement('li');
|
||||||
|
|
||||||
|
var content = document.createElement('div');
|
||||||
|
var strong = document.createElement('strong');
|
||||||
|
strong.textContent = (member.name || '未命名元件') + ' (' + (member.part_no || '-') + ')';
|
||||||
|
content.appendChild(strong);
|
||||||
|
|
||||||
|
var spec = document.createElement('p');
|
||||||
|
spec.className = 'hint';
|
||||||
|
spec.textContent = '规格: ' + (member.specification || '-');
|
||||||
|
content.appendChild(spec);
|
||||||
|
|
||||||
|
var pos = document.createElement('p');
|
||||||
|
pos.className = 'hint';
|
||||||
|
pos.textContent = (member.box_name || '-') + ' / ' + (member.slot_code || '-') + ' | 数量 ' + (member.quantity || 0);
|
||||||
|
content.appendChild(pos);
|
||||||
|
|
||||||
|
if (member.lcsc_code) {
|
||||||
|
var lcsc = document.createElement('p');
|
||||||
|
lcsc.className = 'hint';
|
||||||
|
lcsc.textContent = '立创编号: ' + member.lcsc_code;
|
||||||
|
content.appendChild(lcsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
li.appendChild(content);
|
||||||
|
|
||||||
|
if (member.edit_url) {
|
||||||
|
var editBtn = document.createElement('a');
|
||||||
|
editBtn.className = 'btn btn-light';
|
||||||
|
editBtn.href = member.edit_url;
|
||||||
|
editBtn.textContent = '编辑';
|
||||||
|
li.appendChild(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
section.appendChild(list);
|
||||||
|
groupsWrap.appendChild(section);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auditBtn.addEventListener('click', function () {
|
||||||
|
auditBtn.disabled = true;
|
||||||
|
statusNode.textContent = '正在巡检重复物料,请稍候...';
|
||||||
|
if (warningNode) {
|
||||||
|
warningNode.textContent = '';
|
||||||
|
}
|
||||||
|
clearGroups();
|
||||||
|
|
||||||
|
fetch('{{ url_for('ai_duplicate_audit') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: '{}'
|
||||||
|
})
|
||||||
|
.then(function (resp) {
|
||||||
|
return resp.json().then(function (data) {
|
||||||
|
return { ok: resp.ok, data: data };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
var data = result.data || {};
|
||||||
|
if (!result.ok || !data.ok) {
|
||||||
|
statusNode.textContent = '巡检失败';
|
||||||
|
if (warningNode) {
|
||||||
|
warningNode.textContent = data.message || '服务暂时不可用';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusNode.textContent = data.summary || '巡检已完成';
|
||||||
|
if (warningNode) {
|
||||||
|
warningNode.textContent = data.parse_warning || '';
|
||||||
|
}
|
||||||
|
renderAuditGroups((data.data && data.data.groups) || []);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
statusNode.textContent = '巡检失败';
|
||||||
|
if (warningNode) {
|
||||||
|
warningNode.textContent = '请求失败,请稍后重试';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
auditBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
exportCurrentBtn.addEventListener('click', function () {
|
||||||
|
if (!latestAuditGroups.length) {
|
||||||
|
statusNode.textContent = '请先运行巡检,再导出当前显示结果';
|
||||||
|
if (warningNode) {
|
||||||
|
warningNode.textContent = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var params = new URLSearchParams();
|
||||||
|
params.set('limit', String(latestAuditGroups.length));
|
||||||
|
latestAuditGroups.forEach(function (group) {
|
||||||
|
params.append('group_id', (group.type || '') + '::' + (group.key || ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
var downloadUrl = '{{ url_for('export_duplicate_audit_csv') }}?' + params.toString();
|
||||||
|
window.location.href = downloadUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
exportAllBtn.addEventListener('click', function () {
|
||||||
|
var downloadUrl = '{{ url_for('export_duplicate_audit_csv') }}?limit=1000';
|
||||||
|
window.location.href = downloadUrl;
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user