添加图标

This commit is contained in:
xunbu
2025-07-14 18:05:49 +08:00
parent 03141a0f82
commit c9d18bc45c

View File

@@ -3,8 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocuTranslate - 交互式文档翻译</title>
<title>DocuTranslate - 交互式文档翻译</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<!-- Bootstrap CSS -->
<link href="/static/bootstrap.css" rel="stylesheet" crossorigin="anonymous">
<!-- Bootstrap Icons -->
@@ -60,17 +61,21 @@
transition: all 0.2s ease-in-out;
background-color: var(--bs-body-bg);
}
.file-drop-area.drag-over {
border-color: var(--bs-primary);
background-color: var(--bs-secondary-bg);
}
.file-drop-area.file-selected {
border-style: solid;
border-color: var(--bs-success);
}
.file-drop-area.input-error {
border-color: var(--bs-danger);
}
.file-name-display.input-error-text {
color: var(--bs-danger);
font-weight: bold;
@@ -85,6 +90,7 @@
#previewModal .modal-dialog {
max-width: 95vw;
}
#previewModal .modal-body {
height: 80vh;
}
@@ -95,6 +101,7 @@
border: 1px solid var(--bs-border-color);
border-radius: .375rem;
}
.preview-pane iframe, .preview-pane pre {
width: 100%;
height: 100%;
@@ -119,7 +126,7 @@
</head>
<body>
<div class="container-fluid main-container">
<div class="container-fluid main-container">
<div class="row gx-4">
<!-- Left: Settings Panel -->
<div class="col-lg-4">
@@ -135,7 +142,8 @@
<!-- Parsing Engine Settings -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
<strong><i class="bi bi-file-earmark-binary me-2"></i>解析配置</strong>
</button>
</h2>
@@ -151,9 +159,11 @@
<div class="mb-3" id="mineruTokenGroup">
<label for="mineru_token" class="form-label">
Mineru Token
<a href="https://mineru.net/apiManage/token" target="_blank" class="ms-1" title="获取Mineru Token"><i class="bi bi-box-arrow-up-right"></i></a>
<a href="https://mineru.net/apiManage/token" target="_blank" class="ms-1"
title="获取Mineru Token"><i class="bi bi-box-arrow-up-right"></i></a>
</label>
<input type="password" class="form-control" id="mineru_token" name="mineru_token" placeholder="使用Mineru引擎时需要">
<input type="password" class="form-control" id="mineru_token"
name="mineru_token" placeholder="使用Mineru引擎时需要">
</div>
</div>
</div>
@@ -162,7 +172,8 @@
<!-- AI Settings -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<strong><i class="bi bi-robot me-2"></i>翻译模型</strong>
</button>
</h2>
@@ -175,7 +186,9 @@
<option value="https://api.openai.com/v1">OpenAI</option>
<option value="https://open.bigmodel.cn/api/paas/v4">智谱AI</option>
<option value="https://api.deepseek.com/v1">DeepSeek</option>
<option value="https://dashscope.aliyuncs.com/compatible-mode/v1">阿里云百炼</option>
<option value="https://dashscope.aliyuncs.com/compatible-mode/v1">
阿里云百炼
</option>
<option value="https://www.dmxapi.cn/v1">DMXAPI</option>
<option value="https://openrouter.ai/api/v1">OpenRouter</option>
<option value="https://ark.cn-beijing.volces.com/api/v3">火山引擎</option>
@@ -189,13 +202,16 @@
<div class="mb-3">
<label for="apikey" class="form-label">
API Key
<a href="#" target="_blank" class="ms-1" id="api_href" title="获取API Key"><i class="bi bi-box-arrow-up-right"></i></a>
<a href="#" target="_blank" class="ms-1" id="api_href"
title="获取API Key"><i class="bi bi-box-arrow-up-right"></i></a>
</label>
<input type="password" class="form-control" id="apikey" name="apikey" required placeholder="请输入您的API Key">
<input type="password" class="form-control" id="apikey" name="apikey" required
placeholder="请输入您的API Key">
</div>
<div class="mb-3">
<label for="model_id" class="form-label">模型 ID</label>
<input type="text" class="form-control" id="model_id" name="model_id" required placeholder="例如: gpt-4o, glm-4">
<input type="text" class="form-control" id="model_id" name="model_id" required
placeholder="例如: gpt-4o, glm-4">
</div>
</div>
</div>
@@ -204,7 +220,9 @@
<!-- Translation Settings -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseThree" aria-expanded="false"
aria-controls="collapseThree">
<strong><i class="bi bi-translate me-2"></i>翻译配置</strong>
</button>
</h2>
@@ -221,19 +239,26 @@
</div>
<div class="mb-3">
<label for="custom_prompt_translate" class="form-label">自定义Prompt</label>
<textarea class="form-control" id="custom_prompt_translate" name="custom_prompt_translate" rows="3" placeholder="可选,如“人名保持原文不翻译”"></textarea>
<textarea class="form-control" id="custom_prompt_translate"
name="custom_prompt_translate" rows="3"
placeholder="可选,如“人名保持原文不翻译”"></textarea>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="formula_ocr" name="formula_ocr" checked>
<input class="form-check-input" type="checkbox" role="switch" id="formula_ocr"
name="formula_ocr" checked>
<label class="form-check-label" for="formula_ocr">公式识别</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="code_ocr" name="code_ocr" checked>
<input class="form-check-input" type="checkbox" role="switch" id="code_ocr"
name="code_ocr" checked>
<label class="form-check-label" for="code_ocr">代码识别</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="refine_markdown" name="refine_markdown">
<label class="form-check-label" for="refine_markdown">Markdown修复<span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" title="使用ai对解析后的文本先修复再翻译现不推荐开启">
<input class="form-check-input" type="checkbox" role="switch"
id="refine_markdown" name="refine_markdown">
<label class="form-check-label" for="refine_markdown">Markdown修复<span
class="d-inline-block" tabindex="0" data-bs-toggle="tooltip"
title="使用ai对解析后的文本先修复再翻译现不推荐开启">
<i class="bi bi-question-circle"></i>
</span></label>
@@ -245,32 +270,49 @@
<!-- Other Settings -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingFour">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseFour" aria-expanded="false"
aria-controls="collapseFour">
<strong><i class="bi bi-sliders me-2"></i>高级参数</strong>
</button>
</h2>
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour">
<div class="accordion-body">
<div class="mb-3">
<label for="chunk-size-slider" class="form-label d-flex justify-content-between">
<label for="chunk-size-slider"
class="form-label d-flex justify-content-between">
<span>分块大小: <span id="chunk-size-display"></span></span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 slider-reset-btn" id="chunk-size-reset">重置</button>
<button type="button"
class="btn btn-sm btn-outline-secondary py-0 px-1 slider-reset-btn"
id="chunk-size-reset">重置
</button>
</label>
<input type="range" class="form-range" min="1000" max="6000" step="100" id="chunk-size-slider" name="chunk_size">
<input type="range" class="form-range" min="1000" max="6000" step="100"
id="chunk-size-slider" name="chunk_size">
</div>
<div class="mb-3">
<label for="concurrent-slider" class="form-label d-flex justify-content-between">
<label for="concurrent-slider"
class="form-label d-flex justify-content-between">
<span>并发数: <span id="concurrent-display"></span></span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 slider-reset-btn" id="concurrent-reset">重置</button>
<button type="button"
class="btn btn-sm btn-outline-secondary py-0 px-1 slider-reset-btn"
id="concurrent-reset">重置
</button>
</label>
<input type="range" class="form-range" min="1" max="60" step="1" id="concurrent-slider" name="concurrent">
<input type="range" class="form-range" min="1" max="60" step="1"
id="concurrent-slider" name="concurrent">
</div>
<div class="mb-3">
<label for="temperature-slider" class="form-label d-flex justify-content-between">
<label for="temperature-slider"
class="form-label d-flex justify-content-between">
<span>Temperature: <span id="temperature-display"></span></span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 slider-reset-btn" id="temperature-reset">重置</button>
<button type="button"
class="btn btn-sm btn-outline-secondary py-0 px-1 slider-reset-btn"
id="temperature-reset">重置
</button>
</label>
<input type="range" class="form-range" min="0" max="2" step="0.1" id="temperature-slider" name="temperature">
<input type="range" class="form-range" min="0" max="2" step="0.1"
id="temperature-slider" name="temperature">
</div>
</div>
</div>
@@ -297,7 +339,8 @@
<div class="task-area" id="task-area-container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0"><i class="bi bi-list-task me-2"></i>任务列表</h4>
<button class="btn btn-primary" id="addNewTaskBtn"><i class="bi bi-plus-circle-fill me-2"></i>新建任务</button>
<button class="btn btn-primary" id="addNewTaskBtn"><i class="bi bi-plus-circle-fill me-2"></i>新建任务
</button>
</div>
<div id="task-container">
<!-- Task cards will be injected here -->
@@ -309,10 +352,10 @@
</div>
</div>
</div>
</div>
</div>
<!-- Task Card Template -->
<template id="taskCardTemplate">
<!-- Task Card Template -->
<template id="taskCardTemplate">
<div class="card mb-3 task-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">任务 ID: <code class="task-id-display"></code></span>
@@ -336,7 +379,8 @@
<span class="status-message small text-muted">等待上传文件...</span>
</div>
<div class="progress mt-1" role="progressbar" style="height: 5px; display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width: 100%"></div>
</div>
</div>
</div>
@@ -345,32 +389,40 @@
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="download-buttons" style="display: none;">
<button class="btn btn-sm btn-success preview-html-btn"><i class="bi bi-eye-fill me-1"></i>预览</button>
<button class="btn btn-sm btn-info download-pdf-btn"><i class="bi bi-file-earmark-pdf-fill me-1"></i>下载 PDF</button>
<button class="btn btn-sm btn-info download-pdf-btn"><i class="bi bi-file-earmark-pdf-fill me-1"></i>下载
PDF
</button>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-download me-1"></i>下载其他格式
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item download-html-link" href="#"><i class="bi bi-filetype-html me-2"></i>HTML</a></li>
<li><a class="dropdown-item download-markdown-link" href="#"><i class="bi bi-markdown-fill me-2"></i>Markdown嵌图</a></li>
<li><a class="dropdown-item download-markdown-zip-link" href="#"><i class="bi bi-file-zip-fill me-2"></i>Markdown压缩包</a></li>
<li><a class="dropdown-item download-html-link" href="#"><i
class="bi bi-filetype-html me-2"></i>HTML</a></li>
<li><a class="dropdown-item download-markdown-link" href="#"><i
class="bi bi-markdown-fill me-2"></i>Markdown嵌图</a></li>
<li><a class="dropdown-item download-markdown-zip-link" href="#"><i
class="bi bi-file-zip-fill me-2"></i>Markdown压缩包</a></li>
</ul>
</div>
</div>
<button class="btn btn-primary start-translate-btn ms-auto"><i class="bi bi-play-fill me-1"></i>开始翻译</button>
<button class="btn btn-primary start-translate-btn ms-auto"><i class="bi bi-play-fill me-1"></i>开始翻译
</button>
</div>
</div>
</template>
</template>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalTitle" aria-hidden="true">
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalTitle" aria-hidden="true">
<div class="modal-dialog modal-fullscreen-xl-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalTitle">双语预览</h5>
<div class="btn-group me-auto ms-4" role="group">
<button type="button" class="btn btn-sm btn-primary" id="setBilingualViewBtn">双语</button>
<button type="button" class="btn btn-sm btn-outline-primary" id="setTranslatedOnlyViewBtn">仅译文</button>
<button type="button" class="btn btn-sm btn-outline-primary" id="setTranslatedOnlyViewBtn">仅译文
</button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
@@ -390,31 +442,46 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="printFromPreview"><i class="bi bi-printer-fill me-2"></i>打印/保存为PDF</button>
</div>
<button type="button" class="btn btn-primary" id="printFromPreview"><i
class="bi bi-printer-fill me-2"></i>打印/保存为PDF
</button>
</div>
</div>
</div>
</div>
<!-- Hidden iframe for direct PDF printing -->
<iframe id="printFrame" style="display: none;"></iframe>
<!-- Hidden iframe for direct PDF printing -->
<iframe id="printFrame" style="display: none;"></iframe>
<!-- Theme Switcher -->
<div class="dropdown theme-switch">
<button class="btn btn-secondary dropdown-toggle" type="button" id="theme-switcher" data-bs-toggle="dropdown" aria-expanded="false">
<!-- Theme Switcher -->
<div class="dropdown theme-switch">
<button class="btn btn-secondary dropdown-toggle" type="button" id="theme-switcher" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-circle-half"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="theme-switcher">
<li><button class="dropdown-item" type="button" data-bs-theme-value="light"><i class="bi bi-sun-fill me-2"></i> Light</button></li>
<li><button class="dropdown-item" type="button" data-bs-theme-value="dark"><i class="bi bi-moon-stars-fill me-2"></i> Dark</button></li>
<li><button class="dropdown-item active" type="button" data-bs-theme-value="auto"><i class="bi bi-circle-half me-2"></i> Auto</button></li>
<li>
<button class="dropdown-item" type="button" data-bs-theme-value="light"><i class="bi bi-sun-fill me-2"></i>
Light
</button>
</li>
<li>
<button class="dropdown-item" type="button" data-bs-theme-value="dark"><i
class="bi bi-moon-stars-fill me-2"></i> Dark
</button>
</li>
<li>
<button class="dropdown-item active" type="button" data-bs-theme-value="auto"><i
class="bi bi-circle-half me-2"></i> Auto
</button>
</li>
</ul>
</div>
</div>
<!-- Bootstrap JS -->
<script src="/static/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<!-- Bootstrap JS -->
<script src="/static/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script type="module">
<script type="module">
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
@@ -481,8 +548,21 @@
// --- Utility Functions ---
const generateTaskId = () => Math.random().toString(36).substring(2, 10);
const saveToStorage = (key, value) => { try { localStorage.setItem(key, value); } catch (e) { console.warn("Save to storage failed:", e); } };
const getFromStorage = (key, defaultValue = '') => { try { return localStorage.getItem(key) || defaultValue; } catch (e) { console.warn("Read from storage failed:", e); return defaultValue; } };
const saveToStorage = (key, value) => {
try {
localStorage.setItem(key, value);
} catch (e) {
console.warn("Save to storage failed:", e);
}
};
const getFromStorage = (key, defaultValue = '') => {
try {
return localStorage.getItem(key) || defaultValue;
} catch (e) {
console.warn("Read from storage failed:", e);
return defaultValue;
}
};
function fileToBase64(file) {
return new Promise((resolve, reject) => {
@@ -629,7 +709,7 @@
}
function addEventListenersToCard(taskId) {
const { elements, state } = tasks[taskId];
const {elements, state} = tasks[taskId];
elements.removeBtn.addEventListener('click', () => removeTask(taskId));
@@ -666,7 +746,7 @@
}
function handleFileSelect(taskId) {
const { elements, state } = tasks[taskId];
const {elements, state} = tasks[taskId];
const file = elements.fileInput.files[0];
if (file) {
state.file = file;
@@ -681,7 +761,7 @@
// --- Core Translation Logic ---
async function startTranslation(taskId) {
const { elements, state } = tasks[taskId];
const {elements, state} = tasks[taskId];
// --- Validation ---
if (!state.file) {
@@ -744,7 +824,7 @@
const response = await fetch('/service/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const result = await response.json();
@@ -772,12 +852,12 @@
}
async function cancelTranslation(taskId) {
const { elements } = tasks[taskId];
const {elements} = tasks[taskId];
elements.startBtn.disabled = true;
elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 正在取消...`;
try {
const response = await fetch(`/service/cancel/${taskId}`, { method: 'POST' });
const response = await fetch(`/service/cancel/${taskId}`, {method: 'POST'});
const result = await response.json();
if (response.ok && result.cancelled) {
@@ -797,7 +877,7 @@
// --- Polling ---
function startPolling(taskId) {
stopPolling(taskId);
const { intervals } = tasks[taskId];
const {intervals} = tasks[taskId];
intervals.log = setInterval(() => pollLogs(taskId), 2000);
intervals.status = setInterval(() => pollStatus(taskId), 1500);
pollLogs(taskId);
@@ -805,7 +885,7 @@
}
function stopPolling(taskId) {
const { intervals } = tasks[taskId];
const {intervals} = tasks[taskId];
if (intervals.log) clearInterval(intervals.log);
if (intervals.status) clearInterval(intervals.status);
intervals.log = null;
@@ -813,7 +893,7 @@
}
async function pollLogs(taskId) {
const { elements } = tasks[taskId];
const {elements} = tasks[taskId];
try {
const response = await fetch(`/service/logs/${taskId}`);
if (!response.ok) return;
@@ -828,7 +908,7 @@
}
async function pollStatus(taskId, isRestore = false) {
const { elements, state } = tasks[taskId];
const {elements, state} = tasks[taskId];
try {
const response = await fetch(`/service/status/${taskId}`);
if (!response.ok) {
@@ -887,7 +967,7 @@
// --- Download and Preview ---
function setupPreview(taskId) {
const { state } = tasks[taskId];
const {state} = tasks[taskId];
if (!state.htmlUrl) return;
// Clear previous content
@@ -941,12 +1021,14 @@
try {
translatedPreviewFrame.contentWindow.focus();
translatedPreviewFrame.contentWindow.print();
} catch(e) { alert('打印失败,请使用浏览器打印功能。'); }
} catch (e) {
alert('打印失败,请使用浏览器打印功能。');
}
};
}
function downloadPdf(taskId) {
const { elements, state } = tasks[taskId];
const {elements, state} = tasks[taskId];
if (!state.htmlUrl) return;
elements.pdfBtn.disabled = true;
@@ -1075,7 +1157,9 @@
platformSelect.addEventListener('change', updatePlatformUI);
apikeyInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_apikey`, e.target.value));
modelInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_model_id`, e.target.value));
baseUrlInput.addEventListener('input', e => { if (platformSelect.value === 'custom') saveToStorage('translator_platform_custom_base_url', e.target.value); });
baseUrlInput.addEventListener('input', e => {
if (platformSelect.value === 'custom') saveToStorage('translator_platform_custom_base_url', e.target.value);
});
convertEnginSelect.addEventListener('change', updateConvertEnginUI);
mineruTokenInput.addEventListener('input', e => saveToStorage('translator_mineru_token', e.target.value));
toLangSelect.addEventListener('change', e => saveToStorage('translator_to_lang', e.target.value));
@@ -1148,6 +1232,6 @@
// --- Start the application ---
document.addEventListener('DOMContentLoaded', init);
</script>
</script>
</body>
</html>