增加目标语言选项,异步加载预览界面
This commit is contained in:
@@ -230,7 +230,8 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" class="form-control" id="mineru_token"
|
<input type="password" class="form-control" id="mineru_token"
|
||||||
name="mineru_token" placeholder="使用Mineru引擎时需要">
|
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>
|
<i class="bi bi-eye-slash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,9 +277,11 @@
|
|||||||
title="获取API Key"><i class="bi bi-box-arrow-up-right"></i></a>
|
title="获取API Key"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<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">
|
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>
|
<i class="bi bi-eye-slash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,10 +309,16 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to_lang" class="form-label">目标语言</label>
|
<label for="to_lang" class="form-label">目标语言</label>
|
||||||
<select class="form-select" id="to_lang" name="to_lang">
|
<select class="form-select" id="to_lang" name="to_lang">
|
||||||
<option value="中文">中文</option>
|
<option value="中文">中文(简体中文)</option>
|
||||||
<option value="English">English</option>
|
<option value="英文">英文(English)</option>
|
||||||
<option value="Japanese">Japanese</option>
|
<option value="西班牙文">西班牙文(Español)</option>
|
||||||
<option value="Korean">Korean</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -433,7 +442,8 @@
|
|||||||
<template id="taskCardTemplate">
|
<template id="taskCardTemplate">
|
||||||
<div class="card mb-3 task-card">
|
<div class="card mb-3 task-card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<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>
|
<button type="button" class="btn-close remove-task-btn" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -498,7 +508,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Preview Offcanvas with Resizable Panes -->
|
<!-- 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">
|
<div class="offcanvas-header border-bottom">
|
||||||
<h5 class="offcanvas-title" id="previewOffcanvasLabel">预览</h5>
|
<h5 class="offcanvas-title" id="previewOffcanvasLabel">预览</h5>
|
||||||
<div class="btn-group me-auto ms-4" role="group">
|
<div class="btn-group me-auto ms-4" role="group">
|
||||||
@@ -629,8 +640,21 @@
|
|||||||
|
|
||||||
// --- Utility Functions ---
|
// --- Utility Functions ---
|
||||||
const generateCardId = () => `card_${Math.random().toString(36).substring(2, 10)}`;
|
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 saveToStorage = (key, value) => {
|
||||||
const getFromStorage = (key, defaultValue = '') => { try { return localStorage.getItem(key) || defaultValue; } catch (e) { console.warn("Read from storage failed:", e); return defaultValue; } };
|
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) {
|
function fileToBase64(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -812,7 +836,10 @@
|
|||||||
elements.fileDropArea.addEventListener('click', () => elements.fileInput.click());
|
elements.fileDropArea.addEventListener('click', () => elements.fileInput.click());
|
||||||
elements.fileInput.addEventListener('change', () => handleFileSelect(cardId));
|
elements.fileInput.addEventListener('change', () => handleFileSelect(cardId));
|
||||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
['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 => {
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.add('drag-over'), false);
|
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.add('drag-over'), false);
|
||||||
@@ -1114,10 +1141,17 @@
|
|||||||
const {state} = tasks[cardId];
|
const {state} = tasks[cardId];
|
||||||
if (!state.htmlUrl) return;
|
if (!state.htmlUrl) return;
|
||||||
|
|
||||||
|
// 1. 清除旧内容并设置译文预览的加载状态
|
||||||
const existingOriginalContent = originalPreviewPane.querySelector('iframe, pre, p');
|
const existingOriginalContent = originalPreviewPane.querySelector('iframe, pre, p');
|
||||||
if (existingOriginalContent) existingOriginalContent.remove();
|
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) {
|
if (state.file) {
|
||||||
const fileType = state.file.type;
|
const fileType = state.file.type;
|
||||||
const fileExtension = state.file.name.split('.').pop().toLowerCase();
|
const fileExtension = state.file.name.split('.').pop().toLowerCase();
|
||||||
@@ -1125,7 +1159,9 @@
|
|||||||
|
|
||||||
if (fileType.startsWith('text/') || textLikeExtensions.includes(fileExtension)) {
|
if (fileType.startsWith('text/') || textLikeExtensions.includes(fileExtension)) {
|
||||||
const pre = document.createElement('pre');
|
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);
|
originalPreviewPane.appendChild(pre);
|
||||||
} else if (['application/pdf', 'text/html'].includes(fileType) || ['html', 'htm'].includes(fileExtension)) {
|
} else if (['application/pdf', 'text/html'].includes(fileType) || ['html', 'htm'].includes(fileExtension)) {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
@@ -1144,25 +1180,25 @@
|
|||||||
originalPreviewPane.appendChild(p);
|
originalPreviewPane.appendChild(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 异步加载译文 HTML
|
||||||
fetch(state.htmlUrl)
|
fetch(state.htmlUrl)
|
||||||
.then(resp => resp.ok ? resp.text() : Promise.reject(`HTTP error ${resp.status}`))
|
.then(resp => resp.ok ? resp.text() : Promise.reject(`HTTP error ${resp.status}`))
|
||||||
.then(html => {
|
.then(html => {
|
||||||
translatedPreviewFrame.srcdoc = html;
|
translatedPreviewFrame.srcdoc = html; // 加载成功后更新内容
|
||||||
setPreviewDisplayMode('bilingual');
|
|
||||||
previewOffcanvas.show();
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Preview fetch failed:', err);
|
console.error('Preview fetch failed:', err);
|
||||||
translatedPreviewFrame.srcdoc = `<h3>加载译文失败</h3><p>${err.message}</p>`;
|
translatedPreviewFrame.srcdoc = `<h3>加载译文失败</h3><p>${err.message}</p>`; // 加载失败显示错误信息
|
||||||
setPreviewDisplayMode('bilingual');
|
|
||||||
previewOffcanvas.show();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 5. 打印按钮事件设置
|
||||||
printFromPreview.onclick = () => {
|
printFromPreview.onclick = () => {
|
||||||
try {
|
try {
|
||||||
translatedPreviewFrame.contentWindow.focus();
|
translatedPreviewFrame.contentWindow.focus();
|
||||||
translatedPreviewFrame.contentWindow.print();
|
translatedPreviewFrame.contentWindow.print();
|
||||||
} catch(e) { alert('打印失败,请使用浏览器打印功能。'); }
|
} catch (e) {
|
||||||
|
alert('打印失败,请使用浏览器打印功能。');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1282,7 +1318,8 @@
|
|||||||
const enginList = await enginRes.json();
|
const enginList = await enginRes.json();
|
||||||
Array.from(convertEnginSelect.options).forEach(option => {
|
Array.from(convertEnginSelect.options).forEach(option => {
|
||||||
if (!enginList.includes(option.value)) {
|
if (!enginList.includes(option.value)) {
|
||||||
option.disabled = true; option.textContent += " (不可用)";
|
option.disabled = true;
|
||||||
|
option.textContent += " (不可用)";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
defaultParams = await paramsRes.json();
|
defaultParams = await paramsRes.json();
|
||||||
@@ -1337,7 +1374,9 @@
|
|||||||
platformSelect.addEventListener('change', updatePlatformUI);
|
platformSelect.addEventListener('change', updatePlatformUI);
|
||||||
apikeyInput.addEventListener('input', e => saveToStorage(`translator_platform_${platformSelect.value}_apikey`, e.target.value));
|
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));
|
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);
|
convertEnginSelect.addEventListener('change', updateConvertEnginUI);
|
||||||
mineruTokenInput.addEventListener('input', e => saveToStorage('translator_mineru_token', e.target.value));
|
mineruTokenInput.addEventListener('input', e => saveToStorage('translator_mineru_token', e.target.value));
|
||||||
toLangSelect.addEventListener('change', e => saveToStorage('translator_to_lang', e.target.value));
|
toLangSelect.addEventListener('change', e => saveToStorage('translator_to_lang', e.target.value));
|
||||||
@@ -1352,14 +1391,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Theme switcher logic ---
|
// --- Theme switcher logic ---
|
||||||
const getPreferredTheme = () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme) { return storedTheme; } return 'auto'; };
|
const getPreferredTheme = () => {
|
||||||
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 storedTheme = localStorage.getItem('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'); } };
|
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();
|
const preferredTheme = getPreferredTheme();
|
||||||
setTheme(preferredTheme);
|
setTheme(preferredTheme);
|
||||||
showActiveTheme(preferredTheme);
|
showActiveTheme(preferredTheme);
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme === 'auto' || !storedTheme) { setTheme('auto'); } });
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
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); }); });
|
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 ---
|
// --- Start the application ---
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|||||||
Reference in New Issue
Block a user