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

175 lines
7.8 KiB
HTML
Raw Normal View History

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