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

208 lines
9.1 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>顶部控制栏</title>
<script src="https://cdn.staticfile.org/axios/1.5.0/axios.min.js"></script>
<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", "Microsoft YaHei", sans-serif;
overflow: hidden;
}
/* 容器布局 */
.top-bar {
height: 95px;
display: flex; align-items: center; padding: 0 20px;
background: #fff; border-bottom: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
box-sizing: border-box;
}
.divider { width: 1px; height: 50px; background: #f0f0f0; margin: 0 25px; }
/* 左侧状态 */
.status-group { display: flex; align-items: center; min-width: 160px; }
.status-dot { width: 12px; height: 12px; border-radius: 50%; margin-right: 12px; position: relative; }
.st-run { background: #198754; box-shadow: 0 0 0 4px rgba(25, 135, 84, 0.15); }
.st-stop { background: #dc3545; box-shadow: 0 0 0 4px rgba(220, 53, 69, 0.15); }
.st-wait { background: #ffc107; box-shadow: 0 0 0 4px rgba(255, 193, 7, 0.15); animation: pulse 1.5s infinite; }
.status-main { font-size: 1.1rem; font-weight: 700; color: #343a40; line-height: 1.2; }
.status-sub { font-size: 0.75rem; color: #adb5bd; margin-top: 2px; }
/* 信息与数据 */
.info-group { display: flex; flex-direction: column; justify-content: center; gap: 4px; font-size: 0.85rem; }
.info-row { display: flex; align-items: center; }
.info-label { color: #8898aa; width: 45px; font-weight: 500; }
.info-val { color: #495057; font-family: 'Segoe UI Semibold', sans-serif; }
.stat-group { display: flex; gap: 20px; }
.stat-item { text-align: center; min-width: 70px; }
.stat-label { font-size: 0.7rem; color: #adb5bd; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
.stat-value { font-size: 1.1rem; color: #212529; font-weight: 600; font-family: Consolas, monospace; }
/* 按钮风格 */
.tool-btn {
background: linear-gradient(to bottom, #ffffff, #f1f3f5);
border: 1px solid #dee2e6; color: #495057; padding: 0;
font-size: 0.8rem; font-weight: 600; cursor: pointer;
display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 68px; height: 68px; margin-left: 10px; border-radius: 6px; transition: all 0.1s;
}
.tool-btn:hover { background: linear-gradient(to bottom, #fff, #e2e6ea); border-color: #ced4da; color: #212529; }
.tool-btn:active { background: #e9ecef; box-shadow: inset 0 2px 5px rgba(0,0,0,0.05); transform: translateY(1px); }
.tool-btn i { font-size: 1.4rem; margin-bottom: 5px; color: #6c757d; }
.btn-stop-mode i { color: #dc3545; }
.btn-play-mode i { color: #198754; }
@keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } }
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="top-bar" v-if="cam">
<div class="status-group">
<div class="status-dot" :class="statusStyle.cls"></div>
<div>
<div class="status-main">{{ statusStyle.text }}</div>
<div class="status-sub">ID: {{ cam.id }}</div>
</div>
</div>
<div class="divider"></div>
<div class="info-group">
<div class="info-row">
<span class="info-label">名称</span>
<span class="info-val" :title="cam.name">{{ truncate(cam.name) }}</span>
</div>
<div class="info-row">
<span class="info-label">地址</span>
<span class="info-val">{{ cam.ipAddress }}</span>
</div>
</div>
<div class="divider"></div>
<div class="stat-group me-auto">
<div class="stat-item">
<div class="stat-label">分辨率</div>
<div class="stat-value">{{ cam.width }}<small class="text-muted" style="font-size:0.7em">x{{cam.height}}</small></div>
</div>
<div class="stat-item">
<div class="stat-label">实时帧率</div>
<div class="stat-value">{{ cam.realFps }} <small style="font-size:0.6em">FPS</small></div>
</div>
</div>
<div class="d-flex">
<button class="tool-btn" :class="cam.status === 'Playing' ? 'btn-stop-mode' : 'btn-play-mode'" @click="togglePower" :disabled="loading">
<span v-if="loading" class="spinner-border spinner-border-sm mb-1 text-secondary"></span>
<i v-else :class="cam.status === 'Playing' ? 'bi-power' : 'bi-play-circle-fill'"></i>
<span>{{ btnText }}</span>
</button>
<button class="tool-btn" @click="openEdit" title="修改IP、端口等基础信息">
<i class="bi-pencil-square"></i>
<span>编辑</span>
</button>
<button class="tool-btn" @click="openControl" title="云台、校时、重启">
<i class="bi-dpad-fill"></i>
<span>控制</span>
</button>
<button class="tool-btn" @click="openPre" title="图像预处理">
<i class="bi-sliders2"></i>
<span>预处理</span>
</button>
<button class="tool-btn" @click="openSub" title="流订阅分发">
<i class="bi-diagram-3"></i>
<span>订阅</span>
</button>
</div>
</div>
<div v-else class="top-bar justify-content-center text-muted">
<i class="bi bi-hand-index-thumb me-2"></i> 请从左侧列表选择设备进行操作
</div>
</div>
<script src="https://cdn.staticfile.org/vue/3.3.4/vue.global.prod.min.js"></script>
<script>
const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
const API_BASE = "http://localhost:5000";
createApp({
setup() {
const cam = ref(null);
const loading = ref(false);
let pollTimer = null;
const statusStyle = computed(() => {
if(!cam.value) return {};
const s = cam.value.status;
if(s === 'Playing') return { text: '运行中', cls: 'st-run' };
if(s === 'Connecting') return { text: '启动中...', cls: 'st-wait' };
return { text: '已停止', cls: 'st-stop' };
});
const btnText = computed(() => {
if(loading.value) return '请稍候';
if(cam.value?.status === 'Playing') return '停止';
return '启动';
});
const truncate = (str) => (!str ? '-' : (str.length > 10 ? str.substring(0,10)+'..' : str));
// 状态轮询
const refreshStatus = async () => {
if (!cam.value) return;
try {
const res = await axios.get(`${API_BASE}/api/Monitor/${cam.value.id}`);
if (res.data) Object.assign(cam.value, res.data);
} catch (e) { console.warn("状态轮询失败", e); }
};
const togglePower = () => {
if (!cam.value || loading.value) return;
const isStart = cam.value.status !== 'Playing';
cam.value.status = isStart ? 'Connecting' : 'Stopped';
loading.value = true;
window.parent.postMessage({ type: 'DEVICE_CONTROL', action: isStart ? 'start' : 'stop', deviceId: cam.value.id }, '*');
setTimeout(() => { loading.value = false; }, 2000);
};
// 消息发送
const openSub = () => window.parent.postMessage({ type: 'OPEN_SUBSCRIPTION', id: cam.value.id }, '*');
const openPre = () => window.parent.postMessage({ type: 'OPEN_PREPROCESS', id: cam.value.id }, '*');
// 两个不同的弹窗入口
const openEdit = () => window.parent.postMessage({ type: 'OPEN_CAMERA_EDIT', id: cam.value.id }, '*');
const openControl = () => window.parent.postMessage({ type: 'OPEN_CAMERA_CONTROL', id: cam.value.id }, '*');
onMounted(() => {
window.addEventListener('message', (e) => {
if (e.data.type === 'UPDATE_TOP_INFO') cam.value = e.data.data;
});
pollTimer = setInterval(() => { if(cam.value) refreshStatus(); }, 2000);
});
onUnmounted(() => { if (pollTimer) clearInterval(pollTimer); });
return { cam, statusStyle, btnText, loading, truncate, togglePower, openSub, openPre, openEdit, openControl };
}
}).mount('#app');
</script>
</body>
</html>