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
+97 -11
View File
@@ -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
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;
}
}