main
Satur@it-depot.ru 2025-03-06 16:21:59 +03:00
parent 06c2feb619
commit 2a2dc6a30d
5 changed files with 94 additions and 4 deletions

View File

@ -6,6 +6,10 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY app.py . COPY app.py .
COPY templates/ ./templates/ COPY templates/ ./templates/
# Создаем и копируем папку uploads (если нужно предзаполнение)
RUN mkdir -p /app/uploads
COPY uploads/ ./uploads/
# Экспонируем оба порта # Экспонируем оба порта
EXPOSE 8001 8002 EXPOSE 8001 8002

36
app.py
View File

@ -7,6 +7,9 @@ import markdown
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
import io import io
import os
from fastapi.middleware.cors import CORSMiddleware
from werkzeug.utils import secure_filename
# Создаем два экземпляра FastAPI # Создаем два экземпляра FastAPI
web_app = FastAPI(title="Web Interface") # Для веб-интерфейса (порт 8001) web_app = FastAPI(title="Web Interface") # Для веб-интерфейса (порт 8001)
@ -59,6 +62,15 @@ if not unsorted_folder:
else: else:
unsorted_folder_id = unsorted_folder[0] 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): class Folder(BaseModel):
name: str name: str
@ -275,8 +287,30 @@ async def import_csv(file: UploadFile = File(...), folder_id: int | None = Query
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Ошибка импорта: {str(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 # CORS для API
from fastapi.middleware.cors import CORSMiddleware
api_app.add_middleware( api_app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://10.0.0.10:8001", "http://localhost:8001"], allow_origins=["http://10.0.0.10:8001", "http://localhost:8001"],

View File

@ -7,4 +7,5 @@ services:
- "8002:8002" # API (доступ для POST-запросов) - "8002:8002" # API (доступ для POST-запросов)
volumes: volumes:
- ./db:/db - ./db:/db
- ./uploads:/app/uploads # Монтируем папку uploads
restart: unless-stopped restart: unless-stopped

View File

@ -1,4 +1,5 @@
fastapi fastapi
uvicorn uvicorn
python-multipart python-multipart
markdown markdown
werkzeug

View File

@ -267,6 +267,10 @@
background: #222; background: #222;
} }
#notes-content { margin-top: 10px; } #notes-content { margin-top: 10px; }
#notes-content img {
max-width: 100%; /* Ограничение ширины изображений */
height: auto;
}
#note-modal { #note-modal {
display: none; display: none;
position: fixed; position: fixed;
@ -293,6 +297,12 @@
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #555; 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 button { margin-right: 10px; }
#note-modal.dark-theme button { #note-modal.dark-theme button {
background: #007bff; background: #007bff;
@ -407,6 +417,10 @@
<button onclick="formatItalic()">Курсив</button> <button onclick="formatItalic()">Курсив</button>
<button onclick="formatLink()">Ссылка</button> <button onclick="formatLink()">Ссылка</button>
</div> </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> <textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
<button onclick="saveNote()">Сохранить</button> <button onclick="saveNote()">Сохранить</button>
<button onclick="closeNoteModal()">Закрыть</button> <button onclick="closeNoteModal()">Закрыть</button>
@ -963,6 +977,41 @@
$('#note-textarea').val(currentNote || ''); // Устанавливаем текущую заметку или пустую строку $('#note-textarea').val(currentNote || ''); // Устанавливаем текущую заметку или пустую строку
$('#note-modal').show(); $('#note-modal').show();
$('#modal-overlay').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 = `![${altText || 'image'}](${imageUrl})`;
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() { function saveNote() {
@ -1002,13 +1051,14 @@
$('#modal-overlay').hide(); $('#modal-overlay').hide();
$('#note-textarea').val(''); // Очищаем текстовое поле $('#note-textarea').val(''); // Очищаем текстовое поле
selectedInstallId = null; // Сбрасываем выбранный ID selectedInstallId = null; // Сбрасываем выбранный ID
document.getElementById('image-upload-input').value = ''; // Сбрасываем поле загрузки
} }
function updateNotesPanel() { function updateNotesPanel() {
if (selectedInstallId) { if (selectedInstallId) {
const install = allInstalls.find(i => i.id === selectedInstallId); const install = allInstalls.find(i => i.id === selectedInstallId);
if (install && install.note) { if (install && install.note) {
$('#notes-content').html(marked.parse(install.note)); $('#notes-content').html(marked.parse(install.note)); // Рендеринг Markdown с изображениями
} else { } else {
$('#notes-content').html('<p>Нет заметок</p>'); $('#notes-content').html('<p>Нет заметок</p>');
} }
@ -1056,7 +1106,7 @@
function formatItalic() { function formatItalic() {
const textarea = $('#note-textarea'); const textarea = $('#note-textarea');
const start = textarea[0].selectionStart; const start = textarea[0].selectionStart;
const end = textarea[0].selectionEnd; end = textarea[0].selectionEnd;
const text = textarea.val(); const text = textarea.val();
const selected = text.substring(start, end); const selected = text.substring(start, end);
const newText = `*${selected}*`; const newText = `*${selected}*`;