|
|
<!DOCTYPE html> |
|
|
<html lang="zh"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>翻译助手</title> |
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script> |
|
|
</head> |
|
|
<body class="bg-gray-50"> |
|
|
|
|
|
<nav class="bg-white shadow-md fixed w-full top-0 z-50"> |
|
|
<div class="max-w-7xl mx-auto px-4"> |
|
|
<div class="flex justify-between h-16"> |
|
|
<div class="flex items-center"> |
|
|
<span class="text-xl font-bold text-purple-700">翻译助手</span> |
|
|
</div> |
|
|
<div class="hidden md:flex items-center space-x-8"> |
|
|
<button class="nav-btn text-gray-700 hover:text-purple-700" data-target="text-trans">文本翻译</button> |
|
|
<button class="nav-btn text-gray-700 hover:text-purple-700" data-target="file-trans">文件翻译</button> |
|
|
</div> |
|
|
<button id="mobile-menu-btn" class="md:hidden flex items-center"> |
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="mobile-menu" class="hidden md:hidden bg-white border-t"> |
|
|
<div class="px-2 pt-2 pb-3 space-y-1"> |
|
|
<button class="nav-btn block w-full text-left px-3 py-2 text-gray-700 hover:bg-purple-50" data-target="text-trans">文本翻译</button> |
|
|
<button class="nav-btn block w-full text-left px-3 py-2 text-gray-700 hover:bg-purple-50" data-target="file-trans">文件翻译</button> |
|
|
</div> |
|
|
</div> |
|
|
</nav> |
|
|
|
|
|
|
|
|
<main class="pt-20 px-4 max-w-7xl mx-auto"> |
|
|
|
|
|
<div id="text-trans" class="page"> |
|
|
<div class="flex flex-col lg:flex-row gap-6"> |
|
|
|
|
|
<div class="lg:w-64"> |
|
|
<div class="bg-white rounded-lg shadow p-4 space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">源语言</label> |
|
|
<select id="source-lang" class="w-full border-gray-300 rounded-md shadow-sm"> |
|
|
<option value="AUTO">自动检测</option> |
|
|
<option value="ZH">中文</option> |
|
|
<option value="EN">英语</option> |
|
|
<option value="JA">日语</option> |
|
|
<option value="KO">韩语</option> |
|
|
<option value="FR">法语</option> |
|
|
<option value="DE">德语</option> |
|
|
<option value="ES">西班牙语</option> |
|
|
<option value="RU">俄语</option> |
|
|
<option value="IT">意大利语</option> |
|
|
<option value="PT">葡萄牙语</option> |
|
|
<option value="VI">越南语</option> |
|
|
<option value="ID">印尼语</option> |
|
|
<option value="TH">泰语</option> |
|
|
<option value="MS">马来语</option> |
|
|
<option value="AR">阿拉伯语</option> |
|
|
<option value="HI">印地语</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">目标语言</label> |
|
|
<select id="target-lang" class="w-full border-gray-300 rounded-md shadow-sm"> |
|
|
<option value="ZH">中文</option> |
|
|
<option value="EN">英语</option> |
|
|
<option value="JA">日语</option> |
|
|
<option value="KO">韩语</option> |
|
|
<option value="FR">法语</option> |
|
|
<option value="DE">德语</option> |
|
|
<option value="ES">西班牙语</option> |
|
|
<option value="RU">俄语</option> |
|
|
<option value="IT">意大利语</option> |
|
|
<option value="PT">葡萄牙语</option> |
|
|
<option value="VI">越南语</option> |
|
|
<option value="ID">印尼语</option> |
|
|
<option value="TH">泰语</option> |
|
|
<option value="MS">马来语</option> |
|
|
<option value="AR">阿拉伯语</option> |
|
|
<option value="HI">印地语</option> |
|
|
</select> |
|
|
</div> |
|
|
<button id="switch-lang" class="w-full py-2 px-4 border border-purple-700 text-purple-700 rounded-md hover:bg-purple-50"> |
|
|
切换语言 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex-1"> |
|
|
<div class="grid md:grid-cols-2 gap-6"> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-4"> |
|
|
<textarea |
|
|
id="source-text" |
|
|
class="w-full h-48 p-2 border-0 focus:ring-0 resize-none" |
|
|
placeholder="请输入要翻译的文本" |
|
|
></textarea> |
|
|
<div class="flex justify-between mt-2 text-sm text-gray-500"> |
|
|
<span id="char-count">0/10000</span> |
|
|
<button id="clear-text" class="hover:text-gray-700">清空</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-4"> |
|
|
<div id="target-text" class="h-48 p-2"></div> |
|
|
<div class="flex justify-end mt-2"> |
|
|
<button id="copy-result" class="text-sm text-gray-500 hover:text-gray-700">复制结果</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button id="translate-btn" class="mt-6 mx-auto block py-3 px-8 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"> |
|
|
翻译 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="file-trans" class="page hidden"> |
|
|
<div class="flex flex-col lg:flex-row gap-6"> |
|
|
|
|
|
<div class="lg:w-64 space-y-6"> |
|
|
<div class="bg-white rounded-lg shadow p-4 space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">源语言</label> |
|
|
<select id="file-source-lang" class="w-full border-gray-300 rounded-md shadow-sm"> |
|
|
<option value="AUTO">自动检测</option> |
|
|
<option value="ZH">中文</option> |
|
|
<option value="EN">英语</option> |
|
|
<option value="JA">日语</option> |
|
|
<option value="KO">韩语</option> |
|
|
<option value="FR">法语</option> |
|
|
<option value="DE">德语</option> |
|
|
<option value="ES">西班牙语</option> |
|
|
<option value="RU">俄语</option> |
|
|
<option value="IT">意大利语</option> |
|
|
<option value="PT">葡萄牙语</option> |
|
|
<option value="VI">越南语</option> |
|
|
<option value="ID">印尼语</option> |
|
|
<option value="TH">泰语</option> |
|
|
<option value="MS">马来语</option> |
|
|
<option value="AR">阿拉伯语</option> |
|
|
<option value="HI">印地语</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">目标语言</label> |
|
|
<select id="file-target-lang" class="w-full border-gray-300 rounded-md shadow-sm"> |
|
|
<option value="ZH">中文</option> |
|
|
<option value="EN">英语</option> |
|
|
<option value="JA">日语</option> |
|
|
<option value="KO">韩语</option> |
|
|
<option value="FR">法语</option> |
|
|
<option value="DE">德语</option> |
|
|
<option value="ES">西班牙语</option> |
|
|
<option value="RU">俄语</option> |
|
|
<option value="IT">意大利语</option> |
|
|
<option value="PT">葡萄牙语</option> |
|
|
<option value="VI">越南语</option> |
|
|
<option value="ID">印尼语</option> |
|
|
<option value="TH">泰语</option> |
|
|
<option value="MS">马来语</option> |
|
|
<option value="AR">阿拉伯语</option> |
|
|
<option value="HI">印地语</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-4 space-y-4"> |
|
|
<button id="upload-btn" class="w-full py-2 px-4 bg-purple-600 text-white rounded-md hover:bg-purple-700"> |
|
|
上传文件 |
|
|
</button> |
|
|
<button id="file-translate-btn" class="w-full py-2 px-4 bg-purple-100 text-purple-700 rounded-md hover:bg-purple-200" disabled> |
|
|
开始翻译 |
|
|
</button> |
|
|
<button id="export-btn" class="w-full py-2 px-4 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50" disabled> |
|
|
导出文档 |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="progress-panel" class="bg-white rounded-lg shadow p-4 hidden"> |
|
|
<h3 class="text-sm font-medium text-gray-700 mb-4">翻译进度</h3> |
|
|
<div class="space-y-2"> |
|
|
<div class="flex justify-between text-sm"> |
|
|
<span>总段落数</span> |
|
|
<span id="total-segments">0</span> |
|
|
</div> |
|
|
<div class="flex justify-between text-sm"> |
|
|
<span>已翻译</span> |
|
|
<span id="translated-segments">0</span> |
|
|
</div> |
|
|
<div class="h-2 bg-gray-200 rounded-full"> |
|
|
<div id="progress-bar" class="h-full bg-purple-600 rounded-full w-0 transition-all"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex-1"> |
|
|
|
|
|
<div id="upload-zone" class="h-64 border-2 border-dashed border-purple-400 rounded-lg flex flex-col items-center justify-center bg-white"> |
|
|
<svg class="w-12 h-12 text-purple-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/> |
|
|
</svg> |
|
|
<p class="text-gray-600">拖放文件到这里或点击上传</p> |
|
|
<p class="text-sm text-gray-500 mt-2">支持 TXT、DOCX、PDF、Markdown 格式</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="file-preview" class="hidden"> |
|
|
<div class="bg-white rounded-lg shadow"> |
|
|
<div class="border-b border-gray-200 p-4 flex justify-between items-center"> |
|
|
<span id="file-name" class="font-medium"></span> |
|
|
<button id="close-preview" class="text-gray-500 hover:text-gray-700"> |
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
<div class="p-6"> |
|
|
<div class="grid md:grid-cols-2 gap-6"> |
|
|
<div> |
|
|
<h3 class="text-sm font-medium text-gray-700 mb-4">原文</h3> |
|
|
<div id="file-content" class="space-y-4"></div> |
|
|
</div> |
|
|
<div> |
|
|
<h3 class="text-sm font-medium text-gray-700 mb-4">译文</h3> |
|
|
<div id="file-translation" class="space-y-4"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
|
|
|
|
|
|
<div id="export-modal" class="fixed inset-0 z-50 hidden"> |
|
|
<div class="absolute inset-0 bg-gray-500 bg-opacity-75"></div> |
|
|
<div class="fixed inset-0 z-10 overflow-y-auto"> |
|
|
<div class="flex min-h-full items-end justify-center p-4 sm:items-center"> |
|
|
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-md p-6"> |
|
|
<h3 class="text-lg font-medium text-gray-900 mb-4">导出设置(暂时不支持导出为PDF)</h3> |
|
|
<div class="space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">导出格式</label> |
|
|
<select id="export-format" class="w-full rounded-md border-gray-300"> |
|
|
<option value="auto">自动 (与源文件相同)</option> |
|
|
<option value="txt">纯文本 (TXT)</option> |
|
|
<option value="docx">Word文档 (DOCX)</option> |
|
|
<option value="md">Markdown (MD)</option> |
|
|
<option value="html">网页 (HTML)</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">导出模式</label> |
|
|
<div class="space-y-2"> |
|
|
<label class="inline-flex items-center"> |
|
|
<input type="radio" name="export-mode" value="translated" class="text-purple-600" checked> |
|
|
<span class="ml-2">仅译文</span> |
|
|
</label> |
|
|
<label class="inline-flex items-center ml-4"> |
|
|
<input type="radio" name="export-mode" value="parallel" class="text-purple-600"> |
|
|
<span class="ml-2">对照模式</span> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="mt-6 flex justify-end space-x-3"> |
|
|
<button id="cancel-export" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"> |
|
|
取消 |
|
|
</button> |
|
|
<button id="confirm-export" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"> |
|
|
导出 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const API_ENDPOINTS = { |
|
|
TRANSLATE_TEXT: '/translate_text', |
|
|
UPLOAD_FILE: '/upload', |
|
|
TRANSLATE_FILE: '/translate', |
|
|
EXPORT: '/export' |
|
|
}; |
|
|
|
|
|
|
|
|
const utils = { |
|
|
showError(message) { |
|
|
|
|
|
alert(message); |
|
|
}, |
|
|
|
|
|
async copyToClipboard(text) { |
|
|
try { |
|
|
await navigator.clipboard.writeText(text); |
|
|
return true; |
|
|
} catch (err) { |
|
|
console.error('Copy failed:', err); |
|
|
return false; |
|
|
} |
|
|
}, |
|
|
|
|
|
formatBytes(bytes) { |
|
|
if (bytes === 0) return '0 B'; |
|
|
const k = 1024; |
|
|
const sizes = ['B', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const api = { |
|
|
async translateText(text, sourceLang, targetLang) { |
|
|
try { |
|
|
const response = await fetch(API_ENDPOINTS.TRANSLATE_TEXT, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
text, |
|
|
source_lang: sourceLang, |
|
|
target_lang: targetLang |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
return data; |
|
|
} catch (error) { |
|
|
console.error('Translation failed:', error); |
|
|
throw error; |
|
|
} |
|
|
}, |
|
|
|
|
|
async uploadFile(file, sourceLang, targetLang) { |
|
|
const formData = new FormData(); |
|
|
formData.append('file', file); |
|
|
formData.append('source_lang', sourceLang); |
|
|
formData.append('target_lang', targetLang); |
|
|
|
|
|
try { |
|
|
const response = await fetch(API_ENDPOINTS.UPLOAD_FILE, { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
return await response.json(); |
|
|
} catch (error) { |
|
|
console.error('File upload failed:', error); |
|
|
throw error; |
|
|
} |
|
|
}, |
|
|
|
|
|
async translateFile(segments, sourceLang, targetLang) { |
|
|
try { |
|
|
const response = await fetch(API_ENDPOINTS.TRANSLATE_FILE, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
segments, |
|
|
source_lang: sourceLang, |
|
|
target_lang: targetLang |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
return await response.json(); |
|
|
} catch (error) { |
|
|
console.error('File translation failed:', error); |
|
|
throw error; |
|
|
} |
|
|
}, |
|
|
|
|
|
async exportDocument(segments, format, mode, sourceFileType) { |
|
|
try { |
|
|
const response = await fetch(API_ENDPOINTS.EXPORT, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
segments, |
|
|
format, |
|
|
mode, |
|
|
source_file_type: sourceFileType |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
return await response.blob(); |
|
|
} catch (error) { |
|
|
console.error('Export failed:', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
class TextTranslation { |
|
|
constructor() { |
|
|
this.sourceText = document.getElementById('source-text'); |
|
|
this.targetText = document.getElementById('target-text'); |
|
|
this.translateBtn = document.getElementById('translate-btn'); |
|
|
this.sourceLang = document.getElementById('source-lang'); |
|
|
this.targetLang = document.getElementById('target-lang'); |
|
|
this.charCount = document.getElementById('char-count'); |
|
|
this.clearBtn = document.getElementById('clear-text'); |
|
|
this.copyBtn = document.getElementById('copy-result'); |
|
|
this.switchBtn = document.getElementById('switch-lang'); |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
this.bindEvents(); |
|
|
this.updateCharCount(); |
|
|
} |
|
|
|
|
|
bindEvents() { |
|
|
this.sourceText.addEventListener('input', () => this.updateCharCount()); |
|
|
this.translateBtn.addEventListener('click', () => this.translate()); |
|
|
this.clearBtn.addEventListener('click', () => this.clearText()); |
|
|
this.copyBtn.addEventListener('click', () => this.copyResult()); |
|
|
this.switchBtn.addEventListener('click', () => this.switchLanguages()); |
|
|
} |
|
|
|
|
|
updateCharCount() { |
|
|
const count = this.sourceText.value.length; |
|
|
this.charCount.textContent = `${count}/10000`; |
|
|
this.translateBtn.disabled = count === 0; |
|
|
} |
|
|
|
|
|
clearText() { |
|
|
this.sourceText.value = ''; |
|
|
this.targetText.textContent = ''; |
|
|
this.updateCharCount(); |
|
|
} |
|
|
|
|
|
async copyResult() { |
|
|
const success = await utils.copyToClipboard(this.targetText.textContent); |
|
|
if (success) { |
|
|
this.copyBtn.textContent = '已复制'; |
|
|
setTimeout(() => { |
|
|
this.copyBtn.textContent = '复制结果'; |
|
|
}, 2000); |
|
|
} |
|
|
} |
|
|
|
|
|
switchLanguages() { |
|
|
if (this.sourceLang.value === 'auto') return; |
|
|
[this.sourceLang.value, this.targetLang.value] = |
|
|
[this.targetLang.value, this.sourceLang.value]; |
|
|
} |
|
|
|
|
|
async translate() { |
|
|
if (!this.sourceText.value.trim()) return; |
|
|
|
|
|
this.translateBtn.disabled = true; |
|
|
try { |
|
|
const result = await api.translateText( |
|
|
this.sourceText.value, |
|
|
this.sourceLang.value, |
|
|
this.targetLang.value |
|
|
); |
|
|
|
|
|
this.targetText.textContent = result.translated; |
|
|
} catch (error) { |
|
|
utils.showError('翻译失败,请稍后重试'); |
|
|
} finally { |
|
|
this.translateBtn.disabled = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class FileTranslation { |
|
|
constructor() { |
|
|
this.uploadZone = document.getElementById('upload-zone'); |
|
|
this.filePreview = document.getElementById('file-preview'); |
|
|
this.uploadBtn = document.getElementById('upload-btn'); |
|
|
this.translateBtn = document.getElementById('file-translate-btn'); |
|
|
this.exportBtn = document.getElementById('export-btn'); |
|
|
this.progressPanel = document.getElementById('progress-panel'); |
|
|
this.closePreviewBtn = document.getElementById('close-preview'); |
|
|
|
|
|
this.sourceLang = document.getElementById('file-source-lang'); |
|
|
this.targetLang = document.getElementById('file-target-lang'); |
|
|
|
|
|
this.currentFile = null; |
|
|
this.segments = []; |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
this.bindEvents(); |
|
|
this.initDragDrop(); |
|
|
} |
|
|
|
|
|
bindEvents() { |
|
|
this.uploadBtn.addEventListener('click', () => this.triggerFileInput()); |
|
|
this.translateBtn.addEventListener('click', () => this.translateFile()); |
|
|
this.exportBtn.addEventListener('click', () => this.showExportModal()); |
|
|
this.closePreviewBtn.addEventListener('click', () => this.closePreview()); |
|
|
|
|
|
|
|
|
document.getElementById('confirm-export').addEventListener('click', () => this.exportFile()); |
|
|
document.getElementById('cancel-export').addEventListener('click', () => this.hideExportModal()); |
|
|
} |
|
|
|
|
|
initDragDrop() { |
|
|
this.uploadZone.addEventListener('dragover', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
this.uploadZone.classList.add('border-purple-600'); |
|
|
}); |
|
|
|
|
|
this.uploadZone.addEventListener('dragleave', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
this.uploadZone.classList.remove('border-purple-600'); |
|
|
}); |
|
|
|
|
|
this.uploadZone.addEventListener('drop', async (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
this.uploadZone.classList.remove('border-purple-600'); |
|
|
|
|
|
const file = e.dataTransfer.files[0]; |
|
|
await this.handleFile(file); |
|
|
}); |
|
|
} |
|
|
|
|
|
triggerFileInput() { |
|
|
const input = document.createElement('input'); |
|
|
input.type = 'file'; |
|
|
input.accept = '.txt,.docx,.pdf,.md'; |
|
|
input.onchange = (e) => this.handleFile(e.target.files[0]); |
|
|
input.click(); |
|
|
} |
|
|
|
|
|
async handleFile(file) { |
|
|
if (!file) return; |
|
|
|
|
|
const validTypes = [ |
|
|
'text/plain', |
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
|
|
'application/pdf', |
|
|
'text/markdown' |
|
|
]; |
|
|
|
|
|
if (!validTypes.includes(file.type)) { |
|
|
utils.showError('不支持的文件格式'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.currentFile = file; |
|
|
try { |
|
|
const result = await api.uploadFile( |
|
|
file, |
|
|
this.sourceLang.value, |
|
|
this.targetLang.value |
|
|
); |
|
|
|
|
|
this.segments = result.segments; |
|
|
this.updatePreview(); |
|
|
this.translateBtn.disabled = false; |
|
|
this.showProgressPanel(); |
|
|
} catch (error) { |
|
|
utils.showError('文件上传失败,请重试'); |
|
|
} |
|
|
} |
|
|
|
|
|
updatePreview() { |
|
|
this.uploadZone.classList.add('hidden'); |
|
|
this.filePreview.classList.remove('hidden'); |
|
|
|
|
|
document.getElementById('file-name').textContent = this.currentFile.name; |
|
|
|
|
|
const contentDiv = document.getElementById('file-content'); |
|
|
contentDiv.innerHTML = this.segments.map((segment, index) => ` |
|
|
<div class="p-4 bg-gray-50 rounded"> |
|
|
<div class="text-gray-900">${segment.text}</div> |
|
|
${segment.type === 'heading' ? '<div class="text-xs text-gray-500 mt-1">标题</div>' : ''} |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
async translateFile() { |
|
|
if (!this.segments.length) return; |
|
|
|
|
|
this.translateBtn.disabled = true; |
|
|
try { |
|
|
const result = await api.translateFile( |
|
|
this.segments, |
|
|
this.sourceLang.value, |
|
|
this.targetLang.value |
|
|
); |
|
|
|
|
|
this.segments = result.segments; |
|
|
this.updateTranslations(); |
|
|
this.exportBtn.disabled = false; |
|
|
this.updateProgress(); |
|
|
} catch (error) { |
|
|
utils.showError('翻译失败,请重试'); |
|
|
} finally { |
|
|
this.translateBtn.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
updateTranslations() { |
|
|
const translationDiv = document.getElementById('file-translation'); |
|
|
translationDiv.innerHTML = this.segments.map((segment, index) => ` |
|
|
<div class="p-4 bg-gray-50 rounded"> |
|
|
<div class="text-gray-900">${segment.translated || ''}</div> |
|
|
${segment.confidence ? ` |
|
|
<div class="mt-2 h-1 bg-gray-200 rounded-full overflow-hidden"> |
|
|
<div class="h-full bg-purple-600" style="width: ${segment.confidence * 100}%"></div> |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
showProgressPanel() { |
|
|
this.progressPanel.classList.remove('hidden'); |
|
|
this.updateProgress(); |
|
|
} |
|
|
|
|
|
updateProgress() { |
|
|
const total = this.segments.length; |
|
|
const translated = this.segments.filter(s => s.translated).length; |
|
|
|
|
|
document.getElementById('total-segments').textContent = total; |
|
|
document.getElementById('translated-segments').textContent = translated; |
|
|
document.getElementById('progress-bar').style.width = `${(translated / total * 100)}%`; |
|
|
} |
|
|
|
|
|
closePreview() { |
|
|
this.uploadZone.classList.remove('hidden'); |
|
|
this.filePreview.classList.add('hidden'); |
|
|
this.progressPanel.classList.add('hidden'); |
|
|
this.translateBtn.disabled = true; |
|
|
this.exportBtn.disabled = true; |
|
|
this.currentFile = null; |
|
|
this.segments = []; |
|
|
} |
|
|
|
|
|
showExportModal() { |
|
|
document.getElementById('export-modal').classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
hideExportModal() { |
|
|
document.getElementById('export-modal').classList.add('hidden'); |
|
|
} |
|
|
|
|
|
async exportFile() { |
|
|
const formatSelect = document.getElementById('export-format'); |
|
|
const selectedFormat = formatSelect.value; |
|
|
const mode = document.querySelector('input[name="export-mode"]:checked').value; |
|
|
|
|
|
|
|
|
let exportFormat; |
|
|
if (selectedFormat === 'auto') { |
|
|
|
|
|
exportFormat = this.currentFile.name.split('.').pop(); |
|
|
} else { |
|
|
|
|
|
exportFormat = selectedFormat; |
|
|
} |
|
|
|
|
|
try { |
|
|
const blob = await api.exportDocument( |
|
|
this.segments, |
|
|
exportFormat, |
|
|
mode, |
|
|
this.currentFile.name.split('.').pop() |
|
|
); |
|
|
|
|
|
|
|
|
let fileExtension = exportFormat; |
|
|
if (exportFormat === 'auto') { |
|
|
fileExtension = this.currentFile.name.split('.').pop(); |
|
|
} |
|
|
|
|
|
|
|
|
const originalName = this.currentFile.name.split('.')[0]; |
|
|
const newFileName = `translated_${originalName}.${fileExtension}`; |
|
|
|
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = newFileName; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
|
|
|
this.hideExportModal(); |
|
|
} catch (error) { |
|
|
utils.showError('导出失败,请重试'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class Navigation { |
|
|
constructor() { |
|
|
this.mobileMenuBtn = document.getElementById('mobile-menu-btn'); |
|
|
this.mobileMenu = document.getElementById('mobile-menu'); |
|
|
this.navButtons = document.querySelectorAll('.nav-btn'); |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
this.bindEvents(); |
|
|
} |
|
|
|
|
|
bindEvents() { |
|
|
this.mobileMenuBtn.addEventListener('click', () => this.toggleMobileMenu()); |
|
|
|
|
|
this.navButtons.forEach(btn => { |
|
|
btn.addEventListener('click', () => { |
|
|
this.switchPage(btn.dataset.target); |
|
|
this.mobileMenu.classList.add('hidden'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
toggleMobileMenu() { |
|
|
this.mobileMenu.classList.toggle('hidden'); |
|
|
} |
|
|
|
|
|
switchPage(pageId) { |
|
|
document.querySelectorAll('.page').forEach(page => { |
|
|
page.classList.add('hidden'); |
|
|
}); |
|
|
document.getElementById(pageId).classList.remove('hidden'); |
|
|
|
|
|
this.navButtons.forEach(btn => { |
|
|
if (btn.dataset.target === pageId) { |
|
|
btn.classList.add('text-purple-700'); |
|
|
} else { |
|
|
btn.classList.remove('text-purple-700'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
new Navigation(); |
|
|
new TextTranslation(); |
|
|
new FileTranslation(); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |