增加目标语言选项,异步加载预览界面

This commit is contained in:
xunbu
2025-07-16 10:27:40 +08:00
parent 214873ce8b
commit 0bbad10cc4

View File

@@ -230,7 +230,8 @@
<div class="input-group">
<input type="password" class="form-control" id="mineru_token"
name="mineru_token" placeholder="使用Mineru引擎时需要">
<button class="btn btn-outline-secondary toggle-password" type="button" data-target="mineru_token">
<button class="btn btn-outline-secondary toggle-password" type="button"
data-target="mineru_token">
<i class="bi bi-eye-slash"></i>
</button>
</div>
@@ -276,9 +277,11 @@
title="获取API Key"><i class="bi bi-box-arrow-up-right"></i></a>
</label>
<div class="input-group">
<input type="password" class="form-control" id="apikey" name="apikey" required
<input type="password" class="form-control" id="apikey" name="apikey"
required
placeholder="请输入您的API Key">
<button class="btn btn-outline-secondary toggle-password" type="button" data-target="apikey">
<button class="btn btn-outline-secondary toggle-password" type="button"
data-target="apikey">
<i class="bi bi-eye-slash"></i>
</button>
</div>
@@ -306,10 +309,16 @@
<div class="mb-3">
<label for="to_lang" class="form-label">目标语言</label>
<select class="form-select" id="to_lang" name="to_lang">
<option value="中文">中文</option>
<option value="English">English</option>
<option value="Japanese">Japanese</option>
<option value="Korean">Korean</option>
<option value="中文">中文(简体中文)</option>
<option value="英文">英文(English)</option>
<option value="西班牙文">西班牙文(Español)</option>
<option value="法文">法文(Français)</option>
<option value="德文">德文(Deutsch)</option>
<option value="日文">日文(日本語)</option>
<option value="韩文">韩文(한국어)</option>
<option value="俄文">俄文(Русский)</option>
<option value="葡萄牙文">葡萄牙文(Português)</option>
<option value="阿拉伯文">العربية(阿拉伯文)</option>
</select>
</div>
<div class="mb-3">
@@ -433,7 +442,8 @@
<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"><span class="task-id-placeholder">等待提交...</span></code></span>
<span class="fw-bold">任务 ID: <code class="task-id-display"><span
class="task-id-placeholder">等待提交...</span></code></span>
<button type="button" class="btn-close remove-task-btn" aria-label="Close"></button>
</div>
<div class="card-body">
@@ -498,7 +508,8 @@
</template>
<!-- Preview Offcanvas with Resizable Panes -->
<div class="offcanvas offcanvas-end" data-bs-scroll="true" tabindex="-1" id="previewOffcanvas" aria-labelledby="previewOffcanvasLabel">
<div class="offcanvas offcanvas-end" data-bs-scroll="true" tabindex="-1" id="previewOffcanvas"
aria-labelledby="previewOffcanvasLabel">
<div class="offcanvas-header border-bottom">
<h5 class="offcanvas-title" id="previewOffcanvasLabel">预览</h5>
<div class="btn-group me-auto ms-4" role="group">
@@ -629,8 +640,21 @@
// --- Utility Functions ---
const generateCardId = () => `card_${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) => {
@@ -776,7 +800,7 @@
*/
async function releaseTask(backendTaskId) {
try {
fetch(`/service/release/${backendTaskId}`, { method: 'POST' });
fetch(`/service/release/${backendTaskId}`, {method: 'POST'});
console.log(`[${backendTaskId}] Release request sent to backend.`);
} catch (error) {
console.error(`[${backendTaskId}] Failed to send release request:`, error);
@@ -805,14 +829,17 @@
}
function addEventListenersToCard(cardId) {
const { elements } = tasks[cardId];
const {elements} = tasks[cardId];
elements.removeBtn.addEventListener('click', () => removeTask(cardId));
elements.fileDropArea.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', () => handleFileSelect(cardId));
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
elements.fileDropArea.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }, false);
elements.fileDropArea.addEventListener(eventName, e => {
e.preventDefault();
e.stopPropagation();
}, false);
});
['dragenter', 'dragover'].forEach(eventName => {
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.add('drag-over'), false);
@@ -837,7 +864,7 @@
}
function handleFileSelect(cardId) {
const { elements, state } = tasks[cardId];
const {elements, state} = tasks[cardId];
const file = elements.fileInput.files[0];
if (file) {
state.file = file;
@@ -853,7 +880,7 @@
// --- Core Translation Logic ---
async function startTranslation(cardId) {
const { elements, state } = tasks[cardId];
const {elements, state} = tasks[cardId];
if (!state.file) {
elements.statusMessage.textContent = '请先选择一个文件。';
@@ -926,7 +953,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();
@@ -967,14 +994,14 @@
const task = tasks[cardId];
if (!task || !task.state.backendTaskId) return;
const { elements } = task;
const {elements} = task;
const backendTaskId = task.state.backendTaskId;
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/${backendTaskId}`, { method: 'POST' });
const response = await fetch(`/service/cancel/${backendTaskId}`, {method: 'POST'});
const result = await response.json();
if (response.ok && result.cancelled) {
@@ -1007,7 +1034,7 @@
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
if (!card) return;
const { intervals } = card;
const {intervals} = card;
if (intervals.log) clearInterval(intervals.log);
if (intervals.status) clearInterval(intervals.status);
intervals.log = null;
@@ -1017,7 +1044,7 @@
async function pollLogs(backendTaskId) {
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
if (!card) return;
const { elements } = card;
const {elements} = card;
try {
const response = await fetch(`/service/logs/${backendTaskId}`);
@@ -1043,7 +1070,7 @@
}
return;
}
const { elements, state } = card;
const {elements, state} = card;
const cardId = elements.card.dataset.cardId;
try {
@@ -1111,13 +1138,20 @@
// --- Download and Preview ---
function setupPreview(cardId) {
const { state } = tasks[cardId];
const {state} = tasks[cardId];
if (!state.htmlUrl) return;
// 1. 清除旧内容并设置译文预览的加载状态
const existingOriginalContent = originalPreviewPane.querySelector('iframe, pre, p');
if (existingOriginalContent) existingOriginalContent.remove();
translatedPreviewFrame.src = 'about:blank';
translatedPreviewFrame.src = 'about:blank'; // 清除iframe的src防止残留
translatedPreviewFrame.srcdoc = '<h3><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 正在加载译文...</h3>'; // 立即显示加载提示
// 2. 立即显示预览 Offcanvas 并设置初始显示模式
setPreviewDisplayMode('bilingual'); // 先设置显示模式,确保布局正确
previewOffcanvas.show(); // 立即显示预览框
// 3. 加载原文内容(如果文件可用)
if (state.file) {
const fileType = state.file.type;
const fileExtension = state.file.name.split('.').pop().toLowerCase();
@@ -1125,7 +1159,9 @@
if (fileType.startsWith('text/') || textLikeExtensions.includes(fileExtension)) {
const pre = document.createElement('pre');
state.file.text().then(text => pre.textContent = text).catch(() => pre.textContent = '无法读取原文内容。');
state.file.text()
.then(text => pre.textContent = text)
.catch(() => pre.textContent = '无法读取原文内容。');
originalPreviewPane.appendChild(pre);
} else if (['application/pdf', 'text/html'].includes(fileType) || ['html', 'htm'].includes(fileExtension)) {
const iframe = document.createElement('iframe');
@@ -1144,30 +1180,30 @@
originalPreviewPane.appendChild(p);
}
// 4. 异步加载译文 HTML
fetch(state.htmlUrl)
.then(resp => resp.ok ? resp.text() : Promise.reject(`HTTP error ${resp.status}`))
.then(html => {
translatedPreviewFrame.srcdoc = html;
setPreviewDisplayMode('bilingual');
previewOffcanvas.show();
translatedPreviewFrame.srcdoc = html; // 加载成功后更新内容
})
.catch(err => {
console.error('Preview fetch failed:', err);
translatedPreviewFrame.srcdoc = `<h3>加载译文失败</h3><p>${err.message}</p>`;
setPreviewDisplayMode('bilingual');
previewOffcanvas.show();
translatedPreviewFrame.srcdoc = `<h3>加载译文失败</h3><p>${err.message}</p>`; // 加载失败显示错误信息
});
// 5. 打印按钮事件设置
printFromPreview.onclick = () => {
try {
translatedPreviewFrame.contentWindow.focus();
translatedPreviewFrame.contentWindow.print();
} catch(e) { alert('打印失败,请使用浏览器打印功能。'); }
} catch (e) {
alert('打印失败,请使用浏览器打印功能。');
}
};
}
function downloadPdf(cardId) {
const { elements, state } = tasks[cardId];
const {elements, state} = tasks[cardId];
if (!state.htmlUrl) return;
elements.pdfBtn.disabled = true;
@@ -1282,7 +1318,8 @@
const enginList = await enginRes.json();
Array.from(convertEnginSelect.options).forEach(option => {
if (!enginList.includes(option.value)) {
option.disabled = true; option.textContent += " (不可用)";
option.disabled = true;
option.textContent += " (不可用)";
}
});
defaultParams = await paramsRes.json();
@@ -1337,7 +1374,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));
@@ -1352,17 +1391,49 @@
}
// --- Theme switcher logic ---
const getPreferredTheme = () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme) { return storedTheme; } return 'auto'; };
const setTheme = theme => { if (theme === 'auto') { document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); } else { document.documentElement.setAttribute('data-bs-theme', theme); } };
const showActiveTheme = (theme) => { document.querySelectorAll('[data-bs-theme-value]').forEach(element => { element.classList.remove('active'); }); const activeButton = document.querySelector(`[data-bs-theme-value="${theme}"]`); if (activeButton) { activeButton.classList.add('active'); } };
const getPreferredTheme = () => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
return storedTheme;
}
return 'auto';
};
const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-bs-theme', theme);
}
};
const showActiveTheme = (theme) => {
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active');
});
const activeButton = document.querySelector(`[data-bs-theme-value="${theme}"]`);
if (activeButton) {
activeButton.classList.add('active');
}
};
const preferredTheme = getPreferredTheme();
setTheme(preferredTheme);
showActiveTheme(preferredTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme === 'auto' || !storedTheme) { setTheme('auto'); } });
document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => { toggle.addEventListener('click', () => { const theme = toggle.getAttribute('data-bs-theme-value'); localStorage.setItem('theme', theme); setTheme(theme); showActiveTheme(theme); }); });
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'auto' || !storedTheme) {
setTheme('auto');
}
});
document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value');
localStorage.setItem('theme', theme);
setTheme(theme);
showActiveTheme(theme);
});
});
// --- Start the application ---
document.addEventListener('DOMContentLoaded', init);
</script>
</script>
</body>
</html>