V2.0.0: 新增用户权限动态配置、会员套餐配置、数据包购买功能

新功能:
- 用户权限动态配置(翻译次数、页数限制)
- 会员套餐动态配置(名称、价格、周期)
- 数据包购买套餐管理
- 收入统计功能
- 数据包销售排行

技术更新:
- 新增 DynamicConfig 模型支持动态配置
- 新增 DataPackage 和 UserPackage 模型
- 后台管理增加数据包管理模块
This commit is contained in:
2026-04-07 23:26:53 +08:00
commit 2ef5e6da87
37 changed files with 6507 additions and 0 deletions

286
static/js/main.js Normal file
View File

@@ -0,0 +1,286 @@
/**
* PDF翻译助手前端脚本
*/
// 当前翻译ID
let currentTranslationId = null;
let currentTaskId = null;
// 上传表单处理
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const fileInput = document.getElementById('pdfFile');
const file = fileInput.files[0];
if (!file) {
alert('请选择PDF文件');
return;
}
if (!file.name.toLowerCase().endsWith('.pdf')) {
alert('只支持PDF文件');
return;
}
// 显示进度区域
document.getElementById('progressSection').style.display = 'block';
document.getElementById('resultSection').style.display = 'none';
document.getElementById('cacheNotice').style.display = 'none';
// 显示加载状态
const submitBtn = document.getElementById('submitBtn');
const btnText = document.getElementById('btnText');
const btnSpinner = document.getElementById('btnSpinner');
submitBtn.disabled = true;
btnText.textContent = '上传中...';
btnSpinner.style.display = 'inline-block';
// 构建表单数据
const formData = new FormData();
formData.append('file', file);
const instruction = document.getElementById('instruction')?.value;
if (instruction) {
formData.append('instruction', instruction);
}
try {
// 上传文件
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '上传失败');
}
currentTranslationId = result.translation_id;
currentTaskId = result.task_id;
// 如果使用缓存,直接显示结果
if (result.from_cache) {
document.getElementById('cacheNotice').style.display = 'block';
showResult(currentTranslationId);
} else {
// 开始轮询进度
pollProgress(currentTaskId, currentTranslationId);
}
} catch (error) {
alert('上传失败: ' + error.message);
resetUploadButton();
}
});
// 轮询翻译进度
async function pollProgress(taskId, translationId) {
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const poll = async () => {
try {
// 同时检查任务状态和翻译状态
const taskResponse = await fetch(`/api/task/${taskId}`);
const taskResult = await taskResponse.json();
const transResponse = await fetch(`/api/status/${translationId}`);
const transResult = await transResponse.json();
// 更新进度
if (taskResult.progress) {
progressBar.style.width = taskResult.progress + '%';
progressBar.textContent = taskResult.progress + '%';
}
if (taskResult.message) {
progressMessage.textContent = taskResult.message;
}
// 检查是否完成
if (taskResult.status === 'completed' || transResult.status === 'completed') {
progressBar.style.width = '100%';
progressBar.textContent = '100%';
progressMessage.textContent = '翻译完成!';
// 显示结果
setTimeout(() => showResult(translationId), 500);
return;
}
if (taskResult.status === 'failed') {
progressMessage.textContent = '翻译失败: ' + (taskResult.error || '未知错误');
resetUploadButton();
return;
}
// 继续轮询
setTimeout(poll, 2000);
} catch (error) {
console.error('轮询失败:', error);
setTimeout(poll, 3000);
}
};
poll();
}
// 显示翻译结果
async function showResult(translationId) {
const resultSection = document.getElementById('resultSection');
const resultContent = document.getElementById('resultContent');
try {
const response = await fetch(`/api/result/${translationId}`);
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '获取结果失败');
}
// 渲染Markdown内容
resultContent.innerHTML = renderMarkdown(result.content);
resultSection.style.display = 'block';
resetUploadButton();
} catch (error) {
alert('获取结果失败: ' + error.message);
resetUploadButton();
}
}
// 重置上传按钮
function resetUploadButton() {
const submitBtn = document.getElementById('submitBtn');
const btnText = document.getElementById('btnText');
const btnSpinner = document.getElementById('btnSpinner');
submitBtn.disabled = false;
btnText.textContent = '开始翻译';
btnSpinner.style.display = 'none';
}
// 下载结果
document.getElementById('downloadBtn')?.addEventListener('click', function() {
if (currentTranslationId) {
window.location.href = `/api/download/${currentTranslationId}`;
}
});
// 对比查看
document.getElementById('viewCompare')?.addEventListener('click', async function() {
if (!currentTranslationId) return;
try {
const response = await fetch(`/api/compare/${currentTranslationId}`);
const result = await response.json();
// 显示对比视图
showCompareView(result);
} catch (error) {
alert('获取对比失败: ' + error.message);
}
});
// 显示对比视图
function showCompareView(data) {
const resultContent = document.getElementById('resultContent');
resultContent.innerHTML = `
<div class="compare-container">
<div class="compare-panel original">
<h5>原文</h5>
<div class="content">${escapeHtml(data.original)}</div>
</div>
<div class="compare-panel translated">
<h5>译文</h5>
<div class="content">${renderMarkdown(data.translated)}</div>
</div>
</div>
`;
}
// 重新翻译
document.getElementById('retranslateBtn')?.addEventListener('click', async function() {
if (!currentTranslationId) return;
const instruction = document.getElementById('retranslateInstruction').value;
if (!instruction.trim()) {
alert('请输入翻译要求');
return;
}
try {
const response = await fetch(`/api/retranslate/${currentTranslationId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({instruction: instruction})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '重译请求失败');
}
// 开始新的翻译任务
currentTranslationId = result.translation_id;
document.getElementById('progressSection').style.display = 'block';
document.getElementById('resultSection').style.display = 'none';
pollProgress(null, currentTranslationId);
} catch (error) {
alert('重译失败: ' + error.message);
}
});
// 简单Markdown渲染
function renderMarkdown(text) {
// 标题
text = text.replace(/^## (.*)$/gm, '<h2>$1</h2>');
text = text.replace(/^# (.*)$/gm, '<h1>$1</h1>');
// 分隔线
text = text.replace(/^---$/gm, '<hr>');
// 引用
text = text.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
// 段落
text = text.replace(/\n\n/g, '</p><p>');
text = '<p>' + text + '</p>';
return text;
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 检查用户登录状态
async function checkUserStatus() {
try {
const response = await fetch('/api/user/info');
const result = await response.json();
if (result.user) {
console.log('用户已登录:', result.user.username);
}
} catch (error) {
console.error('检查用户状态失败:', error);
}
}
// 页面加载时检查状态
document.addEventListener('DOMContentLoaded', checkUserStatus);