增加目标语言选项,异步加载预览界面
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user