支持通过网页增加、删除、修改摄像头配置信息
支持摄像头配置信息中句柄的设置,并实测有效
This commit is contained in:
175
SHH.CameraSdk/Htmls/List.html
Normal file
175
SHH.CameraSdk/Htmls/List.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 !important; }
|
||||
body { background-color: #fff; font-size: 0.85rem; height: 100vh; overflow: hidden; display: flex; flex-direction: column; font-family: "Segoe UI", sans-serif; }
|
||||
|
||||
.sidebar-header { padding: 12px; border-bottom: 1px solid #eee; background: #f8f9fa; flex-shrink: 0; }
|
||||
.sidebar-list { flex: 1; overflow-y: auto; }
|
||||
|
||||
/* 经典卡片样式 */
|
||||
.camera-card { padding: 10px 15px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: 0.2s; border-left: 4px solid transparent; }
|
||||
.camera-card:hover { background-color: #f8f9fa; }
|
||||
.camera-card.active { background-color: #e3f2fd; border-left-color: #0d6efd !important; }
|
||||
|
||||
/* 状态边框颜色 */
|
||||
.border-run { border-left-color: #198754; }
|
||||
.border-wait { border-left-color: #ffc107; }
|
||||
.border-err { border-left-color: #dc3545; }
|
||||
|
||||
/* 细节样式 */
|
||||
.ping-dot { font-size: 10px; margin-right: 6px; }
|
||||
.ping-online { color: #198754; text-shadow: 0 0 5px rgba(25, 135, 84, 0.5); }
|
||||
.ping-offline { color: #adb5bd; }
|
||||
|
||||
.device-id { font-weight: bold; color: #333; }
|
||||
.info-row { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; }
|
||||
|
||||
.res-tag {
|
||||
font-size: 0.7rem; color: #666; background: #f0f0f0;
|
||||
padding: 1px 5px; border-radius: 3px; font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app" v-cloak class="d-flex flex-column h-100">
|
||||
<div class="sidebar-header">
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control border-start-0" v-model="searchText" placeholder="搜索 ID / IP / 名称...">
|
||||
|
||||
<button class="btn btn-primary" @click="openAddPage" title="添加新设备">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center px-1">
|
||||
<small class="text-muted">运行中: <span class="text-success fw-bold">{{ playingCount }}</span> / {{ list.length }}</small>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input class="form-check-input" type="checkbox" v-model="autoRefresh" id="ar" style="transform: scale(0.8);">
|
||||
<label class="form-check-label small" for="ar">自动刷新</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-list">
|
||||
<div v-for="cam in filteredList" :key="cam.id"
|
||||
class="camera-card"
|
||||
:class="[{'active': sid === cam.id}, getStatusBorder(cam.status)]"
|
||||
@click="onSelect(cam)">
|
||||
|
||||
<div class="info-row mb-1">
|
||||
<span class="device-id text-truncate" style="max-width: 160px;" :title="cam.name">
|
||||
<i class="bi bi-circle-fill ping-dot" :class="cam.isPhysicalOnline ? 'ping-online' : 'ping-offline'"></i>
|
||||
#{{ cam.id }} {{ cam.name || '未命名相机' }}
|
||||
</span>
|
||||
|
||||
<span v-if="isRun(cam.status)" class="badge bg-success" style="font-size: 0.6rem; padding: 2px 5px;">LIVE</span>
|
||||
<span v-else-if="isWait(cam.status)" class="badge bg-warning text-dark" style="font-size: 0.6rem;">BUSY</span>
|
||||
<span v-else-if="cam.status === 'Faulted'" class="badge bg-danger" style="font-size: 0.6rem;">ERR</span>
|
||||
<span v-else class="badge bg-light text-muted border" style="font-size: 0.6rem;">STOP</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row text-muted small">
|
||||
<span><i class="bi bi-link-45deg"></i> {{ cam.ipAddress }}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span v-if="cam.resolution" class="res-tag" title="当前分辨率">
|
||||
<i class="bi bi-aspect-ratio me-1"></i>{{ cam.resolution }}
|
||||
</span>
|
||||
<span v-if="isRun(cam.status)" class="text-primary fw-bold" style="min-width: 45px; text-align: right;">
|
||||
{{ cam.realFps?.toFixed(1) }} FPS
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredList.length === 0" class="text-center mt-5 text-muted">
|
||||
<i class="bi bi-inbox fs-2 d-block mb-2 opacity-25"></i>
|
||||
未找到设备
|
||||
</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, computed, onMounted } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const API_BASE = "http://localhost:5000";
|
||||
const list = ref([]);
|
||||
const sid = ref(null);
|
||||
const searchText = ref("");
|
||||
const autoRefresh = ref(true);
|
||||
|
||||
// 状态判断
|
||||
const isRun = (s) => s === 'Playing' || s === 'Streaming';
|
||||
const isWait = (s) => ['Connecting', 'Authorizing', 'Reconnecting'].includes(s);
|
||||
const getStatusBorder = (s) => {
|
||||
if (isRun(s)) return 'border-run';
|
||||
if (isWait(s)) return 'border-wait';
|
||||
if (s === 'Faulted') return 'border-err';
|
||||
return '';
|
||||
};
|
||||
|
||||
const fetchList = async () => {
|
||||
try {
|
||||
const res = await axios.get(API_BASE + "/api/Monitor/all");
|
||||
const data = res.data || [];
|
||||
list.value = data.map(item => ({
|
||||
...item,
|
||||
// 兼容大小写
|
||||
resolution: item.resolution || item.Resolution || (item.width ? `${item.width}x${item.height}` : null)
|
||||
}));
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const onSelect = (cam) => {
|
||||
sid.value = cam.id;
|
||||
if(window.parent) window.parent.postMessage({ type: 'DEVICE_SELECTED', data: JSON.parse(JSON.stringify(cam)) }, '*');
|
||||
};
|
||||
|
||||
// 【触发】打开右侧添加页
|
||||
const openAddPage = () => {
|
||||
if(window.parent) window.parent.postMessage({ type: 'OPEN_CAMERA_ADD' }, '*');
|
||||
};
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if(e.data.type === 'REFRESH_LIST') fetchList();
|
||||
});
|
||||
|
||||
const filteredList = computed(() => {
|
||||
const kw = searchText.value.toLowerCase().trim();
|
||||
if (!kw) return list.value;
|
||||
return list.value.filter(i =>
|
||||
String(i.id).includes(kw) ||
|
||||
(i.name && i.name.toLowerCase().includes(kw)) ||
|
||||
(i.ipAddress && i.ipAddress.includes(kw))
|
||||
);
|
||||
});
|
||||
|
||||
const playingCount = computed(() => list.value.filter(i => isRun(i.status)).length);
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
setInterval(() => { if (autoRefresh.value) fetchList(); }, 3000);
|
||||
});
|
||||
|
||||
return {
|
||||
list, sid, searchText, autoRefresh,
|
||||
filteredList, playingCount,
|
||||
onSelect, isRun, isWait, getStatusBorder,
|
||||
openAddPage
|
||||
};
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user