Files
python_script/flask-dev-api/templates/ai_translate.html
DelLevin-Home 05e733e872 22
2026-06-17 10:43:33 +08:00

665 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome.css') }}">
<title>AI 翻译</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f5f5f5; color: #333; height: 100vh; overflow: hidden; display: flex;
}
/* ===== 左侧面板 ===== */
.left-panel {
width: 460px; flex-shrink: 0; display: flex; flex-direction: column;
border-right: 1px solid #e8e8e8; background: #fff; height: 100vh; overflow-y: auto;
}
.left-panel::-webkit-scrollbar { width: 4px; }
.left-panel::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
.left-body { padding: 20px 22px 28px; }
/* 标题 */
.page-header {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;
}
.page-header h1 { font-size: 18px; font-weight: 700; color: #1a1a1a; }
.page-header h1 i { color: #0078d4; margin-right: 8px; font-size: 16px; }
.page-subtitle { color: #aaa; font-size: 12px; margin-bottom: 18px; }
/* 设置按钮 */
.icon-btn {
width: 30px; height: 30px; border: none; background: none;
border-radius: 8px; cursor: pointer; font-size: 14px; color: #bbb;
display: flex; align-items: center; justify-content: center; transition: all 0.15s;
}
.icon-btn:hover { background: #f0f0f0; color: #666; }
/* 设置弹窗 */
.modal-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.35); z-index: 1000;
justify-content: center; align-items: flex-start; padding-top: 80px;
}
.modal-overlay.show { display: flex; }
.modal {
background: #fff; border-radius: 12px; width: 520px;
box-shadow: 0 12px 40px rgba(0,0,0,0.2); overflow: hidden;
}
.modal-head {
padding: 14px 18px; border-bottom: 1px solid #eee;
display: flex; align-items: center; justify-content: space-between;
}
.modal-head h3 { font-size: 14px; font-weight: 600; color: #333; }
.modal-head h3 i { margin-right: 6px; color: #999; }
.modal-close {
width: 26px; height: 26px; border: none; background: #f5f5f5;
border-radius: 6px; cursor: pointer; font-size: 13px; color: #999;
display: flex; align-items: center; justify-content: center;
}
.modal-close:hover { background: #eee; color: #333; }
.modal-body { padding: 16px 18px; }
.modal-foot {
padding: 10px 18px; border-top: 1px solid #eee;
display: flex; justify-content: flex-end; gap: 8px;
}
.modal-foot button {
padding: 6px 18px; font-size: 12px; font-weight: 600;
border-radius: 6px; cursor: pointer; transition: all 0.15s;
}
.btn-cancel { border: 1px solid #e0e0e0; background: #fff; color: #666; }
.btn-cancel:hover { background: #f5f5f5; }
.btn-save { border: none; background: #0078d4; color: #fff; }
.btn-save:hover { background: #006cbd; }
/* 卡片 */
.card {
background: #fafafa; border: 1px solid #f0f0f0; border-radius: 10px;
padding: 14px 16px; margin-bottom: 10px;
}
.card-title {
font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase;
letter-spacing: 0.06em; margin-bottom: 10px; display: flex; align-items: center; gap: 6px;
}
.card-title i { font-size: 11px; }
/* 模型列表 */
.model-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border: 1px solid #e8e8e8; border-radius: 6px;
margin-bottom: 5px; cursor: pointer; transition: all 0.15s;
font-size: 12px; color: #333; background: #fff;
}
.model-item:hover { border-color: #0078d4; background: #f0f7ff; }
.model-item.active { border-color: #0078d4; background: #e3f2fd; }
.model-item .model-name { flex: 1; font-weight: 500; }
.model-item .model-status {
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
}
.model-item .model-status.loaded { background: #27ae60; }
/* 表单 */
.field { margin-bottom: 8px; }
.field:last-child { margin-bottom: 0; }
.field label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 3px; color: #555; }
.field select, .field input[type="text"] {
width: 100%; padding: 7px 10px;
border: 1px solid #e0e0e0; border-radius: 6px;
background: #fff; color: #333; font-size: 13px;
outline: none; transition: all 0.15s;
}
.field select:focus, .field input[type="text"]:focus {
border-color: #0078d4; box-shadow: 0 0 0 2px rgba(0,120,212,0.06);
}
/* 语言行 */
.lang-row { display: flex; gap: 6px; align-items: flex-end; }
.lang-row .lang-col { flex: 1; }
.lang-row .lang-col label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px; color: #555; }
.lang-row .lang-col select {
width: 100%; padding: 8px 10px;
border: 1px solid #e0e0e0; border-radius: 6px;
background: #fff; color: #333; font-size: 13px;
outline: none; transition: all 0.15s;
}
.lang-row .lang-col select:focus {
border-color: #0078d4; box-shadow: 0 0 0 2px rgba(0,120,212,0.06);
}
.lang-swap-btn {
width: 32px; height: 34px; border: 1px solid #e0e0e0; border-radius: 6px;
background: #fff; color: #aaa; cursor: pointer; display: flex; align-items: center;
justify-content: center; font-size: 12px; flex-shrink: 0; transition: all 0.15s;
}
.lang-swap-btn:hover { background: #f0f0f0; color: #333; border-color: #ccc; }
/* textarea */
textarea {
width: 100%; min-height: 320px; padding: 10px; resize: vertical;
border: 1px solid #e0e0e0; border-radius: 6px; background: #fff;
color: #333; font-size: 13px; line-height: 1.6; outline: none;
transition: all 0.15s; font-family: inherit;
}
textarea:focus { border-color: #0078d4; box-shadow: 0 0 0 2px rgba(0,120,212,0.06); }
/* 上传区 */
.upload-area {
border: 2px dashed #e0e0e0; border-radius: 10px;
padding: 24px 16px; text-align: center; cursor: pointer;
transition: all 0.2s; background: #fff;
}
.upload-area:hover, .upload-area.dragover { border-color: #0078d4; background: #f0f7ff; }
.upload-area i { font-size: 24px; color: #ccc; margin-bottom: 6px; display: block; }
.upload-area .label { font-size: 13px; color: #999; }
.upload-area .filename { font-size: 13px; color: #0078d4; font-weight: 600; margin-top: 4px; }
.upload-area input[type="file"] { display: none; }
/* 模式切换 */
.mode-tabs {
display: flex; gap: 4px; background: #f0f0f0; border-radius: 8px;
padding: 3px; margin-bottom: 12px;
}
.mode-tab {
flex: 1; padding: 7px 0; text-align: center; font-size: 13px; font-weight: 500;
border-radius: 6px; cursor: pointer; color: #666; transition: all 0.15s;
border: none; background: transparent;
}
.mode-tab.active { background: #fff; color: #0078d4; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
/* 按钮 */
.btn {
padding: 7px 14px; border-radius: 7px; font-size: 12px; font-weight: 600;
cursor: pointer; transition: all 0.15s; border: none;
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
}
.btn-outline { background: #fff; color: #666; border: 1px solid #e0e0e0; }
.btn-outline:hover { background: #f5f5f5; }
/* 主操作按钮 */
.action-btn {
width: 100%; padding: 10px 18px;
background: linear-gradient(135deg, #0078d4, #005fa3);
color: #fff; border: none; border-radius: 8px;
font-size: 14px; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 8px;
transition: all 0.2s; margin-top: 4px;
box-shadow: 0 2px 8px rgba(0,120,212,0.2);
}
.action-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #006cbd, #004e8a);
box-shadow: 0 4px 12px rgba(0,120,212,0.3); transform: translateY(-1px);
}
.action-btn:active:not(:disabled) { transform: translateY(0); }
.action-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
/* ===== 右侧面板 ===== */
.right-panel {
flex: 1; display: flex; flex-direction: column; height: 100vh; min-width: 0;
}
.right-header {
padding: 14px 20px; border-bottom: 1px solid #e8e8e8; background: #fff;
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
}
.right-header h2 { font-size: 14px; font-weight: 600; color: #333; }
.right-header h2 i { color: #0078d4; margin-right: 6px; }
.right-body { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* 进度 spinner */
.progress-wrap { padding: 16px 20px; flex-shrink: 0; display: none; align-items: center; gap: 12px; }
.progress-wrap.show { display: flex; }
.spinner {
width: 24px; height: 24px; flex-shrink: 0;
border: 3px solid #e0e0e0; border-top-color: #0078d4;
border-radius: 50%; animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.progress-info { font-size: 13px; color: #555; }
.progress-info .pct { font-weight: 600; color: #0078d4; margin-left: 4px; }
/* 结果 */
.result-area { flex: 1; overflow-y: auto; padding: 16px 20px; background: #fff; }
.result-area::-webkit-scrollbar { width: 6px; }
.result-area::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
.result-placeholder { text-align: center; padding: 60px 20px; color: #ccc; }
.result-placeholder i { font-size: 48px; margin-bottom: 12px; display: block; }
.result-placeholder .text { font-size: 14px; }
.result-text { font-size: 14px; line-height: 1.8; color: #333; white-space: pre-wrap; word-break: break-all; }
/* Toast */
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
.toast {
padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); animation: toastIn 0.3s ease;
max-width: 360px;
}
.toast.success { background: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; }
.toast.error { background: #fce4ec; color: #c62828; border: 1px solid #f8bbd0; }
.toast.info { background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb; }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* 全屏加载遮罩 */
.loading-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.45); z-index: 3000;
justify-content: center; align-items: center; flex-direction: column; gap: 16px;
}
.loading-overlay.show { display: flex; }
.loading-overlay .loading-spinner {
width: 40px; height: 40px;
border: 4px solid rgba(255,255,255,0.3); border-top-color: #fff;
border-radius: 50%; animation: spin 0.8s linear infinite;
}
.loading-overlay .loading-text { color: #fff; font-size: 15px; font-weight: 500; }
</style>
</head>
<body>
<div class="toast-container" id="toastContainer"></div>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
<div class="loading-text" id="loadingText">正在加载模型,请稍候...</div>
</div>
<!-- ===== 左侧面板 ===== -->
<div class="left-panel">
<div class="left-body">
<!-- 标题 -->
<div class="page-header">
<h1><i class="fas fa-language"></i>AI 翻译</h1>
<button class="icon-btn" onclick="openSettings()" title="设置"><i class="fas fa-gear"></i></button>
</div>
<div class="page-subtitle">NLLB-200 本地翻译 · 点击齿轮配置模型目录</div>
<!-- 设置弹窗 -->
<div class="modal-overlay" id="settingsOverlay" onclick="closeSettings(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-head">
<h3><i class="fas fa-gear"></i>NLLB-200 模型配置</h3>
<button class="modal-close" onclick="closeSettings()"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<div class="field">
<label>模型目录HF 目录或模型 ID</label>
<input type="text" id="dir_nllb" value="{{ config.nllb_dir }}" placeholder="E:\AI\ai-translation\nllb">
</div>
<div style="margin-top:10px;padding:8px 10px;background:#f0f7ff;border:1px solid #d0e3f7;border-radius:6px;font-size:12px;display:flex;align-items:center;gap:6px;">
<span style="color:#999;">当前模型:</span>
<span id="currentModelName" style="color:#0078d4;font-weight:600;">未加载</span>
</div>
<div style="margin-top:12px;">
<label style="display:block;font-size:12px;font-weight:500;margin-bottom:6px;color:#555;">选择模型</label>
<div id="modelListContent">
<div style="color:#999;font-size:12px;">加载中...</div>
</div>
</div>
</div>
<div class="modal-foot">
<button class="btn-cancel" onclick="unloadModel()" style="margin-right:auto;border:1px solid #e74c3c;color:#e74c3c;">卸载模型</button>
<button class="btn-cancel" onclick="closeSettings()">取消</button>
<button class="btn-save" onclick="saveSettings()">保存</button>
</div>
</div>
</div>
<!-- 模式切换 -->
<div class="mode-tabs">
<button class="mode-tab active" id="tabText" onclick="switchMode('text')">文本翻译</button>
<button class="mode-tab" id="tabFile" onclick="switchMode('file')">字幕翻译</button>
</div>
<!-- 语言设置 -->
<div class="card">
<div class="card-title" style="justify-content:space-between;">
<span><i class="fas fa-globe"></i> 语言设置</span>
<button onclick="saveLangs()" style="border:none;background:#0078d4;color:#fff;font-size:11px;padding:3px 10px;border-radius:4px;cursor:pointer;text-transform:none;letter-spacing:0;">保存</button>
</div>
<div class="lang-row">
<div class="lang-col">
<label>源语言</label>
<select id="srcLang">
<option value="auto" {% if config.last_src_lang == 'auto' %}selected{% endif %}>自动检测</option>
{% for code, name in languages.items() %}{% if code != 'auto' %}
<option value="{{ code }}" {% if code == config.last_src_lang %}selected{% endif %}>{{ name }}</option>
{% endif %}{% endfor %}
</select>
</div>
<button class="lang-swap-btn" onclick="swapLangs()" title="交换语言">
<i class="fas fa-exchange-alt"></i>
</button>
<div class="lang-col">
<label>目标语言</label>
<select id="tgtLang">
{% for code, name in languages.items() %}{% if code != 'auto' %}
<option value="{{ code }}" {% if code == config.last_tgt_lang %}selected{% endif %}>{{ name }}</option>
{% endif %}{% endfor %}
</select>
</div>
</div>
</div>
<!-- 文本翻译 -->
<div id="textSection">
<div class="card">
<div class="card-title"><i class="fas fa-pen"></i> 输入文本</div>
<div class="field">
<textarea id="inputText" placeholder="请输入要翻译的文本,每行一句或多段落..."></textarea>
</div>
</div>
<button class="action-btn" id="btnTranslateText" onclick="translateText()">
<i class="fas fa-language"></i> 开始翻译
</button>
</div>
<!-- 字幕翻译 -->
<div id="fileSection" style="display:none;">
<div class="card">
<div class="card-title"><i class="fas fa-file"></i> 上传字幕</div>
<div class="upload-area" id="uploadArea" onclick="document.getElementById('subtitleFile').click()">
<i class="fas fa-file"></i>
<div class="label" id="uploadLabel">点击或拖拽上传字幕文件SRT / ASS / VTT</div>
<div class="filename" id="uploadFilename" style="display:none;"></div>
<input type="file" id="subtitleFile" accept=".srt,.ass,.ssa,.vtt">
</div>
</div>
<button class="action-btn" id="btnTranslateFile" onclick="translateFile()">
<i class="fas fa-language"></i> 翻译字幕
</button>
</div>
</div>
</div>
<!-- ===== 右侧面板 ===== -->
<div class="right-panel">
<div class="right-header">
<h2><i class="fas fa-language"></i>翻译结果</h2>
<button class="btn btn-outline" id="btnDownload" style="display:none;margin-left:auto;padding:5px 14px;font-size:12px;" onclick="downloadResult()">
<i class="fas fa-download"></i> 下载字幕
</button>
</div>
<div class="right-body">
<div class="progress-wrap" id="progressWrap">
<div class="spinner"></div>
<div class="progress-info">翻译中<span class="pct" id="progressText">0%</span></div>
</div>
<div class="result-area" id="resultArea">
<div class="result-placeholder">
<i class="fas fa-language"></i>
<div class="text">翻译结果将显示在这里</div>
</div>
</div>
</div>
</div>
<script>
var currentMode = 'text';
var currentTaskId = null;
// ===== Toast =====
function showMessage(text, type, duration) {
type = type || 'info'; duration = duration || 3000;
var t = document.createElement('div');
t.className = 'toast ' + type;
t.textContent = text;
document.getElementById('toastContainer').appendChild(t);
setTimeout(function() { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(function() { t.remove(); }, 300); }, duration);
}
// ===== 设置弹窗 =====
function openSettings() {
document.getElementById('settingsOverlay').classList.add('show');
scanModels();
refreshModelStatus();
}
function closeSettings(e) {
if (e && e.target !== document.getElementById('settingsOverlay')) return;
document.getElementById('settingsOverlay').classList.remove('show');
}
function refreshModelStatus() {
fetch('/ai-translate/model-status').then(function(r) { return r.json(); }).then(function(data) {
var el = document.getElementById('currentModelName');
if (data.loaded && data.model_name) {
el.textContent = data.model_name;
el.style.color = '#27ae60';
} else {
el.textContent = '未加载';
el.style.color = '#999';
}
});
}
function unloadModel() {
var overlay = document.getElementById('loadingOverlay');
document.getElementById('loadingText').textContent = '正在卸载模型...';
overlay.classList.add('show');
fetch('/ai-translate/unload-model', {method: 'POST'})
.then(function(r) { return r.json(); }).then(function(data) {
overlay.classList.remove('show');
document.getElementById('loadingText').textContent = '正在加载模型,请稍候...';
showMessage('模型已卸载', 'success');
refreshModelStatus();
scanModels();
}).catch(function() {
overlay.classList.remove('show');
document.getElementById('loadingText').textContent = '正在加载模型,请稍候...';
showMessage('卸载失败', 'error');
});
}
function saveSettings() {
fetch('/ai-translate/config', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ nllb_dir: document.getElementById('dir_nllb').value.trim() })
}).then(function(r) { return r.json(); }).then(function() {
showMessage('目录配置已保存', 'success');
scanModels();
});
}
// ===== 模式切换 =====
function switchMode(mode) {
currentMode = mode;
document.getElementById('tabText').className = 'mode-tab' + (mode === 'text' ? ' active' : '');
document.getElementById('tabFile').className = 'mode-tab' + (mode === 'file' ? ' active' : '');
document.getElementById('textSection').style.display = mode === 'text' ? '' : 'none';
document.getElementById('fileSection').style.display = mode === 'file' ? '' : 'none';
document.getElementById('btnDownload').style.display = 'none';
}
// ===== 语言 =====
function swapLangs() {
var src = document.getElementById('srcLang'), tgt = document.getElementById('tgtLang');
if (src.value === 'auto') { showMessage('源语言为"自动检测"时无法交换', 'error'); return; }
var tmp = src.value; src.value = tgt.value; tgt.value = tmp;
}
function saveLangs() {
fetch('/ai-translate/config', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
last_src_lang: document.getElementById('srcLang').value,
last_tgt_lang: document.getElementById('tgtLang').value
})
}).then(function(r) { return r.json(); }).then(function() {
showMessage('语言设置已保存', 'success');
});
}
// ===== 扫描模型 =====
function scanModels() {
var content = document.getElementById('modelListContent');
content.innerHTML = '<div style="color:#999;font-size:12px;">加载中...</div>';
fetch('/ai-translate/models')
.then(function(r) { return r.json(); })
.then(function(data) {
var models = data.models || [];
if (!models.length) {
content.innerHTML = '<div style="color:#e74c3c;font-size:12px;">未找到模型,请在设置中配置目录</div>';
return;
}
var lastModel = data.last_model || '';
var html = '';
models.forEach(function(m) {
var isActive = m.name === lastModel;
html += '<div class="model-item' + (isActive ? ' active' : '') + '" onclick="loadModel(\'' + m.name.replace(/'/g, "\\'") + '\')">';
html += '<span class="model-name">' + m.name + '</span>';
if (isActive) html += '<span class="model-status loaded"></span>';
html += '</div>';
});
content.innerHTML = html;
}).catch(function() {
content.innerHTML = '<div style="color:#e74c3c;font-size:12px;">加载失败,请刷新重试</div>';
});
}
// ===== 加载模型 =====
function loadModel(name) {
var overlay = document.getElementById('loadingOverlay');
overlay.classList.add('show');
fetch('/ai-translate/load-model', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({model_name: name, engine_type: 'nllb'})
})
.then(function(r) { return r.json(); })
.then(function(data) {
overlay.classList.remove('show');
if (data.success) {
showMessage('模型加载成功: ' + name, 'success');
} else {
showMessage(data.error || '加载失败', 'error');
}
scanModels();
refreshModelStatus();
}).catch(function() {
overlay.classList.remove('show');
showMessage('加载失败', 'error');
});
}
// ===== 翻译 =====
function translateText() {
var text = document.getElementById('inputText').value.trim();
if (!text) { showMessage('请输入要翻译的文本', 'error'); return; }
document.getElementById('btnTranslateText').disabled = true;
showProgress(0);
fetch('/ai-translate/translate-text', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
text: text,
src_lang: document.getElementById('srcLang').value,
tgt_lang: document.getElementById('tgtLang').value
})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) { currentTaskId = data.task_id; pollTask(); }
else { showMessage(data.error || '翻译失败', 'error'); resetBtns(); hideProgress(); }
}).catch(function() { resetBtns(); hideProgress(); });
}
function translateFile() {
var fi = document.getElementById('subtitleFile');
if (!fi.files.length) { showMessage('请上传字幕文件', 'error'); return; }
document.getElementById('btnTranslateFile').disabled = true;
showProgress(0);
var fd = new FormData();
fd.append('subtitle', fi.files[0]);
fd.append('src_lang', document.getElementById('srcLang').value);
fd.append('tgt_lang', document.getElementById('tgtLang').value);
fetch('/ai-translate/translate-file', {method: 'POST', body: fd})
.then(function(r) { return r.json(); }).then(function(data) {
if (data.success) { currentTaskId = data.task_id; pollTask(); }
else { showMessage(data.error || '翻译失败', 'error'); resetBtns(); hideProgress(); }
}).catch(function() { resetBtns(); hideProgress(); });
}
function pollTask() {
if (!currentTaskId) return;
fetch('/ai-translate/task-status/' + currentTaskId)
.then(function(r) { return r.json(); }).then(function(data) {
if (!data.success) return;
if (data.progress !== undefined) showProgress(data.progress);
if (data.status === 'done') {
showResult(data.result || '');
if (data.file_path) document.getElementById('btnDownload').style.display = '';
showMessage('翻译完成', 'success');
resetBtns(); hideProgress(); currentTaskId = null;
} else if (data.status === 'error') {
showMessage('翻译失败: ' + (data.error || '未知错误'), 'error');
resetBtns(); hideProgress(); currentTaskId = null;
} else { setTimeout(pollTask, 500); }
});
}
function showResult(text) {
var el = document.getElementById('resultArea');
el.innerHTML = '<div class="result-text"></div>';
el.querySelector('.result-text').textContent = text;
}
function resetBtns() {
document.getElementById('btnTranslateText').disabled = false;
document.getElementById('btnTranslateFile').disabled = false;
}
function showProgress(p) {
document.getElementById('progressWrap').classList.add('show');
document.getElementById('progressText').textContent = p + '%';
}
function hideProgress() { document.getElementById('progressWrap').classList.remove('show'); }
// ===== 下载 =====
function downloadResult() {
if (currentTaskId) window.open('/ai-translate/download/' + currentTaskId, '_blank');
}
// ===== 文件上传 =====
var ua = document.getElementById('uploadArea');
var sf = document.getElementById('subtitleFile');
ua.addEventListener('dragover', function(e) { e.preventDefault(); ua.classList.add('dragover'); });
ua.addEventListener('dragleave', function() { ua.classList.remove('dragover'); });
ua.addEventListener('drop', function(e) {
e.preventDefault(); ua.classList.remove('dragover');
if (e.dataTransfer.files.length) { sf.files = e.dataTransfer.files; onFileSel(); }
});
sf.addEventListener('change', onFileSel);
function onFileSel() {
var f = sf.files[0];
if (f) {
document.getElementById('uploadLabel').style.display = 'none';
var fn = document.getElementById('uploadFilename');
fn.style.display = ''; fn.textContent = f.name;
}
}
// ===== 初始化 =====
(function() {
// 页面加载时检查模型是否正在加载中
fetch('/ai-translate/model-status').then(function(r) { return r.json(); }).then(function(data) {
if (data.loading) {
var overlay = document.getElementById('loadingOverlay');
overlay.classList.add('show');
// 轮询直到加载完成
var timer = setInterval(function() {
fetch('/ai-translate/model-status').then(function(r) { return r.json(); }).then(function(d) {
if (!d.loading) {
clearInterval(timer);
overlay.classList.remove('show');
if (d.loaded) {
showMessage('模型加载成功: ' + (d.model_name || ''), 'success');
}
}
});
}, 1000);
}
});
})();
</script>
</body>
</html>