📸 Загрузка картинок через веб-админку
✅ Реализовано: - Форма загрузки в веб-админке (/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:
parent
f0a081a988
commit
819bed706b
2 changed files with 125 additions and 7 deletions
52
web/app.py
52
web/app.py
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue