📸 Загрузка картинок через веб-админку
✅ Реализовано: - Форма загрузки в веб-админке (/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")
|
app = FastAPI(title="Домовой Бот - MEGA Admin Panel", version="3.0.0")
|
||||||
templates = Jinja2Templates(directory="web/templates")
|
templates = Jinja2Templates(directory="web/templates")
|
||||||
app.mount("/static", StaticFiles(directory="web/static"), name="static")
|
app.mount("/static", StaticFiles(directory="web/static"), name="static")
|
||||||
|
app.mount("/static/service_images", StaticFiles(directory="data/service_images"), name="service_images")
|
||||||
security = HTTPBasic()
|
security = HTTPBasic()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -218,13 +219,62 @@ async def api_add_service(request: Request, username: str = Depends(get_current_
|
||||||
phone=data['phone'],
|
phone=data['phone'],
|
||||||
category=data.get('category', 'other'),
|
category=data.get('category', 'other'),
|
||||||
description=data.get('description', ''),
|
description=data.get('description', ''),
|
||||||
verified_by_admin=True
|
verified_by_admin=True,
|
||||||
|
image_path=data.get('image_path', None)
|
||||||
)
|
)
|
||||||
session.add(service)
|
session.add(service)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return JSONResponse({"success": True, "message": "Служба добавлена"})
|
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")
|
@app.post("/api/service/{service_id}/update")
|
||||||
async def api_update_service(service_id: int, request: Request,
|
async def api_update_service(service_id: int, request: Request,
|
||||||
username: str = Depends(get_current_admin)):
|
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 { 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); }
|
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
|
||||||
.content { padding: 2rem; }
|
.content { padding: 2rem; }
|
||||||
|
.service-image-preview { max-width: 200px; max-height: 200px; object-fit: contain; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -49,11 +50,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<select class="form-select" id="serviceCategory">
|
<select class="form-select" id="serviceCategory">
|
||||||
<option value="emergency">Экстренные</option>
|
<option value="emergency">🚨 Экстренные</option>
|
||||||
<option value="utility">ЖКХ</option>
|
<option value="utility">🔧 ЖКХ</option>
|
||||||
<option value="masters">Мастера</option>
|
<option value="masters">👷 Мастера</option>
|
||||||
<option value="police">Полиция</option>
|
<option value="police">👮 Полиция</option>
|
||||||
<option value="other">Другое</option>
|
<option value="other">📋 Другое</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
@ -66,9 +67,38 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<h5>Существующие службы:</h5>
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -76,6 +106,7 @@
|
||||||
<th>Название</th>
|
<th>Название</th>
|
||||||
<th>Телефон</th>
|
<th>Телефон</th>
|
||||||
<th>Описание</th>
|
<th>Описание</th>
|
||||||
|
<th>Картинка</th>
|
||||||
<th>Действия</th>
|
<th>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -98,6 +129,15 @@
|
||||||
<td>{{ service.name }}</td>
|
<td>{{ service.name }}</td>
|
||||||
<td><a href="tel:{{ service.phone }}">{{ service.phone }}</a></td>
|
<td><a href="tel:{{ service.phone }}">{{ service.phone }}</a></td>
|
||||||
<td>{{ service.description or '—' }}</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>
|
<td>
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteService({{ service.service_id }})">
|
<button class="btn btn-sm btn-danger" onclick="deleteService({{ service.service_id }})">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
|
|
@ -117,6 +157,7 @@
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Добавление службы
|
||||||
document.getElementById('addServiceForm').addEventListener('submit', async (e) => {
|
document.getElementById('addServiceForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const data = {
|
||||||
|
|
@ -131,7 +172,34 @@
|
||||||
});
|
});
|
||||||
const result = await r.json();
|
const result = await r.json();
|
||||||
if (result.success) { alert('✅ Служба добавлена'); location.reload(); }
|
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) {
|
async function deleteService(id) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue