This commit is contained in:
2025-08-25 16:25:12 +08:00
parent b68431828c
commit 54f1365d24
3 changed files with 351 additions and 374 deletions

View File

@@ -57,6 +57,23 @@
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-light to-gray-100 dark:from-dark dark:to-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300">
<script>
function showError(message) {
const errorElement = document.getElementById('error-message');
if (errorElement) {
errorElement.textContent = message;
errorElement.classList.remove('hidden');
// Show error section if it's hidden
const statusSection = document.getElementById('statusSection');
if (statusSection) statusSection.classList.remove('hidden');
// Auto-hide after 5 seconds
setTimeout(() => errorElement.classList.add('hidden'), 5000);
} else {
console.error('Error: showError - error-message element not found');
alert(message);
}
}
</script>
<!-- 导航栏 -->
<header class="sticky top-0 z-50 glass-effect dark:glass-effect-dark border-b border-gray-200 dark:border-gray-700 shadow-sm">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
@@ -275,36 +292,20 @@
<i class="fa fa-magic mr-2"></i>生成图像
</button>
</div>
</section>
<!-- 生成状态区域 -->
<section id="statusSection" class="mb-16 hidden">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 md:p-8">
<div class="flex flex-col items-center text-center space-y-6">
<div id="thinkingIndicator" class="flex flex-col items-center space-y-4">
<div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center animate-pulse-slow">
<i class="fa fa-cog fa-spin text-2xl text-primary"></i>
</div>
<h3 class="text-xl font-semibold">AI 正在创作中...</h3>
<p class="text-gray-600 dark:text-gray-300 max-w-md">
请稍候,我们的 AI 正在努力为您生成精美的图像
</p>
</div>
<div id="progressBarContainer"
class="w-full max-w-2xl bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
<div id="progressBar"
class="bg-gradient-to-r from-primary to-secondary h-full w-0 transition-all duration-300"></div>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
<span id="progressText">0%</span> 完成
</div>
<!-- 新增状态显示区域 -->
<div id="statusSection" class="hidden p-6 bg-gray-50 dark:bg-gray-900 rounded-b-lg">
<div class="progress-container mb-4">
<div id="progress-bar" class="progress-bar h-2 bg-primary rounded-full transition-all duration-300"
style="width: 0%"></div>
<div id="progress-text" class="text-right text-sm mt-1 text-gray-600 dark:text-gray-300">0%</div>
</div>
<div id="status-display" class="text-gray-700 dark:text-gray-300 mb-2"></div>
<div id="error-message" class="error-message text-red-500 hidden"></div>
</div>
</section>
<!-- 结果展示区域 -->
<!-- 生成结果区域 -->
<section id="resultSection" class="mb-16 hidden">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<div class="p-6 md:p-8 border-b border-gray-200 dark:border-gray-700">
@@ -429,51 +430,6 @@
</div>
</footer>
<script>
// ==============================================
// 全局函数 - 必须放在所有事件监听和调用之前
// ==============================================
function checkTaskStatus(taskId) {
if (!taskId) {
console.error('任务ID不存在');
return;
}
fetch(`/api/mj/task_status?task_id=${encodeURIComponent(taskId)}`)
.then(response => {
if (!response.ok) throw new Error('网络响应不正常');
return response.json();
})
.then(data => {
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const statusDisplay = document.getElementById('status-display');
if (progressBar && progressText) {
const progress = data.progress || 0;
progressBar.style.width = `${progress}%`;
progressText.textContent = `${progress}%`;
}
if (statusDisplay) {
statusDisplay.textContent = `状态: ${data.status || '未知'}`;
}
if (data.status === 'completed') {
stopPolling();
displayResult(data.result);
addToHistory(data);
} else if (data.status === 'failed') {
stopPolling();
showError(data.error || '生成失败,请重试');
}
})
.catch(error => {
console.error('获取任务状态时出错:', error);
stopPolling();
showError('获取状态失败: ' + error.message);
});
}
document.addEventListener('DOMContentLoaded', function () {
// 主题切换功能
const themeToggle = document.getElementById('themeToggle');
@@ -500,6 +456,7 @@
const textToImagePanel = document.getElementById('textToImagePanel');
const imageToImagePanel = document.getElementById('imageToImagePanel');
textToImageTab.addEventListener('click', function () {
textToImageTab.classList.add('text-primary', 'border-primary');
textToImageTab.classList.remove('text-gray-500', 'border-transparent');
@@ -517,219 +474,281 @@
imageToImagePanel.classList.remove('hidden');
textToImagePanel.classList.add('hidden');
});
});
// 图生图相关功能
const browseImage = document.getElementById('browseImage');
const imageUpload = document.getElementById('imageUpload');
const imageUploadArea = document.getElementById('imageUploadArea');
const imagePreview = document.getElementById('imagePreview');
const previewImage = document.getElementById('previewImage');
const removeImage = document.getElementById('removeImage');
const imageStrength = document.getElementById('imageStrength');
const strengthValue = document.getElementById('strengthValue');
// 图生图相关功能
const browseImage = document.getElementById('browseImage');
const imageUpload = document.getElementById('imageUpload');
const imageUploadArea = document.getElementById('imageUploadArea');
const imagePreview = document.getElementById('imagePreview');
const previewImage = document.getElementById('previewImage');
const removeImage = document.getElementById('removeImage');
const imageStrength = document.getElementById('imageStrength');
const strengthValue = document.getElementById('strengthValue');
browseImage.addEventListener('click', function () {
imageUpload.click();
});
browseImage.addEventListener('click', function () {
imageUpload.click();
});
imageUploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
imageUploadArea.classList.add('border-primary');
});
imageUploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
imageUploadArea.classList.add('border-primary');
});
imageUploadArea.addEventListener('dragleave', function () {
imageUploadArea.classList.remove('border-primary');
});
imageUploadArea.addEventListener('dragleave', function () {
imageUploadArea.classList.remove('border-primary');
});
imageUploadArea.addEventListener('drop', function (e) {
e.preventDefault();
imageUploadArea.classList.remove('border-primary');
if (e.dataTransfer.files.length) {
imageUpload.files = e.dataTransfer.files;
handleImageUpload();
}
});
imageUpload.addEventListener('change', handleImageUpload);
function handleImageUpload() {
if (imageUpload.files && imageUpload.files[0]) {
const reader = new FileReader();
reader.onload = function (e) {
previewImage.src = e.target.result;
imagePreview.classList.remove('hidden');
};
reader.readAsDataURL(imageUpload.files[0]);
}
imageUploadArea.addEventListener('drop', function (e) {
e.preventDefault();
imageUploadArea.classList.remove('border-primary');
if (e.dataTransfer.files.length) {
imageUpload.files = e.dataTransfer.files;
handleImageUpload();
}
});
removeImage.addEventListener('click', function () {
imagePreview.classList.add('hidden');
imageUpload.value = '';
});
imageUpload.addEventListener('change', handleImageUpload);
imageStrength.addEventListener('input', function () {
strengthValue.textContent = this.value;
});
function handleImageUpload() {
if (imageUpload.files && imageUpload.files[0]) {
const reader = new FileReader();
reader.onload = function (e) {
previewImage.src = e.target.result;
imagePreview.classList.remove('hidden');
};
reader.readAsDataURL(imageUpload.files[0]);
}
}
// 生成按钮点击事件
const generateBtn = document.getElementById('generateBtn');
generateBtn.addEventListener('click', generateImage);
removeImage.addEventListener('click', function () {
imagePreview.classList.add('hidden');
imageUpload.value = '';
});
// 重新生成按钮点击事件
const regenerateBtn = document.getElementById('regenerateBtn');
regenerateBtn.addEventListener('click', function () {
// 隐藏结果区域
document.getElementById('resultSection').classList.add('hidden');
// 显示状态区域
document.getElementById('statusSection').classList.remove('hidden');
// 重新生成
generateImage();
});
imageStrength.addEventListener('input', function () {
strengthValue.textContent = this.value;
});
// 放大按钮点击事件
const upscaleBtn = document.getElementById('upscaleBtn');
upscaleBtn.addEventListener('click', function () {
alert('图像放大功能即将实现');
});
// 生成按钮点击事件
const generateBtn = document.getElementById('generateBtn');
generateBtn.addEventListener('click', generateImage);
// 变体按钮点击事件
const variationBtn = document.getElementById('variationBtn');
variationBtn.addEventListener('click', function () {
alert('生成变体功能即将实现');
});
// 重新生成按钮点击事件
const regenerateBtn = document.getElementById('regenerateBtn');
regenerateBtn.addEventListener('click', function () {
// 隐藏结果区域
document.getElementById('resultSection').classList.add('hidden');
// 显示状态区域
document.getElementById('statusSection').classList.remove('hidden');
// 重新生成
generateImage();
});
// 下载按钮点击事件
const downloadBtn = document.getElementById('downloadBtn');
downloadBtn.addEventListener('click', function () {
const resultImage = document.getElementById('resultImage');
const link = document.createElement('a');
link.href = resultImage.src;
link.download = 'midjourney-image.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// 放大按钮点击事件
const upscaleBtn = document.getElementById('upscaleBtn');
upscaleBtn.addEventListener('click', function () {
alert('图像放大功能即将实现');
});
// 清除历史记录按钮点击事件
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
clearHistoryBtn.addEventListener('click', function () {
if (confirm('确定要清除所有历史记录吗?')) {
localStorage.removeItem('mjHistory');
updateHistoryGrid();
// 变体按钮点击事件
const variationBtn = document.getElementById('variationBtn');
variationBtn.addEventListener('click', function () {
alert('生成变体功能即将实现');
});
// 下载按钮点击事件
const downloadBtn = document.getElementById('downloadBtn');
downloadBtn.addEventListener('click', function () {
const resultImage = document.getElementById('resultImage');
const link = document.createElement('a');
link.href = resultImage.src;
link.download = 'midjourney-image.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// 清除历史记录按钮点击事件
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
clearHistoryBtn.addEventListener('click', function () {
if (confirm('确定要清除所有历史记录吗?')) {
localStorage.removeItem('mjHistory');
updateHistoryGrid();
}
});
// 生成图像函数
async function generateImage() {
let prompt, base64Array = null;
// 检查是文生图还是图生图
if (!textToImagePanel.classList.contains('hidden')) {
// 文生图
prompt = document.getElementById('prompt').value.trim();
if (!prompt) {
alert('请输入提示词');
return;
}
});
// 生成图像函数
async function generateImage() {
let prompt, base64Array = null;
// 检查是文生图还是图生图
if (!textToImagePanel.classList.contains('hidden')) {
// 文生图
prompt = document.getElementById('prompt').value.trim();
if (!prompt) {
alert('请输入提示词');
return;
}
} else {
// 图生图
prompt = document.getElementById('imagePrompt').value.trim();
if (!prompt) {
alert('请输入提示词');
return;
}
if (!imageUpload.files || !imageUpload.files[0]) {
alert('请上传参考图片');
return;
}
// 转换图片为base64
const reader = new FileReader();
reader.onload = function (e) {
base64Array = [e.target.result.split(',')[1]];
submitGenerateRequest(prompt, base64Array);
};
reader.readAsDataURL(imageUpload.files[0]);
} else {
// 图生图
prompt = document.getElementById('imagePrompt').value.trim();
if (!prompt) {
alert('请输入提示词');
return;
}
// 提交生成请求
submitGenerateRequest(prompt, base64Array);
}
// 提交生成请求函数
async function submitGenerateRequest(prompt, base64Array) {
try {
// 禁用生成按钮
generateBtn.disabled = true;
generateBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>正在提交...';
// 准备请求数据
const requestData = {
prompt: prompt,
base64_array: base64Array
};
// 发送请求
const response = await fetch('/api/mj/imagine', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(`提交请求失败: ${response.statusText}`);
}
const data = await response.json();
const taskId = data.task_id;
// 显示状态区域
document.getElementById('statusSection').classList.remove('hidden');
// 开始轮询任务状态
pollTaskStatus(taskId, prompt);
} catch (error) {
console.error('生成图像失败:', error);
alert(`生成图像失败: ${error.message}`);
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
if (!imageUpload.files || !imageUpload.files[0]) {
alert('请上传参考图片');
return;
}
// 转换图片为base64
const reader = new FileReader();
reader.onload = function (e) {
base64Array = [e.target.result.split(',')[1]];
submitGenerateRequest(prompt, base64Array);
};
reader.readAsDataURL(imageUpload.files[0]);
return;
}
// 轮询任务状态函数
function pollTaskStatus(taskId, prompt) {
// 轮询配置
const queryInterval = 3000; // 3秒查询一次
const maxQueries = 120; // 最多查询120次6分钟
let queryCount = 0; // 当前查询次数
const startTime = new Date().getTime(); // 开始时间
let timerId = null; // 用于存储setTimeout的ID
// 获取表单元素并进行空值检查
const promptElement = document.getElementById('prompt');
const aspectRatioElement = document.getElementById('aspectRatio');
const qualityElement = document.getElementById('quality');
const styleElement = document.getElementById('style');
// 检查是否所有必要元素都存在
if (!promptElement || !aspectRatioElement || !qualityElement || !styleElement) {
showError('表单元素缺失,请检查页面配置');
return;
}
// 创建FormData并添加值
const formData = new FormData();
formData.append('prompt', promptElement.value);
formData.append('aspect_ratio', aspectRatioElement.value);
formData.append('quality', qualityElement.value);
formData.append('style', styleElement.value);
// 调用提交函数
submitGenerateRequest(formData);
}
// 提交生成请求函数
async function submitGenerateRequest(formData) {
// 验证formData是有效的FormData对象
if (!(formData instanceof FormData)) {
showError('无效的表单数据格式');
return;
}
// 提取表单数据
const prompt = formData.get('prompt');
const aspect_ratio = formData.get('aspect_ratio');
const quality = formData.get('quality');
const style = formData.get('style');
// 验证必填字段
if (!prompt) {
showError('请输入提示词');
return;
}
// 从formData提取值确保表单元素有正确的name属性
const promptText = formData.get('prompt')?.trim();
const selectedAspectRatio = formData.get('aspect_ratio');
const selectedQuality = formData.get('quality');
const selectedStyle = formData.get('style');
// 验证必要参数
if (!promptText) {
showError('提示词不能为空');
return;
}
if (!selectedAspectRatio || !selectedQuality || !selectedStyle) {
showError('请选择完整的生成参数');
return;
}
try {
const response = await fetch('/api/mj/imagine', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: promptText,
aspect_ratio: selectedAspectRatio,
quality: selectedQuality,
style: selectedStyle
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(`生成请求失败: ${errorData?.message || response.statusText}`);
}
const result = await response.json();
console.log('Backend response:', result); // Add this line
if (!result || !result.task_id) {
showError('生成失败: 未获取到任务ID');
return null;
}
// 显示状态区域并开始轮询
document.getElementById('statusSection').classList.remove('hidden');
pollTaskStatus(result.task_id, promptText);
return result.task_id;
} catch (error) {
console.error('提交生成请求失败:', error);
showError('提交请求失败: ' + error.message);
return null;
}
}
// 轮询任务状态函数
// 轮询任务状态函数
function pollTaskStatus(taskId, prompt) {
// 轮询配置
const queryInterval = 3000; // 3秒查询一次
const maxQueries = 120; // 最多查询120次6分钟
let queryCount = 0; // 当前查询次数
const startTime = new Date().getTime(); // 开始时间
let timerId = null; // 用于存储setTimeout的ID
// 定义内部轮询函数
function checkStatus() {
// 检查是否超过最大查询次数
if (queryCount >= maxQueries) {
const elapsedTime = Math.round((new Date().getTime() - startTime) / 1000);
document.getElementById('statusMessage').innerHTML = `<i class="fa fa-exclamation-circle"></i> 图像生成超时(已等待${elapsedTime}秒),您可以稍后在历史记录中查看,或重新生成。`;
document.getElementById('progressText').textContent = '生成超时';
document.getElementById('status-display').innerHTML = `<i class="fa fa-exclamation-circle"></i> 图像生成超时(已等待${elapsedTime}秒),您可以稍后在历史记录中查看,或重新生成。`;
document.getElementById('progress-text').textContent = '生成超时';
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
return;
}
// 发起状态查询请求
fetch(`/api/mj/task_status?task_id=${taskId}`)
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
queryCount++;
// 更新状态信息
const statusMessage = document.getElementById('statusMessage');
const progressFill = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
// 获取DOM元素
const statusMessage = document.getElementById('status-display');
const progressFill = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
// 处理错误状态
if (data.error) {
statusMessage.innerHTML = `<i class="fa fa-exclamation-circle"></i> 生成失败: ${data.error}`;
statusMessage.style.color = '#ef4444';
@@ -739,13 +758,13 @@
return;
}
// 更新状态信息
statusMessage.innerHTML = `<i class="fa fa-spinner fa-spin"></i> AI 正在创作中...`;
statusMessage.style.color = ''; // 重置颜色
// 更新进度条
let progress = data.progress || 0;
if (progress > 100) progress = 100;
// 计算已等待时间
const elapsedTime = Math.round((new Date().getTime() - startTime) / 1000);
progressFill.style.width = `${progress}%`;
progressText.textContent = `${Math.round(progress)}%`;
@@ -757,9 +776,8 @@
statusMessage.style.color = '#10b981';
// 确保进度为100%
progress = 100;
progressFill.style.width = `${progress}%`;
progressText.textContent = `${Math.round(progress)}%`;
progressFill.style.width = '100%';
progressText.textContent = '100%';
// 显示结果
setTimeout(() => {
@@ -771,23 +789,29 @@
clearTimeout(timerId);
timerId = null;
}
} else if (data.status === 'failed') {
}
// 任务失败
else if (data.status === 'failed') {
statusMessage.innerHTML = `<i class="fa fa-exclamation-circle"></i> 生成失败: ${data.error || '未知错误'}`;
statusMessage.style.color = '#ef4444';
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
} else {
// 继续
timerId = setTimeout(checkTaskStatus, queryInterval);
}
// 继续
else {
timerId = setTimeout(checkStatus, queryInterval);
}
})
.catch(error => {
console.error('检查任务状态失败:', error);
// 只有在任务未完成且未超时时才继续查询
// 在错误情况下仍继续轮询直到达到最大次数
if (queryCount < maxQueries) {
timerId = setTimeout(checkTaskStatus, queryInterval);
queryCount++;
timerId = setTimeout(checkStatus, queryInterval);
} else {
document.getElementById('status-display').innerHTML = `<i class="fa fa-exclamation-circle"></i> 连接服务器失败,请重试`;
document.getElementById('status-display').style.color = '#ef4444';
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
@@ -795,9 +819,9 @@
});
}
// 立即检查一次
checkTaskStatus();
})
// 立即开始第一次检查
checkStatus();
}
// 显示结果函数
function showResult(imageUrl, taskId, prompt) {
@@ -895,10 +919,21 @@
// 初始化历史记录网格
updateHistoryGrid();
}
</script>
<!-- 添加随机示例功能的JavaScript -->
<script>
// 添加 showError function after checkTaskStatus
function showError(message) {
const errorElement = document.getElementById('error-message');
if (errorElement) {
errorElement.textContent = message;
errorElement.classList.remove('hidden');
// Auto-hide after 5 seconds
setTimeout(() => errorElement.classList.add('hidden'), 5000);
} else {
// Fallback if error element not found
alert(message);
}
}
// 文生图示例提示词库
const textPromptExamples = [
"一只穿着太空服的柯基犬在火星表面行走,背景是红色星球和远处的地球,科幻风格,高清细节",