initial commit
This commit is contained in:
128
templates/index.html
Normal file
128
templates/index.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Real-time Image Display via WebSocket</title>
|
||||
<!-- 引入 Socket.IO 客户端库 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.image-container {
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.image-container img {
|
||||
width: 400px;
|
||||
height: auto;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
#status {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
#button-container {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Live Stereo Camera Feed (via WebSocket)</h1>
|
||||
<!-- 添加导航链接 -->
|
||||
<p style="text-align: center;"><a href="/list">View Saved Images</a></p>
|
||||
<div id="status">Connecting to server...</div>
|
||||
|
||||
<!-- 新增按钮容器 -->
|
||||
<div id="button-container">
|
||||
<button id="capBtn">触发采集</button>
|
||||
<!-- <div id="button-response">Button response will appear here.</div> -->
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<h3>Left Camera</h3>
|
||||
<img id="left-img" src="" alt="No Left Image">
|
||||
<div class="timestamp" id="left-ts">No timestamp</div>
|
||||
</div>
|
||||
<div class="image-container">
|
||||
<h3>Right Camera</h3>
|
||||
<img id="right-img" src="" alt="No Right Image">
|
||||
<div class="timestamp" id="right-ts">No timestamp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 建立 Socket.IO 连接
|
||||
const socket = io('http://' + document.domain + ':' + location.port);
|
||||
|
||||
socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
document.getElementById('status').innerHTML = 'Connected to server. Waiting for images...';
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
document.getElementById('status').innerHTML = 'Disconnected from server.';
|
||||
});
|
||||
|
||||
// 监听 'update_image' 事件
|
||||
socket.on('update_image', function(data) {
|
||||
// 更新左图
|
||||
if (data.left_image) {
|
||||
document.getElementById('left-img').src = 'data:image/jpeg;base64,' + data.left_image;
|
||||
}
|
||||
// 更新右图
|
||||
if (data.right_image) {
|
||||
document.getElementById('right-img').src = 'data:image/jpeg;base64,' + data.right_image;
|
||||
}
|
||||
// 更新时间戳
|
||||
if (data.timestamp) {
|
||||
document.getElementById('left-ts').textContent = 'Timestamp: ' + data.timestamp;
|
||||
document.getElementById('right-ts').textContent = 'Timestamp: ' + data.timestamp;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', function(data) {
|
||||
console.error('WebSocket Error:', data);
|
||||
document.getElementById('status').innerHTML = 'Error: ' + data.message;
|
||||
});
|
||||
|
||||
// --- 新增:处理按钮点击事件 ---
|
||||
document.getElementById('capBtn').onclick = function() {
|
||||
console.log("触发采集.");
|
||||
socket.emit('capture_button', {
|
||||
client_time: new Date().toISOString(),
|
||||
// button_id: 'myButton'
|
||||
});
|
||||
};
|
||||
|
||||
// --- 新增:监听服务器对按钮点击的响应 ---
|
||||
socket.on('button_response', function(data) {
|
||||
console.log('Received button response from server:', data);
|
||||
document.getElementById('button-response').textContent =
|
||||
`Response: ${data.message} (Server Time: ${new Date(data.server_time * 1000).toISOString()})`;
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
260
templates/list_images.html
Normal file
260
templates/list_images.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Saved Images List</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js "></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.controls { margin-bottom: 20px; }
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin-right: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background-color: #0056b3; }
|
||||
button:disabled { background-color: #cccccc; cursor: not-allowed; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
.image-preview { width: 100px; height: auto; }
|
||||
input[type="checkbox"] { transform: scale(1.2); }
|
||||
#status { padding: 10px; margin: 10px 0; background-color: #f0f0f0; }
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Saved Images</h1>
|
||||
<!-- 添加导航链接 -->
|
||||
<p><a href="/">Back to Live Feed</a></p>
|
||||
<div id="status">Loading images...</div>
|
||||
<div class="controls">
|
||||
<button id="refreshBtn">Refresh List</button>
|
||||
<button id="exportBtn" disabled>Export Selected</button>
|
||||
<button id="deleteSelectedBtn" disabled>Delete Selected</button>
|
||||
</div>
|
||||
<table id="imagesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" id="selectAllCheckbox">
|
||||
<label for="selectAllCheckbox" style="display: inline-block; margin-left: 5px; cursor: pointer;">Select All</label>
|
||||
</th>
|
||||
<th>ID</th>
|
||||
<th>Left Image</th>
|
||||
<th>Right Image</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Created At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="imagesTableBody">
|
||||
<!-- 动态加载行 -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
const socket = io('http://' + document.domain + ':' + location.port);
|
||||
let allImages = []; // 存储所有图片数据
|
||||
|
||||
// 获取图片列表
|
||||
async function loadImages() {
|
||||
document.getElementById('status').textContent = 'Loading images...';
|
||||
try {
|
||||
const response = await fetch('/api/images');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
allImages = await response.json();
|
||||
renderTable(allImages);
|
||||
document.getElementById('status').textContent = `Loaded ${allImages.length} images.`;
|
||||
updateSelectAllCheckbox(); // 加载后更新全选框状态
|
||||
} catch (error) {
|
||||
console.error('Error loading images:', error);
|
||||
document.getElementById('status').textContent = 'Error loading images: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染表格
|
||||
function renderTable(images) {
|
||||
const tbody = document.getElementById('imagesTableBody');
|
||||
tbody.innerHTML = ''; // 清空现有内容
|
||||
|
||||
images.forEach(image => {
|
||||
const row = tbody.insertRow();
|
||||
row.insertCell(0).innerHTML = `<input type="checkbox" class="selectCheckbox" data-id="${image.id}">`;
|
||||
row.insertCell(1).textContent = image.id;
|
||||
|
||||
const leftCell = row.insertCell(2);
|
||||
const leftImg = document.createElement('img');
|
||||
// 修改这里:使用 Flask 静态文件路径
|
||||
leftImg.src = `/static/received/left/${image.left_filename}`;
|
||||
leftImg.alt = "Left Image";
|
||||
leftImg.className = 'image-preview';
|
||||
leftImg.onerror = function() { this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="75" viewBox="0 0 100 75"><rect width="100" height="75" fill="%23eee"/><text x="50" y="40" font-family="Arial" font-size="12" fill="%23999" text-anchor="middle">No Image</text></svg>'; };
|
||||
leftCell.appendChild(leftImg);
|
||||
|
||||
const rightCell = row.insertCell(3);
|
||||
const rightImg = document.createElement('img');
|
||||
// 修改这里:使用 Flask 静态文件路径
|
||||
rightImg.src = `/static/received/right/${image.right_filename}`;
|
||||
rightImg.alt = "Right Image";
|
||||
rightImg.className = 'image-preview';
|
||||
rightImg.onerror = function() { this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="75" viewBox="0 0 100 75"><rect width="100" height="75" fill="%23eee"/><text x="50" y="40" font-family="Arial" font-size="12" fill="%23999" text-anchor="middle">No Image</text></svg>'; };
|
||||
rightCell.appendChild(rightImg);
|
||||
|
||||
row.insertCell(4).textContent = new Date(image.timestamp * 1000).toISOString();
|
||||
row.insertCell(5).textContent = image.created_at;
|
||||
row.insertCell(6).innerHTML = `<button onclick="deleteImage(${image.id})">Delete</button>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
async function deleteImage(id) {
|
||||
if (!confirm(`Are you sure you want to delete image ID ${id}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/images', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// 从本地数组中移除
|
||||
allImages = allImages.filter(img => img.id !== id);
|
||||
renderTable(allImages);
|
||||
updateSelectAllCheckbox(); // 删除后更新全选框状态
|
||||
document.getElementById('status').textContent = `Image ID ${id} deleted.`;
|
||||
} catch (error) {
|
||||
console.error('Error deleting image:', error);
|
||||
document.getElementById('status').textContent = 'Error deleting image: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的图片 ID
|
||||
function getSelectedIds() {
|
||||
const checkboxes = document.querySelectorAll('.selectCheckbox:checked');
|
||||
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));
|
||||
}
|
||||
|
||||
// 更新全选复选框的状态
|
||||
function updateSelectAllCheckbox() {
|
||||
const checkboxes = document.querySelectorAll('.selectCheckbox');
|
||||
const allSelected = checkboxes.length > 0 && checkboxes.length === document.querySelectorAll('.selectCheckbox:checked').length;
|
||||
document.getElementById('selectAllCheckbox').checked = allSelected;
|
||||
updateExportDeleteButtons();
|
||||
}
|
||||
|
||||
// 更新导出和删除按钮的启用状态
|
||||
function updateExportDeleteButtons() {
|
||||
const selectedCount = getSelectedIds().length;
|
||||
document.getElementById('exportBtn').disabled = selectedCount === 0;
|
||||
document.getElementById('deleteSelectedBtn').disabled = selectedCount === 0;
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadImages);
|
||||
|
||||
// --- 修改:使用 fetch API 发起导出请求 ---
|
||||
document.getElementById('exportBtn').addEventListener('click', async function() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Please select at least one image to export.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 fetch API 发起 POST 请求
|
||||
const response = await fetch('/api/images/export', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // 设置正确的 Content-Type
|
||||
},
|
||||
body: JSON.stringify({ ids: selectedIds }) // 发送 JSON 格式的请求体
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 如果响应是成功的,浏览器会接收到 ZIP 文件,通常会自动触发下载
|
||||
// 我们可以通过创建一个隐藏的链接来模拟下载
|
||||
const blob = await response.blob(); // 获取响应的 blob 数据
|
||||
const url = window.URL.createObjectURL(blob); // 创建一个 URL 对象
|
||||
const a = document.createElement('a'); // 创建一个隐藏的 <a> 标签
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = 'exported_images.zip'; // 设置下载的文件名
|
||||
document.body.appendChild(a); // 将 <a> 标签添加到页面
|
||||
a.click(); // 模拟点击 <a> 标签
|
||||
window.URL.revokeObjectURL(url); // 释放 URL 对象
|
||||
document.body.removeChild(a); // 移除 <a> 标签
|
||||
} catch (error) {
|
||||
console.error('Error exporting images:', error);
|
||||
document.getElementById('status').textContent = 'Error exporting images: ' + error.message;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('deleteSelectedBtn').addEventListener('click', async function() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Please select at least one image to delete.');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure you want to delete ${selectedIds.length} selected images?`)) return;
|
||||
|
||||
for (const id of selectedIds) {
|
||||
// 逐个调用删除 API,或者可以优化为批量删除 API
|
||||
try {
|
||||
const response = await fetch('/api/images', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting image ID ${id}:`, error);
|
||||
document.getElementById('status').textContent = `Error deleting image ID ${id}: ${error.message}`;
|
||||
return; // 停止删除后续图片
|
||||
}
|
||||
}
|
||||
// 成功后刷新列表
|
||||
loadImages();
|
||||
});
|
||||
|
||||
// 全选复选框事件监听
|
||||
document.getElementById('selectAllCheckbox').addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const checkboxes = document.querySelectorAll('.selectCheckbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
updateExportDeleteButtons();
|
||||
});
|
||||
|
||||
// 监听单个复选框变化以更新全选框和按钮状态
|
||||
document.getElementById('imagesTable').addEventListener('change', function(e) {
|
||||
if (e.target.classList.contains('selectCheckbox')) {
|
||||
updateSelectAllCheckbox();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时获取图片列表
|
||||
loadImages();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user