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

147 lines
8.2 KiB
HTML

<!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; }
body { background: #fff; margin: 0; padding: 0; font-family: "Segoe UI", sans-serif; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
.modal-header-custom { padding: 12px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; }
.content-body { flex: 1; padding: 20px; overflow-y: auto; display: flex; gap: 20px; }
/* 左侧:云台控制区 */
.ptz-section { flex: 0 0 240px; display: flex; flex-direction: column; align-items: center; border-right: 1px solid #eee; padding-right: 20px; }
.d-pad { position: relative; width: 180px; height: 180px; background: #f1f3f5; border-radius: 50%; margin-bottom: 20px; box-shadow: inset 0 2px 10px rgba(0,0,0,0.05); }
.d-btn { position: absolute; width: 50px; height: 50px; border: none; background: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); color: #495057; transition: 0.1s; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.d-btn:active { background: #e9ecef; transform: scale(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); color: #0d6efd; }
.d-btn i { font-size: 1.5rem; }
.btn-up { top: 10px; left: 65px; }
.btn-down { bottom: 10px; left: 65px; }
.btn-left { top: 65px; left: 10px; }
.btn-right { top: 65px; right: 10px; }
.btn-center { top: 65px; left: 65px; border-radius: 50%; background: #e7f1ff; color: #0d6efd; font-weight: bold; font-size: 0.8rem; }
.zoom-ctrl { display: flex; width: 100%; gap: 10px; justify-content: center; }
.zoom-btn { flex: 1; padding: 8px; border: 1px solid #dee2e6; background: #fff; border-radius: 6px; font-size: 0.9rem; font-weight: 600; color: #555; }
.zoom-btn:active { background: #f8f9fa; border-color: #adb5bd; }
/* 右侧:系统维护区 */
.sys-section { flex: 1; display: flex; flex-direction: column; gap: 15px; }
.func-card { border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; background: #fff; transition: 0.2s; }
.func-card:hover { border-color: #dee2e6; box-shadow: 0 2px 8px rgba(0,0,0,0.02); }
.func-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 10px; color: #343a40; display: flex; align-items: center; }
.time-box { background: #f8f9fa; padding: 8px 12px; border-radius: 4px; font-family: monospace; font-size: 1.1rem; letter-spacing: 1px; color: #0d6efd; text-align: center; border: 1px solid #e9ecef; margin-bottom: 10px; }
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="modal-header-custom">
<h6 class="m-0 fw-bold"><i class="bi bi-joystick me-2 text-primary"></i>设备控制台</h6>
<div class="text-muted small">ID: {{ deviceId }}</div>
</div>
<div class="content-body">
<div class="ptz-section">
<h6 class="text-muted small mb-3 fw-bold">云台控制 (PTZ)</h6>
<div class="d-pad">
<button class="d-btn btn-up" @mousedown="ptz('up')" @mouseup="ptzStop"><i class="bi bi-caret-up-fill"></i></button>
<button class="d-btn btn-left" @mousedown="ptz('left')" @mouseup="ptzStop"><i class="bi bi-caret-left-fill"></i></button>
<button class="d-btn btn-center" @click="ptz('home')" title="回原点">HOME</button>
<button class="d-btn btn-right" @mousedown="ptz('right')" @mouseup="ptzStop"><i class="bi bi-caret-right-fill"></i></button>
<button class="d-btn btn-down" @mousedown="ptz('down')" @mouseup="ptzStop"><i class="bi bi-caret-down-fill"></i></button>
</div>
<div class="zoom-ctrl">
<button class="zoom-btn" @mousedown="ptz('zoomIn')" @mouseup="ptzStop"><i class="bi bi-zoom-in me-1"></i>放大</button>
<button class="zoom-btn" @mousedown="ptz('zoomOut')" @mouseup="ptzStop"><i class="bi bi-zoom-out me-1"></i>缩小</button>
</div>
<div class="mt-2 text-muted small" style="font-size: 0.75rem;"><i class="bi bi-info-circle me-1"></i>长按移动,松开停止</div>
</div>
<div class="sys-section">
<div class="func-card">
<div class="func-title"><i class="bi bi-clock-history me-2 text-success"></i>时间同步</div>
<div class="text-muted small mb-2">设备当前时间:</div>
<div class="time-box">{{ deviceTime || '--:--:--' }}</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary flex-fill" @click="getDeviceTime"><i class="bi bi-arrow-clockwise me-1"></i>刷新</button>
<button class="btn btn-sm btn-success flex-fill" @click="syncTime" :disabled="syncing">
<span v-if="syncing" class="spinner-border spinner-border-sm me-1"></span>
<i v-else class="bi bi-check2-circle me-1"></i>同步本机
</button>
</div>
</div>
<div class="func-card">
<div class="func-title"><i class="bi bi-tools me-2 text-warning"></i>系统维护</div>
<button class="btn btn-sm btn-light border w-100 text-start" @click="reboot"><i class="bi bi-bootstrap-reboot me-2"></i>重启设备</button>
</div>
</div>
</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, onMounted } = Vue;
let API_BASE = "";
createApp({
setup() {
const deviceId = ref(0);
const deviceTime = ref("");
const syncing = ref(false);
// PTZ控制
const ptz = async (action) => {
log(`PTZ: ${action}`);
try { await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/ptz?action=${action}&speed=5`); } catch(e) {}
};
const ptzStop = async () => {
try { await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/ptz?action=stop`); } catch(e) {}
};
// 校时逻辑
const getDeviceTime = async () => {
const now = new Date(); now.setMinutes(now.getMinutes() - 5); // 模拟
deviceTime.value = now.toLocaleTimeString();
};
const syncTime = async () => {
syncing.value = true;
try {
await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/sync-time`, { time: new Date().toISOString() });
alert("指令已下发");
deviceTime.value = new Date().toLocaleTimeString();
} catch(e) { alert("失败: " + e.message); }
finally { syncing.value = false; }
};
const reboot = async () => {
if(confirm("确定要重启设备吗?")) {
try { await axios.post(`${API_BASE}/api/Cameras/${deviceId.value}/reboot`); alert("重启中..."); } catch(e){}
}
};
const log = (msg) => window.parent.postMessage({ type: 'API_LOG', log: { method: 'PTZ', url: msg, status: 200, msg: 'Sent' } }, '*');
onMounted(() => {
window.addEventListener('message', (e) => {
if (e.data.type === 'LOAD_CTRL_DATA') {
if(e.data.apiBase) API_BASE = e.data.apiBase;
deviceId.value = e.data.deviceId;
getDeviceTime();
}
});
});
return { deviceId, deviceTime, syncing, ptz, ptzStop, getDeviceTime, syncTime, reboot };
}
}).mount('#app');
</script>
</body>
</html>