📸 Загрузка картинок через веб-админку

 Реализовано:
- Форма загрузки в веб-админке (/phones)
- API endpoint /api/service/upload_image
- Авто-привязка картинки ко всем службам категории
- Раздача статики через /static/service_images

🎨 Как использовать:
1. Веб-админка → Телефоны
2. Прокрути до 'Загрузить картинку'
3. Выбери категорию и файл
4. Загрузи
5. Готово!

💡 Преимущества:
- Не нужен SSH
- Загрузка из браузера
- Уникальные имена файлов
- Авто-обновление БД

📁 Файлы:
- web/app.py — API загрузки
- web/templates/phones.html — форма загрузки
- data/service_images/ — папка для картинок

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Admin 2026-03-02 10:21:21 +00:00
parent f0a081a988
commit 819bed706b
2 changed files with 125 additions and 7 deletions

View file

@ -27,6 +27,7 @@ logger = logging.getLogger(__name__)
app = FastAPI(title="Домовой Бот - MEGA Admin Panel", version="3.0.0")
templates = Jinja2Templates(directory="web/templates")
app.mount("/static", StaticFiles(directory="web/static"), name="static")
app.mount("/static/service_images", StaticFiles(directory="data/service_images"), name="service_images")
security = HTTPBasic()
@ -218,13 +219,62 @@ async def api_add_service(request: Request, username: str = Depends(get_current_
phone=data['phone'],
category=data.get('category', 'other'),
description=data.get('description', ''),
verified_by_admin=True
verified_by_admin=True,
image_path=data.get('image_path', None)
)
session.add(service)
await session.commit()
return JSONResponse({"success": True, "message": "Служба добавлена"})
@app.post("/api/service/upload_image")
async def api_upload_service_image(
file: UploadFile = File(...),
category: str = Form(...),
username: str = Depends(get_current_admin)
):
"""Загрузить картинку для службы"""
import os
import uuid
from pathlib import Path
# Создаём папку для картинок
images_dir = Path("data/service_images")
images_dir.mkdir(parents=True, exist_ok=True)
# Генерируем уникальное имя файла
file_extension = file.filename.split(".")[-1] if "." in file.filename else "jpg"
unique_filename = f"{category}_{uuid.uuid4().hex}.{file_extension}"
file_path = images_dir / unique_filename
# Сохраняем файл
try:
with open(file_path, "wb") as f:
content = await file.read()
f.write(content)
# Обновляем все службы этой категории
async with AsyncSessionLocal() as session:
await session.execute(
update(Service)
.where(Service.category == category)
.values(image_path=str(file_path))
)
await session.commit()
return JSONResponse({
"success": True,
"message": f"Картинка загружена",
"file_path": str(file_path)
})
except Exception as e:
logger.error(f"Ошибка загрузки картинки: {e}")
return JSONResponse({
"success": False,
"error": str(e)
}, status_code=500)
@app.post("/api/service/{service_id}/update")
async def api_update_service(service_id: int, request: Request,
username: str = Depends(get_current_admin)):

View file

@ -10,6 +10,7 @@
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
.content { padding: 2rem; }
.service-image-preview { max-width: 200px; max-height: 200px; object-fit: contain; }
</style>
</head>
<body>
@ -49,11 +50,11 @@
</div>
<div class="col-md-2">
<select class="form-select" id="serviceCategory">
<option value="emergency">Экстренные</option>
<option value="utility">ЖКХ</option>
<option value="masters">Мастера</option>
<option value="police">Полиция</option>
<option value="other">Другое</option>
<option value="emergency">🚨 Экстренные</option>
<option value="utility">🔧 ЖКХ</option>
<option value="masters">👷 Мастера</option>
<option value="police">👮 Полиция</option>
<option value="other">📋 Другое</option>
</select>
</div>
<div class="col-md-3">
@ -66,9 +67,38 @@
</div>
</div>
<!-- Загрузить картинку -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-image"></i> Загрузить картинку для службы</h5>
</div>
<div class="card-body">
<form id="uploadImageForm" class="row g-3" enctype="multipart/form-data">
<div class="col-md-3">
<select class="form-select" id="imageCategory" required>
<option value="emergency">🚨 Экстренные</option>
<option value="utility">🔧 ЖКХ</option>
<option value="masters">👷 Мастера</option>
<option value="police">👮 Полиция</option>
<option value="other">📋 Другое</option>
</select>
</div>
<div class="col-md-4">
<input type="file" class="form-control" id="imageFile" accept="image/*" required>
<small class="text-muted">JPG, PNG до 5MB</small>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-success w-100">Загрузить</button>
</div>
</form>
<div id="uploadResult" class="mt-3"></div>
</div>
</div>
<!-- Список служб -->
<div class="card">
<div class="card-body">
<h5>Существующие службы:</h5>
<table class="table table-hover">
<thead>
<tr>
@ -76,6 +106,7 @@
<th>Название</th>
<th>Телефон</th>
<th>Описание</th>
<th>Картинка</th>
<th>Действия</th>
</tr>
</thead>
@ -98,6 +129,15 @@
<td>{{ service.name }}</td>
<td><a href="tel:{{ service.phone }}">{{ service.phone }}</a></td>
<td>{{ service.description or '—' }}</td>
<td>
{% if service.image_path %}
<img src="/static/service_images/{{ service.image_path.split('/')[-1] }}"
class="service-image-preview" alt="Картинка">
<br><small class="text-muted"></small>
{% else %}
<span class="text-muted">❌ Нет</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteService({{ service.service_id }})">
<i class="bi bi-trash"></i>
@ -117,6 +157,7 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Добавление службы
document.getElementById('addServiceForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
@ -131,7 +172,34 @@
});
const result = await r.json();
if (result.success) { alert('✅ Служба добавлена'); location.reload(); }
else { alert('❌ Ошибка'); }
else { alert('❌ Ошибка: ' + (result.error || 'Неизвестная')); }
});
// Загрузка картинки
document.getElementById('uploadImageForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('category', document.getElementById('imageCategory').value);
formData.append('file', document.getElementById('imageFile').files[0]);
const resultDiv = document.getElementById('uploadResult');
resultDiv.innerHTML = '<div class="alert alert-info">Загрузка...</div>';
try {
const r = await fetch('/api/service/upload_image', {
method: 'POST',
body: formData
});
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}<br>Файл: ${result.file_path}</div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error}</div>`;
}
} catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
}
});
async function deleteService(id) {