feat: v1.1.0 - 图片按日期分文件夹保存、设置面板、序号显示模式、自动刷新

This commit is contained in:
2026-04-16 10:42:36 +08:00
parent 3503ee35ab
commit f703c0491c
9 changed files with 639 additions and 220 deletions
+163 -70
View File
@@ -2,20 +2,119 @@
const API_BASE = '';
let currentImageId = null;
let refreshTimer = null;
let refreshInterval = 5;
// ============== 初始化 ==============
document.addEventListener('DOMContentLoaded', () => {
loadConfig();
refreshAll();
setInterval(refreshAll, 5000); // 每5秒刷新
startAutoRefresh();
});
function startAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = setInterval(refreshAll, refreshInterval * 1000);
document.getElementById('refresh-info').textContent = `刷新间隔: ${refreshInterval}`;
}
function refreshAll() {
loadStatus();
loadDateFolders();
loadImages();
loadEvents();
}
// ============== 配置相关 ==============
async function loadConfig() {
try {
const res = await fetch(`${API_BASE}/api/config`);
const config = await res.json();
refreshInterval = config.refresh_interval || 5;
startAutoRefresh();
// 更新显示数量提示
document.getElementById('display-limit-note')?.textContent =
`显示最近 ${config.display_limit || 20}`;
} catch (e) {
console.error('加载配置失败:', e);
}
}
function openSettingsModal() {
loadSettingsForm();
document.getElementById('settings-modal').classList.add('active');
}
function closeSettingsModal() {
document.getElementById('settings-modal').classList.remove('active');
}
async function loadSettingsForm() {
try {
const res = await fetch(`${API_BASE}/api/config`);
const config = await res.json();
document.getElementById('setting-images-dir').value = config.images_dir || '';
document.getElementById('setting-interval').value = config.capture_interval || 60;
document.getElementById('setting-camera-index').value = config.camera_index || 0;
document.getElementById('setting-auto-analyze').checked = config.auto_analyze !== false;
document.getElementById('setting-display-limit').value = config.display_limit || 20;
document.getElementById('setting-refresh-interval').value = config.refresh_interval || 5;
document.getElementById('setting-api-url').value = config.vision_api_url || '';
document.getElementById('setting-api-key').value = config.vision_api_key || '';
document.getElementById('setting-model').value = config.vision_model || '';
} catch (e) {
console.error('加载设置失败:', e);
}
}
async function saveSettings() {
const settings = {
images_dir: document.getElementById('setting-images-dir').value,
capture_interval: parseInt(document.getElementById('setting-interval').value),
camera_index: parseInt(document.getElementById('setting-camera-index').value),
auto_analyze: document.getElementById('setting-auto-analyze').checked,
display_limit: parseInt(document.getElementById('setting-display-limit').value),
refresh_interval: parseInt(document.getElementById('setting-refresh-interval').value),
vision_api_url: document.getElementById('setting-api-url').value,
vision_api_key: document.getElementById('setting-api-key').value,
vision_model: document.getElementById('setting-model').value
};
try {
const res = await fetch(`${API_BASE}/api/config`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(settings)
});
const data = await res.json();
if (data.success) {
alert('设置已保存!');
// 更新刷新间隔
refreshInterval = settings.refresh_interval;
startAutoRefresh();
closeSettingsModal();
refreshAll();
}
} catch (e) {
alert('保存失败: ' + e.message);
}
}
function browseImagesDir() {
// 提示用户手动输入路径
alert('请手动输入图片保存目录路径\n例如: D:\\vision-images\n每天的图片会自动保存到日期子文件夹');
}
// ============== 状态相关 ==============
async function loadStatus() {
@@ -45,19 +144,11 @@ async function loadStatus() {
document.getElementById('capture-count').textContent =
`拍照次数: ${data.scheduler.capture_count}`;
// 更新输入框
document.getElementById('interval-input').value = data.scheduler.interval;
document.getElementById('auto-analyze').checked = data.scheduler.auto_analyze;
} catch (e) {
console.error('加载状态失败:', e);
}
}
function updateStatusBar(status) {
// 已在 loadStatus 中处理
}
function updateStats(stats) {
document.getElementById('total-images').textContent = stats.total_images;
document.getElementById('analyzed-images').textContent = stats.analyzed_images;
@@ -67,7 +158,6 @@ function updateStats(stats) {
const eventFilter = document.getElementById('event-filter');
const currentValue = eventFilter.value;
// 清空并重建选项
eventFilter.innerHTML = '<option value="all">全部类型</option>';
stats.event_types.forEach(item => {
const opt = document.createElement('option');
@@ -82,11 +172,8 @@ function updateStats(stats) {
// ============== 控制相关 ==============
async function startScheduler() {
const interval = parseInt(document.getElementById('interval-input').value);
const autoAnalyze = document.getElementById('auto-analyze').checked;
try {
const res = await fetch(`${API_BASE}/api/scheduler/start?interval=${interval}&auto_analyze=${autoAnalyze}`, {
const res = await fetch(`${API_BASE}/api/scheduler/start`, {
method: 'POST'
});
const data = await res.json();
@@ -118,29 +205,6 @@ async function stopScheduler() {
}
}
async function setInterval() {
const interval = parseInt(document.getElementById('interval-input').value);
if (interval < 10) {
alert('间隔不能小于10秒');
return;
}
try {
const res = await fetch(`${API_BASE}/api/scheduler/interval?interval=${interval}`, {
method: 'POST'
});
const data = await res.json();
if (data.success) {
alert('间隔已更新为 ' + interval + ' 秒');
refreshAll();
}
} catch (e) {
alert('请求失败: ' + e.message);
}
}
async function captureNow() {
try {
const res = await fetch(`${API_BASE}/api/capture`, {
@@ -149,7 +213,7 @@ async function captureNow() {
const data = await res.json();
if (data.success) {
alert('拍照成功!图片ID: ' + data.image_id);
alert(`拍照成功!\n图片ID: #${data.image_id}\n保存到: ${data.date_folder}`);
refreshAll();
} else {
alert('拍照失败: ' + data.error);
@@ -173,54 +237,78 @@ async function analyzeUnanalyzed() {
}
}
// ============== 日期文件夹相关 ==============
async function loadDateFolders() {
try {
const res = await fetch(`${API_BASE}/api/date-folders`);
const data = await res.json();
const dateFilter = document.getElementById('date-filter');
const currentValue = dateFilter.value;
dateFilter.innerHTML = '<option value="all">全部日期</option>';
data.folders.forEach(item => {
const opt = document.createElement('option');
opt.value = item.date;
opt.textContent = `${item.date} (${item.count}张)`;
dateFilter.appendChild(opt);
});
dateFilter.value = currentValue;
} catch (e) {
console.error('加载日期文件夹失败:', e);
}
}
// ============== 图片相关 ==============
async function loadImages() {
const filter = document.getElementById('image-filter').value;
const analyzedOnly = filter === 'analyzed';
const dateFilter = document.getElementById('date-filter').value;
const imageFilter = document.getElementById('image-filter').value;
const analyzedOnly = imageFilter === 'analyzed';
try {
const res = await fetch(`${API_BASE}/api/images?limit=50&analyzed_only=${analyzedOnly}`);
let url = `${API_BASE}/api/images?limit=100&analyzed_only=${analyzedOnly}`;
if (dateFilter !== 'all') {
url += `&date_folder=${dateFilter}`;
}
const res = await fetch(url);
const data = await res.json();
renderImages(data.images);
renderImagesList(data.images);
} catch (e) {
console.error('加载图片失败:', e);
}
}
function renderImages(images) {
const grid = document.getElementById('images-grid');
grid.innerHTML = '';
function renderImagesList(images) {
const list = document.getElementById('images-list');
list.innerHTML = '';
if (images.length === 0) {
grid.innerHTML = '<p style="color: #888;">暂无图片</p>';
list.innerHTML = '<p style="color: #888; text-align: center;">暂无图片记录</p>';
return;
}
images.forEach(img => {
const card = document.createElement('div');
card.className = 'image-card';
card.onclick = () => openImageModal(img.id);
const imgEl = document.createElement('img');
imgEl.src = `/images/${img.path.split('/').pop()}`;
imgEl.alt = `图片 ${img.id}`;
const info = document.createElement('div');
info.className = 'image-card-info';
images.forEach((img, index) => {
const item = document.createElement('div');
item.className = `image-item ${img.analyzed ? 'analyzed' : 'unanalyzed'}`;
item.onclick = () => openImageModal(img.id);
const time = new Date(img.timestamp).toLocaleString('zh-CN');
info.innerHTML = `
<div class="time">${time}</div>
<div class="${img.analyzed ? 'analyzed' : 'unanalyzed'}">
item.innerHTML = `
<span class="image-number">#${img.id}</span>
<span class="image-time">${time}</span>
<span class="image-status ${img.analyzed ? 'analyzed' : 'unanalyzed'}">
${img.analyzed ? '✓ 已分析' : '○ 未分析'}
</div>
</span>
<span class="image-events-summary">${img.events_summary || '无事件'}</span>
`;
card.appendChild(imgEl);
card.appendChild(info);
grid.appendChild(card);
list.appendChild(item);
});
}
@@ -235,11 +323,15 @@ async function openImageModal(imageId) {
const modalImg = document.getElementById('modal-image');
const modalTitle = document.getElementById('modal-title');
const modalTime = document.getElementById('modal-time');
const modalFolder = document.getElementById('modal-folder');
const modalEvents = document.getElementById('modal-events');
modalImg.src = `/images/${data.path.split('/').pop()}`;
// 获取图片文件名
const filename = data.path.split('/').pop().split('\\').pop();
modalImg.src = `/images/${filename}`;
modalTitle.textContent = `图片 #${data.id}`;
modalTime.textContent = `时间: ${new Date(data.timestamp).toLocaleString('zh-CN')}`;
modalFolder.textContent = `日期文件夹: ${data.date_folder || '未知'}`;
modalEvents.innerHTML = '';
if (data.events && data.events.length > 0) {
@@ -263,7 +355,7 @@ async function openImageModal(imageId) {
}
}
function closeModal() {
function closeImageModal() {
document.getElementById('image-modal').classList.remove('active');
currentImageId = null;
}
@@ -279,7 +371,7 @@ async function analyzeCurrentImage() {
if (data.success) {
alert('分析完成!');
openImageModal(currentImageId); // 刷新详情
openImageModal(currentImageId);
refreshAll();
} else {
alert('分析失败: ' + data.error);
@@ -301,7 +393,7 @@ async function deleteCurrentImage() {
const data = await res.json();
if (data.success) {
closeModal();
closeImageModal();
refreshAll();
}
} catch (e) {
@@ -316,7 +408,7 @@ async function loadEvents() {
const eventType = filter === 'all' ? '' : filter;
try {
let url = `${API_BASE}/api/events?limit=50`;
let url = `${API_BASE}/api/events?limit=100`;
if (eventType) {
url += `&event_type=${encodeURIComponent(eventType)}`;
}
@@ -335,7 +427,7 @@ function renderEvents(events) {
list.innerHTML = '';
if (events.length === 0) {
list.innerHTML = '<p style="color: #888;">暂无事件</p>';
list.innerHTML = '<p style="color: #888; text-align: center;">暂无事件记录</p>';
return;
}
@@ -360,5 +452,6 @@ function renderEvents(events) {
}
// 筛选变化监听
document.getElementById('date-filter').addEventListener('change', loadImages);
document.getElementById('image-filter').addEventListener('change', loadImages);
document.getElementById('event-filter').addEventListener('change', loadEvents);
+82 -16
View File
@@ -14,6 +14,7 @@
<div class="status-bar">
<span id="scheduler-status">状态: 加载中...</span>
<span id="capture-count">拍照次数: 0</span>
<span id="refresh-info">刷新间隔: 5秒</span>
</div>
</header>
@@ -21,15 +22,6 @@
<section class="control-panel">
<div class="panel-section">
<h3>📹 摄像头控制</h3>
<div class="control-row">
<label>拍照间隔 (秒):</label>
<input type="number" id="interval-input" value="60" min="10" max="3600">
<button onclick="setInterval()">设置</button>
</div>
<div class="control-row">
<label>自动分析:</label>
<input type="checkbox" id="auto-analyze" checked>
</div>
<div class="control-buttons">
<button onclick="startScheduler()" id="start-btn" class="btn-primary">▶ 启动</button>
<button onclick="stopScheduler()" id="stop-btn" class="btn-danger">⏹ 停止</button>
@@ -37,6 +29,11 @@
</div>
</div>
<div class="panel-section">
<h3>⚙️ 系统设置</h3>
<button onclick="openSettingsModal()" class="btn-settings">🔧 设置参数</button>
</div>
<div class="panel-section">
<h3>📊 系统统计</h3>
<div class="stats-grid" id="stats-display">
@@ -62,15 +59,18 @@
<section class="images-section">
<h2>📷 图片时间线</h2>
<div class="filter-bar">
<select id="date-filter">
<option value="all">全部日期</option>
</select>
<select id="image-filter">
<option value="all">全部</option>
<option value="analyzed">已分析</option>
<option value="unanalyzed">未分析</option>
</select>
<button onclick="analyzeUnanalyzed()" class="btn-small">分析未分析图片</button>
<button onclick="analyzeUnanalyzed()" class="btn-small">分析未处理</button>
</div>
<div class="images-grid" id="images-grid">
<!-- 图片卡片将通过 JS 动态加载 -->
<div class="images-list" id="images-list">
<!-- 图片列表将显示序号+分析信息 -->
</div>
</section>
@@ -80,25 +80,91 @@
<div class="filter-bar">
<select id="event-filter">
<option value="all">全部类型</option>
<!-- 动态加载事件类型 -->
</select>
</div>
<div class="events-list" id="events-list">
<!-- 事件卡片将通过 JS 动态加载 -->
<!-- 事件卡片 -->
</div>
</section>
</div>
<!-- 设置模态框 -->
<div class="modal" id="settings-modal">
<div class="modal-content settings-modal-content">
<span class="modal-close" onclick="closeSettingsModal()">×</span>
<h2>⚙️ 系统设置</h2>
<div class="settings-section">
<h4>保存设置</h4>
<div class="setting-item">
<label>图片保存目录:</label>
<input type="text" id="setting-images-dir" placeholder="例如: D:\vision-images">
<button onclick="browseImagesDir()" class="btn-small">选择目录</button>
</div>
<p class="setting-note">每天的图片会自动保存到子文件夹(如 2026-04-16)</p>
</div>
<div class="settings-section">
<h4>拍照设置</h4>
<div class="setting-item">
<label>拍照间隔 (秒):</label>
<input type="number" id="setting-interval" value="60" min="10" max="3600">
</div>
<div class="setting-item">
<label>摄像头索引:</label>
<input type="number" id="setting-camera-index" value="0" min="0" max="10">
</div>
<div class="setting-item">
<label>自动分析:</label>
<input type="checkbox" id="setting-auto-analyze" checked>
</div>
</div>
<div class="settings-section">
<h4>显示设置</h4>
<div class="setting-item">
<label>显示最近数量:</label>
<input type="number" id="setting-display-limit" value="20" min="5" max="100">
</div>
<div class="setting-item">
<label>页面刷新间隔 (秒):</label>
<input type="number" id="setting-refresh-interval" value="5" min="1" max="60">
</div>
</div>
<div class="settings-section">
<h4>API 设置</h4>
<div class="setting-item">
<label>Vision API URL:</label>
<input type="text" id="setting-api-url" placeholder="API URL">
</div>
<div class="setting-item">
<label>API Key:</label>
<input type="password" id="setting-api-key" placeholder="API Key">
</div>
<div class="setting-item">
<label>模型名称:</label>
<input type="text" id="setting-model" placeholder="模型名称">
</div>
</div>
<div class="settings-actions">
<button onclick="saveSettings()" class="btn-primary">保存设置</button>
<button onclick="closeSettingsModal()" class="btn-secondary">取消</button>
</div>
</div>
</div>
<!-- 图片详情模态框 -->
<div class="modal" id="image-modal">
<div class="modal-content">
<span class="modal-close" onclick="closeModal()">×</span>
<span class="modal-close" onclick="closeImageModal()">×</span>
<img id="modal-image" src="" alt="图片详情">
<div class="modal-info">
<h3 id="modal-title">图片详情</h3>
<p id="modal-time">时间: </p>
<p id="modal-folder">日期文件夹: </p>
<div id="modal-events" class="modal-events">
<!-- 事件详情 -->
</div>
<div class="modal-actions">
<button onclick="analyzeCurrentImage()" class="btn-secondary">🔍 分析</button>
+121 -47
View File
@@ -66,33 +66,9 @@ body {
font-size: 16px;
}
.control-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.control-row label {
min-width: 100px;
}
.control-row input[type="number"] {
width: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
}
.control-row input[type="checkbox"] {
width: 20px;
height: 20px;
}
.control-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
/* 按钮 */
@@ -143,6 +119,15 @@ button {
background: #757575;
}
.btn-settings {
background: #ff9800;
color: white;
}
.btn-settings:hover {
background: #f57c00;
}
/* 统计网格 */
.stats-grid {
display: grid;
@@ -204,51 +189,72 @@ button {
border-radius: 5px;
}
/* 图片网格 */
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
/* 图片列表 - 序号+分析信息 */
.images-list {
max-height: 500px;
overflow-y: auto;
}
.image-card {
background: #f5f5f5;
.image-item {
background: #f9f9f9;
padding: 12px 15px;
margin-bottom: 8px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s;
display: flex;
align-items: center;
gap: 15px;
border-left: 4px solid #667eea;
}
.image-card:hover {
transform: scale(1.05);
.image-item:hover {
background: #f0f0f0;
}
.image-card img {
width: 100%;
height: 120px;
object-fit: cover;
.image-item.analyzed {
border-left-color: #4CAF50;
}
.image-card-info {
padding: 8px;
font-size: 12px;
.image-item.unanalyzed {
border-left-color: #f44336;
}
.image-card-info .time {
color: #888;
.image-item:hover {
background: #f0f0f0;
}
.image-card-info .analyzed {
color: #4CAF50;
.image-number {
font-size: 18px;
font-weight: bold;
color: #667eea;
min-width: 60px;
}
.image-card-info .unanalyzed {
.image-time {
color: #888;
font-size: 13px;
min-width: 140px;
}
.image-status {
font-size: 13px;
min-width: 80px;
}
.image-status.analyzed {
color: #4CAF50;
}
.image-status.unanalyzed {
color: #f44336;
}
.image-events-summary {
color: #555;
font-size: 13px;
flex: 1;
}
/* 事件列表 */
.events-list {
max-height: 500px;
@@ -261,6 +267,11 @@ button {
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #667eea;
cursor: pointer;
}
.event-card:hover {
background: #f0f0f0;
}
.event-card.type-人物活动 {
@@ -341,6 +352,10 @@ button {
position: relative;
}
.settings-modal-content {
max-width: 600px;
}
.modal-close {
position: absolute;
top: 10px;
@@ -383,6 +398,61 @@ button {
gap: 10px;
}
/* 设置模态框 */
.settings-section {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.settings-section:last-of-type {
border-bottom: none;
}
.settings-section h4 {
margin-bottom: 15px;
color: #667eea;
}
.setting-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.setting-item label {
min-width: 140px;
color: #555;
}
.setting-item input[type="text"],
.setting-item input[type="number"],
.setting-item input[type="password"] {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
}
.setting-item input[type="checkbox"] {
width: 20px;
height: 20px;
}
.setting-note {
color: #888;
font-size: 12px;
margin-top: 5px;
}
.settings-actions {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
}
/* 响应式 */
@media (max-width: 768px) {
.control-panel {
@@ -396,4 +466,8 @@ button {
.stats-grid {
grid-template-columns: 1fr;
}
.image-item {
flex-wrap: wrap;
}
}