移动端前端适配v1
This commit is contained in:
@@ -16,6 +16,10 @@
|
|||||||
background-color: var(--bs-body-bg);
|
background-color: var(--bs-body-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- 响应式布局修改 --- */
|
||||||
|
|
||||||
|
/* 桌面端 (lg及以上): 保持左右分栏固定高度布局 */
|
||||||
|
@media (min-width: 992px) {
|
||||||
.main-container {
|
.main-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -24,22 +28,33 @@
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-panel {
|
.settings-panel, .task-area {
|
||||||
height: calc(100vh - 2rem);
|
height: calc(100vh - 2rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 15px; /* for scrollbar */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-area {
|
.settings-panel {
|
||||||
height: calc(100vh - 2rem);
|
padding-right: 15px; /* for scrollbar */
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端 (lg以下): 布局变为自然文档流,可滚动 */
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.main-container {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.settings-panel {
|
||||||
|
margin-bottom: 2rem; /* 在移动端堆叠时增加间距 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- 结束响应式布局修改 --- */
|
||||||
|
|
||||||
|
|
||||||
.task-card {
|
.task-card {
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 修改点: 为等待ID的状态添加样式 === */
|
|
||||||
.task-id-placeholder {
|
.task-id-placeholder {
|
||||||
color: var(--bs-secondary-color);
|
color: var(--bs-secondary-color);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -88,11 +103,13 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#printFrame, #translatedPreviewFrame {
|
#printFrame {
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: none; /* 保持隐藏 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- 预览侧边栏样式 --- */
|
||||||
#previewOffcanvas {
|
#previewOffcanvas {
|
||||||
--bs-offcanvas-width: 95vw;
|
--bs-offcanvas-width: 95vw;
|
||||||
max-width: 1600px;
|
max-width: 1600px;
|
||||||
@@ -100,14 +117,14 @@
|
|||||||
|
|
||||||
.preview-split-container {
|
.preview-split-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row; /* 桌面端默认为行 */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-pane-wrapper {
|
.preview-pane-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden; /* Important for split.js */
|
overflow: hidden; /* 对Split.js很重要 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-pane-wrapper h6 {
|
.preview-pane-wrapper h6 {
|
||||||
@@ -116,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-pane-wrapper .preview-pane {
|
.preview-pane-wrapper .preview-pane {
|
||||||
flex-grow: 1; /* Make the inner pane grow */
|
flex-grow: 1;
|
||||||
border: 1px solid var(--bs-border-color);
|
border: 1px solid var(--bs-border-color);
|
||||||
border-radius: .375rem;
|
border-radius: .375rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -124,17 +141,34 @@
|
|||||||
|
|
||||||
.gutter {
|
.gutter {
|
||||||
background-color: var(--bs-tertiary-bg);
|
background-color: var(--bs-tertiary-bg);
|
||||||
border-left: 1px solid var(--bs-border-color);
|
background-repeat: no-repeat;
|
||||||
border-right: 1px solid var(--bs-border-color);
|
background-position: 50%;
|
||||||
}
|
}
|
||||||
.gutter.gutter-horizontal {
|
.gutter.gutter-horizontal {
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
|
border-left: 1px solid var(--bs-border-color);
|
||||||
|
border-right: 1px solid var(--bs-border-color);
|
||||||
|
}
|
||||||
|
.gutter.gutter-vertical {
|
||||||
|
cursor: row-resize;
|
||||||
|
border-top: 1px solid var(--bs-border-color);
|
||||||
|
border-bottom: 1px solid var(--bs-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端预览: 上下分栏 */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.preview-split-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.preview-pane-wrapper {
|
||||||
|
/* 当Split.js不活动时,让每个面板占据一半高度 */
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.preview-pane iframe, .preview-pane pre {
|
.preview-pane iframe, .preview-pane pre {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 95%;
|
height: 100%; /* 占满其容器 */
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -162,7 +196,7 @@
|
|||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="settings-panel">
|
<div class="settings-panel">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h4 class="mb-0"><i class="bi me-2"></i>Docutranslate</h4>
|
<h4 class="mb-0"><i class="bi bi-translate me-2"></i>DocuTranslate</h4>
|
||||||
<span id="versionDisplay" class="badge bg-success"></span>
|
<span id="versionDisplay" class="badge bg-success"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -174,7 +208,7 @@
|
|||||||
<h2 class="accordion-header" id="headingOne">
|
<h2 class="accordion-header" id="headingOne">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
|
data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
|
||||||
<strong><i class="bi bi-file-earmark-binary me-2"></i>解析配置</strong>
|
<i class="bi bi-file-earmark-binary me-2"></i>解析配置
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne">
|
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne">
|
||||||
@@ -204,7 +238,7 @@
|
|||||||
<h2 class="accordion-header" id="headingTwo">
|
<h2 class="accordion-header" id="headingTwo">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||||
<strong><i class="bi bi-robot me-2"></i>翻译模型</strong>
|
<i class="bi bi-robot me-2"></i>翻译模型
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo">
|
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo">
|
||||||
@@ -253,7 +287,7 @@
|
|||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
data-bs-target="#collapseThree" aria-expanded="false"
|
data-bs-target="#collapseThree" aria-expanded="false"
|
||||||
aria-controls="collapseThree">
|
aria-controls="collapseThree">
|
||||||
<strong><i class="bi bi-translate me-2"></i>翻译配置</strong>
|
<i class="bi bi-translate me-2"></i>翻译配置
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree">
|
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree">
|
||||||
@@ -303,7 +337,7 @@
|
|||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
data-bs-target="#collapseFour" aria-expanded="false"
|
data-bs-target="#collapseFour" aria-expanded="false"
|
||||||
aria-controls="collapseFour">
|
aria-controls="collapseFour">
|
||||||
<strong><i class="bi bi-sliders me-2"></i>高级参数</strong>
|
<i class="bi bi-sliders me-2"></i>高级参数
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour">
|
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour">
|
||||||
@@ -351,9 +385,9 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- 项目信息 -->
|
<!-- 项目信息 -->
|
||||||
<div class="mt-4 text-left text-muted small">
|
<div class="mt-4 text-start text-muted small">
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
项目主页(欢迎star❤): <br/>
|
项目主页 (欢迎star❤): <br/>
|
||||||
<a href="https://github.com/xunbu/docutranslate" target="_blank" rel="noopener noreferrer">https://github.com/xunbu/docutranslate</a>
|
<a href="https://github.com/xunbu/docutranslate" target="_blank" rel="noopener noreferrer">https://github.com/xunbu/docutranslate</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
@@ -388,7 +422,6 @@
|
|||||||
<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">
|
||||||
<!-- === 修改点: 初始显示占位符,而不是ID === -->
|
|
||||||
<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>
|
||||||
@@ -426,7 +459,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
<div class="download-buttons" style="display: none;">
|
<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-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>下载
|
<button class="btn btn-sm btn-info download-pdf-btn"><i class="bi bi-file-earmark-pdf-fill me-1"></i>下载
|
||||||
@@ -453,7 +486,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- MODIFIED: Preview Offcanvas with Resizable Panes -->
|
<!-- Preview Offcanvas -->
|
||||||
<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>
|
||||||
@@ -476,7 +509,7 @@
|
|||||||
</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="offcanvas-footer mt-2 pt-2 border-top d-flex justify-content-end align-items-center">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="offcanvas">关闭</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="offcanvas">关闭</button>
|
||||||
<button type="button" class="btn btn-primary ms-2" id="printFromPreview">
|
<button type="button" class="btn btn-primary ms-2" id="printFromPreview">
|
||||||
<i class="bi bi-printer-fill me-2"></i>打印/保存为PDF
|
<i class="bi bi-printer-fill me-2"></i>打印/保存为PDF
|
||||||
@@ -486,7 +519,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden iframe for direct PDF printing -->
|
<!-- Hidden iframe for direct PDF printing -->
|
||||||
<iframe id="printFrame" style="display: none;"></iframe>
|
<iframe id="printFrame"></iframe>
|
||||||
|
|
||||||
<!-- Theme Switcher -->
|
<!-- Theme Switcher -->
|
||||||
<div class="dropdown theme-switch">
|
<div class="dropdown theme-switch">
|
||||||
@@ -515,14 +548,14 @@
|
|||||||
|
|
||||||
<!-- 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 -->
|
<!-- Split.js for resizable panes -->
|
||||||
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
|
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
// === 以下是修改后的 JavaScript 代码 ===
|
// --- Initialize Bootstrap Tooltips ---
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
// --- DOM Elements ---
|
// --- DOM Elements ---
|
||||||
const settingsForm = document.getElementById('translateForm');
|
const settingsForm = document.getElementById('translateForm');
|
||||||
const platformSelect = document.getElementById('platform_select');
|
const platformSelect = document.getElementById('platform_select');
|
||||||
@@ -570,7 +603,6 @@
|
|||||||
|
|
||||||
// --- Global State ---
|
// --- Global State ---
|
||||||
let defaultParams = {};
|
let defaultParams = {};
|
||||||
// === 修改点: tasks对象的键现在是前端生成的cardId, 用于管理UI。后端返回的taskId将存在state中。
|
|
||||||
const tasks = {}; // { cardId: { elements: {...}, state: {...}, intervals: {...} } }
|
const tasks = {}; // { cardId: { elements: {...}, state: {...}, intervals: {...} } }
|
||||||
let isAdminMode = false;
|
let isAdminMode = false;
|
||||||
let previewSplitInstance = null;
|
let previewSplitInstance = null;
|
||||||
@@ -587,7 +619,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- Utility Functions ---
|
// --- Utility Functions ---
|
||||||
// === 修改点: 此函数现在生成的是临时的UI卡片ID (cardId)
|
|
||||||
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) => { 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 getFromStorage = (key, defaultValue = '') => { try { return localStorage.getItem(key) || defaultValue; } catch (e) { console.warn("Read from storage failed:", e); return defaultValue; } };
|
||||||
@@ -618,12 +649,8 @@
|
|||||||
baseUrlGroup.classList.add('d-none');
|
baseUrlGroup.classList.add('d-none');
|
||||||
baseUrlInput.required = false;
|
baseUrlInput.required = false;
|
||||||
baseUrlInput.value = selectedPlatformValue;
|
baseUrlInput.value = selectedPlatformValue;
|
||||||
if (apiHrefMap[baseUrlInput.value]) {
|
apiHref.href = apiHrefMap[baseUrlInput.value] || '#';
|
||||||
apiHref.href = apiHrefMap[baseUrlInput.value];
|
apiHref.classList.toggle('d-none', !apiHrefMap[baseUrlInput.value]);
|
||||||
apiHref.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
apiHref.classList.add('d-none');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
saveToStorage('translator_last_platform', selectedPlatformValue);
|
saveToStorage('translator_last_platform', selectedPlatformValue);
|
||||||
}
|
}
|
||||||
@@ -638,48 +665,49 @@
|
|||||||
saveToStorage('translator_convert_engin', selectedEngin);
|
saveToStorage('translator_convert_engin', selectedEngin);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSliderUpdater(slider, display, resetBtn, key, params) {
|
/**
|
||||||
return () => {
|
* REFINED: Helper function to set up a slider with its display and reset button.
|
||||||
|
* @param {HTMLInputElement} slider
|
||||||
|
* @param {HTMLElement} display
|
||||||
|
* @param {HTMLButtonElement} resetBtn
|
||||||
|
* @param {string} key The key for localStorage and defaultParams.
|
||||||
|
* @param {object} params The default parameters object.
|
||||||
|
*/
|
||||||
|
function setupSlider(slider, display, resetBtn, key, params) {
|
||||||
|
slider.value = getFromStorage(key, params[key]);
|
||||||
|
const updater = () => {
|
||||||
const value = slider.value;
|
const value = slider.value;
|
||||||
display.textContent = value;
|
display.textContent = value;
|
||||||
resetBtn.style.visibility = value !== String(params[key]) ? 'visible' : 'hidden';
|
resetBtn.style.visibility = value !== String(params[key]) ? 'visible' : 'hidden';
|
||||||
saveToStorage(key, value);
|
saveToStorage(key, value);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function setupSlider(slider, display, resetBtn, key, params) {
|
|
||||||
slider.value = getFromStorage(key, params[key]);
|
|
||||||
const updater = createSliderUpdater(slider, display, resetBtn, key, params);
|
|
||||||
slider.addEventListener('input', updater);
|
slider.addEventListener('input', updater);
|
||||||
resetBtn.addEventListener('click', () => {
|
resetBtn.addEventListener('click', () => {
|
||||||
slider.value = params[key];
|
slider.value = params[key];
|
||||||
updater();
|
updater();
|
||||||
});
|
});
|
||||||
updater(); // Initial call
|
updater(); // Initial call to set value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateTaskPlaceholderVisibility() {
|
function updateTaskPlaceholderVisibility() {
|
||||||
noTaskPlaceholder.style.display = Object.keys(tasks).length === 0 ? 'block' : 'none';
|
noTaskPlaceholder.style.display = Object.keys(tasks).length === 0 ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTaskIds() {
|
function saveTaskIds() {
|
||||||
if (isAdminMode) return;
|
if (isAdminMode) return;
|
||||||
// === 修改点: 保存的是已经从后端获取到ID的任务
|
|
||||||
const submittedTaskIds = Object.values(tasks)
|
const submittedTaskIds = Object.values(tasks)
|
||||||
.map(task => task.state.backendTaskId)
|
.map(task => task.state.backendTaskId)
|
||||||
.filter(id => id); // 过滤掉null或undefined
|
.filter(id => id);
|
||||||
saveToStorage('active_task_ids', JSON.stringify(submittedTaskIds));
|
saveToStorage('active_task_ids', JSON.stringify(submittedTaskIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Task Card Management ---
|
// --- Task Card Management ---
|
||||||
// === 修改点: taskId现在是后端ID, cardId是前端ID。恢复时taskId就是cardId。
|
|
||||||
function createTaskCard(backendTaskId = null, restoreState = false) {
|
function createTaskCard(backendTaskId = null, restoreState = false) {
|
||||||
// cardId是用于在前端tasks对象中唯一标识一个卡片的ID
|
|
||||||
const cardId = backendTaskId || generateCardId();
|
const cardId = backendTaskId || generateCardId();
|
||||||
|
|
||||||
const cardFragment = taskCardTemplate.content.cloneNode(true);
|
const cardFragment = taskCardTemplate.content.cloneNode(true);
|
||||||
const cardElement = cardFragment.querySelector('.task-card');
|
const cardElement = cardFragment.querySelector('.task-card');
|
||||||
cardElement.dataset.cardId = cardId; // 使用cardId作为标识
|
cardElement.dataset.cardId = cardId;
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
card: cardElement,
|
card: cardElement,
|
||||||
@@ -703,34 +731,21 @@
|
|||||||
startBtn: cardElement.querySelector('.start-translate-btn'),
|
startBtn: cardElement.querySelector('.start-translate-btn'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// === 修改点: 如果是恢复任务,直接显示ID,否则显示占位符
|
|
||||||
if (restoreState && backendTaskId) {
|
if (restoreState && backendTaskId) {
|
||||||
elements.taskIdDisplay.textContent = backendTaskId;
|
elements.taskIdDisplay.textContent = backendTaskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks[cardId] = {
|
tasks[cardId] = {
|
||||||
elements,
|
elements,
|
||||||
state: {
|
state: { backendTaskId, isTranslating: false, file: null, htmlUrl: null, fileNameStem: null, isSubmitted: restoreState },
|
||||||
backendTaskId: backendTaskId, // 存储从后端获取的真实ID
|
intervals: { log: null, status: null }
|
||||||
isTranslating: false,
|
|
||||||
file: null,
|
|
||||||
htmlUrl: null,
|
|
||||||
fileNameStem: null,
|
|
||||||
isSubmitted: restoreState
|
|
||||||
},
|
|
||||||
intervals: {
|
|
||||||
log: null,
|
|
||||||
status: null
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
addEventListenersToCard(cardId);
|
addEventListenersToCard(cardId);
|
||||||
|
|
||||||
taskContainer.prepend(cardElement);
|
taskContainer.prepend(cardElement);
|
||||||
updateTaskPlaceholderVisibility();
|
updateTaskPlaceholderVisibility();
|
||||||
|
|
||||||
if (restoreState && backendTaskId) {
|
if (restoreState && backendTaskId) {
|
||||||
// 如果是恢复任务,立即用backendTaskId检查状态
|
|
||||||
pollStatus(backendTaskId, true);
|
pollStatus(backendTaskId, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -755,17 +770,11 @@
|
|||||||
const { elements } = tasks[cardId];
|
const { elements } = tasks[cardId];
|
||||||
|
|
||||||
elements.removeBtn.addEventListener('click', () => removeTask(cardId));
|
elements.removeBtn.addEventListener('click', () => removeTask(cardId));
|
||||||
|
|
||||||
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);
|
||||||
});
|
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.toggle('drag-over', ['dragenter', 'dragover'].includes(eventName)), false);
|
||||||
['dragenter', 'dragover'].forEach(eventName => {
|
|
||||||
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.add('drag-over'), false);
|
|
||||||
});
|
|
||||||
['dragleave', 'drop'].forEach(eventName => {
|
|
||||||
elements.fileDropArea.addEventListener(eventName, () => elements.fileDropArea.classList.remove('drag-over'), false);
|
|
||||||
});
|
});
|
||||||
elements.fileDropArea.addEventListener('drop', e => {
|
elements.fileDropArea.addEventListener('drop', e => {
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
@@ -810,7 +819,8 @@
|
|||||||
}
|
}
|
||||||
const requiredInputs = [apikeyInput, modelInput];
|
const requiredInputs = [apikeyInput, modelInput];
|
||||||
if (platformSelect.value === 'custom') requiredInputs.push(baseUrlInput);
|
if (platformSelect.value === 'custom') requiredInputs.push(baseUrlInput);
|
||||||
if (convertEnginSelect.value === 'mineru' && !mineruTokenInput.value.trim() && (!["md", "txt"].includes(state.file.name.split('.').pop()))) {
|
const fileExt = state.file.name.split('.').pop().toLowerCase();
|
||||||
|
if (convertEnginSelect.value === 'mineru' && !mineruTokenInput.value.trim() && !["md", "txt"].includes(fileExt)) {
|
||||||
requiredInputs.push(mineruTokenInput);
|
requiredInputs.push(mineruTokenInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,8 +849,6 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContentBase64 = await fileToBase64(state.file);
|
const fileContentBase64 = await fileToBase64(state.file);
|
||||||
|
|
||||||
// === 修改点: payload不再包含task_id ===
|
|
||||||
const payload = {
|
const payload = {
|
||||||
base_url: baseUrlInput.value,
|
base_url: baseUrlInput.value,
|
||||||
apikey: apikeyInput.value,
|
apikey: apikeyInput.value,
|
||||||
@@ -867,38 +875,31 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && result.task_started) {
|
if (response.ok && result.task_started) {
|
||||||
// === 修改点: 从后端响应中获取 task_id ===
|
|
||||||
const backendTaskId = result.task_id;
|
const backendTaskId = result.task_id;
|
||||||
state.backendTaskId = backendTaskId;
|
state.backendTaskId = backendTaskId;
|
||||||
state.isSubmitted = true;
|
state.isSubmitted = true;
|
||||||
|
|
||||||
// === 修改点: 更新UI显示 task_id ===
|
|
||||||
elements.taskIdDisplay.textContent = backendTaskId;
|
elements.taskIdDisplay.textContent = backendTaskId;
|
||||||
elements.taskIdDisplay.classList.remove('task-id-placeholder');
|
elements.taskIdDisplay.classList.remove('task-id-placeholder');
|
||||||
|
saveTaskIds();
|
||||||
saveTaskIds(); // 保存已提交的任务ID
|
|
||||||
|
|
||||||
elements.statusMessage.textContent = result.message || '任务已开始,正在处理...';
|
elements.statusMessage.textContent = result.message || '任务已开始,正在处理...';
|
||||||
elements.statusMessage.className = 'status-message small text-info';
|
elements.statusMessage.className = 'status-message small text-info';
|
||||||
elements.startBtn.innerHTML = `<i class="bi bi-stop-circle-fill me-1"></i>取消翻译`;
|
elements.startBtn.innerHTML = `<i class="bi bi-stop-circle-fill me-1"></i>取消翻译`;
|
||||||
elements.startBtn.classList.replace('btn-primary', 'btn-danger');
|
elements.startBtn.classList.replace('btn-primary', 'btn-danger');
|
||||||
elements.startBtn.disabled = false;
|
elements.startBtn.disabled = false;
|
||||||
|
|
||||||
// === 修改点: 使用从后端获取的ID开始轮询 ===
|
|
||||||
startPolling(backendTaskId);
|
startPolling(backendTaskId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || `请求失败 (${response.status})`);
|
throw new Error(result.message || `请求失败 (${response.status})`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.isSubmitted = false;
|
|
||||||
console.error('请求失败:', error);
|
console.error('请求失败:', error);
|
||||||
|
state.isSubmitted = false;
|
||||||
|
state.isTranslating = false;
|
||||||
elements.statusMessage.textContent = `启动失败: ${error.message}`;
|
elements.statusMessage.textContent = `启动失败: ${error.message}`;
|
||||||
elements.statusMessage.className = 'status-message small text-danger';
|
elements.statusMessage.className = 'status-message small text-danger';
|
||||||
elements.startBtn.innerHTML = `<i class="bi bi-play-fill me-1"></i>开始翻译`;
|
elements.startBtn.innerHTML = `<i class="bi bi-play-fill me-1"></i>开始翻译`;
|
||||||
elements.startBtn.classList.replace('btn-danger', 'btn-primary');
|
elements.startBtn.classList.replace('btn-danger', 'btn-primary');
|
||||||
elements.startBtn.disabled = false;
|
elements.startBtn.disabled = false;
|
||||||
elements.progress.style.display = 'none';
|
elements.progress.style.display = 'none';
|
||||||
state.isTranslating = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,14 +909,12 @@
|
|||||||
|
|
||||||
const { elements } = task;
|
const { elements } = task;
|
||||||
const backendTaskId = task.state.backendTaskId;
|
const backendTaskId = task.state.backendTaskId;
|
||||||
|
|
||||||
elements.startBtn.disabled = true;
|
elements.startBtn.disabled = true;
|
||||||
elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 正在取消...`;
|
elements.startBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 正在取消...`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/service/cancel/${backendTaskId}`, { method: 'POST' });
|
const response = await fetch(`/service/cancel/${backendTaskId}`, { method: 'POST' });
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && result.cancelled) {
|
if (response.ok && result.cancelled) {
|
||||||
elements.statusMessage.textContent = result.message || '取消请求已发送。';
|
elements.statusMessage.textContent = result.message || '取消请求已发送。';
|
||||||
elements.statusMessage.className = 'status-message small text-warning';
|
elements.statusMessage.className = 'status-message small text-warning';
|
||||||
@@ -931,13 +930,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Polling ---
|
// --- Polling ---
|
||||||
// === 修改点: taskId参数现在总是后端的ID
|
|
||||||
function startPolling(backendTaskId) {
|
function startPolling(backendTaskId) {
|
||||||
stopPolling(backendTaskId);
|
|
||||||
// 找到对应的卡片
|
|
||||||
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
stopPolling(backendTaskId);
|
||||||
card.intervals.log = setInterval(() => pollLogs(backendTaskId), 2000);
|
card.intervals.log = setInterval(() => pollLogs(backendTaskId), 2000);
|
||||||
card.intervals.status = setInterval(() => pollStatus(backendTaskId), 1500);
|
card.intervals.status = setInterval(() => pollStatus(backendTaskId), 1500);
|
||||||
pollLogs(backendTaskId);
|
pollLogs(backendTaskId);
|
||||||
@@ -947,7 +943,6 @@
|
|||||||
function stopPolling(backendTaskId) {
|
function stopPolling(backendTaskId) {
|
||||||
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
const { intervals } = card;
|
const { intervals } = card;
|
||||||
if (intervals.log) clearInterval(intervals.log);
|
if (intervals.log) clearInterval(intervals.log);
|
||||||
if (intervals.status) clearInterval(intervals.status);
|
if (intervals.status) clearInterval(intervals.status);
|
||||||
@@ -958,15 +953,13 @@
|
|||||||
async function pollLogs(backendTaskId) {
|
async function pollLogs(backendTaskId) {
|
||||||
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const { elements } = card;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/service/logs/${backendTaskId}`);
|
const response = await fetch(`/service/logs/${backendTaskId}`);
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.logs && data.logs.length > 0) {
|
if (data.logs && data.logs.length > 0) {
|
||||||
elements.logArea.innerHTML += data.logs.map(log => log.replace(/</g, "<").replace(/>/g, ">") + '<br>').join('');
|
card.elements.logArea.innerHTML += data.logs.map(log => log.replace(/</g, "<").replace(/>/g, ">") + '<br>').join('');
|
||||||
elements.logArea.scrollTop = elements.logArea.scrollHeight;
|
card.elements.logArea.scrollTop = card.elements.logArea.scrollHeight;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[${backendTaskId}] Error polling logs:`, error);
|
console.warn(`[${backendTaskId}] Error polling logs:`, error);
|
||||||
@@ -976,12 +969,9 @@
|
|||||||
async function pollStatus(backendTaskId, isRestore = false) {
|
async function pollStatus(backendTaskId, isRestore = false) {
|
||||||
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
const card = Object.values(tasks).find(t => t.state.backendTaskId === backendTaskId);
|
||||||
if (!card) {
|
if (!card) {
|
||||||
// 如果是恢复任务时找不到卡片,可能是在其他地方被删除了
|
|
||||||
if (isRestore) {
|
if (isRestore) {
|
||||||
console.warn(`Restored task ${backendTaskId} not found in UI, removing from storage.`);
|
|
||||||
const savedTaskIds = JSON.parse(getFromStorage('active_task_ids', '[]'));
|
const savedTaskIds = JSON.parse(getFromStorage('active_task_ids', '[]'));
|
||||||
const newIds = savedTaskIds.filter(id => id !== backendTaskId);
|
saveToStorage('active_task_ids', JSON.stringify(savedTaskIds.filter(id => id !== backendTaskId)));
|
||||||
saveToStorage('active_task_ids', JSON.stringify(newIds));
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -991,9 +981,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/service/status/${backendTaskId}`);
|
const response = await fetch(`/service/status/${backendTaskId}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404 && isRestore) {
|
if (response.status === 404 && isRestore) await removeTask(cardId);
|
||||||
await removeTask(cardId);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const status = await response.json();
|
const status = await response.json();
|
||||||
@@ -1018,14 +1006,11 @@
|
|||||||
elements.statusMessage.className = 'status-message small text-success';
|
elements.statusMessage.className = 'status-message small text-success';
|
||||||
state.htmlUrl = status.downloads.html;
|
state.htmlUrl = status.downloads.html;
|
||||||
state.fileNameStem = status.original_filename_stem;
|
state.fileNameStem = status.original_filename_stem;
|
||||||
|
|
||||||
elements.htmlLink.href = status.downloads.html;
|
elements.htmlLink.href = status.downloads.html;
|
||||||
elements.mdLink.href = status.downloads.markdown;
|
elements.mdLink.href = status.downloads.markdown;
|
||||||
elements.mdZipLink.href = status.downloads.markdown_zip;
|
elements.mdZipLink.href = status.downloads.markdown_zip;
|
||||||
|
|
||||||
elements.previewBtn.onclick = () => setupPreview(cardId);
|
elements.previewBtn.onclick = () => setupPreview(cardId);
|
||||||
elements.pdfBtn.onclick = () => downloadPdf(cardId);
|
elements.pdfBtn.onclick = () => downloadPdf(cardId);
|
||||||
|
|
||||||
elements.downloadButtons.style.display = 'flex';
|
elements.downloadButtons.style.display = 'flex';
|
||||||
} else {
|
} else {
|
||||||
elements.downloadButtons.style.display = 'none';
|
elements.downloadButtons.style.display = 'none';
|
||||||
@@ -1037,7 +1022,6 @@
|
|||||||
elements.startBtn.disabled = false;
|
elements.startBtn.disabled = false;
|
||||||
elements.progress.style.display = 'block';
|
elements.progress.style.display = 'block';
|
||||||
elements.downloadButtons.style.display = 'none';
|
elements.downloadButtons.style.display = 'none';
|
||||||
|
|
||||||
if (isRestore && !card.intervals.status) {
|
if (isRestore && !card.intervals.status) {
|
||||||
startPolling(backendTaskId);
|
startPolling(backendTaskId);
|
||||||
}
|
}
|
||||||
@@ -1050,41 +1034,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Download and Preview (No changes needed here, they use cardId to get state) ---
|
// --- Download and Preview ---
|
||||||
function setupPreview(cardId) {
|
function setupPreview(cardId) {
|
||||||
const { state } = tasks[cardId];
|
const { state } = tasks[cardId];
|
||||||
if (!state.htmlUrl) return;
|
if (!state.htmlUrl) return;
|
||||||
|
|
||||||
const existingOriginalContent = originalPreviewPane.querySelector('iframe, pre, p');
|
// Clear previous content
|
||||||
if (existingOriginalContent) existingOriginalContent.remove();
|
originalPreviewPane.innerHTML = '';
|
||||||
translatedPreviewFrame.src = 'about:blank';
|
translatedPreviewFrame.src = 'about:blank';
|
||||||
|
|
||||||
|
// Show original file content
|
||||||
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 fileExt = state.file.name.split('.').pop().toLowerCase();
|
||||||
const textLikeExtensions = ['md', 'json', 'xml', 'log', 'py', 'js', 'css', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'rb', 'php', 'swift', 'kt', 'go', 'rs', 'ts', 'txt'];
|
const textLikeExts = ['md', 'json', 'xml', 'log', 'py', 'js', 'css', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'rb', 'php', 'swift', 'kt', 'go', 'rs', 'ts', 'txt'];
|
||||||
|
|
||||||
if (fileType.startsWith('text/') || textLikeExtensions.includes(fileExtension)) {
|
if (fileType.startsWith('text/') || textLikeExts.includes(fileExt)) {
|
||||||
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(fileExt)) {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.src = URL.createObjectURL(state.file);
|
iframe.src = URL.createObjectURL(state.file);
|
||||||
originalPreviewPane.appendChild(iframe);
|
originalPreviewPane.appendChild(iframe);
|
||||||
} else {
|
} else {
|
||||||
const p = document.createElement('p');
|
originalPreviewPane.innerHTML = `<p class="p-3 text-muted">无法直接预览此文件类型 (${fileType || '未知: ' + fileExt})。</p>`;
|
||||||
p.className = 'p-3 text-muted';
|
|
||||||
p.textContent = `无法直接预览此文件类型 (${fileType || '未知: ' + fileExtension})。`;
|
|
||||||
originalPreviewPane.appendChild(p);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const p = document.createElement('p');
|
originalPreviewPane.innerHTML = '<p class="p-3 text-muted">未找到原文文件缓存。</p>';
|
||||||
p.className = 'p-3 text-muted';
|
|
||||||
p.textContent = '未找到原文文件缓存。';
|
|
||||||
originalPreviewPane.appendChild(p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch and show translated content
|
||||||
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 => {
|
||||||
@@ -1128,6 +1108,7 @@
|
|||||||
elements.pdfBtn.disabled = false;
|
elements.pdfBtn.disabled = false;
|
||||||
elements.pdfBtn.innerHTML = `<i class="bi bi-file-earmark-pdf-fill me-1"></i>下载 PDF`;
|
elements.pdfBtn.innerHTML = `<i class="bi bi-file-earmark-pdf-fill me-1"></i>下载 PDF`;
|
||||||
printFrameEl.onload = null;
|
printFrameEl.onload = null;
|
||||||
|
printFrameEl.srcdoc = 'about:blank'; // Clean up
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
@@ -1141,31 +1122,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPreviewDisplayMode(mode) {
|
function setPreviewDisplayMode(mode) {
|
||||||
if (previewSplitInstance) {
|
|
||||||
previewSplitInstance.destroy();
|
|
||||||
previewSplitInstance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
originalPreviewContainer.style.display = 'flex';
|
originalPreviewContainer.style.display = 'flex';
|
||||||
originalPreviewContainer.style.width = '';
|
originalPreviewContainer.style.width = '';
|
||||||
translatedPreviewContainer.style.width = '';
|
translatedPreviewContainer.style.width = '';
|
||||||
|
// Reset inline styles from Split.js if any
|
||||||
|
originalPreviewContainer.style.height = '';
|
||||||
|
translatedPreviewContainer.style.height = '';
|
||||||
|
|
||||||
if (mode === 'bilingual') {
|
if (mode === 'bilingual') {
|
||||||
previewOffcanvasLabel.textContent = '双语预览';
|
previewOffcanvasLabel.textContent = '双语预览';
|
||||||
previewSplitInstance = Split(['#originalPreviewContainer', '#translatedPreviewContainer'], {
|
setBilingualViewBtn.classList.add('btn-primary', 'active');
|
||||||
sizes: [50, 50], minSize: 200, gutterSize: 10, cursor: 'col-resize',
|
|
||||||
});
|
|
||||||
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', 'active');
|
||||||
setTranslatedOnlyViewBtn.classList.add('btn-outline-primary');
|
setTranslatedOnlyViewBtn.classList.add('btn-outline-primary');
|
||||||
} else {
|
} else { // 'translationOnly'
|
||||||
previewOffcanvasLabel.textContent = '译文预览';
|
previewOffcanvasLabel.textContent = '译文预览';
|
||||||
originalPreviewContainer.style.display = 'none';
|
originalPreviewContainer.style.display = 'none';
|
||||||
translatedPreviewContainer.style.width = '100%';
|
translatedPreviewContainer.style.width = '100%';
|
||||||
setTranslatedOnlyViewBtn.classList.add('btn-primary');
|
setTranslatedOnlyViewBtn.classList.add('btn-primary', 'active');
|
||||||
setTranslatedOnlyViewBtn.classList.remove('btn-outline-primary');
|
setTranslatedOnlyViewBtn.classList.remove('btn-outline-primary');
|
||||||
setBilingualViewBtn.classList.remove('btn-primary');
|
setBilingualViewBtn.classList.remove('btn-primary', 'active');
|
||||||
setBilingualViewBtn.classList.add('btn-outline-primary');
|
setBilingualViewBtn.classList.add('btn-outline-primary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1197,11 +1173,12 @@
|
|||||||
convertEnginSelect.value = getFromStorage('translator_convert_engin', 'mineru');
|
convertEnginSelect.value = getFromStorage('translator_convert_engin', 'mineru');
|
||||||
updateConvertEnginUI();
|
updateConvertEnginUI();
|
||||||
toLangSelect.value = getFromStorage('translator_to_lang', '中文');
|
toLangSelect.value = getFromStorage('translator_to_lang', '中文');
|
||||||
formulaCheckbox.checked = getFromStorage('translator_formula_ocr') === 'true';
|
formulaCheckbox.checked = getFromStorage('translator_formula_ocr', 'true') === 'true';
|
||||||
codeCheckbox.checked = getFromStorage('translator_code_ocr') === 'true';
|
codeCheckbox.checked = getFromStorage('translator_code_ocr', 'true') === 'true';
|
||||||
refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true';
|
refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true';
|
||||||
customPromptTranslateArea.value = getFromStorage("custom_prompt_translate");
|
customPromptTranslateArea.value = getFromStorage("custom_prompt_translate");
|
||||||
|
|
||||||
|
// REFINED: Use helper function for sliders
|
||||||
setupSlider(chunkSizeSlider, chunkSizeDisplay, chunkSizeReset, 'chunk_size', defaultParams);
|
setupSlider(chunkSizeSlider, chunkSizeDisplay, chunkSizeReset, 'chunk_size', defaultParams);
|
||||||
setupSlider(concurrentSlider, concurrentDisplay, concurrentReset, 'concurrent', defaultParams);
|
setupSlider(concurrentSlider, concurrentDisplay, concurrentReset, 'concurrent', defaultParams);
|
||||||
setupSlider(temperatureSlider, temperatureDisplay, temperatureReset, 'temperature', defaultParams);
|
setupSlider(temperatureSlider, temperatureDisplay, temperatureReset, 'temperature', defaultParams);
|
||||||
@@ -1209,15 +1186,13 @@
|
|||||||
if (isAdminMode) {
|
if (isAdminMode) {
|
||||||
document.title = "DocuTranslate - Admin Panel";
|
document.title = "DocuTranslate - Admin Panel";
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/service/task-list');
|
const allTaskIds = await (await fetch('/service/task-list')).json();
|
||||||
if (!response.ok) throw new Error(`Failed to fetch task list: ${response.statusText}`);
|
if (Array.isArray(allTaskIds) && allTaskIds.length > 0) {
|
||||||
const allTaskIds = await response.json();
|
|
||||||
if (allTaskIds && Array.isArray(allTaskIds) && allTaskIds.length > 0) {
|
|
||||||
allTaskIds.reverse().forEach(taskId => createTaskCard(taskId, true));
|
allTaskIds.reverse().forEach(taskId => createTaskCard(taskId, true));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Admin mode: Failed to load task list from server.", error);
|
console.error("Admin mode: Failed to load task list.", error);
|
||||||
alert("无法从服务器加载任务列表,请检查后台连接。");
|
alert("无法从服务器加载任务列表。");
|
||||||
}
|
}
|
||||||
updateTaskPlaceholderVisibility();
|
updateTaskPlaceholderVisibility();
|
||||||
} else {
|
} else {
|
||||||
@@ -1229,6 +1204,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event Listeners for settings
|
||||||
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));
|
||||||
@@ -1241,20 +1217,52 @@
|
|||||||
refineCheckbox.addEventListener('change', e => saveToStorage('translator_refine_markdown', e.target.checked));
|
refineCheckbox.addEventListener('change', e => saveToStorage('translator_refine_markdown', e.target.checked));
|
||||||
customPromptTranslateArea.addEventListener('input', () => saveToStorage("custom_prompt_translate", customPromptTranslateArea.value));
|
customPromptTranslateArea.addEventListener('input', () => saveToStorage("custom_prompt_translate", customPromptTranslateArea.value));
|
||||||
|
|
||||||
|
// Event Listeners for actions
|
||||||
addNewTaskBtn.addEventListener('click', () => createTaskCard());
|
addNewTaskBtn.addEventListener('click', () => createTaskCard());
|
||||||
setBilingualViewBtn.addEventListener('click', () => setPreviewDisplayMode('bilingual'));
|
setBilingualViewBtn.addEventListener('click', () => setPreviewDisplayMode('bilingual'));
|
||||||
setTranslatedOnlyViewBtn.addEventListener('click', () => setPreviewDisplayMode('translationOnly'));
|
setTranslatedOnlyViewBtn.addEventListener('click', () => setPreviewDisplayMode('translationOnly'));
|
||||||
|
|
||||||
|
// NEW: Responsive Split.js initialization
|
||||||
|
previewOffcanvasEl.addEventListener('shown.bs.offcanvas', () => {
|
||||||
|
if (previewSplitInstance) previewSplitInstance.destroy();
|
||||||
|
if (window.innerWidth >= 768 && setBilingualViewBtn.classList.contains('active')) {
|
||||||
|
previewSplitInstance = Split(['#originalPreviewContainer', '#translatedPreviewContainer'], {
|
||||||
|
sizes: [50, 50], minSize: 100, gutterSize: 10, direction: 'horizontal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previewOffcanvasEl.addEventListener('hide.bs.offcanvas', () => {
|
||||||
|
if (previewSplitInstance) {
|
||||||
|
previewSplitInstance.destroy();
|
||||||
|
previewSplitInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Theme switcher logic ---
|
// --- Theme switcher logic ---
|
||||||
const getPreferredTheme = () => { const storedTheme = localStorage.getItem('theme'); if (storedTheme) { return storedTheme; } return 'auto'; };
|
const getPreferredTheme = () => localStorage.getItem('theme') || '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 setTheme = 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 themeToSet = theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
|
||||||
const preferredTheme = getPreferredTheme();
|
document.documentElement.setAttribute('data-bs-theme', themeToSet);
|
||||||
setTheme(preferredTheme);
|
};
|
||||||
showActiveTheme(preferredTheme);
|
const 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(el => el.classList.remove('active'));
|
||||||
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); }); });
|
document.querySelector(`[data-bs-theme-value="${theme}"]`)?.classList.add('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme());
|
||||||
|
showActiveTheme(getPreferredTheme());
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
if (getPreferredTheme() === 'auto') 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