Files
Ayay/SHH.CameraSdk/Htmls/Preprocessing.html
twice109 2ee25a4f7c 支持通过网页增加、删除、修改摄像头配置信息
支持摄像头配置信息中句柄的设置,并实测有效
2025-12-28 08:07:55 +08:00

375 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图像预处理配置</title>
<link href="https://cdn.staticfile.org/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.staticfile.org/bootstrap-icons/1.10.0/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
[v-cloak] { display: none; }
/* 1. 基础布局全屏、Flex */
html, body {
height: 100%; margin: 0; padding: 0;
background: #fff; font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
overflow: hidden;
}
#app {
height: 100%; display: flex; flex-direction: column;
}
/* 2. 头部样式 */
.page-header {
padding: 15px 30px;
border-bottom: 1px solid #e9ecef;
background: #fff;
display: flex; justify-content: space-between; align-items: center;
flex-shrink: 0;
}
.page-title { font-size: 1.2rem; font-weight: 700; color: #343a40; display: flex; align-items: center; }
/* 3. 中间内容区 */
.content-body {
flex: 1; overflow-y: auto; padding: 30px 40px;
max-width: 1200px; margin: 0 auto; width: 100%;
background: #fff;
}
.content-body::-webkit-scrollbar { width: 8px; }
.content-body::-webkit-scrollbar-track { background: #f8f9fa; }
.content-body::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; }
/* 4. 分区标题:亮蓝色风格 */
.section-header {
display: flex; align-items: center; margin-top: 10px; margin-bottom: 25px;
padding-bottom: 10px; border-bottom: 2px solid #e7f1ff;
}
.section-header h6 {
margin: 0; font-weight: 800; color: #0d6efd;
font-size: 1rem; text-transform: uppercase; letter-spacing: 0.5px;
}
/* 5. 组件样式 */
.form-label { font-weight: 600; color: #495057; margin-bottom: 8px; font-size: 0.9rem; }
.form-control { padding: 10px 15px; border-radius: 6px; border-color: #dee2e6; }
.form-control:focus { border-color: #86b7fe; box-shadow: 0 0 0 3px rgba(13,110,253,0.1); }
.preset-badge {
cursor: pointer; user-select: none; border: 1px solid #dee2e6;
color: #555; background: #fff; transition: all 0.2s;
padding: 8px 16px; border-radius: 6px; font-size: 0.85rem; margin-right: 8px; margin-bottom: 8px;
display: inline-block;
}
.preset-badge:hover { background-color: #f8f9fa; border-color: #adb5bd; }
.preset-badge.active { background-color: #e7f1ff; color: #0d6efd; border-color: #0d6efd; font-weight: 600; }
.source-info-bar {
background-color: #f8f9fa; border: 1px dashed #ced4da; border-radius: 8px;
padding: 12px 20px; margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center;
}
/* 6. 底部按钮栏 */
.footer-bar {
padding: 15px 40px; border-top: 1px solid #e9ecef; background: #fff;
display: flex; justify-content: space-between; align-items: center;
flex-shrink: 0; z-index: 10;
}
.success-msg { color: #198754; font-weight: 500; display: flex; align-items: center; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.is-invalid { border-color: #dc3545 !important; }
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="page-header">
<div class="page-title">
<i class="bi bi-sliders2 me-2 text-primary"></i>
图像预处理配置 <span class="text-muted ms-2 fw-normal fs-6">(ID: {{ deviceId }})</span>
</div>
</div>
<div class="content-body">
<div class="section-header">
<h6><i class="bi bi-aspect-ratio me-2"></i>分辨率控制 (Resolution)</h6>
</div>
<div class="row mb-4">
<div class="col-md-4">
<div>
<span class="text-muted me-2"><i class="bi bi-camera-video"></i> 源画面尺寸:</span>
<strong class="font-monospace fs-5 text-dark">{{ baseRes.w }} x {{ baseRes.h }}</strong>
<span class="badge bg-light text-secondary border me-2" v-if="currentScale !== '1.00'">缩放比: {{ currentScale }}x</span>
</div>
</div>
<div class="col-md-4">
<div class="form-check form-switch p-2 ps-5 border rounded bg-white">
<input class="form-check-input" type="checkbox" v-model="config.allowShrink" style="margin-left: -2.5em;">
<label class="form-check-label fw-bold">允许缩小 (Shrink)</label>
<div class="text-muted small mt-1">当源分辨率大于目标时生效,通常建议开启以节省带宽。</div>
</div>
</div>
<div class="col-md-4">
<div class="form-check form-switch p-2 ps-5 border rounded bg-white">
<input class="form-check-input" type="checkbox" v-model="config.allowEnlarge" @change="validateInputLimit" style="margin-left: -2.5em;">
<label class="form-check-label fw-bold">允许放大 (Expand)</label>
<div class="text-muted small mt-1">允许输出比源画面更大的分辨率(可能会增加系统负载)。</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="d-flex justify-content-between mb-2">
<label class="form-label">目标分辨率 (Target Size)</label>
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="lockRatio" v-model="lockRatio">
<label class="form-check-label small text-muted" for="lockRatio"><i class="bi bi-link-45deg"></i> 锁定比例</label>
</div>
</div>
<div class="row g-2 align-items-center">
<div class="col-5">
<div class="input-group">
<span class="input-group-text bg-light text-muted">W</span>
<input type="number" class="form-control font-monospace" v-model.number="config.width"
@input="onWidthChange" @blur="validateInputLimit" :class="{'is-invalid': inputError.w}">
</div>
</div>
<div class="col-auto text-muted"><i class="bi bi-x-lg"></i></div>
<div class="col-5">
<div class="input-group">
<span class="input-group-text bg-light text-muted">H</span>
<input type="number" class="form-control font-monospace" v-model.number="config.height"
@input="onHeightChange" @blur="validateInputLimit" :class="{'is-invalid': inputError.h}">
</div>
</div>
</div>
<div v-if="inputError.msg" class="text-danger small mt-2"><i class="bi bi-exclamation-circle me-1"></i>{{ inputError.msg }}</div>
</div>
<div class="col-md-5">
<label class="form-label d-block mb-3">常用预设 (Presets)</label>
<span v-for="p in presets" :key="p.label" class="preset-badge"
:class="{ 'active': config.width === p.w && config.height === p.h }" @click="applyPreset(p)">
{{ p.label }} <span class="opacity-50 ms-1" v-if="p.w > 0">{{ p.w }}x{{ p.h }}</span>
</span>
</div>
</div>
<div class="mb-5">
<label class="form-label d-flex justify-content-between">
<span>快速缩放</span>
<span class="text-primary font-monospace">{{ sliderScale }}x</span>
</label>
<div class="d-flex align-items-center gap-2 mt-2">
<span class="small text-muted">0.1x</span>
<input type="range" class="form-range" min="0.1" max="2.0" step="0.1"
v-model.number="sliderScale" @input="onSliderChange">
<span class="small text-muted">2.0x</span>
</div>
</div>
<div class="section-header">
<h6><i class="bi bi-magic me-2"></i>图像增强 (Enhancement)</h6>
</div>
<div class="row">
<div class="col-12">
<div class="form-check form-switch mb-3 ps-5">
<input class="form-check-input" type="checkbox" v-model="config.enableGain" style="margin-left: -2.5em; transform: scale(1.2);">
<label class="form-check-label fw-bold">启用亮度/增益调节 (Brightness)</label>
</div>
</div>
<div class="col-md-8 transition-box" v-show="config.enableGain">
<div class="p-3 bg-light border rounded">
<div class="d-flex justify-content-between mb-2">
<span class="small fw-bold text-muted">强度 (Intensity)</span>
<span class="fw-bold text-primary">{{ config.brightness }}</span>
</div>
<div class="d-flex align-items-center gap-3">
<i class="bi bi-sun text-muted"></i>
<input type="range" class="form-range" min="0" max="255" step="1" v-model.number="config.brightness">
<i class="bi bi-sun-fill text-warning"></i>
</div>
</div>
</div>
</div>
<div style="height: 40px;"></div>
</div>
<div class="footer-bar">
<div class="me-auto">
<transition name="fade">
<div v-if="saveSuccess" class="success-msg">
<i class="bi bi-check-circle-fill me-2 fs-5"></i>
<span>配置已生效 (Applied)</span>
</div>
</transition>
<span v-if="errorMsg" class="text-danger small"><i class="bi bi-exclamation-triangle me-1"></i>{{ errorMsg }}</span>
</div>
<button class="btn btn-light border px-4" @click="closeMode">取消</button>
<button class="btn btn-primary px-4 shadow-sm" @click="saveConfig" :disabled="loading || !!inputError.msg">
<span v-if="loading" class="spinner-border spinner-border-sm me-1"></span>
<i v-else class="bi bi-floppy2-fill me-1"></i>
保存应用
</button>
</div>
</div>
<script src="https://cdn.staticfile.org/vue/3.3.4/vue.global.prod.min.js"></script>
<script src="https://cdn.staticfile.org/axios/1.5.0/axios.min.js"></script>
<script>
const { createApp, ref, reactive, computed, onMounted } = Vue;
let API_BASE = "";
createApp({
setup() {
const deviceId = ref(0);
const loading = ref(false);
const saveSuccess = ref(false);
const errorMsg = ref("");
const lockRatio = ref(true);
const sliderScale = ref(1.0);
const baseRes = reactive({ w: 0, h: 0 });
const config = reactive({ width: 1280, height: 720, allowShrink: true, allowEnlarge: false, enableGain: false, brightness: 128 });
const inputError = reactive({ w: false, h: false, msg: '' });
const presets = [
{ label: 'Original', w: 0, h: 0 },
{ label: '2K', w: 2560, h: 1440 },
{ label: '1080P', w: 1920, h: 1080 },
{ label: '720P', w: 1280, h: 720 },
{ label: 'D1', w: 704, h: 576 },
{ label: 'Yolo', w: 640, h: 640 }
];
const currentScale = computed(() => (!baseRes.w ? "1.00" : (config.width / baseRes.w).toFixed(2)));
const logToParent = (status, msg, url, method = 'POST') => {
if (window.parent) window.parent.postMessage({ type: 'API_LOG', log: { method, url, status, msg } }, '*');
};
const loadDeviceInfo = async (id) => {
deviceId.value = id;
loading.value = true;
const urlStatus = `${API_BASE}/api/Monitor/${id}`;
const urlConfig = `${API_BASE}/api/cameras/${id}/processing`;
try {
const [resStatus, resConfig] = await Promise.allSettled([ axios.get(urlStatus), axios.get(urlConfig) ]);
if (resStatus.status === 'fulfilled' && resStatus.value.data) {
baseRes.w = resStatus.value.data.width || 2560;
baseRes.h = resStatus.value.data.height || 1440;
} else { baseRes.w = 2560; baseRes.h = 1440; }
if (resConfig.status === 'fulfilled' && resConfig.value.data) {
const d = resConfig.value.data;
if (d.targetWidth) config.width = d.targetWidth;
if (d.targetHeight) config.height = d.targetHeight;
config.allowShrink = d.enableShrink ?? true;
config.allowEnlarge = d.enableExpand ?? false;
config.enableGain = d.enableBrightness ?? false;
config.brightness = d.brightness ?? 128;
logToParent(200, '配置回显成功', urlConfig, 'GET');
}
if(baseRes.w > 0) sliderScale.value = (config.width / baseRes.w).toFixed(1);
} catch (e) { console.error(e); }
finally { loading.value = false; validateInputLimit(); }
};
const validateInputLimit = () => {
inputError.w = false; inputError.h = false; inputError.msg = "";
if (!config.allowEnlarge && baseRes.w > 0) {
let corrected = false;
if (config.width > baseRes.w) { config.width = baseRes.w; inputError.w = true; corrected = true; }
if (config.height > baseRes.h) { config.height = baseRes.h; inputError.h = true; corrected = true; }
if (corrected) {
inputError.msg = "尺寸限制:最大为源分辨率 (需开启放大)";
setTimeout(() => inputError.msg = "", 3000);
}
}
if(baseRes.w > 0) sliderScale.value = (config.width / baseRes.w).toFixed(1);
};
const onSliderChange = () => {
config.width = Math.round(baseRes.w * sliderScale.value);
config.height = Math.round(baseRes.h * sliderScale.value);
validateInputLimit();
};
const onWidthChange = () => {
if (lockRatio.value && baseRes.w > 0) config.height = Math.round(config.width * (baseRes.h / baseRes.w));
};
const onHeightChange = () => {
if (lockRatio.value && baseRes.h > 0) config.width = Math.round(config.height * (baseRes.w / baseRes.h));
};
const applyPreset = (p) => {
if (p.label === 'Original') { config.width = baseRes.w; config.height = baseRes.h; }
else { config.width = p.w; config.height = p.h; }
validateInputLimit();
};
const saveConfig = async () => {
validateInputLimit();
if(inputError.msg) return;
loading.value = true;
saveSuccess.value = false;
errorMsg.value = "";
const url = `${API_BASE}/api/cameras/${deviceId.value}/processing`;
const payload = {
DeviceId: deviceId.value, EnableShrink: config.allowShrink, EnableExpand: config.allowEnlarge,
TargetWidth: parseInt(config.width), TargetHeight: parseInt(config.height),
EnableBrightness: config.enableGain, Brightness: parseInt(config.brightness)
};
try {
await axios.post(url, payload);
logToParent(200, '配置已应用', url);
saveSuccess.value = true;
setTimeout(() => { saveSuccess.value = false; }, 2500);
} catch (e) {
const msg = e.response?.data?.message || e.message;
errorMsg.value = "提交失败: " + msg;
logToParent('ERROR', msg, url);
} finally {
loading.value = false;
}
};
// 【修改】关闭模式,而非关闭弹窗
const closeMode = () => window.parent.postMessage({ type: 'CLOSE_PREPROCESS_MODE' }, '*');
onMounted(() => {
window.addEventListener('message', (e) => {
if (e.data.type === 'LOAD_PREPROCESS_DATA') {
if (e.data.apiBase) API_BASE = e.data.apiBase;
loadDeviceInfo(e.data.deviceId);
}
});
});
return {
deviceId, config, baseRes, presets, loading, errorMsg, saveSuccess,
lockRatio, sliderScale, currentScale, inputError,
saveConfig, closeMode, applyPreset, onWidthChange, onHeightChange, validateInputLimit, onSliderChange
};
}
}).mount('#app');
</script>
</body>
</html>