convert-service: дизайн Nubes, лого, favicon, человеческое описание

This commit is contained in:
“Naeel”
2026-07-16 10:14:57 +04:00
parent 470cc0a404
commit fcb82d2988
+89 -46
View File
@@ -10,66 +10,109 @@ HTML = """<!DOCTYPE html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Конвертер .doc → .docx</title> <title>Конвертер документов — Nubes</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%232563eb'/><text x='16' y='24' text-anchor='middle' font-size='24'>📄</text></svg>">
<style> <style>
body{font-family:system-ui,sans-serif;max-width:600px;margin:40px auto;padding:0 16px;color:#1a1a1a} :root{--brand-primary:#2563eb;--brand-primary-dark:#1d4ed8;--brand-grey-light:#f3f4f6;--brand-gray:#d1d5db}
h1{font-size:20px;margin-bottom:8px} *{box-sizing:border-box;margin:0;padding:0}
.sub{color:#888;font-size:13px;margin-bottom:24px} body{font-family:system-ui,-apple-system,sans-serif;color:#1a1a1a;min-height:100vh;background:linear-gradient(135deg,#f0f4ff 0%,#fff 50%,#f0f4ff 100%)}
form{background:#f5f5f5;padding:24px;border-radius:8px} .logo-bar{display:flex;align-items:center;gap:12px;padding:20px 24px}
input[type=file]{margin-bottom:16px;display:block} .logo-bar svg{height:22px}
button{background:#0066cc;color:#fff;border:none;padding:10px 24px;border-radius:6px;cursor:pointer;font-size:14px} .logo-bar .badge{background:var(--brand-primary);color:#fff;font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;letter-spacing:.5px}
button:hover{background:#0052a3} main{max-width:640px;margin:0 auto;padding:0 16px 40px}
button:disabled{opacity:0.5;cursor:not-allowed} .card{background:#fff;border:1px solid var(--brand-gray);border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden;margin-bottom:20px}
.result{margin-top:20px;padding:16px;border-radius:8px} .card-header{background:var(--brand-grey-light);padding:12px 16px;font-size:14px;font-weight:600;color:#374151}
.ok{background:#e6ffe6;border:1px solid #b3e6b3} .card-body{padding:16px}
.err{background:#ffe6e6;border:1px solid #e6b3b3} h1{font-size:22px;font-weight:700;margin-bottom:6px}
.info{background:#e6f0ff;border:1px solid #b3d4e6;margin:16px 0;padding:12px;border-radius:6px;font-size:13px} .sub{color:#6b7280;font-size:14px;line-height:1.5;margin-bottom:20px}
code{background:#eee;padding:1px 4px;border-radius:3px;font-size:12px} .api-info{font-size:13px;color:#6b7280;line-height:1.7}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #ccc;border-top-color:#0066cc;border-radius:50%;animation:spin 0.8s linear infinite;margin-right:6px;vertical-align:middle} .api-info code{background:#eef2ff;color:var(--brand-primary);padding:2px 6px;border-radius:4px;font-size:12px;font-weight:500}
.form-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.file-label{flex:1;min-width:200px;display:block;border:2px dashed var(--brand-gray);border-radius:8px;padding:28px 16px;text-align:center;cursor:pointer;transition:border-color .2s;color:#6b7280;font-size:14px}
.file-label:hover{border-color:var(--brand-primary)}
.file-label.has-file{border-style:solid;border-color:var(--brand-primary);background:#eef2ff}
.file-label input{display:none}
.file-label .filename{color:#1a1a1a;font-weight:500;word-break:break-all}
.btn{background:var(--brand-primary);color:#fff;border:none;padding:10px 28px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:500;white-space:nowrap;transition:background .15s}
.btn:hover{background:var(--brand-primary-dark)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.result{margin-top:16px;padding:12px 16px;border-radius:8px;font-size:14px;display:none}
.result.show{display:block}
.result.ok{background:#ecfdf5;border:1px solid #a7f3d0;color:#065f46}
.result.err{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}
.result a{color:var(--brand-primary);font-weight:600}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #ccc;border-top-color:var(--brand-primary);border-radius:50%;animation:spin .8s linear infinite;margin-right:6px;vertical-align:middle}
.footer{text-align:center;padding:0 16px 40px;font-size:12px;color:#9ca3af}
@keyframes spin{to{transform:rotate(360deg)}} @keyframes spin{to{transform:rotate(360deg)}}
</style> </style>
</head> </head>
<body> <body>
<h1>📄 Конвертер .doc → .docx</h1> <div class="logo-bar">
<p class="sub">LibreOffice в headless-режиме. Поддерживает .doc, .rtf, .odt.</p> <svg viewBox="0 0 8222 1821" style="height:22px"><path fill="#001C34" d="M477 480l-414 0-64 276 179 0 1 1029 0 27 276 0 0-872c0-101 82-184 184-184l589 4c101 0 184 82 184 184l0 871 276 0 0-871c0-254-206-460-460-460l-589-4-162 1 0-1zM6665 1813l1181 0c212 0 376-175 376-387 0-212-173-385-385-385l-653 0c-60 0-109-49-109-109l0-66c0-60 49-109 109-109l696 0 64-276-760 0c-212 0-384 173-384 385l0 66c0 212 172 385 384 385l654 0c60 0 109 49 109 109s-49 116-109 116l-1119 0-54 271zM6201 484l-723 0c-216 0-392 175-392 391l0 486c0 248 202 450 450 450l990 0 62-268-1052 0c-96 0-175-86-175-182l-1-40 923 0 309-2 0-238 0-206c0-216-176-391-392-391zm116 560l-955 0 0-169c0-64 52-116 116-116l723 0c64 0 116 52 116 116l0 169zM4683 1194l0 168c0 101-81 179-182 179l-590 1c-93 0-170-70-182-160l0-458c12-90 89-160 182-160l589 3c101 0 183 83 183 184l0 30 0 213zm-1230-345l-1-795 277-54 0 526c56-24 117-38 182-38l589 4c253 0 459 206 459 458l0 31 0 213 0 168c0 253-206 459-459 459l-589-4c-253 0-459-206-459-459l0-171 0-241 1-98zM3041 1151l0 204c0 101-82 183-183 183l-582-3c-101 0-183-82-183-183l0-68 2 0 0-862-277 54 0 103 0 773c0 253 205 458 458 458l582 4c253 0 459-206 459-459l0-70 0 0 0-863-277 54 1 675z"/></svg>
<div class="info"> <span class="badge">КОНВЕРТЕР</span>
<strong>API:</strong> <code>POST /convert</code> с <code>multipart/form-data</code>, поле <code>file</code>.<br>
Ответ: файл .docx или <code>{"ok":false,"error":"..."}</code>.<br>
<strong>Health:</strong> <code>GET /health</code> → <code>{"ok":true}</code>
</div> </div>
<hr style="margin:24px 0;border:none;border-top:1px solid #e0e0e0"> <main>
<h2 style="font-size:16px;margin-bottom:12px">Конвертировать файл</h2> <h1>Конвертация старых документов Word</h1>
<form id="convForm" enctype="multipart/form-data"> <p class="sub">
Загрузите файл в формате <strong>.doc</strong> (Microsoft Word 972003),
<strong>.rtf</strong> или <strong>.odt</strong> — получите современный
<strong>.docx</strong>. Конвертация происходит на сервере через LibreOffice,
исходный файл никуда не сохраняется.
</p>
<div class="card">
<div class="card-header">📦 Конвертировать файл</div>
<div class="card-body">
<form id="convForm">
<div class="form-row">
<label class="file-label" id="fileLabel">
<span id="fileText">Выберите файл .doc, .rtf или .odt</span>
<input type="file" name="file" accept=".doc,.rtf,.odt" required id="fileInput"> <input type="file" name="file" accept=".doc,.rtf,.odt" required id="fileInput">
<button type="submit" id="submitBtn">Конвертировать</button> </label>
<button type="submit" class="btn" id="submitBtn">Конвертировать</button>
</div>
</form> </form>
<div id="result"></div> <div class="result" id="result"></div>
</div>
</div>
<div class="card">
<div class="card-header">🔌 API для разработчиков</div>
<div class="card-body">
<div class="api-info">
Отправьте <code>POST /convert</code> с файлом в <code>multipart/form-data</code>
в поле <code>file</code>.<br>
В ответ придёт готовый <code>.docx</code> — сразу сохраняйте.<br>
Ошибки возвращаются как <code>{"ok": false, "error": "..."}</code> с кодом 500.<br>
Проверка доступности: <code>GET /health</code> → <code>{"ok": true}</code>
</div>
</div>
</div>
</main>
<footer class="footer">Nubes © 2026</footer>
<script> <script>
var f = document.getElementById('fileInput');
var t = document.getElementById('fileText');
var l = document.getElementById('fileLabel');
f.onchange = function() {
if (f.files.length) { t.className='filename'; t.textContent=f.files[0].name; l.classList.add('has-file'); }
else { t.className=''; t.textContent='Выберите файл .doc, .rtf или .odt'; l.classList.remove('has-file'); }
};
document.getElementById('convForm').onsubmit = async function(e) { document.getElementById('convForm').onsubmit = async function(e) {
e.preventDefault(); e.preventDefault();
var btn = document.getElementById('submitBtn'); var b = document.getElementById('submitBtn'), r = document.getElementById('result');
var result = document.getElementById('result'); b.disabled = true; b.innerHTML = '<span class="spinner"></span>Идёт конвертация...';
btn.disabled = true; r.className = 'result'; r.innerHTML = '';
btn.innerHTML = '<span class="spinner"></span>Конвертирую...';
result.innerHTML = '';
try { try {
var fd = new FormData(); var fd = new FormData(); fd.append('file', f.files[0]);
fd.append('file', document.getElementById('fileInput').files[0]);
var resp = await fetch('/convert', { method: 'POST', body: fd }); var resp = await fetch('/convert', { method: 'POST', body: fd });
if (resp.ok && resp.headers.get('Content-Type').includes('application/vnd')) { if (resp.ok && resp.headers.get('Content-Type').includes('application/vnd')) {
var blob = await resp.blob(); var blob = await resp.blob(), url = URL.createObjectURL(blob);
var url = URL.createObjectURL(blob); var fn = f.files[0].name.replace(/\\.[^.]+$/,'') + '.docx';
var fn = document.getElementById('fileInput').files[0].name.replace(/\\.[^.]+$/,'') + '.docx'; r.className = 'result ok show';
result.innerHTML = '<div class="result ok">✅ Готово! <a href="'+url+'" download="'+fn+'">Скачать '+fn+'</a></div>'; r.innerHTML = '✅ Готово! <a href=\"'+url+'\" download=\"'+fn+'\">Скачать '+fn+'</a>';
} else { } else { var j = await resp.json();
var err = await resp.json(); r.className = 'result err show'; r.textContent = '' + (j.error || 'Неизвестная ошибка'); }
result.innerHTML = '<div class="result err">❌ ' + (err.error || 'Ошибка') + '</div>'; } catch(ex) { r.className = 'result err show'; r.textContent = 'Ошибка соединения: ' + ex.message; }
} b.disabled = false; b.textContent = 'Конвертировать';
} catch(ex) {
result.innerHTML = '<div class="result err">❌ Сеть: ' + ex.message + '</div>';
}
btn.disabled = false;
btn.textContent = 'Конвертировать';
}; };
</script> </script>
</body> </body>