修复预览窗口溢出问题

This commit is contained in:
xunbu
2025-07-14 21:12:14 +08:00
parent 3691126c02
commit acd0ae3c46

View File

@@ -84,30 +84,56 @@
#printFrame, #translatedPreviewFrame { #printFrame, #translatedPreviewFrame {
border: none; border: none;
width: 100%; width: 100%;
}
/* --- MODIFIED: Styles for the new Offcanvas Preview --- */
#previewOffcanvas {
--bs-offcanvas-width: 95vw;
max-width: 1600px;
}
.preview-split-container {
display: flex;
flex-direction: row;
height: 100%; height: 100%;
} }
#previewModal .modal-dialog { .preview-pane-wrapper {
max-width: 95vw; display: flex;
flex-direction: column;
overflow: hidden; /* Important for split.js */
} }
#previewModal .modal-body { .preview-pane-wrapper h6 {
height: 80vh; flex-shrink: 0;
padding: 0.25rem;
} }
.preview-pane { .preview-pane-wrapper .preview-pane {
height: 100%; flex-grow: 1; /* Make the inner pane grow */
overflow: hidden;
border: 1px solid var(--bs-border-color); border: 1px solid var(--bs-border-color);
border-radius: .375rem; border-radius: .375rem;
overflow: auto;
} }
/* split.js gutter style */
.gutter {
background-color: var(--bs-tertiary-bg);
border-left: 1px solid var(--bs-border-color);
border-right: 1px solid var(--bs-border-color);
}
.gutter.gutter-horizontal {
cursor: col-resize;
}
/* --- END OF MODIFICATION --- */
.preview-pane iframe, .preview-pane pre { .preview-pane iframe, .preview-pane pre {
width: 100%; width: 100%;
height: 100%; height: 95%;
border: none; border: none;
margin: 0; margin: 0;
padding: 10px; padding: 0;
overflow: auto; overflow: auto;
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
} }
@@ -413,41 +439,36 @@
</div> </div>
</template> </template>
<!-- Preview Modal --> <!-- MODIFIED: Preview Offcanvas with Resizable Panes -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalTitle" aria-hidden="true"> <div class="offcanvas offcanvas-end" data-bs-scroll="true" tabindex="-1" id="previewOffcanvas" aria-labelledby="previewOffcanvasLabel">
<div class="modal-dialog modal-fullscreen-xl-down"> <div class="offcanvas-header border-bottom">
<div class="modal-content"> <h5 class="offcanvas-title" id="previewOffcanvasLabel">预览</h5>
<div class="modal-header">
<h5 class="modal-title" id="previewModalTitle">双语预览</h5>
<div class="btn-group me-auto ms-4" role="group"> <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-primary" id="setBilingualViewBtn">双语</button>
<button type="button" class="btn btn-sm btn-outline-primary" id="setTranslatedOnlyViewBtn">仅译文 <button type="button" class="btn btn-sm btn-outline-primary" id="setTranslatedOnlyViewBtn">仅译文</button>
</button>
</div> </div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="offcanvas-body d-flex flex-column p-2">
<div class="row h-100 gx-2"> <div class="preview-split-container flex-grow-1">
<div class="col-6" id="originalPreviewContainer"> <div id="originalPreviewContainer" class="preview-pane-wrapper">
<h6 class="text-center text-muted small">原文</h6> <h6 class="text-center text-muted small">原文</h6>
<div class="preview-pane" id="originalPreviewPane"></div> <div class="preview-pane" id="originalPreviewPane"></div>
</div> </div>
<div class="col-6" id="translatedPreviewContainer"> <div id="translatedPreviewContainer" class="preview-pane-wrapper">
<h6 class="text-center text-muted small">译文</h6> <h6 class="text-center text-muted small">译文</h6>
<div class="preview-pane"> <div class="preview-pane">
<iframe id="translatedPreviewFrame" src="about:blank"></iframe> <iframe id="translatedPreviewFrame" src="about:blank"></iframe>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="offcanvas-footer mt-2 pt-3 border-top d-flex justify-content-end align-items-center">
<div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="offcanvas">关闭</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> <button type="button" class="btn btn-primary ms-2" id="printFromPreview">
<button type="button" class="btn btn-primary" id="printFromPreview"><i <i class="bi bi-printer-fill me-2"></i>打印/保存为PDF
class="bi bi-printer-fill me-2"></i>打印/保存为PDF
</button> </button>
</div> </div>
</div> </div>
</div>
</div> </div>
<!-- Hidden iframe for direct PDF printing --> <!-- Hidden iframe for direct PDF printing -->
@@ -480,8 +501,10 @@
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="/static/bootstrap.bundle.min.js" crossorigin="anonymous"></script> <script src="/static/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<!-- MODIFIED: Split.js for resizable panes -->
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
<script type="module"> <script type="module">
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
@@ -518,9 +541,10 @@
const noTaskPlaceholder = document.getElementById('no-task-placeholder'); const noTaskPlaceholder = document.getElementById('no-task-placeholder');
const taskCardTemplate = document.getElementById('taskCardTemplate'); const taskCardTemplate = document.getElementById('taskCardTemplate');
// Modal and preview elements // MODIFIED: Offcanvas and preview elements
const previewModal = new bootstrap.Modal(document.getElementById('previewModal')); const previewOffcanvasEl = document.getElementById('previewOffcanvas');
const previewModalTitle = document.getElementById('previewModalTitle'); const previewOffcanvas = new bootstrap.Offcanvas(previewOffcanvasEl);
const previewOffcanvasLabel = document.getElementById('previewOffcanvasLabel');
const originalPreviewPane = document.getElementById('originalPreviewPane'); const originalPreviewPane = document.getElementById('originalPreviewPane');
const translatedPreviewFrame = document.getElementById('translatedPreviewFrame'); const translatedPreviewFrame = document.getElementById('translatedPreviewFrame');
const originalPreviewContainer = document.getElementById('originalPreviewContainer'); const originalPreviewContainer = document.getElementById('originalPreviewContainer');
@@ -534,6 +558,7 @@
let defaultParams = {}; let defaultParams = {};
const tasks = {}; // { taskId: { elements: {...}, state: {...}, intervals: {...} } } const tasks = {}; // { taskId: { elements: {...}, state: {...}, intervals: {...} } }
let isAdminMode = false; // Flag for admin view let isAdminMode = false; // Flag for admin view
let previewSplitInstance = null; // To hold the Split.js instance
const apiHrefMap = { const apiHrefMap = {
"https://openrouter.ai/api/v1": "https://openrouter.ai/settings/keys", "https://openrouter.ai/api/v1": "https://openrouter.ai/settings/keys",
@@ -909,19 +934,14 @@
} }
const status = await response.json(); const status = await response.json();
// ==================== MODIFICATION START ====================
// Restore filename display from status // Restore filename display from status
// This runs only if state.file is not present (e.g., after a page refresh)
// and the backend provides the original filename.
if (status.original_filename && !state.file) { if (status.original_filename && !state.file) {
elements.fileNameDisplay.textContent = `已上传: ${status.original_filename}`; elements.fileNameDisplay.textContent = `已上传: ${status.original_filename}`;
elements.fileDropArea.classList.add('file-selected'); elements.fileDropArea.classList.add('file-selected');
elements.fileDropPrompt.style.display = 'none'; elements.fileDropPrompt.style.display = 'none';
// Also clear any potential error states from a previous failed attempt
elements.fileDropArea.classList.remove('input-error'); elements.fileDropArea.classList.remove('input-error');
elements.fileNameDisplay.classList.remove('input-error-text'); elements.fileNameDisplay.classList.remove('input-error-text');
} }
// ==================== MODIFICATION END ====================
elements.statusMessage.textContent = status.status_message || '正在获取状态...'; elements.statusMessage.textContent = status.status_message || '正在获取状态...';
elements.statusMessage.className = `status-message small ${status.error_flag ? 'text-danger' : 'text-info'}`; elements.statusMessage.className = `status-message small ${status.error_flag ? 'text-danger' : 'text-info'}`;
@@ -968,7 +988,7 @@
} }
} }
// --- Download and Preview --- // --- MODIFIED: Download and Preview ---
function setupPreview(taskId) { function setupPreview(taskId) {
const { state } = tasks[taskId]; const { state } = tasks[taskId];
if (!state.htmlUrl) return; if (!state.htmlUrl) return;
@@ -1011,13 +1031,13 @@
.then(html => { .then(html => {
translatedPreviewFrame.srcdoc = html; translatedPreviewFrame.srcdoc = html;
setPreviewDisplayMode('bilingual'); setPreviewDisplayMode('bilingual');
previewModal.show(); 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'); setPreviewDisplayMode('bilingual');
previewModal.show(); previewOffcanvas.show();
}); });
printFromPreview.onclick = () => { printFromPreview.onclick = () => {
@@ -1062,26 +1082,49 @@
} }
function setPreviewDisplayMode(mode) { function setPreviewDisplayMode(mode) {
// Always destroy the previous split instance to avoid errors
if (previewSplitInstance) {
previewSplitInstance.destroy();
previewSplitInstance = null;
}
// Ensure containers are visible and reset styles before applying new ones
originalPreviewContainer.style.display = 'flex';
originalPreviewContainer.style.width = '';
translatedPreviewContainer.style.width = '';
if (mode === 'bilingual') { if (mode === 'bilingual') {
originalPreviewContainer.style.display = 'block'; previewOffcanvasLabel.textContent = '双语预览';
translatedPreviewContainer.classList.remove('col-12');
translatedPreviewContainer.classList.add('col-6'); // Re-initialize Split.js for resizable panes
previewModalTitle.textContent = '双语预览'; previewSplitInstance = Split(['#originalPreviewContainer', '#translatedPreviewContainer'], {
sizes: [50, 50],
minSize: 200, // Minimum pane size in pixels
gutterSize: 10,
cursor: 'col-resize',
});
// Update button states
setBilingualViewBtn.classList.add('btn-primary'); setBilingualViewBtn.classList.add('btn-primary');
setBilingualViewBtn.classList.remove('btn-outline-primary'); setBilingualViewBtn.classList.remove('btn-outline-primary');
setTranslatedOnlyViewBtn.classList.remove('btn-primary'); setTranslatedOnlyViewBtn.classList.remove('btn-primary');
setTranslatedOnlyViewBtn.classList.add('btn-outline-primary'); setTranslatedOnlyViewBtn.classList.add('btn-outline-primary');
} else { // translationOnly } else { // translationOnly
previewOffcanvasLabel.textContent = '译文预览';
// Hide original, make translated pane full-width
originalPreviewContainer.style.display = 'none'; originalPreviewContainer.style.display = 'none';
translatedPreviewContainer.classList.remove('col-6'); translatedPreviewContainer.style.width = '100%';
translatedPreviewContainer.classList.add('col-12');
previewModalTitle.textContent = '译文预览'; // Update button states
setTranslatedOnlyViewBtn.classList.add('btn-primary'); setTranslatedOnlyViewBtn.classList.add('btn-primary');
setTranslatedOnlyViewBtn.classList.remove('btn-outline-primary'); setTranslatedOnlyViewBtn.classList.remove('btn-outline-primary');
setBilingualViewBtn.classList.remove('btn-primary'); setBilingualViewBtn.classList.remove('btn-primary');
setBilingualViewBtn.classList.add('btn-outline-primary'); setBilingualViewBtn.classList.add('btn-outline-primary');
} }
} }
// --- End of MODIFICATION ---
// --- Initialization --- // --- Initialization ---
async function init() { async function init() {
@@ -1172,45 +1215,38 @@
setTranslatedOnlyViewBtn.addEventListener('click', () => setPreviewDisplayMode('translationOnly')); setTranslatedOnlyViewBtn.addEventListener('click', () => setPreviewDisplayMode('translationOnly'));
} }
// --- MODIFIED: Theme switcher logic --- // --- Theme switcher logic ---
const getPreferredTheme = () => { const getPreferredTheme = () => {
const storedTheme = localStorage.getItem('theme'); const storedTheme = localStorage.getItem('theme');
if (storedTheme) { if (storedTheme) {
return storedTheme; return storedTheme;
} }
// Default to 'auto' if no preference is stored
return 'auto'; return 'auto';
}; };
const setTheme = theme => { const setTheme = theme => {
if (theme === 'auto') { if (theme === 'auto') {
// Set theme based on system preference
document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
} else { } else {
// Set theme based on user's choice (light/dark)
document.documentElement.setAttribute('data-bs-theme', theme); document.documentElement.setAttribute('data-bs-theme', theme);
} }
}; };
const showActiveTheme = (theme) => { const showActiveTheme = (theme) => {
// Remove active class from all buttons
document.querySelectorAll('[data-bs-theme-value]').forEach(element => { document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active'); element.classList.remove('active');
}); });
// Add active class to the button corresponding to the current theme
const activeButton = document.querySelector(`[data-bs-theme-value="${theme}"]`); const activeButton = document.querySelector(`[data-bs-theme-value="${theme}"]`);
if (activeButton) { if (activeButton) {
activeButton.classList.add('active'); activeButton.classList.add('active');
} }
}; };
// On page load, set theme and update UI
const preferredTheme = getPreferredTheme(); const preferredTheme = getPreferredTheme();
setTheme(preferredTheme); setTheme(preferredTheme);
showActiveTheme(preferredTheme); showActiveTheme(preferredTheme);
// When system theme changes, update if in 'auto' mode
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = localStorage.getItem('theme'); const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'auto' || !storedTheme) { if (storedTheme === 'auto' || !storedTheme) {
@@ -1218,7 +1254,6 @@
} }
}); });
// Add click listeners to theme switch buttons
document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => { document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value'); const theme = toggle.getAttribute('data-bs-theme-value');