742 lines
39 KiB
HTML
742 lines
39 KiB
HTML
{% extends "app/base.html" %}
|
||
{% load static %}
|
||
|
||
{% block title %}{{ T.nav_biz_algorithms|default:T.nav_algorithms|default:'业务算法' }} - {{ settings.name|default:"Monitor" }}{% endblock %}
|
||
{% block nav_algorithms %}active{% endblock %}
|
||
{% block page_title %}{{ T.nav_biz_algorithms|default:T.nav_algorithms|default:'业务算法' }}{% endblock %}
|
||
|
||
{% block extra_head %}
|
||
<style>
|
||
.biz-page{max-width:1200px;margin:0 auto;}
|
||
.biz-card{background:#fff;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.05);overflow:hidden;}
|
||
.biz-head{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;
|
||
background:linear-gradient(135deg,#f8fbfa 0%,#f0f9f6 100%);border-bottom:1px solid #eef2f5;gap:10px;flex-wrap:wrap;}
|
||
.biz-head h3{margin:0;font-size:15px;font-weight:600;color:#2c3e50;display:flex;align-items:center;gap:8px;}
|
||
.biz-head h3 .dot{width:8px;height:8px;border-radius:50%;background:#169F85;box-shadow:0 0 0 3px rgba(22,159,133,.15);}
|
||
.biz-table{width:100%;border-collapse:collapse;}
|
||
.biz-table th,.biz-table td{padding:10px 14px;font-size:13px;text-align:left;border-bottom:1px solid #f3f4f6;}
|
||
.biz-table th{background:#fafbfc;color:#6b7280;font-weight:600;font-size:12px;}
|
||
.biz-table tr:hover td{background:#f9fafb;}
|
||
.biz-id-cell{color:#6b7280;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap;}
|
||
.biz-actions{display:flex;gap:6px;flex-wrap:wrap;}
|
||
.biz-actions .btn{padding:4px 10px;font-size:11px;}
|
||
.tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;}
|
||
.tag-on{background:#d1fae5;color:#065f46;}
|
||
.tag-off{background:#f3f4f6;color:#6b7280;}
|
||
.tag-flow{background:#e0f2fe;color:#075985;}
|
||
.tag-post{background:#fef3c7;color:#92400e;}
|
||
.btn{padding:7px 14px;font-size:12px;border-radius:6px;border:1px solid #dcdfe6;background:#fff;cursor:pointer;transition:.15s;}
|
||
.btn:hover{color:#169F85;border-color:#169F85;background:#f0f9f6;}
|
||
.btn-primary{background:linear-gradient(135deg,#14b89a 0%,#169F85 100%);color:#fff;border-color:transparent;}
|
||
.btn-primary:hover{background:#0e7a65;color:#fff;}
|
||
.biz-empty{padding:48px 20px;text-align:center;color:#9ca3af;font-size:14px;}
|
||
|
||
/* 编辑弹框 — 对齐大模型管理风格 */
|
||
.biz-modal-overlay{position:fixed;inset:0;z-index:400;background:rgba(15,23,42,.38);backdrop-filter:blur(5px);
|
||
display:none;align-items:center;justify-content:center;padding:20px;}
|
||
.biz-modal-overlay.show{display:flex;}
|
||
.biz-modal{width:min(560px,96vw);max-height:92vh;background:#fff;border-radius:14px;
|
||
box-shadow:0 20px 60px rgba(15,23,42,.18);border:1px solid #e2e8f0;display:flex;flex-direction:column;overflow:hidden;
|
||
animation:bizModalIn .22s ease;}
|
||
@keyframes bizModalIn{from{opacity:0;transform:scale(.97) translateY(8px)}to{opacity:1;transform:none}}
|
||
.biz-modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:18px 22px 16px;
|
||
border-bottom:1px solid #eef2f5;background:linear-gradient(135deg,#f8fbfa 0%,#f0f9f6 100%);}
|
||
.biz-modal-title{margin:0;font-size:16px;font-weight:600;color:#1e293b;display:flex;align-items:center;gap:8px;}
|
||
.biz-modal-title::before{content:'';width:8px;height:8px;border-radius:50%;background:#169F85;box-shadow:0 0 0 3px rgba(22,159,133,.15);}
|
||
.biz-modal-sub{margin:6px 0 0;font-size:12px;color:#64748b;line-height:1.5;}
|
||
.biz-modal-close{width:34px;height:34px;border:1px solid #e2e8f0;border-radius:9px;background:#fff;color:#64748b;
|
||
font-size:20px;line-height:1;cursor:pointer;flex-shrink:0;}
|
||
.biz-modal-close:hover{border-color:#169F85;color:#169F85;background:#f0f9f6;}
|
||
.biz-modal-body{padding:18px 22px;overflow:auto;flex:1;}
|
||
.biz-modal-foot{padding:14px 22px;border-top:1px solid #eef2f5;display:flex;justify-content:flex-end;gap:10px;background:#fafbfc;}
|
||
.biz-section{margin-bottom:18px;}
|
||
.biz-section:last-child{margin-bottom:0;}
|
||
.biz-section-title{font-size:11px;font-weight:700;color:#169F85;text-transform:uppercase;letter-spacing:.06em;
|
||
margin-bottom:12px;padding-bottom:6px;border-bottom:1px dashed #e2e8f0;}
|
||
.biz-field{margin-bottom:14px;}
|
||
.biz-field:last-child{margin-bottom:0;}
|
||
.biz-field label{display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;}
|
||
.biz-input{width:100%;padding:10px 12px;font-size:13px;border:1px solid #e2e8f0;border-radius:8px;
|
||
background:#fff;color:#1e293b;box-sizing:border-box;transition:border-color .15s,box-shadow .15s;}
|
||
.biz-input:focus{outline:none;border-color:#169F85;box-shadow:0 0 0 3px rgba(22,159,133,.12);}
|
||
.biz-input[disabled]{background:#f8fafc;color:#94a3b8;}
|
||
textarea.biz-input{resize:vertical;min-height:80px;font-family:inherit;line-height:1.5;}
|
||
select.biz-input{cursor:pointer;}
|
||
select.biz-multi-select{width:100%;min-height:140px;padding:6px 8px;font-size:13px;line-height:1.5;
|
||
border:1px solid #e2e8f0;border-radius:8px;background:#fff;color:#1e293b;box-sizing:border-box;}
|
||
select.biz-multi-select:focus{outline:none;border-color:#169F85;box-shadow:0 0 0 3px rgba(22,159,133,.12);}
|
||
select.biz-multi-select option{padding:6px 8px;border-radius:4px;margin:1px 0;}
|
||
select.biz-multi-select option:checked{background:#169F85;color:#fff;}
|
||
.biz-hint{font-size:11px;color:#94a3b8;margin-top:5px;line-height:1.4;}
|
||
.flow-panel{display:none;}
|
||
.flow-panel.show{display:block;}
|
||
|
||
/* 模型文件状态徽标 */
|
||
.model-badge{display:inline-block;padding:1px 6px;border-radius:8px;font-size:10px;font-weight:600;margin-left:4px;vertical-align:middle;}
|
||
.model-badge.ok{background:#d1fae5;color:#065f46;}
|
||
.model-badge.miss{background:#fee2e2;color:#991b1b;}
|
||
.model-name-wrap{display:inline-flex;align-items:center;gap:2px;}
|
||
.model-sub{font-size:10px;color:#9ca3af;display:block;margin-top:2px;}
|
||
|
||
/* 分配布控弹框 */
|
||
.assign-modal{width:min(640px,96vw);}
|
||
.assign-toolbar{display:flex;gap:8px;align-items:center;padding:10px 0 12px;border-bottom:1px solid #f3f4f6;margin-bottom:8px;}
|
||
.assign-list{max-height:360px;overflow:auto;border:1px solid #e2e8f0;border-radius:8px;}
|
||
.assign-item{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid #f3f4f6;cursor:pointer;}
|
||
.assign-item:last-child{border-bottom:none;}
|
||
.assign-item:hover{background:#f9fafb;}
|
||
.assign-item input{margin:0;accent-color:#169F85;}
|
||
.assign-item .nm{font-size:13px;color:#1f2937;font-weight:500;flex:1;}
|
||
.assign-item .sub{font-size:11px;color:#9ca3af;}
|
||
.assign-empty{padding:32px;text-align:center;color:#9ca3af;font-size:13px;}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="biz-page">
|
||
<div class="biz-card">
|
||
<div class="biz-head">
|
||
<h3><span class="dot"></span>{{ T.biz_algo_title|default:'业务算法' }}</h3>
|
||
<button type="button" class="btn btn-primary" onclick="openAdd()">+ {{ T.biz_algo_add|default:'添加业务算法' }}</button>
|
||
</div>
|
||
<table class="biz-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ T.th_id|default:'ID' }}</th>
|
||
<th>{{ T.col_name|default:'名称' }}</th>
|
||
<th>{{ T.biz_flow_type|default:'流程' }}</th>
|
||
<th>{{ T.biz_small_model|default:'小模型' }}</th>
|
||
<th>{{ T.biz_targets|default:'目标' }}</th>
|
||
<th>{{ T.biz_llm|default:'大模型' }}</th>
|
||
<th>{{ T.biz_post_process|default:'后处理' }}</th>
|
||
<th>{{ T.biz_zone_count|default:'布控' }}</th>
|
||
<th>{{ T.col_state|default:'状态' }}</th>
|
||
<th>{{ T.col_actions|default:'操作' }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tbody"></tbody>
|
||
</table>
|
||
<div class="biz-empty" id="empty" style="display:none;">{{ T.biz_algo_empty|default:'暂无业务算法,点击右上角添加' }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加/编辑 -->
|
||
<div class="biz-modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
|
||
<div class="biz-modal" role="dialog">
|
||
<div class="biz-modal-head">
|
||
<div>
|
||
<h3 class="biz-modal-title" id="modalTitle">{{ T.biz_algo_add|default:'添加业务算法' }}</h3>
|
||
<p class="biz-modal-sub" id="modalSub">{{ T.biz_modal_sub|default:'配置推理流程与后处理业务逻辑,保存后在布控管理中绑定区域' }}</p>
|
||
</div>
|
||
<button type="button" class="biz-modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="biz-modal-body">
|
||
<input type="hidden" id="f_id">
|
||
<div class="biz-section">
|
||
<div class="biz-section-title">{{ T.biz_section_basic|default:'基本信息' }}</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_algo_name|default:'业务算法名称' }}</label>
|
||
<input type="text" class="biz-input" id="f_name" placeholder="{{ T.biz_algo_name_ph|default:'如:周界入侵-人员' }}">
|
||
</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_flow_type|default:'流程类型' }}</label>
|
||
<select class="biz-input" id="f_flow_type" onchange="onFlowChange()">
|
||
<option value="1">{{ T.biz_flow_1|default:'小模型 + 后处理' }}</option>
|
||
<option value="2">{{ T.biz_flow_2|default:'大模型 + 后处理' }}</option>
|
||
<option value="3">{{ T.biz_flow_3|default:'小模型 + 大模型 + 后处理' }}</option>
|
||
<option value="4">{{ T.biz_flow_4|default:'检测小模型 + ReID小模型 + 后处理' }}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="biz-section flow-panel show" id="panelSmall">
|
||
<div class="biz-section-title" id="panelSmallTitle">{{ T.biz_section_small|default:'小模型推理' }}</div>
|
||
<div class="biz-field" id="fieldDetector" style="display:none;">
|
||
<label>{{ T.biz_detector_model|default:'检测小模型 (YOLO)' }} *</label>
|
||
<select class="biz-input" id="f_detector_model" onchange="onDetectorModelChange()"></select>
|
||
<div class="biz-hint" id="detectorModelHint" style="display:none;"></div>
|
||
</div>
|
||
<div class="biz-field">
|
||
<label id="labelSmallModel">{{ T.biz_small_model|default:'小模型' }}</label>
|
||
<select class="biz-input" id="f_small_model" onchange="onSmallModelChange()"></select>
|
||
<div class="biz-hint" id="smallModelHint" style="display:none;"></div>
|
||
</div>
|
||
<div class="biz-field" id="fieldReidHint" style="display:none;">
|
||
<div class="biz-hint" style="color:#1e40af;background:#eff6ff;padding:8px 10px;border-radius:6px;">
|
||
{{ T.biz_reid_flow_hint|default:'ReID 不能单独检测全图:先用 YOLO 定位目标,再用 OSNet 提取特征并关联 track_id,最后执行后处理(如越线计数)。' }}
|
||
</div>
|
||
</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_targets|default:'检测目标' }}</label>
|
||
<select class="biz-multi-select" id="f_target_labels" multiple size="6"></select>
|
||
<div class="biz-hint">{{ T.biz_targets_multi_hint|default:'单击选择;按住 Ctrl(Mac 为 ⌘)可多选' }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="biz-section flow-panel" id="panelLlm">
|
||
<div class="biz-section-title">{{ T.biz_section_llm|default:'大模型推理' }}</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_llm|default:'大模型' }}</label>
|
||
<select class="biz-input" id="f_llm"></select>
|
||
</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_llm_prompt|default:'提示词' }}</label>
|
||
<textarea class="biz-input" id="f_llm_prompt" placeholder="{{ T.biz_llm_prompt_ph|default:'描述需要大模型判断的场景…' }}"></textarea>
|
||
</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_llm_validate|default:'提示词校验值' }}</label>
|
||
<input type="text" class="biz-input" id="f_llm_validate" placeholder="{{ T.biz_llm_validate_ph|default:'多个关键词用英文逗号分隔' }}">
|
||
<div class="biz-hint">{{ T.biz_llm_validate_hint|default:'大模型回复中包含任一关键词则通过校验' }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="biz-section">
|
||
<div class="biz-section-title">{{ T.biz_section_post|default:'后处理' }}</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.biz_post_process|default:'后处理逻辑' }}</label>
|
||
<div style="display:flex;gap:8px;align-items:center;">
|
||
<select class="biz-input" id="f_post_process" style="flex:1;">
|
||
<option value="AREA">{{ T.biz_post_area|default:'区域入侵' }}</option>
|
||
<option value="LINE_CROSS">{{ T.biz_post_line_cross|default:'越线检测' }}</option>
|
||
<option value="LINE_COUNT">{{ T.biz_post_line_count|default:'越线计数' }}</option>
|
||
<option value="DIRECTION">{{ T.biz_post_direction|default:'方向入侵' }}</option>
|
||
<option value="DENSITY">{{ T.biz_post_density|default:'密度报警' }}</option>
|
||
<option value="DWELL">{{ T.biz_post_dwell|default:'滞留报警' }}</option>
|
||
</select>
|
||
<button type="button" class="btn" style="white-space:nowrap;" onclick="openPostHelp()">?{{ T.biz_post_help|default:'说明' }}</button>
|
||
</div>
|
||
<div class="biz-hint" id="postHint">{{ T.biz_post_area_hint|default:'目标中心进入布控多边形即触发报警' }}</div>
|
||
</div>
|
||
<div class="biz-field" id="panelLineCount" style="display:none;">
|
||
<label>{{ T.biz_line_count_threshold|default:'计数报警阈值' }}</label>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||
<div>
|
||
<span style="font-size:11px;color:#64748b;">{{ T.biz_forward_count|default:'正向 (A→B)' }}</span>
|
||
<input type="number" class="biz-input" id="f_forward_count_threshold" min="0" value="0" placeholder="0=不报警">
|
||
</div>
|
||
<div>
|
||
<span style="font-size:11px;color:#64748b;">{{ T.biz_reverse_count|default:'逆向 (B→A)' }}</span>
|
||
<input type="number" class="biz-input" id="f_reverse_count_threshold" min="0" value="0" placeholder="0=不报警">
|
||
</div>
|
||
</div>
|
||
<div class="biz-hint">{{ T.biz_line_count_threshold_hint|default:'累计过线人数达到阈值时触发报警;0 表示该方向不计数报警。布控需绘制矩形区域 + 方向线 A→B 定义正向。' }}</div>
|
||
</div>
|
||
<div class="biz-field">
|
||
<label>{{ T.col_state|default:'状态' }}</label>
|
||
<select class="biz-input" id="f_state">
|
||
<option value="1">{{ T.state_enabled|default:'启用' }}</option>
|
||
<option value="0">{{ T.state_disabled|default:'禁用' }}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="biz-modal-foot">
|
||
<button type="button" class="btn" onclick="closeModal()">{{ T.btn_cancel|default:'取消' }}</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveForm()">{{ T.btn_save|default:'保存' }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分配布控 -->
|
||
<div class="biz-modal-overlay" id="assignModal" onclick="if(event.target===this)closeAssignModal()">
|
||
<div class="biz-modal assign-modal" role="dialog">
|
||
<div class="biz-modal-head">
|
||
<div>
|
||
<h3 class="biz-modal-title" id="assignTitle">{{ T.biz_assign_zones|default:'分配布控' }}</h3>
|
||
<p class="biz-modal-sub" id="assignSub">{{ T.biz_assign_zones_sub|default:'勾选需要绑定该业务算法的布控区域' }}</p>
|
||
</div>
|
||
<button type="button" class="biz-modal-close" onclick="closeAssignModal()">×</button>
|
||
</div>
|
||
<div class="biz-modal-body" style="padding-top:8px;">
|
||
<div class="assign-toolbar">
|
||
<button type="button" class="btn" style="padding:4px 10px;font-size:11px;" onclick="toggleAllZones(true)">{{ T.alg_select_all|default:'全选' }}</button>
|
||
<button type="button" class="btn" style="padding:4px 10px;font-size:11px;" onclick="toggleAllZones(false)">{{ T.alg_select_none|default:'全不选' }}</button>
|
||
<span id="assignCount" style="font-size:11px;color:#6b7280;margin-left:auto;"></span>
|
||
</div>
|
||
<div class="assign-list" id="assignList"></div>
|
||
</div>
|
||
<div class="biz-modal-foot">
|
||
<button type="button" class="btn" onclick="closeAssignModal()">{{ T.btn_cancel|default:'取消' }}</button>
|
||
<button type="button" class="btn btn-primary" onclick="submitAssign()">{{ T.biz_save_assign|default:'保存分配' }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 后处理说明 -->
|
||
<div class="biz-modal-overlay" id="postHelpModal" onclick="if(event.target===this)closePostHelp()">
|
||
<div class="biz-modal" role="dialog" style="width:min(680px,96vw);">
|
||
<div class="biz-modal-head">
|
||
<div>
|
||
<h3 class="biz-modal-title">{{ T.biz_post_help_title|default:'后处理方案说明' }}</h3>
|
||
<p class="biz-modal-sub">{{ T.biz_post_help_sub|default:'共 6 种后处理,按场景选择' }}</p>
|
||
</div>
|
||
<button type="button" class="biz-modal-close" onclick="closePostHelp()">×</button>
|
||
</div>
|
||
<div class="biz-modal-body">
|
||
<div class="post-help-item" style="padding:14px 0;border-bottom:1px solid #f3f4f6;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||
<span class="tag tag-post" style="background:#d1fae5;color:#065f46;">AREA</span>
|
||
<b style="font-size:14px;color:#1e293b;">{{ T.biz_post_area|default:'区域入侵' }}</b>
|
||
</div>
|
||
<div style="font-size:13px;color:#475569;line-height:1.6;">
|
||
{{ T.biz_post_area_desc|default:'目标中心点进入布控多边形内即触发报警。适用于周界防范、禁入区域、危险区域等场景。' }}
|
||
</div>
|
||
</div>
|
||
<div class="post-help-item" style="padding:14px 0;border-bottom:1px solid #f3f4f6;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||
<span class="tag tag-post" style="background:#dbeafe;color:#1e40af;">LINE_CROSS</span>
|
||
<b style="font-size:14px;color:#1e293b;">{{ T.biz_post_line_cross|default:'越线检测' }}</b>
|
||
</div>
|
||
<div style="font-size:13px;color:#475569;line-height:1.6;">
|
||
{{ T.biz_post_line_cross_desc|default:'有人沿方向线正向(A→B)跨过警戒线时,立即触发一次报警。适用于围栏越界、禁止闯入等「一过线就报」的场景。' }}
|
||
</div>
|
||
</div>
|
||
<div class="post-help-item" style="padding:14px 0;border-bottom:1px solid #f3f4f6;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||
<span class="tag tag-post" style="background:#cffafe;color:#0e7490;">LINE_COUNT</span>
|
||
<b style="font-size:14px;color:#1e293b;">{{ T.biz_post_line_count|default:'越线计数' }}</b>
|
||
</div>
|
||
<div style="font-size:13px;color:#475569;line-height:1.6;">
|
||
{{ T.biz_post_line_count_desc|default:'按方向线 A→B 分别累计正向、逆向过线人数;累计达到阈值才报警。适用于人流统计、限流等场景。' }}
|
||
</div>
|
||
<div style="font-size:12px;color:#64748b;margin-top:8px;padding:8px 10px;background:#f8fafc;border-radius:6px;">
|
||
{{ T.biz_post_line_diff|default:'与越线检测的区别:越线检测是「过线立刻报」;越线计数是「累计到 N 人才报」,并可分别统计正向/逆向。' }}
|
||
</div>
|
||
</div>
|
||
<div class="post-help-item" style="padding:14px 0;border-bottom:1px solid #f3f4f6;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||
<span class="tag tag-post" style="background:#fef3c7;color:#92400e;">DIRECTION</span>
|
||
<b style="font-size:14px;color:#1e293b;">{{ T.biz_post_direction|default:'方向入侵' }}</b>
|
||
</div>
|
||
<div style="font-size:13px;color:#475569;line-height:1.6;">
|
||
{{ T.biz_post_direction_desc|default:'目标在区域内移动时,若运动方向与设定方向一致则报警。适用于单向通行、逆行检测等场景。' }}
|
||
</div>
|
||
</div>
|
||
<div class="post-help-item" style="padding:14px 0;border-bottom:1px solid #f3f4f6;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||
<span class="tag tag-post" style="background:#f3e8ff;color:#6b21a8;">DENSITY</span>
|
||
<b style="font-size:14px;color:#1e293b;">{{ T.biz_post_density|default:'密度报警' }}</b>
|
||
</div>
|
||
<div style="font-size:13px;color:#475569;line-height:1.6;">
|
||
{{ T.biz_post_density_desc|default:'布控区域内同时存在的目标数达到或超过阈值时报警。适用于人群聚集、排队过长、车辆拥堵等场景。' }}
|
||
</div>
|
||
</div>
|
||
<div class="post-help-item" style="padding:14px 0;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||
<span class="tag tag-post" style="background:#fee2e2;color:#991b1b;">DWELL</span>
|
||
<b style="font-size:14px;color:#1e293b;">{{ T.biz_post_dwell|default:'滞留报警' }}</b>
|
||
</div>
|
||
<div style="font-size:13px;color:#475569;line-height:1.6;">
|
||
{{ T.biz_post_dwell_desc|default:'目标在布控区域内停留时间超过滞留阈值(秒)时报警。与区域入侵不同:区域入侵是进入即报,滞留是待够时间才报。' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="biz-modal-foot">
|
||
<button type="button" class="btn btn-primary" onclick="closePostHelp()">{{ T.btn_ok|default:'我知道了' }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
var g_options = {small_models:[], llms:[]};
|
||
var g_assignBizId = 0;
|
||
|
||
function _t(k,d){ return (window.T && window.T[k]) || d; }
|
||
function esc(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"'); }
|
||
|
||
function loadOptions(cb){
|
||
Api.get('/algorithm/openOptions').then(function(res){
|
||
if(res.code===1000){ g_options = res.data || g_options; fillSelects(); if(cb) cb(); }
|
||
});
|
||
}
|
||
|
||
function fillSelects(){
|
||
refreshModelSelects();
|
||
var ll = document.getElementById('f_llm');
|
||
ll.innerHTML = '<option value="">'+_t('please_select','请选择')+'</option>';
|
||
(g_options.llms||[]).forEach(function(m){
|
||
var label = m.name || m.model_name || (m.id ? ('#'+m.id) : '');
|
||
ll.innerHTML += '<option value="'+m.id+'">'+esc(label)+'</option>';
|
||
});
|
||
}
|
||
|
||
function isDetectModel(m){
|
||
return m && (m.task_type || 'detect').toLowerCase() !== 'reid';
|
||
}
|
||
function isReidModel(m){
|
||
return m && (m.task_type || '').toLowerCase() === 'reid';
|
||
}
|
||
|
||
function modelOptionLabel(m){
|
||
var flag = m.model_file_exists ? ' [✓就绪]' : (m.model_file ? ' [✗文件缺失]' : ' [✗未配置]');
|
||
return esc(m.name) + flag;
|
||
}
|
||
|
||
function refreshModelSelects(){
|
||
var ft = getFlowType();
|
||
var isReidFlow = ft === 4;
|
||
var detSel = document.getElementById('f_detector_model');
|
||
var smSel = document.getElementById('f_small_model');
|
||
var prevDet = detSel ? detSel.value : '';
|
||
var prevSm = smSel ? smSel.value : '';
|
||
if(detSel){
|
||
detSel.innerHTML = '<option value="">'+_t('please_select','请选择')+'</option>';
|
||
(g_options.small_models||[]).filter(isDetectModel).forEach(function(m){
|
||
detSel.innerHTML += '<option value="'+m.id+'">'+modelOptionLabel(m)+'</option>';
|
||
});
|
||
if(prevDet) detSel.value = prevDet;
|
||
}
|
||
if(smSel){
|
||
smSel.innerHTML = '<option value="">'+_t('please_select','请选择')+'</option>';
|
||
var list = isReidFlow
|
||
? (g_options.small_models||[]).filter(isReidModel)
|
||
: (g_options.small_models||[]).filter(isDetectModel);
|
||
list.forEach(function(m){
|
||
smSel.innerHTML += '<option value="'+m.id+'">'+modelOptionLabel(m)+'</option>';
|
||
});
|
||
if(prevSm) smSel.value = prevSm;
|
||
}
|
||
document.getElementById('fieldDetector').style.display = isReidFlow ? '' : 'none';
|
||
document.getElementById('fieldReidHint').style.display = isReidFlow ? '' : 'none';
|
||
document.getElementById('labelSmallModel').textContent = isReidFlow
|
||
? _t('biz_reid_model','ReID 小模型 (OSNet)')
|
||
: _t('biz_small_model','小模型');
|
||
document.getElementById('panelSmallTitle').textContent = isReidFlow
|
||
? _t('biz_section_detect_reid','检测 + ReID 推理')
|
||
: _t('biz_section_small','小模型推理');
|
||
}
|
||
|
||
function onFlowChange(){
|
||
var ft = getFlowType();
|
||
document.getElementById('panelSmall').classList.toggle('show', ft===1 || ft===3 || ft===4);
|
||
document.getElementById('panelLlm').classList.toggle('show', ft===2 || ft===3);
|
||
refreshModelSelects();
|
||
onDetectorModelChange();
|
||
onSmallModelChange();
|
||
}
|
||
|
||
function showModelHint(el, m){
|
||
if(!el) return;
|
||
if(!m){ el.style.display='none'; return; }
|
||
el.style.display = 'block';
|
||
if(m.model_file_exists){
|
||
el.style.color = '#065f46';
|
||
el.innerHTML = '✓ 模型文件已就绪:<b>'+esc(m.model_file)+'</b>'+(m.engine?(' · 引擎 '+esc(m.engine)):'');
|
||
} else if(m.model_file){
|
||
el.style.color = '#991b1b';
|
||
el.innerHTML = '✗ 模型文件不存在:<b>'+esc(m.model_file)+'</b>,请到「小模型管理」上传或修改路径';
|
||
} else {
|
||
el.style.color = '#991b1b';
|
||
el.innerHTML = '✗ 未配置模型文件,请到「小模型管理」设置模型文件路径';
|
||
}
|
||
}
|
||
|
||
function onDetectorModelChange(){
|
||
if(getFlowType() !== 4) return;
|
||
var id = parseInt(document.getElementById('f_detector_model').value, 10);
|
||
var m = (g_options.small_models||[]).find(function(x){ return x.id===id; });
|
||
fillTargetSelect(m ? m.labels : [], getSelectedTargets());
|
||
showModelHint(document.getElementById('detectorModelHint'), m);
|
||
}
|
||
|
||
function getFlowType(){
|
||
return parseInt(document.getElementById('f_flow_type').value, 10) || 1;
|
||
}
|
||
|
||
function onSmallModelChange(){
|
||
var ft = getFlowType();
|
||
var id = parseInt(document.getElementById('f_small_model').value, 10);
|
||
var m = (g_options.small_models||[]).find(function(x){ return x.id===id; });
|
||
if(ft === 4){
|
||
showModelHint(document.getElementById('smallModelHint'), m);
|
||
return;
|
||
}
|
||
fillTargetSelect(m ? m.labels : [], []);
|
||
showModelHint(document.getElementById('smallModelHint'), m);
|
||
}
|
||
|
||
function fillTargetSelect(labels, selected){
|
||
var sel = document.getElementById('f_target_labels');
|
||
if(!sel) return;
|
||
sel.innerHTML = '';
|
||
if(!labels || !labels.length){
|
||
var o = document.createElement('option');
|
||
o.disabled = true;
|
||
o.textContent = _t('biz_no_labels','请先选择小模型');
|
||
sel.appendChild(o);
|
||
return;
|
||
}
|
||
var selSet = {};
|
||
(selected||[]).forEach(function(lb){ selSet[lb] = true; });
|
||
labels.forEach(function(lb){
|
||
var o = document.createElement('option');
|
||
o.value = lb;
|
||
o.textContent = lb;
|
||
if(selSet[lb]) o.selected = true;
|
||
sel.appendChild(o);
|
||
});
|
||
}
|
||
|
||
function getSelectedTargets(){
|
||
var sel = document.getElementById('f_target_labels');
|
||
if(!sel) return [];
|
||
return Array.from(sel.selectedOptions).map(function(o){ return o.value; });
|
||
}
|
||
|
||
function sortByIdDesc(list){
|
||
return (list || []).slice().sort(function(a, b){ return (parseInt(b.id, 10) || 0) - (parseInt(a.id, 10) || 0); });
|
||
}
|
||
|
||
function loadList(){
|
||
Api.get('/algorithm/openIndex').then(function(res){
|
||
var rows = sortByIdDesc(res.data || []);
|
||
document.getElementById('empty').style.display = rows.length ? 'none' : 'block';
|
||
document.getElementById('tbody').innerHTML = rows.map(function(r){
|
||
var targets = (r.target_labels||[]).join(', ') || '—';
|
||
var st = r.state===1
|
||
? '<span class="tag tag-on">'+_t('state_enabled','启用')+'</span>'
|
||
: '<span class="tag tag-off">'+_t('state_disabled','禁用')+'</span>';
|
||
// 小模型列:显示名称 + 文件状态徽标
|
||
var smCell = '—';
|
||
if(r.flow_type === 4){
|
||
var parts = [];
|
||
if(r.detector_model_name){
|
||
parts.push('<div><span style="font-size:10px;color:#64748b;">YOLO</span> <b>'+esc(r.detector_model_name)+'</b></div>');
|
||
}
|
||
if(r.small_model_name){
|
||
parts.push('<div><span style="font-size:10px;color:#64748b;">ReID</span> <b>'+esc(r.small_model_name)+'</b></div>');
|
||
}
|
||
smCell = parts.length ? parts.join('') : '—';
|
||
} else if(r.small_model_name){
|
||
var hasFile = !!r.small_model_file_exists;
|
||
var badge = hasFile
|
||
? '<span class="model-badge ok">✓就绪</span>'
|
||
: '<span class="model-badge miss">✗'+(r.small_model_file?'文件缺失':'未配置')+'</span>';
|
||
var sub = hasFile
|
||
? '<span class="model-sub">'+esc(r.small_model_engine||'')+(r.small_model_file?' · '+esc(r.small_model_file):'')+'</span>'
|
||
: '<span class="model-sub">'+(r.small_model_file?('文件: '+esc(r.small_model_file)):'未配置模型文件')+'</span>';
|
||
smCell = '<div class="model-name-wrap"><b>'+esc(r.small_model_name)+'</b>'+badge+'</div>'+sub;
|
||
}
|
||
return '<tr>'
|
||
+'<td class="biz-id-cell">'+r.id+'</td>'
|
||
+'<td><b>'+esc(r.name)+'</b></td>'
|
||
+'<td><span class="tag tag-flow">'+esc(r.flow_type_name)+'</span></td>'
|
||
+'<td>'+smCell+'</td>'
|
||
+'<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="'+esc(targets)+'">'+esc(targets)+'</td>'
|
||
+'<td>'+esc(r.llm_name||'—')+'</td>'
|
||
+'<td><span class="tag tag-post">'+esc(r.post_process_name)+'</span></td>'
|
||
+'<td>'+(r.zone_count||0)+'</td>'
|
||
+'<td>'+st+'</td>'
|
||
+'<td><div class="biz-actions">'
|
||
+'<button class="btn" onclick="openEdit('+r.id+')">'+_t('btn_edit','编辑')+'</button>'
|
||
+'<button class="btn" onclick="openAssignModal('+r.id+')">'+_t('biz_assign','分配')+'</button>'
|
||
+'<button class="btn" onclick="delRow('+r.id+')">'+_t('btn_delete','删除')+'</button>'
|
||
+'</div></td></tr>';
|
||
}).join('');
|
||
});
|
||
}
|
||
|
||
function openAdd(){
|
||
document.getElementById('modalTitle').textContent = _t('biz_algo_add','添加业务算法');
|
||
document.getElementById('f_id').value = '';
|
||
document.getElementById('f_name').value = '';
|
||
document.getElementById('f_flow_type').value = '1';
|
||
document.getElementById('f_detector_model').value = '';
|
||
document.getElementById('f_small_model').value = '';
|
||
document.getElementById('f_llm').value = '';
|
||
document.getElementById('f_llm_prompt').value = '';
|
||
document.getElementById('f_llm_validate').value = '';
|
||
document.getElementById('f_post_process').value = 'AREA';
|
||
document.getElementById('f_forward_count_threshold').value = '0';
|
||
document.getElementById('f_reverse_count_threshold').value = '0';
|
||
document.getElementById('f_state').value = '1';
|
||
onFlowChange();
|
||
onPostChange();
|
||
fillTargetSelect([], []);
|
||
onSmallModelChange();
|
||
document.getElementById('modal').classList.add('show');
|
||
}
|
||
|
||
function openEdit(id){
|
||
Api.get('/algorithm/openIndex').then(function(res){
|
||
var r = (res.data||[]).find(function(x){ return x.id===id; });
|
||
if(!r) return;
|
||
document.getElementById('modalTitle').textContent = _t('biz_algo_edit','编辑业务算法');
|
||
document.getElementById('f_id').value = r.id;
|
||
document.getElementById('f_name').value = r.name;
|
||
document.getElementById('f_flow_type').value = String(r.flow_type);
|
||
onFlowChange();
|
||
document.getElementById('f_detector_model').value = r.detector_model_id || '';
|
||
document.getElementById('f_small_model').value = r.small_model_id || '';
|
||
if(r.flow_type === 4){
|
||
var det = (g_options.small_models||[]).find(function(x){ return x.id===r.detector_model_id; });
|
||
fillTargetSelect(det ? det.labels : [], r.target_labels || []);
|
||
onDetectorModelChange();
|
||
} else {
|
||
var m = (g_options.small_models||[]).find(function(x){ return x.id===r.small_model_id; });
|
||
fillTargetSelect(m ? m.labels : [], r.target_labels || []);
|
||
}
|
||
onSmallModelChange();
|
||
(function(){
|
||
var llSel = document.getElementById('f_llm');
|
||
// 清理上次编辑时插入的“已禁用回显项”
|
||
Array.from(llSel.querySelectorAll('option[data-echo="1"]')).forEach(function(o){ o.remove(); });
|
||
// 若引用的 LLM 被禁用(不在启用列表中),补一个回显项,避免看起来像配置丢失
|
||
if (r.llm_id && !llSel.querySelector('option[value="'+r.llm_id+'"]')) {
|
||
var opt = document.createElement('option');
|
||
opt.value = r.llm_id;
|
||
opt.textContent = (r.llm_name || ('#'+r.llm_id)) + ' ['+_t('state_disabled','禁用')+']';
|
||
opt.setAttribute('data-echo','1');
|
||
llSel.appendChild(opt);
|
||
}
|
||
llSel.value = r.llm_id || '';
|
||
})();
|
||
document.getElementById('f_llm_prompt').value = r.llm_prompt || '';
|
||
document.getElementById('f_llm_validate').value = r.llm_validate || '';
|
||
document.getElementById('f_post_process').value = r.post_process || 'AREA';
|
||
document.getElementById('f_forward_count_threshold').value = r.forward_count_threshold || 0;
|
||
document.getElementById('f_reverse_count_threshold').value = r.reverse_count_threshold || 0;
|
||
document.getElementById('f_state').value = String(r.state);
|
||
onPostChange();
|
||
document.getElementById('modal').classList.add('show');
|
||
});
|
||
}
|
||
|
||
function closeModal(){ document.getElementById('modal').classList.remove('show'); }
|
||
|
||
function saveForm(){
|
||
var payload = {
|
||
id: document.getElementById('f_id').value,
|
||
name: document.getElementById('f_name').value.trim(),
|
||
flow_type: getFlowType(),
|
||
small_model_id: document.getElementById('f_small_model').value,
|
||
detector_model_id: document.getElementById('f_detector_model').value,
|
||
target_labels: getSelectedTargets(),
|
||
llm_id: document.getElementById('f_llm').value,
|
||
llm_prompt: document.getElementById('f_llm_prompt').value.trim(),
|
||
llm_validate: document.getElementById('f_llm_validate').value.trim(),
|
||
post_process: document.getElementById('f_post_process').value,
|
||
forward_count_threshold: parseInt(document.getElementById('f_forward_count_threshold').value, 10) || 0,
|
||
reverse_count_threshold: parseInt(document.getElementById('f_reverse_count_threshold').value, 10) || 0,
|
||
state: document.getElementById('f_state').value
|
||
};
|
||
var url = payload.id ? '/algorithm/openEdit' : '/algorithm/openAdd';
|
||
Api.post(url, payload).then(function(res){
|
||
if(res.code===1000){ closeModal(); loadList(); showToast(res.msg||_t('msg_success','成功'), 'success'); }
|
||
else showToast(res.msg||_t('msg_failed','失败'), 'error');
|
||
});
|
||
}
|
||
|
||
function delRow(id){
|
||
// 删除前先查询该算法是否被布控引用,有引用则直接弹提示,不进入删除流程
|
||
Api.get('/algorithm/openAssignContext', {biz_algorithm_id: id}).then(function(res){
|
||
if(res.code!==1000 || !res.data){
|
||
// 查询失败,回退到直接删除(后端会再次校验)
|
||
_doDelBiz(id);
|
||
return;
|
||
}
|
||
var zones = (res.data.zones || []).filter(function(z){ return z.selected; });
|
||
if(zones.length){
|
||
var names = zones.map(function(z){
|
||
return (z.stream_name||('#'+z.stream_id)) + '/' + (z.zone_name||('#'+z.id));
|
||
}).join('、');
|
||
showToast('该算法已被 '+zones.length+' 个布控引用,请先在「分配布控」中解除绑定后再删除:'+names, 'error', 6000);
|
||
return;
|
||
}
|
||
_doDelBiz(id);
|
||
}).catch(function(){ _doDelBiz(id); });
|
||
}
|
||
|
||
function _doDelBiz(id){
|
||
if(!confirm(_t('confirm_delete','确定删除?'))) return;
|
||
Api.post('/algorithm/openDel', {id:id}).then(function(res){
|
||
if(res.code===1000) loadList();
|
||
showToast(res.msg||(res.code===1000?_t('msg_success','成功'):_t('msg_failed','失败')), res.code===1000?'success':'error');
|
||
});
|
||
}
|
||
|
||
function openAssignModal(bizId){
|
||
g_assignBizId = bizId;
|
||
document.getElementById('assignModal').classList.add('show');
|
||
Api.get('/algorithm/openAssignContext', {biz_algorithm_id: bizId}).then(function(res){
|
||
if(res.code!==1000){ showToast(res.msg||_t('msg_failed','失败'), 'error'); return; }
|
||
var biz = res.data.biz_algorithm || {};
|
||
document.getElementById('assignTitle').textContent = _t('biz_assign_for','分配布控 · ')+ (biz.name||('#'+bizId));
|
||
var zones = res.data.zones || [];
|
||
var list = document.getElementById('assignList');
|
||
if(!zones.length){
|
||
list.innerHTML = '<div class="assign-empty">'+_t('biz_no_zones','暂无布控区域,请先到布控管理添加')+'</div>';
|
||
updateAssignCount();
|
||
return;
|
||
}
|
||
list.innerHTML = zones.map(function(z){
|
||
var stateTag = z.state === 1
|
||
? ''
|
||
: ' · <span style="color:#9ca3af;">'+_t('zone_disabled','禁用')+'</span>';
|
||
return '<label class="assign-item">'
|
||
+'<input type="checkbox" value="'+z.id+'"'+(z.selected?' checked':'')+'>'
|
||
+'<span class="nm">'+esc(z.zone_name)+stateTag+'</span>'
|
||
+'<span class="sub">'+esc(z.stream_name||('stream#'+z.stream_id))+'</span>'
|
||
+'</label>';
|
||
}).join('');
|
||
list.querySelectorAll('input').forEach(function(cb){ cb.addEventListener('change', updateAssignCount); });
|
||
updateAssignCount();
|
||
});
|
||
}
|
||
|
||
function closeAssignModal(){ document.getElementById('assignModal').classList.remove('show'); }
|
||
|
||
function toggleAllZones(check){
|
||
document.querySelectorAll('#assignList input[type=checkbox]').forEach(function(cb){ cb.checked = check; });
|
||
updateAssignCount();
|
||
}
|
||
|
||
function updateAssignCount(){
|
||
var n = document.querySelectorAll('#assignList input[type=checkbox]:checked').length;
|
||
document.getElementById('assignCount').textContent = _t('biz_zones_selected','已选 {n} 个布控').replace('{n}', n);
|
||
}
|
||
|
||
function submitAssign(){
|
||
var ids = Array.from(document.querySelectorAll('#assignList input[type=checkbox]:checked')).map(function(cb){
|
||
return parseInt(cb.value, 10);
|
||
});
|
||
Api.post('/algorithm/openAssignZones', {biz_algorithm_id: g_assignBizId, zone_ids: ids}).then(function(res){
|
||
showToast(res.msg||(res.code===1000?_t('msg_success','成功'):_t('msg_failed','失败')), res.code===1000?'success':'error');
|
||
if(res.code===1000){ closeAssignModal(); loadList(); }
|
||
});
|
||
}
|
||
|
||
document.addEventListener('keydown', function(e){
|
||
if(e.key!=='Escape') return;
|
||
if(document.getElementById('modal').classList.contains('show')) closeModal();
|
||
if(document.getElementById('assignModal').classList.contains('show')) closeAssignModal();
|
||
if(document.getElementById('postHelpModal').classList.contains('show')) closePostHelp();
|
||
});
|
||
|
||
// 后处理说明弹框
|
||
function openPostHelp(){ document.getElementById('postHelpModal').classList.add('show'); }
|
||
function closePostHelp(){ document.getElementById('postHelpModal').classList.remove('show'); }
|
||
|
||
// 下拉联动:切换提示文案
|
||
var POST_HINTS = {
|
||
'AREA': '{{ T.biz_post_area_hint|default:"目标中心进入布控多边形即触发报警" }}',
|
||
'LINE_CROSS': '{{ T.biz_post_line_cross_hint|default:"目标轨迹跨过警戒线 line_a→line_b 触发,需在布控区域配置警戒线" }}',
|
||
'LINE_COUNT': '{{ T.biz_post_line_count_hint|default:"按方向线 A→B 统计正向/逆向过线,达到阈值报警;布控需绘制矩形+方向线" }}',
|
||
'DIRECTION': '{{ T.biz_post_direction_hint|default:"目标移动方向匹配 ref_angle 触发,参考角度 0=东 90=南 180=西 270=北" }}',
|
||
'DENSITY': '{{ T.biz_post_density_hint|default:"区域内目标数 ≥ density_threshold 触发,需在布控区域配置密度阈值" }}',
|
||
'DWELL': '{{ T.biz_post_dwell_hint|default:"目标在区域内停留 ≥ loiter_threshold 秒触发" }}'
|
||
};
|
||
function onPostChange(){
|
||
var v = document.getElementById('f_post_process').value;
|
||
document.getElementById('postHint').textContent = POST_HINTS[v] || '';
|
||
var panel = document.getElementById('panelLineCount');
|
||
if(panel) panel.style.display = (v === 'LINE_COUNT') ? 'block' : 'none';
|
||
}
|
||
document.getElementById('f_post_process').addEventListener('change', onPostChange);
|
||
|
||
loadOptions(function(){ loadList(); });
|
||
</script>
|
||
{% endblock %}
|