Files
Ayay/SHH.CameraSdk/Htmls/Preprocessing.html

375 lines
18 KiB
HTML
Raw Normal View History

<!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>