images
parent
06c2feb619
commit
2a2dc6a30d
|
|
@ -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
36
app.py
|
|
@ -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"],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
python-multipart
|
python-multipart
|
||||||
markdown
|
markdown
|
||||||
|
werkzeug
|
||||||
|
|
@ -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 = ``;
|
||||||
|
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}*`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue