fix: wizard step 2 — separate assigned/added cameras, show loading state for features

This commit is contained in:
tian 2026-07-17 17:06:59 +08:00
parent 3f3a2a0987
commit b50ccda82d

View File

@ -52,10 +52,15 @@
<div id="wizard-step-2" class="wizard-panel" style="display:none">
<h3 style="margin-top:0">配置 <span id="step2-device-name">-</span> 的检测任务</h3>
<div id="source-list" style="display:flex;flex-direction:column;gap:12px">
<div style="font-size:12px;font-weight:600;margin-bottom:8px">已配置的摄像头</div>
<div id="assigned-sources" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px">
<div class="empty-state compact" id="no-assigned-sources">
<div class="muted">该设备尚未配置摄像头。</div>
</div>
</div>
<div style="margin-top:16px;padding:12px;border:1px dashed var(--border);border-radius:var(--radius)" id="add-source-panel">
<div style="padding:12px;border:1px dashed var(--border);border-radius:var(--radius);margin-bottom:16px">
<div style="font-size:12px;font-weight:600;margin-bottom:8px">+ 添加摄像头</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" id="new-source-name" placeholder="摄像头名称(如:车间东门)" style="width:160px;font-size:12px">
@ -64,10 +69,10 @@
</div>
</div>
<div style="margin-top:16px;padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft)" id="feature-selector">
<div style="padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft)">
<div style="font-size:12px;font-weight:600;margin-bottom:8px">检测功能</div>
<div id="feature-list" style="display:flex;gap:12px;flex-wrap:wrap">
<span class="muted small">请先选择设备...</span>
<span class="muted small" id="feature-placeholder">加载设备能力中...</span>
</div>
</div>
@ -125,22 +130,19 @@
.feature-check label{font-size:12px;cursor:pointer}
</style>
<script src="/ui/assets/vendor/hls.min.js"></script>
<script>
(function(){
var selectedDevice = null;
var selectedDeviceName = "";
var step1Sources = [];
var step1AllVideoSources = [];
var step1Features = null;
var step2Sources = [];
var allVideoSources = [];
var step2Assigned = [];
var step2Added = [];
var step2Features = [];
function loadAllVideoSources() {
return {{.WizardVideoSourcesJSON}} || [];
}
step1AllVideoSources = loadAllVideoSources();
allVideoSources = loadAllVideoSources();
var currentStep = 1;
@ -175,88 +177,112 @@
if (!selectedDevice) return;
document.getElementById("step2-device-name").textContent = selectedDeviceName;
setStep(2);
document.getElementById("feature-placeholder").textContent = "加载设备能力中...";
document.getElementById("feature-placeholder").style.display = "";
step2Assigned = [];
step2Added = [];
step2Features = [];
renderAssignedSources();
renderFeatureList();
fetch("/ui/wizard/device-info?device_id=" + encodeURIComponent(selectedDevice))
.then(function(r){return r.json()})
.then(function(info){
step1Features = (info.capabilities || []).filter(function(c){return c.available});
var existingSources = info.sources || [];
step2Sources = [];
var sourceNames = [];
existingSources.forEach(function(sn){
var src = step1AllVideoSources.find(function(v){return v.name === sn});
if (src) {
step2Sources.push({name:src.name, url:src.url, existing:true});
sourceNames.push(src.name);
}
});
// Also add unassigned sources
step1AllVideoSources.forEach(function(src){
if (sourceNames.indexOf(src.name) < 0) {
step2Sources.push({name:src.name, url:src.url, existing:false});
sourceNames.push(src.name);
var existingNames = (info.sources || []).slice();
existingNames.forEach(function(sn){
var found = allVideoSources.find(function(v){return v.name === sn});
if (found) {
step2Assigned.push({name: found.name, url: found.url});
}
});
renderAssignedSources();
step2Features = [];
renderStep2();
var caps = (info.capabilities || []).filter(function(c){return c.available});
document.getElementById("feature-placeholder").style.display = "none";
renderFeatures(caps);
})
.catch(function(){
document.getElementById("feature-placeholder").textContent = "无法获取设备信息";
});
});
function renderStep2() {
var list = document.getElementById("source-list");
list.innerHTML = "";
step2Sources.forEach(function(src, i){
function renderAssignedSources() {
var container = document.getElementById("assigned-sources");
var emptyEl = document.getElementById("no-assigned-sources");
var allSources = step2Assigned.concat(step2Added);
var existingCards = container.querySelectorAll(".assigned-source-card");
existingCards.forEach(function(c){c.remove()});
allSources.forEach(function(src){
var div = document.createElement("div");
div.className = "source-row";
div.className = "assigned-source-card source-row";
div.innerHTML =
'<span class="source-row-name">'+src.name+'</span>'+
'<span class="source-row-url mono">'+src.url+'</span>'+
'<label style="font-size:11px;display:flex;align-items:center;gap:4px;cursor:pointer">'+
'<input type="checkbox" class="source-check" data-index="'+i+'" '+(src.existing ? 'checked' : '')+' style="width:14px;height:14px">'+
'启用</label>'+
(!src.existing ? '<button class="btn ghost icon-only" style="min-height:22px;width:22px;height:22px;font-size:11px;color:var(--red)" onclick="removeSource('+i+')">×</button>' : '');
list.appendChild(div);
'<button class="btn ghost icon-only" style="min-height:22px;width:22px;height:22px;font-size:12px;color:var(--red);flex-shrink:0" data-remove="'+src.name+'">×</button>';
container.appendChild(div);
});
renderFeatureList();
if (allSources.length > 0) {
if (emptyEl) emptyEl.style.display = "none";
} else {
if (emptyEl) emptyEl.style.display = "";
}
}
function renderFeatureList() {
var features = step1Features || [];
document.getElementById("assigned-sources").addEventListener("click", function(e){
var btn = e.target.closest("[data-remove]");
if (!btn) return;
var name = btn.getAttribute("data-remove");
step2Assigned = step2Assigned.filter(function(s){return s.name !== name});
step2Added = step2Added.filter(function(s){return s.name !== name});
renderAssignedSources();
});
function renderFeatures(caps) {
var featDiv = document.getElementById("feature-list");
featDiv.innerHTML = "";
if (features.length === 0) {
if (!caps || caps.length === 0) {
featDiv.innerHTML = '<span class="muted small">该设备无可用的检测功能。</span>';
return;
}
features.forEach(function(f){
caps.forEach(function(f){
var enabled = step2Features.indexOf(f.key) >= 0;
var label = document.createElement("label");
label.className = "feature-check" + (step2Features.indexOf(f.key) >= 0 ? " enabled" : "");
label.className = "feature-check" + (enabled ? " enabled" : "");
label.innerHTML =
'<input type="checkbox" value="'+f.key+'" '+(step2Features.indexOf(f.key) >= 0 ? 'checked' : '')+' style="width:14px;height:14px">'+
'<input type="checkbox" value="'+f.key+'" '+(enabled?'checked':'')+' style="width:14px;height:14px">'+
'<span>'+f.label+'</span>';
label.querySelector("input").addEventListener("change", function(){
var checked = this.checked;
var key = this.value;
if (checked) {
if (this.checked) {
if (step2Features.indexOf(key) < 0) step2Features.push(key);
} else {
step2Features = step2Features.filter(function(k){return k !== key});
}
renderFeatureList();
renderFeatures(caps);
});
featDiv.appendChild(label);
});
}
function renderFeatureList() {
var featDiv = document.getElementById("feature-list");
featDiv.innerHTML = '<span class="muted small" id="feature-placeholder">加载设备能力中...</span>';
}
document.getElementById("btn-add-source").addEventListener("click", function(){
var nameEl = document.getElementById("new-source-name");
var urlEl = document.getElementById("new-source-url");
var name = nameEl.value.trim();
var url = urlEl.value.trim();
if (!name || !url) return;
if (step2Assigned.find(function(s){return s.name===name}) || step2Added.find(function(s){return s.name===name})) {
return;
}
var btn = this;
btn.disabled = true;
btn.textContent = "添加中...";
@ -266,11 +292,11 @@
fetch("/ui/wizard/create-source", {method:"POST",body:formData,headers:{"Content-Type":"application/x-www-form-urlencoded"}})
.then(function(r){return r.json()})
.then(function(data){
step2Sources.push({name:name, url:url, existing:false});
step1AllVideoSources.push({name:name, url:url});
step2Added.push({name: name, url: url});
allVideoSources.push({name: name, url: url});
nameEl.value = "";
urlEl.value = "";
renderStep2();
renderAssignedSources();
btn.disabled = false;
btn.textContent = "添加";
})
@ -280,39 +306,28 @@
});
});
window.removeSource = function(i) {
step2Sources.splice(i, 1);
renderStep2();
};
document.getElementById("btn-prev-step").addEventListener("click", function(){setStep(1)});
document.getElementById("btn-prev-step3").addEventListener("click", function(){setStep(2)});
document.getElementById("btn-goto-step3").addEventListener("click", function(){
setStep(3);
var sources = [];
document.querySelectorAll(".source-check:checked").forEach(function(cb){
var idx = parseInt(cb.getAttribute("data-index"));
sources.push(step2Sources[idx].name);
});
var allSources = step2Assigned.concat(step2Added);
var sourceNames = allSources.map(function(s){return s.name});
document.getElementById("summary-device").textContent = selectedDeviceName;
document.getElementById("summary-sources").textContent = sources.length > 0 ? sources.join("、") : "未选择";
document.getElementById("summary-sources").textContent = sourceNames.length > 0 ? sourceNames.join("、") : "未选择";
document.getElementById("summary-features").textContent = step2Features.length > 0 ? step2Features.map(featuresToLabel).join("、") : "未选择";
});
document.getElementById("btn-deploy").addEventListener("click", function(){
var sources = [];
document.querySelectorAll(".source-check:checked").forEach(function(cb){
var idx = parseInt(cb.getAttribute("data-index"));
sources.push(step2Sources[idx].name);
});
var allSources = step2Assigned.concat(step2Added);
var sourceNames = allSources.map(function(s){return s.name});
var btn = this;
btn.disabled = true;
btn.textContent = "下发中...";
fetch("/ui/wizard/apply", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({device_id:selectedDevice, features:step2Features, source_names:sources})
body: JSON.stringify({device_id:selectedDevice, features:step2Features, source_names:sourceNames})
})
.then(function(r){return r.json()})
.then(function(result){