feat: v1.1.0 - 图片按日期分文件夹保存、设置面板、序号显示模式、自动刷新
This commit is contained in:
+97
-11
@@ -3,12 +3,11 @@ Web 后端 - FastAPI
|
||||
"""
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||
from pathlib import Path
|
||||
import uvicorn
|
||||
import json
|
||||
|
||||
from config import WEB_PORT, WEB_HOST, IMAGES_DIR
|
||||
from config import WEB_PORT, WEB_HOST, config_mgr, DATA_DIR
|
||||
from database import db
|
||||
from scheduler import scheduler
|
||||
from camera import list_available_cameras, CameraCapture
|
||||
@@ -20,7 +19,17 @@ app = FastAPI(title="视觉记录系统")
|
||||
# 静态文件目录
|
||||
STATIC_DIR = Path(__file__).parent / "static"
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
app.mount("/images", StaticFiles(directory=str(IMAGES_DIR)), name="images")
|
||||
|
||||
# 图片目录 - 使用配置中的目录
|
||||
@app.get("/images/{filename}")
|
||||
async def get_image(filename: str):
|
||||
"""获取图片(需要在实际目录中查找)"""
|
||||
# 从数据库查找图片路径
|
||||
images = db.get_images(limit=1000)
|
||||
for img in images:
|
||||
if img['path'].endswith(filename):
|
||||
return FileResponse(img['path'])
|
||||
raise HTTPException(status_code=404, detail="图片不存在")
|
||||
|
||||
|
||||
# ============== 页面路由 ==============
|
||||
@@ -34,17 +43,55 @@ async def index():
|
||||
return HTMLResponse("<h1>请创建 index.html</h1>")
|
||||
|
||||
|
||||
# ============== API 路由 ==============
|
||||
# ============== 配置 API ==============
|
||||
|
||||
@app.get("/api/config")
|
||||
async def get_config():
|
||||
"""获取所有配置"""
|
||||
return config_mgr.get_all()
|
||||
|
||||
|
||||
@app.post("/api/config")
|
||||
async def update_config(data: dict):
|
||||
"""更新配置"""
|
||||
config_mgr.update(data)
|
||||
return {"success": True, "config": config_mgr.get_all()}
|
||||
|
||||
|
||||
@app.get("/api/config/images-dir")
|
||||
async def get_images_dir():
|
||||
"""获取图片保存目录"""
|
||||
return {
|
||||
"images_dir": config_mgr.get('images_dir'),
|
||||
"today_folder": str(config_mgr.get_images_dir())
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/config/images-dir")
|
||||
async def set_images_dir(path: str):
|
||||
"""设置图片保存目录"""
|
||||
from pathlib import Path
|
||||
try:
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
config_mgr.set('images_dir', path)
|
||||
return {"success": True, "images_dir": path}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# ============== 状态 API ==============
|
||||
|
||||
@app.get("/api/status")
|
||||
async def get_status():
|
||||
"""获取系统状态"""
|
||||
stats = db.get_stats()
|
||||
scheduler_status = scheduler.get_status()
|
||||
config = config_mgr.get_all()
|
||||
|
||||
return {
|
||||
"scheduler": scheduler_status,
|
||||
"stats": stats
|
||||
"stats": stats,
|
||||
"config": config
|
||||
}
|
||||
|
||||
|
||||
@@ -54,16 +101,19 @@ async def get_cameras():
|
||||
cameras = list_available_cameras()
|
||||
details = []
|
||||
for cam_index in cameras:
|
||||
cam = CameraCapture(cam_index)
|
||||
cam = CameraCapture()
|
||||
cam.camera_index = cam_index
|
||||
details.append(cam.get_camera_info())
|
||||
cam.close()
|
||||
return {"cameras": details}
|
||||
|
||||
|
||||
# ============== 调度控制 API ==============
|
||||
|
||||
@app.post("/api/scheduler/start")
|
||||
async def start_scheduler(interval: int = None, auto_analyze: bool = None):
|
||||
async def start_scheduler():
|
||||
"""启动定时拍照"""
|
||||
result = scheduler.start(interval=interval, auto_analyze=auto_analyze)
|
||||
result = scheduler.start()
|
||||
if not result['success']:
|
||||
raise HTTPException(status_code=400, detail=result['error'])
|
||||
return result
|
||||
@@ -78,6 +128,8 @@ async def stop_scheduler():
|
||||
@app.post("/api/scheduler/interval")
|
||||
async def set_interval(interval: int):
|
||||
"""设置拍照间隔"""
|
||||
if interval < 10:
|
||||
raise HTTPException(status_code=400, detail="间隔不能小于10秒")
|
||||
return scheduler.set_interval(interval)
|
||||
|
||||
|
||||
@@ -90,6 +142,8 @@ async def capture_now():
|
||||
return result
|
||||
|
||||
|
||||
# ============== 分析 API ==============
|
||||
|
||||
@app.post("/api/analyze/{image_id}")
|
||||
async def analyze_image(image_id: int):
|
||||
"""分析指定图片"""
|
||||
@@ -106,10 +160,29 @@ async def analyze_unanalyzed():
|
||||
return {"results": results}
|
||||
|
||||
|
||||
# ============== 图片 API ==============
|
||||
|
||||
@app.get("/api/images")
|
||||
async def get_images(limit: int = 50, offset: int = 0, analyzed_only: bool = False):
|
||||
async def get_images(
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
analyzed_only: bool = False,
|
||||
date_folder: str = None
|
||||
):
|
||||
"""获取图片列表"""
|
||||
images = db.get_images(limit=limit, offset=offset, analyzed_only=analyzed_only)
|
||||
# 使用配置中的显示数量
|
||||
config_limit = config_mgr.get('display_limit', 20)
|
||||
if limit > config_limit:
|
||||
limit = config_limit
|
||||
|
||||
images = db.get_images(limit=limit, offset=offset, analyzed_only=analyzed_only, date_folder=date_folder)
|
||||
|
||||
# 为每张图片添加事件摘要
|
||||
for img in images:
|
||||
events = db.get_events_by_image(img['id'])
|
||||
img['events_summary'] = ', '.join([e['event_type'] for e in events]) if events else '无'
|
||||
img['events_count'] = len(events)
|
||||
|
||||
return {"images": images, "total": len(images)}
|
||||
|
||||
|
||||
@@ -134,9 +207,22 @@ async def delete_image(image_id: int):
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/date-folders")
|
||||
async def get_date_folders():
|
||||
"""获取日期文件夹列表"""
|
||||
stats = db.get_stats()
|
||||
return {"folders": stats['date_folders']}
|
||||
|
||||
|
||||
# ============== 事件 API ==============
|
||||
|
||||
@app.get("/api/events")
|
||||
async def get_events(limit: int = 50, offset: int = 0, event_type: str = None):
|
||||
"""获取事件列表"""
|
||||
config_limit = config_mgr.get('display_limit', 20)
|
||||
if limit > config_limit:
|
||||
limit = config_limit
|
||||
|
||||
events = db.get_events(limit=limit, offset=offset, event_type=event_type)
|
||||
return {"events": events, "total": len(events)}
|
||||
|
||||
|
||||
+163
-70
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user