Document picker code and architecture
This commit is contained in:
@@ -5,27 +5,32 @@ from flask import Flask, render_template, send_from_directory
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
# Конфигурация демо содержит только разрешённые расширения для browser picker.
|
||||
with (ROOT / "config.json").open(encoding="utf-8") as config_file:
|
||||
CONFIG = json.load(config_file)
|
||||
|
||||
|
||||
VERSION = "0.1.9"
|
||||
|
||||
# Flask нужен здесь только как статический сервер HTML, CSS и ES-модулей.
|
||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""Возвращает demo-страницу и передаёт в неё версию и JSON-конфигурацию."""
|
||||
return render_template("index.html", version=VERSION, config=CONFIG)
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
"""Возвращает 200 для liveness-проверки платформы."""
|
||||
return "ok", 200
|
||||
|
||||
|
||||
@app.get("/upload-frontend/<path:filename>")
|
||||
def upload_frontend(filename):
|
||||
"""Отдаёт один frontend-файл из изолированного каталога upload/frontend."""
|
||||
return send_from_directory(ROOT / "upload" / "frontend", filename)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* Базовые цвета и типографика всего demo-picker. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
@@ -5,22 +6,29 @@
|
||||
background: #e9eee8;
|
||||
}
|
||||
|
||||
/* Фон страницы намеренно отделён от карточной логики: picker не требует backend UI. */
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(circle at 10% 0%, #f8fbf4, transparent 38%), #e9eee8;
|
||||
}
|
||||
|
||||
/* Ограничивает ширину рабочей области и сохраняет адаптивные боковые поля. */
|
||||
.page { max-width: 980px; margin: 0 auto; padding: 64px 24px; }
|
||||
/* Служебный идентификатор версии приложения. */
|
||||
.eyebrow { color: #56745f; font: 700 12px/1.2 sans-serif; letter-spacing: 2px; }
|
||||
/* Заголовок и пояснение вводят пользователя в сценарий выбора документов. */
|
||||
h1 { max-width: 620px; margin: 12px 0; font-size: clamp(38px, 7vw, 72px); line-height: .95; font-weight: 400; }
|
||||
.lede { max-width: 560px; color: #5d6a61; font: 16px/1.6 sans-serif; }
|
||||
/* Группа действий: выбор файлов, папки и очистка состояния. */
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin: 36px 0 16px; }
|
||||
button { border: 0; border-radius: 4px; padding: 12px 17px; color: #f7faf4; background: #1f5a3b; font: 700 13px sans-serif; cursor: pointer; }
|
||||
button.secondary { color: #1f5a3b; background: #cbdcc9; }
|
||||
button.quiet { color: #5d6a61; background: transparent; }
|
||||
button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
/* Резервная область сообщения интегратора сохраняет высоту и не двигает таблицу. */
|
||||
.status { min-height: 22px; color: #56745f; font: 13px sans-serif; }
|
||||
/* Горизонтальный scroll на узких экранах не ломает таблицу дерева. */
|
||||
.table-wrap { overflow-x: auto; border-top: 1px solid #b9c7ba; }
|
||||
table { width: 100%; border-collapse: collapse; font: 14px/1.4 sans-serif; }
|
||||
th, td { padding: 14px 10px; border-bottom: 1px solid #cbd5cc; text-align: left; }
|
||||
@@ -31,14 +39,17 @@ td:nth-child(2) { width: 140px; color: #68766c; }
|
||||
.tree-folder { color: #1f5a3b; }
|
||||
.tree-zip { color: #8a5a20; }
|
||||
.tree-chevron { display: inline-block; width: 12px; color: #78917d; font-size: 10px; }
|
||||
/* Удаление работает для leaf и групп и обрабатывается делегированием в initUploadTable. */
|
||||
.remove-btn { padding: 2px 7px; color: #8c5148; background: transparent; font: 20px/1 sans-serif; }
|
||||
.remove-btn:hover { color: #b32f22; }
|
||||
.tree-row td:first-child { white-space: nowrap; }
|
||||
.count { color: #5d6a61; font: 13px sans-serif; }
|
||||
/* Legacy-стили результата сохранены для совместимости, текущий picker их не использует. */
|
||||
.result { margin-top: 44px; padding-top: 18px; border-top: 2px solid #1f5a3b; }
|
||||
.result h2 { font-size: 26px; font-weight: 400; }
|
||||
.result li { margin: 8px 0; font: 14px sans-serif; }
|
||||
@media (max-width: 600px) {
|
||||
/* На мобильном экране кнопки получают равномерную ширину. */
|
||||
.page { padding: 36px 16px; }
|
||||
.toolbar button { flex: 1 1 42%; }
|
||||
}
|
||||
@@ -7,12 +7,15 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Основной контейнер демо: Flask только подставляет version, остальное работает в браузере. -->
|
||||
<main class="page">
|
||||
<!-- Заголовок показывает назначение picker и текущую версию сборки. -->
|
||||
<header>
|
||||
<p class="eyebrow">FILE INTAKE / {{ version }}</p>
|
||||
<h1>Загрузка документов</h1>
|
||||
<p class="lede">Выберите отдельные файлы или целую папку. Архивы будут раскрыты автоматически.</p>
|
||||
</header>
|
||||
<!-- Скрытые input дают браузеру FileList; кнопки ниже программно вызывают click(). -->
|
||||
<section class="toolbar" aria-label="Выбор файлов">
|
||||
<input id="file-input" type="file" accept=".pdf,.doc,.docx,.txt,.md,.zip" multiple hidden>
|
||||
<input id="folder-input" type="file" accept=".pdf,.doc,.docx,.txt,.md,.zip" webkitdirectory directory multiple hidden>
|
||||
@@ -20,17 +23,22 @@
|
||||
<button id="folder-btn" type="button" class="secondary">Выбрать папку</button>
|
||||
<button id="clear-btn" type="button" class="quiet">Очистить</button>
|
||||
</section>
|
||||
<!-- Сюда могут выводиться сообщения интегратора; таблица использует отдельный render(). -->
|
||||
<p id="status" class="status" role="status"></p>
|
||||
<!-- tableBodyEl является обязательным DOM-контрактом initUploadTable. -->
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Путь</th><th>Размер</th><th>Статус</th><th></th></tr></thead>
|
||||
<tbody id="table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- countEl обновляется render() и включает leaf-файлы даже в свернутых группах. -->
|
||||
<p id="count" class="count"></p>
|
||||
</main>
|
||||
<!-- fflate должен быть загружен до ES-модулей, потому что ZIP-парсер использует global fflate. -->
|
||||
<script src="{{ url_for('static', filename='vendor/fflate.min.js') }}"></script>
|
||||
<script type="module">
|
||||
// Конфигурация приходит из Flask; DOM-ссылки передаются в переиспользуемый picker.
|
||||
import { initUploadTable } from "/upload-frontend/table/init_upload_table.js";
|
||||
|
||||
const cfg = {{ config|tojson }};
|
||||
@@ -41,7 +49,9 @@
|
||||
tableBodyEl: document.querySelector('#table-body'),
|
||||
countEl: document.querySelector('#count'),
|
||||
});
|
||||
// status зарезервирован для интегратора; picker сейчас обновляет только таблицу и count.
|
||||
const status = document.querySelector('#status');
|
||||
// Кнопки не содержат собственной логики выбора: весь state остаётся внутри table API.
|
||||
document.querySelector('#files-btn').onclick = () => table.pickFiles();
|
||||
document.querySelector('#folder-btn').onclick = () => table.pickFolder();
|
||||
document.querySelector('#clear-btn').onclick = () => table.clear();
|
||||
|
||||
Reference in New Issue
Block a user