From f703c0491c7840247d0b886694f022dcdcd2957f Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Thu, 16 Apr 2026 10:42:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v1.1.0=20-=20=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=8C=89=E6=97=A5=E6=9C=9F=E5=88=86=E6=96=87=E4=BB=B6=E5=A4=B9?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=80=81=E8=AE=BE=E7=BD=AE=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E3=80=81=E5=BA=8F=E5=8F=B7=E6=98=BE=E7=A4=BA=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E3=80=81=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analyzer.py | 21 ++-- camera.py | 27 +++-- config.py | 98 +++++++++++++++--- database.py | 57 ++++++++--- scheduler.py | 49 +++++---- web/app.py | 108 ++++++++++++++++++-- web/static/app.js | 233 +++++++++++++++++++++++++++++------------- web/static/index.html | 98 +++++++++++++++--- web/static/style.css | 168 +++++++++++++++++++++--------- 9 files changed, 639 insertions(+), 220 deletions(-) diff --git a/analyzer.py b/analyzer.py index f75eeed..8e204a8 100644 --- a/analyzer.py +++ b/analyzer.py @@ -6,16 +6,16 @@ import requests import json import re from pathlib import Path -from config import LLM_API_URL, LLM_API_KEY, LLM_MODEL, ANALYSIS_PROMPT +from config import config_mgr, ANALYSIS_PROMPT class ImageAnalyzer: """图片分析器""" def __init__(self): - self.api_url = LLM_API_URL - self.api_key = LLM_API_KEY - self.model = LLM_MODEL + self.api_url = config_mgr.get('vision_api_url') + self.api_key = config_mgr.get('vision_api_key') + self.model = config_mgr.get('vision_model') def encode_image(self, image_path): """将图片转为 base64""" @@ -29,6 +29,11 @@ class ImageAnalyzer: dict: {'success': bool, 'events': list, 'error': str} """ try: + # 更新配置(可能已被修改) + self.api_url = config_mgr.get('vision_api_url') + self.api_key = config_mgr.get('vision_api_key') + self.model = config_mgr.get('vision_model') + # 编码图片 image_base64 = self.encode_image(image_path) @@ -105,10 +110,6 @@ class ImageAnalyzer: }] # 尝试解析结构化格式 - # 事件类型: xxx - # 描述: xxx - # 置信度: xxx - pattern = r"事件类型[::]\s*(.+?)\s*描述[::]\s*(.+?)\s*置信度[::]\s*(.+)" matches = re.findall(pattern, content, re.DOTALL) @@ -137,11 +138,11 @@ def analyze_image(image_path): if __name__ == "__main__": - # 测试分析(需要先有一张测试图片) + # 测试分析 test_image = Path(__file__).parent / "data" / "images" / "test.jpg" if test_image.exists(): print(f"分析图片: {test_image}") result = analyze_image(test_image) print(f"结果: {json.dumps(result, ensure_ascii=False, indent=2)}") else: - print("没有测试图片,请先拍照") \ No newline at end of file + print("没有测试图片") \ No newline at end of file diff --git a/camera.py b/camera.py index a5568b6..885fce4 100644 --- a/camera.py +++ b/camera.py @@ -2,22 +2,22 @@ 摄像头拍照模块 - Windows 兼容 """ import cv2 -import os import datetime from pathlib import Path -from config import IMAGES_DIR, CAMERA_INDEX +from config import config_mgr class CameraCapture: """摄像头拍照管理""" - def __init__(self, camera_index=CAMERA_INDEX): - self.camera_index = camera_index + def __init__(self): + self.camera_index = config_mgr.get('camera_index', 0) self.cap = None def open(self): """打开摄像头""" if self.cap is None or not self.cap.isOpened(): + self.camera_index = config_mgr.get('camera_index', 0) self.cap = cv2.VideoCapture(self.camera_index, cv2.CAP_DSHOW) # Windows DirectShow if not self.cap.isOpened(): raise Exception(f"无法打开摄像头 {self.camera_index}") @@ -33,10 +33,10 @@ class CameraCapture: self.cap = None def capture(self, save_path=None): - """拍照并保存 + """拍照并保存(按日期文件夹组织) Returns: - dict: {'success': bool, 'path': str, 'timestamp': str, 'error': str} + dict: {'success': bool, 'path': str, 'timestamp': str, 'date_folder': str, 'error': str} """ try: self.open() @@ -49,11 +49,17 @@ class CameraCapture: if not ret: return {'success': False, 'error': '无法捕获图像'} - # 生成文件名 - timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + # 获取当前日期 + now = datetime.datetime.now() + date_str = now.strftime('%Y-%m-%d') + timestamp = now.strftime('%Y%m%d_%H%M%S') + + # 获取按日期的保存目录 + date_folder = config_mgr.get_images_dir(date_str) + if save_path is None: filename = f"capture_{timestamp}.jpg" - save_path = IMAGES_DIR / filename + save_path = date_folder / filename # 保存图片 cv2.imwrite(str(save_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 90]) @@ -62,6 +68,7 @@ class CameraCapture: 'success': True, 'path': str(save_path), 'timestamp': timestamp, + 'date_folder': date_str, 'filename': save_path.name } @@ -112,7 +119,7 @@ if __name__ == "__main__": if cams: print("\n测试拍照...") - camera = CameraCapture(cams[0]) + camera = CameraCapture() result = camera.capture() print(f"结果: {result}") camera.close() \ No newline at end of file diff --git a/config.py b/config.py index 099448d..e77b1e4 100644 --- a/config.py +++ b/config.py @@ -2,30 +2,35 @@ 配置管理模块 """ import os +import json from pathlib import Path +from datetime import datetime # 基础路径 BASE_DIR = Path(__file__).parent.absolute() DATA_DIR = BASE_DIR / "data" -IMAGES_DIR = DATA_DIR / "images" DB_PATH = DATA_DIR / "events.db" +CONFIG_PATH = DATA_DIR / "config.json" # 确保目录存在 -IMAGES_DIR.mkdir(parents=True, exist_ok=True) +DATA_DIR.mkdir(parents=True, exist_ok=True) # Web 服务配置 WEB_PORT = 19016 WEB_HOST = "0.0.0.0" -# 摄像头配置 -CAMERA_INDEX = 0 # 默认摄像头 -CAPTURE_INTERVAL = 60 # 拍照间隔(秒) - -# 大模型配置(用于图片分析) -LLM_PROVIDER = "bailian" # bailian / openai / local -LLM_API_URL = os.environ.get("VISION_API_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") -LLM_API_KEY = os.environ.get("VISION_API_KEY", "sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j") -LLM_MODEL = os.environ.get("VISION_MODEL", "qwen-vl-plus") # 视觉模型 +# 默认配置 +DEFAULT_CONFIG = { + "images_dir": str(DATA_DIR / "images"), + "camera_index": 0, + "capture_interval": 60, + "auto_analyze": True, + "refresh_interval": 5, # 页面刷新间隔(秒) + "display_limit": 20, # 显示最近多少条 + "vision_api_url": os.environ.get("VISION_API_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"), + "vision_api_key": os.environ.get("VISION_API_KEY", "sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j"), + "vision_model": os.environ.get("VISION_MODEL", "qwen-vl-plus") +} # 分析配置 ANALYSIS_PROMPT = """请分析这张图片,识别其中的重要事件或变化。 @@ -46,12 +51,18 @@ ANALYSIS_PROMPT = """请分析这张图片,识别其中的重要事件或变 # 数据库配置 DB_SCHEMA = """ +CREATE TABLE IF NOT EXISTS config ( + key TEXT PRIMARY KEY, + value TEXT +); + CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, camera_id INTEGER DEFAULT 0, - analyzed BOOLEAN DEFAULT 0 + analyzed BOOLEAN DEFAULT 0, + date_folder TEXT ); CREATE TABLE IF NOT EXISTS events ( @@ -65,5 +76,66 @@ CREATE TABLE IF NOT EXISTS events ( ); CREATE INDEX IF NOT EXISTS idx_images_timestamp ON images(timestamp); +CREATE INDEX IF NOT EXISTS idx_images_date_folder ON images(date_folder); CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type); -""" \ No newline at end of file +""" + + +class ConfigManager: + """配置管理器""" + + def __init__(self): + self.config = DEFAULT_CONFIG.copy() + self.load() + + def load(self): + """加载配置""" + if CONFIG_PATH.exists(): + try: + with open(CONFIG_PATH, 'r', encoding='utf-8') as f: + saved = json.load(f) + self.config.update(saved) + except: + pass + + def save(self): + """保存配置""" + with open(CONFIG_PATH, 'w', encoding='utf-8') as f: + json.dump(self.config, f, ensure_ascii=False, indent=2) + + def get(self, key, default=None): + """获取配置项""" + return self.config.get(key, default) + + def set(self, key, value): + """设置配置项""" + self.config[key] = value + self.save() + + def get_images_dir(self, date=None): + """获取图片保存目录(按日期)""" + base_dir = Path(self.config['images_dir']) + + if date is None: + date = datetime.now().strftime('%Y-%m-%d') + + # 每天的图片放在单独文件夹 + date_folder = base_dir / date + date_folder.mkdir(parents=True, exist_ok=True) + + return date_folder + + def get_all(self): + """获取所有配置""" + return self.config.copy() + + def update(self, updates): + """批量更新配置""" + for key, value in updates.items(): + if key in self.config: + self.config[key] = value + self.save() + + +# 全局配置实例 +config_mgr = ConfigManager() \ No newline at end of file diff --git a/database.py b/database.py index 69b95d9..08b61d1 100644 --- a/database.py +++ b/database.py @@ -25,7 +25,7 @@ class Database: """获取数据库连接""" return sqlite3.connect(self.db_path) - def add_image(self, path, camera_id=0): + def add_image(self, path, camera_id=0, date_folder=None): """添加图片记录 Returns: @@ -34,8 +34,8 @@ class Database: conn = self._get_conn() cursor = conn.cursor() cursor.execute( - "INSERT INTO images (path, camera_id, timestamp) VALUES (?, ?, ?)", - (path, camera_id, datetime.datetime.now().isoformat()) + "INSERT INTO images (path, camera_id, timestamp, date_folder) VALUES (?, ?, ?, ?)", + (path, camera_id, datetime.datetime.now().isoformat(), date_folder) ) image_id = cursor.lastrowid conn.commit() @@ -59,22 +59,28 @@ class Database: conn.commit() conn.close() - def get_images(self, limit=50, offset=0, analyzed_only=False): + def get_images(self, limit=50, offset=0, analyzed_only=False, date_folder=None): """获取图片列表""" conn = self._get_conn() conn.row_factory = sqlite3.Row - if analyzed_only: - cursor = conn.execute( - "SELECT * FROM images WHERE analyzed = 1 ORDER BY timestamp DESC LIMIT ? OFFSET ?", - (limit, offset) - ) - else: - cursor = conn.execute( - "SELECT * FROM images ORDER BY timestamp DESC LIMIT ? OFFSET ?", - (limit, offset) - ) + query = "SELECT * FROM images" + params = [] + conditions = [] + if analyzed_only: + conditions.append("analyzed = 1") + if date_folder: + conditions.append("date_folder = ?") + params.append(date_folder) + + if conditions: + query += " WHERE " + " AND ".join(conditions) + + query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + cursor = conn.execute(query, params) rows = cursor.fetchall() conn.close() @@ -87,7 +93,7 @@ class Database: if event_type: cursor = conn.execute( - """SELECT e.*, i.path as image_path, i.timestamp as image_timestamp + """SELECT e.*, i.path as image_path, i.timestamp as image_timestamp, i.id as image_id FROM events e JOIN images i ON e.image_id = i.id WHERE e.event_type = ? ORDER BY e.timestamp DESC LIMIT ? OFFSET ?""", @@ -95,7 +101,7 @@ class Database: ) else: cursor = conn.execute( - """SELECT e.*, i.path as image_path, i.timestamp as image_timestamp + """SELECT e.*, i.path as image_path, i.timestamp as image_timestamp, i.id as image_id FROM events e JOIN images i ON e.image_id = i.id ORDER BY e.timestamp DESC LIMIT ? OFFSET ?""", (limit, offset) @@ -144,6 +150,12 @@ class Database: ) event_types = [{'type': row[0], 'count': row[1]} for row in cursor.fetchall()] + # 日期文件夹统计 + cursor = conn.execute( + "SELECT date_folder, COUNT(*) as count FROM images WHERE date_folder IS NOT NULL GROUP BY date_folder ORDER BY date_folder DESC" + ) + date_folders = [{'date': row[0], 'count': row[1]} for row in cursor.fetchall()] + conn.close() return { @@ -151,7 +163,8 @@ class Database: 'analyzed_images': analyzed_images, 'unanalyzed_images': total_images - analyzed_images, 'total_events': total_events, - 'event_types': event_types + 'event_types': event_types, + 'date_folders': date_folders } def delete_image(self, image_id): @@ -190,6 +203,16 @@ class Database: rows = cursor.fetchall() conn.close() return [dict(row) for row in rows] + + def get_date_folders(self): + """获取所有日期文件夹""" + conn = self._get_conn() + cursor = conn.execute( + "SELECT DISTINCT date_folder FROM images WHERE date_folder IS NOT NULL ORDER BY date_folder DESC" + ) + folders = [row[0] for row in cursor.fetchall()] + conn.close() + return folders # 全局实例 diff --git a/scheduler.py b/scheduler.py index eb16a69..07dab85 100644 --- a/scheduler.py +++ b/scheduler.py @@ -7,7 +7,7 @@ import datetime from camera import CameraCapture from analyzer import ImageAnalyzer from database import db -from config import CAPTURE_INTERVAL +from config import config_mgr class VisionScheduler: @@ -17,9 +17,7 @@ class VisionScheduler: self.camera = CameraCapture() self.analyzer = ImageAnalyzer() self.running = False - self.interval = CAPTURE_INTERVAL self.timer = None - self.auto_analyze = True # 自动分析 # 统计 self.capture_count = 0 @@ -27,28 +25,19 @@ class VisionScheduler: self.last_analyze_time = None self.errors = [] - def start(self, interval=None, auto_analyze=None): - """启动定时拍照 - - Args: - interval: 拍照间隔(秒) - auto_analyze: 是否自动分析 - """ + def start(self): + """启动定时拍照""" if self.running: return {'success': False, 'error': '已在运行中'} - if interval: - self.interval = interval - if auto_analyze is not None: - self.auto_analyze = auto_analyze - self.running = True + interval = config_mgr.get('capture_interval', 60) self._schedule_next() return { 'success': True, - 'interval': self.interval, - 'auto_analyze': self.auto_analyze + 'interval': interval, + 'auto_analyze': config_mgr.get('auto_analyze', True) } def stop(self): @@ -64,7 +53,8 @@ class VisionScheduler: """安排下一次拍照""" if not self.running: return - self.timer = threading.Timer(self.interval, self._capture_task) + interval = config_mgr.get('capture_interval', 60) + self.timer = threading.Timer(interval, self._capture_task) self.timer.start() def _capture_task(self): @@ -78,12 +68,15 @@ class VisionScheduler: if result['success']: # 记录到数据库 - image_id = db.add_image(result['path']) + image_id = db.add_image( + result['path'], + date_folder=result.get('date_folder') + ) self.capture_count += 1 self.last_capture_time = datetime.datetime.now().isoformat() # 自动分析 - if self.auto_analyze: + if config_mgr.get('auto_analyze', True): self._analyze_task(image_id, result['path']) else: self.errors.append({ @@ -133,12 +126,15 @@ class VisionScheduler: result = self.camera.capture() if result['success']: - image_id = db.add_image(result['path']) + image_id = db.add_image( + result['path'], + date_folder=result.get('date_folder') + ) self.capture_count += 1 self.last_capture_time = datetime.datetime.now().isoformat() # 如果自动分析开启,立即分析 - if self.auto_analyze: + if config_mgr.get('auto_analyze', True): threading.Thread( target=self._analyze_task, args=(image_id, result['path']) @@ -148,7 +144,8 @@ class VisionScheduler: 'success': True, 'image_id': image_id, 'path': result['path'], - 'timestamp': result['timestamp'] + 'timestamp': result['timestamp'], + 'date_folder': result.get('date_folder') } return result @@ -201,8 +198,8 @@ class VisionScheduler: """获取调度器状态""" return { 'running': self.running, - 'interval': self.interval, - 'auto_analyze': self.auto_analyze, + 'interval': config_mgr.get('capture_interval', 60), + 'auto_analyze': config_mgr.get('auto_analyze', True), 'capture_count': self.capture_count, 'last_capture_time': self.last_capture_time, 'last_analyze_time': self.last_analyze_time, @@ -211,7 +208,7 @@ class VisionScheduler: def set_interval(self, interval): """设置拍照间隔""" - self.interval = interval + config_mgr.set('capture_interval', interval) if self.running: # 重启定时器 self.stop() diff --git a/web/app.py b/web/app.py index 2d68e07..abf6077 100644 --- a/web/app.py +++ b/web/app.py @@ -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("

请创建 index.html

") -# ============== 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)} diff --git a/web/static/app.js b/web/static/app.js index d302bff..2e22f10 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -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 = ''; 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 = ''; + 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 = '

暂无图片

'; + list.innerHTML = '

暂无图片记录

'; 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 = ` -
${time}
-
+ + item.innerHTML = ` + #${img.id} + ${time} + ${img.analyzed ? '✓ 已分析' : '○ 未分析'} -
+ + ${img.events_summary || '无事件'} `; - 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 = '

暂无事件

'; + list.innerHTML = '

暂无事件记录

'; 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); \ No newline at end of file diff --git a/web/static/index.html b/web/static/index.html index bf2122a..58abf50 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -14,6 +14,7 @@
状态: 加载中... 拍照次数: 0 + 刷新间隔: 5秒
@@ -21,15 +22,6 @@

📹 摄像头控制

-
- - - -
-
- - -
@@ -37,6 +29,11 @@
+
+

⚙️ 系统设置

+ +
+

📊 系统统计

@@ -62,15 +59,18 @@

📷 图片时间线

+ - +
-
- +
+
@@ -80,25 +80,91 @@
- +
+ + +