convert-service: дизайн Nubes, лого, favicon, человеческое описание
This commit is contained in:
+91
-48
@@ -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">
|
||||||
<input type="file" name="file" accept=".doc,.rtf,.odt" required id="fileInput">
|
Загрузите файл в формате <strong>.doc</strong> (Microsoft Word 97–2003),
|
||||||
<button type="submit" id="submitBtn">Конвертировать</button>
|
<strong>.rtf</strong> или <strong>.odt</strong> — получите современный
|
||||||
</form>
|
<strong>.docx</strong>. Конвертация происходит на сервере через LibreOffice,
|
||||||
<div id="result"></div>
|
исходный файл никуда не сохраняется.
|
||||||
|
</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">
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="btn" id="submitBtn">Конвертировать</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user