init: UI выбора файлов — input[file], таблица, Flask skeleton
Deploy loadtest / validate (push) Waiting to run
Deploy loadtest / validate (push) Waiting to run
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
from flask import Flask, render_template
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* app.js — UI выбора файлов из локали.
|
||||
*
|
||||
* Выбираем файлы через <input type="file" multiple>,
|
||||
* показываем в таблице: имя, размер, кнопка удаления.
|
||||
*
|
||||
* Никакой загрузки на сервер — чисто клиент.
|
||||
*/
|
||||
|
||||
var files = []; // [{ name, size, lastModified }]
|
||||
|
||||
var fileInput = document.getElementById('fileInput');
|
||||
var fileTable = document.getElementById('fileTable');
|
||||
var fileSummary = document.getElementById('fileSummary');
|
||||
var clearBtn = document.getElementById('clearBtn');
|
||||
|
||||
fileInput.addEventListener('change', function () {
|
||||
addFiles(fileInput.files);
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
/** formatSize(bytes) → "1.2 KB", "3.4 MB", "900 B" */
|
||||
function formatSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '—';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1073741824).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
/** addFiles(fileList) — добавить файлы в массив и перерисовать */
|
||||
function addFiles(fileList) {
|
||||
for (var i = 0; i < fileList.length; i++) {
|
||||
var f = fileList[i];
|
||||
files.push({
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
lastModified: f.lastModified
|
||||
});
|
||||
}
|
||||
renderTable();
|
||||
}
|
||||
|
||||
/** removeFile(i) — удалить файл по индексу */
|
||||
function removeFile(i) {
|
||||
files.splice(i, 1);
|
||||
renderTable();
|
||||
}
|
||||
|
||||
/** renderTable() — перерисовать таблицу и сводку */
|
||||
function renderTable() {
|
||||
if (files.length === 0) {
|
||||
fileTable.innerHTML = '<tr class="empty-row"><td colspan="3">Нет файлов — выберите файлы</td></tr>';
|
||||
fileSummary.textContent = 'Нет файлов';
|
||||
clearBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var totalSize = 0;
|
||||
var rows = [];
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i];
|
||||
totalSize += f.size;
|
||||
rows.push(
|
||||
'<tr>' +
|
||||
'<td class="name-cell" title="' + escHtml(f.name) + '">' + escHtml(f.name) + '</td>' +
|
||||
'<td class="num-cell">' + formatSize(f.size) + '</td>' +
|
||||
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить">✕</button></td>' +
|
||||
'</tr>'
|
||||
);
|
||||
}
|
||||
fileTable.innerHTML = rows.join('');
|
||||
|
||||
fileSummary.textContent = files.length + ' файл., ' + formatSize(totalSize);
|
||||
clearBtn.disabled = false;
|
||||
}
|
||||
|
||||
/** clearAll() — очистить всё */
|
||||
function clearAll() {
|
||||
files = [];
|
||||
renderTable();
|
||||
}
|
||||
|
||||
/** escHtml(s) — экранирование HTML */
|
||||
function escHtml(s) {
|
||||
if (s == null) return '';
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Первичный рендер
|
||||
renderTable();
|
||||
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loadtest — загрузка файлов</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--border: #e0e0e0;
|
||||
--text: #1a1a1a;
|
||||
--muted: #888;
|
||||
--brand: #2563eb;
|
||||
--red: #ef4444;
|
||||
--green: #22c55e;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font: 14px system-ui, sans-serif; color: var(--text); background: var(--bg); min-height: 100vh; }
|
||||
.topbar {
|
||||
background: var(--card); border-bottom: 1px solid var(--border);
|
||||
padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 12px;
|
||||
font-weight: 600; font-size: 15px;
|
||||
}
|
||||
.content { max-width: 780px; margin: 28px auto; padding: 0 16px; }
|
||||
.card {
|
||||
background: var(--card); border-radius: 10px;
|
||||
border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-header {
|
||||
background: #fafafa; padding: 10px 16px;
|
||||
font-weight: 600; font-size: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.card-body { padding: 16px; }
|
||||
|
||||
.file-input-wrap { margin-bottom: 12px; }
|
||||
.file-input-wrap input[type="file"] { width: 100%; font-size: 14px; }
|
||||
|
||||
.table-wrap {
|
||||
border: 1px solid var(--border); border-radius: 6px;
|
||||
overflow-y: auto; max-height: 60vh;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th {
|
||||
background: #fafafa; text-transform: uppercase; padding: 6px 10px;
|
||||
border-right: 1px solid var(--border); text-align: left;
|
||||
font-weight: 600; font-size: 11px; color: var(--muted);
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px; border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
tr:hover td { background: rgba(37,99,235,.03); }
|
||||
.name-cell { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.num-cell { text-align: right; white-space: nowrap; }
|
||||
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
|
||||
|
||||
.remove-btn {
|
||||
cursor: pointer; color: #f87171; background: none; border: none;
|
||||
padding: 2px 6px; font-size: 15px; line-height: 1;
|
||||
}
|
||||
.remove-btn:hover { color: var(--red); }
|
||||
|
||||
.footer-bar {
|
||||
margin-top: 12px; display: flex; justify-content: space-between; align-items: center;
|
||||
font-size: 13px; color: var(--muted);
|
||||
}
|
||||
.btn {
|
||||
height: 34px; border-radius: 6px; padding: 0 16px; font-size: 13px;
|
||||
font-family: inherit; cursor: pointer; border: 1px solid var(--border);
|
||||
background: var(--card); color: var(--text);
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
|
||||
.btn-primary:hover { opacity: .9; }
|
||||
.btn:disabled { opacity: .4; cursor: not-allowed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
📦 Loadtest — загрузка файлов
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
<div class="card-header">Выбор файлов</div>
|
||||
<div class="card-body">
|
||||
<div class="file-input-wrap">
|
||||
<input type="file" id="fileInput" multiple>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th style="width:100px;">Размер</th>
|
||||
<th style="width:40px;">✕</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fileTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer-bar">
|
||||
<span id="fileSummary">Нет файлов</span>
|
||||
<button class="btn" id="clearBtn" onclick="clearAll()" disabled>Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user