| <!DOCTYPE html> |
| <html lang="zh"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AI Translation Assistant</title> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet"> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| :root { |
| --primary-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); |
| --secondary-gradient: linear-gradient(135deg, #f472b6 0%, #c084fc 100%); |
| } |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background: #f8fafc; |
| } |
| |
| .gradient-text { |
| background: var(--primary-gradient); |
| background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .gradient-border { |
| position: relative; |
| background: linear-gradient(#fff, #fff) padding-box, |
| var(--primary-gradient) border-box; |
| border: 2px solid transparent; |
| border-radius: 0.75rem; |
| } |
| |
| .glass-effect { |
| background: rgba(255, 255, 255, 0.7); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| } |
| |
| .nav-link { |
| position: relative; |
| transition: all 0.3s ease; |
| } |
| |
| .nav-link::after { |
| content: ''; |
| position: absolute; |
| width: 0; |
| height: 2px; |
| bottom: -2px; |
| left: 0; |
| background: var(--primary-gradient); |
| transition: width 0.3s ease; |
| } |
| |
| .nav-link:hover::after { |
| width: 100%; |
| } |
| |
| .floating { |
| animation: floating 3s ease-in-out infinite; |
| } |
| |
| @keyframes floating { |
| 0% { transform: translateY(0px); } |
| 50% { transform: translateY(-10px); } |
| 100% { transform: translateY(0px); } |
| } |
| |
| .progress-bar-animation { |
| background-size: 200% 200%; |
| animation: gradientMove 2s linear infinite; |
| } |
| |
| @keyframes gradientMove { |
| 0% { background-position: 200% 0; } |
| 100% { background-position: -200% 0; } |
| } |
| |
| .card-hover { |
| transition: all 0.3s ease; |
| } |
| |
| .card-hover:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.1); |
| } |
| </style> |
| </head> |
| <body> |
| |
| <nav class="glass-effect fixed w-full top-0 z-50 border-b border-gray-200"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div class="flex justify-between h-16"> |
| <div class="flex items-center"> |
| <a href="#" class="flex items-center space-x-2"> |
| <div class="w-8 h-8 rounded-lg bg-gradient-to-r from-indigo-500 to-purple-500 flex items-center justify-center"> |
| <i class="fas fa-language text-white"></i> |
| </div> |
| <span class="text-xl font-bold gradient-text">TranslateAI</span> |
| </a> |
| </div> |
| |
| <div class="hidden md:flex items-center space-x-8"> |
| <a href="#" class="nav-link text-gray-700 hover:text-indigo-600 font-medium" data-target="text-trans"> |
| <i class="fas fa-text-width mr-2"></i>文本翻译 |
| </a> |
| <a href="#" class="nav-link text-gray-700 hover:text-indigo-600 font-medium" data-target="file-trans"> |
| <i class="fas fa-file-alt mr-2"></i>文件翻译 |
| </a> |
| <a href="#" class="nav-link text-gray-700 hover:text-indigo-600 font-medium" data-target="about"> |
| <i class="fas fa-info-circle mr-2"></i>关于我们 |
| </a> |
| </div> |
|
|
| <div class="flex items-center md:hidden"> |
| <button id="mobile-menu-btn" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-bars text-xl"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="mobile-menu" class="hidden md:hidden animated fadeIn faster"> |
| <div class="glass-effect border-t border-gray-200 py-2"> |
| <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600" data-target="text-trans"> |
| <i class="fas fa-text-width mr-2"></i>文本翻译 |
| </a> |
| <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600" data-target="file-trans"> |
| <i class="fas fa-file-alt mr-2"></i>文件翻译 |
| </a> |
| <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600" data-target="about"> |
| <i class="fas fa-info-circle mr-2"></i>关于我们 |
| </a> |
| </div> |
| </div> |
| </nav> |
|
|
| |
| <main class="pt-20 px-4 pb-12"> |
| |
| <div id="text-trans" class="page max-w-7xl mx-auto"> |
| <div class="mb-8"> |
| <h1 class="text-3xl font-bold gradient-text mb-2">智能文本翻译</h1> |
| <p class="text-gray-600">支持多语言实时翻译,让沟通无国界</p> |
| </div> |
|
|
| <div class="flex flex-col lg:flex-row gap-6"> |
| |
| <div class="lg:w-64"> |
| <div class="gradient-border bg-white p-6 space-y-6"> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-2">源语言</label> |
| <select id="source-lang" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring focus:ring-indigo-200"> |
| <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-2">目标语言</label> |
| <select id="target-lang" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring focus:ring-indigo-200"> |
| <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> |
|
|
| <button id="switch-lang" class="w-full py-2 px-4 rounded-lg border-2 border-indigo-500 text-indigo-600 hover:bg-indigo-50 transition-colors flex items-center justify-center space-x-2"> |
| <i class="fas fa-exchange-alt"></i> |
| <span>切换语言</span> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1"> |
| <div class="grid md:grid-cols-2 gap-6"> |
| |
| <div class="gradient-border bg-white p-6"> |
| <div class="mb-4 flex items-center justify-between"> |
| <span class="text-sm font-medium text-gray-700">原文</span> |
| <button id="clear-text" class="text-gray-400 hover:text-gray-600"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <textarea |
| id="source-text" |
| class="w-full h-48 p-4 border-0 focus:ring-0 resize-none rounded-lg bg-gray-50" |
| placeholder="请输入要翻译的文本..." |
| ></textarea> |
| <div class="mt-2 flex justify-between text-sm text-gray-500"> |
| <span id="char-count">0/10000</span> |
| |
| |
| |
| </div> |
| </div> |
|
|
| |
| <div class="gradient-border bg-white p-6"> |
| <div class="mb-4 flex items-center justify-between"> |
| <span class="text-sm font-medium text-gray-700">译文</span> |
| <button id="copy-result" class="text-gray-400 hover:text-gray-600"> |
| <i class="fas fa-copy"></i> |
| </button> |
| </div> |
| <div id="target-text" class="w-full h-48 p-4 border-0 focus:ring-0 resize-none rounded-lg bg-gray-50 overflow-auto"></div> |
| <div class="mt-2 flex justify-end"> |
| <div class="flex space-x-4 text-sm"> |
| |
| |
| |
| |
| |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <button id="translate-btn" class="mt-6 w-full py-4 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center space-x-2"> |
| <i class="fas fa-language text-xl"></i> |
| <span class="text-lg font-medium">立即翻译</span> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="file-trans" class="page max-w-7xl mx-auto hidden"> |
| <div class="mb-8"> |
| <h1 class="text-3xl font-bold gradient-text mb-2">文档智能翻译</h1> |
| <p class="text-gray-600">支持多种格式文档翻译,保留原文档排版</p> |
| </div> |
|
|
| <div class="flex flex-col lg:flex-row gap-6"> |
| |
| <div class="lg:w-64 space-y-6"> |
| |
| <div class="gradient-border bg-white p-6 space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-2">源语言</label> |
| <select id="file-source-lang" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring focus:ring-indigo-200"> |
| <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-2">目标语言</label> |
| <select id="file-target-lang" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring focus:ring-indigo-200"> |
| <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="gradient-border bg-white p-6 space-y-4"> |
| <button id="upload-btn" class="w-full py-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center space-x-2"> |
| <i class="fas fa-cloud-upload-alt"></i> |
| <span>上传文件</span> |
| </button> |
| <button id="file-translate-btn" class="w-full py-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center space-x-2" disabled> |
| <i class="fas fa-language"></i> |
| <span>开始翻译</span> |
| </button> |
| <button id="export-btn" class="w-full py-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center space-x-2" disabled> |
| <i class="fas fa-download"></i> |
| <span>导出文档</span> |
| </button> |
| </div> |
|
|
| |
| <div id="progress-panel" class="gradient-border bg-white p-6 hidden"> |
| <h3 class="text-sm font-medium text-gray-700 mb-4">翻译进度</h3> |
| <div class="space-y-4"> |
| <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-100 rounded-full overflow-hidden"> |
| <div id="progress-bar" class="h-full bg-gradient-to-r from-indigo-500 to-purple-500 progress-bar-animation w-0"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1"> |
| |
| <div id="upload-zone" class="gradient-border bg-white h-96 flex flex-col items-center justify-center cursor-pointer group"> |
| <div class="text-center p-8 rounded-2xl"> |
| <div class="w-20 h-20 mx-auto mb-6 floating"> |
| <i class="fas fa-cloud-upload-alt text-6xl text-indigo-500 group-hover:text-indigo-600"></i> |
| </div> |
| <h3 class="text-xl font-medium text-gray-700 mb-2">拖放文件到这里或点击上传</h3> |
| <p class="text-gray-500">支持 TXT、DOCX、PDF、Markdown 格式</p> |
| <p class="text-sm text-gray-400 mt-2">最大支持 20MB</p> |
| </div> |
| </div> |
|
|
| |
| <div id="file-preview" class="hidden"> |
| <div class="gradient-border bg-white"> |
| <div class="border-b border-gray-200 p-4 flex justify-between items-center"> |
| <div class="flex items-center space-x-3"> |
| <i class="fas fa-file-alt text-indigo-500 text-xl"></i> |
| <span id="file-name" class="font-medium"></span> |
| </div> |
| <button id="close-preview" class="text-gray-400 hover:text-gray-600"> |
| <i class="fas fa-times"></i> |
| </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 flex items-center"> |
| <i class="fas fa-file-alt mr-2 text-indigo-500"></i>原文 |
| </h3> |
| <div id="file-content" class="space-y-4"></div> |
| </div> |
| <div> |
| <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center"> |
| <i class="fas fa-language mr-2 text-indigo-500"></i>译文 |
| </h3> |
| <div id="file-translation" class="space-y-4"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="about" class="page max-w-7xl mx-auto hidden"> |
| <div class="mb-12 text-center"> |
| <h1 class="text-4xl font-bold gradient-text mb-4">关于 TranslateAI</h1> |
| <p class="text-xl text-gray-600 max-w-2xl mx-auto"> |
| 我们致力于打造最智能、最准确的翻译助手,让语言不再成为沟通的障碍 |
| </p> |
| </div> |
|
|
| |
| <div class="grid md:grid-cols-3 gap-8 mb-16"> |
| <div class="card-hover gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="w-16 h-16 mx-auto mb-6 rounded-full bg-indigo-100 flex items-center justify-center"> |
| <i class="fas fa-brain text-2xl text-indigo-600"></i> |
| </div> |
| <h3 class="text-xl font-semibold mb-4">AI 智能翻译</h3> |
| <p class="text-gray-600">采用先进的人工智能技术,提供准确、流畅的翻译服务</p> |
| </div> |
|
|
| <div class="card-hover gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="w-16 h-16 mx-auto mb-6 rounded-full bg-purple-100 flex items-center justify-center"> |
| <i class="fas fa-file-alt text-2xl text-purple-600"></i> |
| </div> |
| <h3 class="text-xl font-semibold mb-4">多格式支持</h3> |
| <p class="text-gray-600">支持txt、docx、pdf等多种文件格式,保留原文档排版</p> |
| </div> |
|
|
| <div class="card-hover gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="w-16 h-16 mx-auto mb-6 rounded-full bg-pink-100 flex items-center justify-center"> |
| <i class="fas fa-globe text-2xl text-pink-600"></i> |
| </div> |
| <h3 class="text-xl font-semibold mb-4">多语言支持</h3> |
| <p class="text-gray-600">支持全球100+种语言互译,满足各类翻译需求</p> |
| </div> |
| </div> |
|
|
| |
| <div class="grid md:grid-cols-4 gap-6 mb-16"> |
| <div class="gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="text-3xl font-bold gradient-text mb-2">100+</div> |
| <div class="text-gray-600">支持语言</div> |
| </div> |
| <div class="gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="text-3xl font-bold gradient-text mb-2">99%</div> |
| <div class="text-gray-600">翻译准确率</div> |
| </div> |
| <div class="gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="text-3xl font-bold gradient-text mb-2">1M+</div> |
| <div class="text-gray-600">用户数量</div> |
| </div> |
| <div class="gradient-border bg-white p-6 rounded-xl text-center"> |
| <div class="text-3xl font-bold gradient-text mb-2">10M+</div> |
| <div class="text-gray-600">日翻译量</div> |
| </div> |
| </div> |
|
|
| |
| <div class="gradient-border bg-white p-8 rounded-xl text-center"> |
| <h2 class="text-2xl font-bold mb-6">联系我们</h2> |
| <div class="flex justify-center space-x-8"> |
| <a href="#" class="text-gray-600 hover:text-indigo-600 transition-colors"> |
| <i class="fab fa-github text-2xl"></i> |
| </a> |
| <a href="#" class="text-gray-600 hover:text-indigo-600 transition-colors"> |
| <i class="fab fa-twitter text-2xl"></i> |
| </a> |
| <a href="#" class="text-gray-600 hover:text-indigo-600 transition-colors"> |
| <i class="fas fa-envelope text-2xl"></i> |
| </a> |
| </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 gradient-border bg-white w-full max-w-md p-6 rounded-xl"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">导出设置</h3> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-2">导出格式</label> |
| <select id="export-format" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring focus:ring-indigo-200"> |
| <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-2">导出模式</label> |
| <div class="space-y-2"> |
| <label class="inline-flex items-center"> |
| <input type="radio" name="export-mode" value="translated" class="text-indigo-600"> |
| <span class="ml-2">仅译文</span> |
| </label> |
| <label class="inline-flex items-center ml-4"> |
| <input type="radio" name="export-mode" value="parallel" class="text-indigo-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-lg text-gray-700 hover:bg-gray-50"> |
| 取消 |
| </button> |
| <button id="confirm-export" class="px-4 py-2 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-lg hover:opacity-90"> |
| 导出 |
| </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.navLinks = document.querySelectorAll('a[data-target="text-trans"], a[data-target="file-trans"], a[data-target="about"]'); |
| this.mobileMenuBtn = document.getElementById('mobile-menu-btn'); |
| this.mobileMenu = document.getElementById('mobile-menu'); |
| this.pages = { |
| 'text-trans': document.getElementById('text-trans'), |
| 'file-trans': document.getElementById('file-trans'), |
| 'about': document.getElementById('about') |
| }; |
| |
| this.init(); |
| } |
| |
| init() { |
| this.bindEvents(); |
| |
| this.switchPage('text-trans'); |
| } |
| |
| bindEvents() { |
| |
| this.mobileMenuBtn.addEventListener('click', () => { |
| this.mobileMenu.classList.toggle('hidden'); |
| }); |
| |
| |
| this.navLinks.forEach(link => { |
| link.addEventListener('click', (e) => { |
| e.preventDefault(); |
| const target = link.getAttribute('data-target'); |
| this.switchPage(target); |
| |
| if(!this.mobileMenu.classList.contains('hidden')) { |
| this.mobileMenu.classList.add('hidden'); |
| } |
| }); |
| }); |
| } |
| |
| switchPage(pageId) { |
| |
| Object.values(this.pages).forEach(page => { |
| page.classList.add('hidden'); |
| }); |
| |
| |
| if(this.pages[pageId]) { |
| this.pages[pageId].classList.remove('hidden'); |
| } |
| |
| |
| this.navLinks.forEach(link => { |
| const isActive = link.getAttribute('data-target') === pageId; |
| link.classList.toggle('text-indigo-600', isActive); |
| link.classList.toggle('text-gray-700', !isActive); |
| }); |
| } |
| } |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| new Navigation(); |
| new TextTranslation(); |
| new FileTranslation(); |
| }); |
| </script> |
| </body> |
| </html> |