images
parent
06c2feb619
commit
2a2dc6a30d
|
|
@ -6,6 +6,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||
COPY app.py .
|
||||
COPY templates/ ./templates/
|
||||
|
||||
# Создаем и копируем папку uploads (если нужно предзаполнение)
|
||||
RUN mkdir -p /app/uploads
|
||||
COPY uploads/ ./uploads/
|
||||
|
||||
# Экспонируем оба порта
|
||||
EXPOSE 8001 8002
|
||||
|
||||
|
|
|
|||
36
app.py
36
app.py
|
|
@ -7,6 +7,9 @@ import markdown
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
import io
|
||||
import os
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
# Создаем два экземпляра FastAPI
|
||||
web_app = FastAPI(title="Web Interface") # Для веб-интерфейса (порт 8001)
|
||||
|
|
@ -59,6 +62,15 @@ if not unsorted_folder:
|
|||
else:
|
||||
unsorted_folder_id = unsorted_folder[0]
|
||||
|
||||
# Папка для загрузки изображений
|
||||
UPLOAD_FOLDER = "/app/uploads"
|
||||
if not os.path.exists(UPLOAD_FOLDER):
|
||||
os.makedirs(UPLOAD_FOLDER)
|
||||
|
||||
# Монтируем папку uploads для доступа к изображениям
|
||||
api_app.mount("/uploads", StaticFiles(directory=UPLOAD_FOLDER), name="uploads")
|
||||
web_app.mount("/uploads", StaticFiles(directory=UPLOAD_FOLDER), name="uploads")
|
||||
|
||||
# Модели данных
|
||||
class Folder(BaseModel):
|
||||
name: str
|
||||
|
|
@ -275,8 +287,30 @@ async def import_csv(file: UploadFile = File(...), folder_id: int | None = Query
|
|||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Ошибка импорта: {str(e)}")
|
||||
|
||||
# Новый эндпоинт для загрузки изображений
|
||||
@api_app.post("/api/upload-image")
|
||||
async def upload_image(install_id: int = Query(..., description="ID записи для ассоциации изображения"), file: UploadFile = File(...)):
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No file provided")
|
||||
|
||||
# Проверка расширения файла
|
||||
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
if not secure_filename(file.filename).rsplit('.', 1)[1].lower() in allowed_extensions:
|
||||
raise HTTPException(status_code=400, detail="Invalid file format. Use png, jpg, jpeg, or gif")
|
||||
|
||||
# Генерация безопасного имени файла
|
||||
filename = secure_filename(f"{install_id}_{file.filename}")
|
||||
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
|
||||
# Сохранение файла
|
||||
with open(file_path, "wb") as buffer:
|
||||
buffer.write(await file.read())
|
||||
|
||||
# Возвращаем URL изображения
|
||||
url = f"/uploads/{filename}"
|
||||
return {"url": url}
|
||||
|
||||
# CORS для API
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
api_app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://10.0.0.10:8001", "http://localhost:8001"],
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ services:
|
|||
- "8002:8002" # API (доступ для POST-запросов)
|
||||
volumes:
|
||||
- ./db:/db
|
||||
- ./uploads:/app/uploads # Монтируем папку uploads
|
||||
restart: unless-stopped
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
python-multipart
|
||||
markdown
|
||||
markdown
|
||||
werkzeug
|
||||
|
|
@ -267,6 +267,10 @@
|
|||
background: #222;
|
||||
}
|
||||
#notes-content { margin-top: 10px; }
|
||||
#notes-content img {
|
||||
max-width: 100%; /* Ограничение ширины изображений */
|
||||
height: auto;
|
||||
}
|
||||
#note-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
|
|
@ -293,6 +297,12 @@
|
|||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
#note-modal .image-upload {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#note-modal .image-upload input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#note-modal button { margin-right: 10px; }
|
||||
#note-modal.dark-theme button {
|
||||
background: #007bff;
|
||||
|
|
@ -407,6 +417,10 @@
|
|||
<button onclick="formatItalic()">Курсив</button>
|
||||
<button onclick="formatLink()">Ссылка</button>
|
||||
</div>
|
||||
<div class="image-upload">
|
||||
<input type="file" id="image-upload-input" accept="image/*" onchange="uploadImage()">
|
||||
<button onclick="document.getElementById('image-upload-input').click()">Добавить изображение</button>
|
||||
</div>
|
||||
<textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
|
||||
<button onclick="saveNote()">Сохранить</button>
|
||||
<button onclick="closeNoteModal()">Закрыть</button>
|
||||
|
|
@ -963,6 +977,41 @@
|
|||
$('#note-textarea').val(currentNote || ''); // Устанавливаем текущую заметку или пустую строку
|
||||
$('#note-modal').show();
|
||||
$('#modal-overlay').show();
|
||||
document.getElementById('image-upload-input').value = ''; // Сбрасываем поле загрузки
|
||||
}
|
||||
|
||||
function uploadImage() {
|
||||
const fileInput = document.getElementById('image-upload-input');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('install_id', selectedInstallId);
|
||||
|
||||
$.ajax({
|
||||
url: `${API_URL}/upload-image`,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (response) {
|
||||
console.log('Image uploaded:', response);
|
||||
const imageUrl = response.url; // Предполагаем, что сервер возвращает { url: "path/to/image" }
|
||||
const textarea = $('#note-textarea');
|
||||
const cursorPos = textarea[0].selectionStart;
|
||||
const text = textarea.val();
|
||||
const altText = prompt('Введите альтернативный текст для изображения:', file.name);
|
||||
const markdownImage = ``;
|
||||
textarea.val(text.substring(0, cursorPos) + markdownImage + text.substring(cursorPos));
|
||||
textarea[0].selectionStart = cursorPos + markdownImage.length;
|
||||
textarea[0].selectionEnd = cursorPos + markdownImage.length;
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error uploading image:', status, error);
|
||||
alert(`Не удалось загрузить изображение: ${xhr.responseJSON?.detail || error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveNote() {
|
||||
|
|
@ -1002,13 +1051,14 @@
|
|||
$('#modal-overlay').hide();
|
||||
$('#note-textarea').val(''); // Очищаем текстовое поле
|
||||
selectedInstallId = null; // Сбрасываем выбранный ID
|
||||
document.getElementById('image-upload-input').value = ''; // Сбрасываем поле загрузки
|
||||
}
|
||||
|
||||
function updateNotesPanel() {
|
||||
if (selectedInstallId) {
|
||||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||||
if (install && install.note) {
|
||||
$('#notes-content').html(marked.parse(install.note));
|
||||
$('#notes-content').html(marked.parse(install.note)); // Рендеринг Markdown с изображениями
|
||||
} else {
|
||||
$('#notes-content').html('<p>Нет заметок</p>');
|
||||
}
|
||||
|
|
@ -1056,7 +1106,7 @@
|
|||
function formatItalic() {
|
||||
const textarea = $('#note-textarea');
|
||||
const start = textarea[0].selectionStart;
|
||||
const end = textarea[0].selectionEnd;
|
||||
end = textarea[0].selectionEnd;
|
||||
const text = textarea.val();
|
||||
const selected = text.substring(start, end);
|
||||
const newText = `*${selected}*`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue