This commit is contained in:
2025-08-25 19:18:40 +08:00
parent 54f1365d24
commit ddddbfbb7c
3 changed files with 549 additions and 634 deletions

View File

@@ -36,8 +36,8 @@ logger.addHandler(handler)
@asynccontextmanager
async def lifespan(_: FastAPI):
pool = await init_postgres_pool()
app.state.pool = pool
#pool = await init_postgres_pool()
#app.state.pool = pool
asyncio.create_task(train_document_task())
@@ -45,7 +45,8 @@ async def lifespan(_: FastAPI):
yield
finally:
# 应用关闭时销毁连接池
await close_postgres_pool(pool)
#await close_postgres_pool(pool)
pass
app = FastAPI(lifespan=lifespan)

View File

@@ -8,7 +8,7 @@
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入 Font Awesome -->
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Tailwind 配置 -->
<!-- Tailwind 配置 - 保留在HTML中 -->
<script>
tailwind.config = {
theme: {
@@ -30,6 +30,8 @@
}
}
</script>
<!-- 引入外部JS文件 -->
<script src="mj.js"></script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
@@ -57,23 +59,7 @@
</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">
@@ -85,9 +71,7 @@
<nav class="hidden md:flex items-center space-x-6">
<a href="#generator"
class="font-medium hover:text-primary dark:hover:text-primary transition-colors">生成器</a>
<a href="#history"
class="font-medium hover:text-primary dark:hover:text-primary transition-colors">历史记录</a>
<a href="#gallery" class="font-medium hover:text-primary dark:hover:text-primary transition-colors">画廊</a>
<a href="#gallery" class="font-medium hover:text-primary dark:hover:text-primary transition-colors">历史</a>
</nav>
<div class="flex items-center space-x-3">
<button id="themeToggle"
@@ -106,7 +90,6 @@
class="hidden md:hidden glass-effect dark:glass-effect-dark border-t border-gray-200 dark:border-gray-700">
<div class="container mx-auto px-4 py-2 flex flex-col space-y-3">
<a href="#generator" class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">生成器</a>
<a href="#history" class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">历史记录</a>
<a href="#gallery"
class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">画廊</a>
</div>
@@ -285,7 +268,7 @@
<div class="p-6 md:p-8 bg-gray-50 dark:bg-gray-900 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<div class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
<i class="fa fa-lightbulb-o text-yellow-500 mr-2"></i>
<span>提示:生成高质量图像可能需要几秒钟时间</span>
<span>提示:生成高质量图像可能需要几秒钟时间</span>
</div>
<button id="generateBtn"
class="px-8 py-3 bg-primary hover:bg-primary/90 text-white rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all-300 font-medium flex items-center">
@@ -345,58 +328,48 @@
</div>
</section>
<!-- 历史记录区域 -->
<section id="history" class="mb-16">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-bold">历史记录</h3>
<button id="clearHistoryBtn"
class="text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 transition-colors">
清除全部
</button>
</div>
<div id="historyGrid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<!-- 历史记录项会通过 JavaScript 动态添加 -->
<div class="col-span-full text-center py-12 text-gray-500 dark:text-gray-400">
<i class="fa fa-history text-4xl mb-4"></i>
<p>暂无历史记录</p>
<p class="text-sm mt-2">开始生成图像以保存到历史记录</p>
</div>
</div>
</section>
<!-- 画廊区域 -->
<!-- 历史图像 -->
<section id="gallery" class="mb-16">
<h3 class="text-2xl font-bold mb-6">精选画廊</h3>
<h3 class="text-2xl font-bold mb-6">历史图像</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<!-- 添加第一张图片 -->
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://picsum.photos/seed/art1/600/400" alt="艺术作品"
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/fu_gu_80_nian_dai_feng_ge_de_d0936ca1-6a6b-403f-b547-3ec6852411ba.png"
alt="80年代风格的复古汽车"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">80年代风格的复古汽车</p>
</div>
</div>
<!-- 添加第二张图片 -->
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/ke_ai_de_ka_tong_xiong_mao_yu_c5dc58b6-5db2-4785-ab87-3ffb42e55c43.png"
alt="可爱的卡通熊猫"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">可爱的卡通熊猫</p>
</div>
</div>
<!-- 添加第三张图片 -->
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/wei_lai_ke_ji_gan_de_pao_che_b3a725a5-d7b1-4cc2-8e27-cfd138aa6fbb.png"
alt="未来科技感的跑车"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">未来科技感的跑车</p>
</div>
</div>
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/zhong_shi_ji_qi_huan_cheng_bao_zuo_8e6c3267-335d-463c-8320-ba3c649140e1.png" alt="艺术作品"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">梦幻城堡,超现实主义风格</p>
</div>
</div>
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://picsum.photos/seed/art2/600/400" alt="艺术作品"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">未来城市,赛博朋克风格</p>
</div>
</div>
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://picsum.photos/seed/art3/600/400" alt="艺术作品"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">奇幻森林,童话风格</p>
</div>
</div>
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
<img src="https://picsum.photos/seed/art4/600/400" alt="艺术作品"
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
<div class="p-4 bg-white dark:bg-gray-800">
<p class="font-medium truncate">抽象艺术,色彩斑斓</p>
</div>
</div>
</div>
</section>
</main>
@@ -429,571 +402,6 @@
</div>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function () {
// 主题切换功能
const themeToggle = document.getElementById('themeToggle');
themeToggle.addEventListener('click', function () {
document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
});
// 检查本地存储的主题偏好
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
// 移动端菜单切换
const menuToggle = document.getElementById('menuToggle');
const mobileMenu = document.getElementById('mobileMenu');
menuToggle.addEventListener('click', function () {
mobileMenu.classList.toggle('hidden');
});
// 选项卡切换
const textToImageTab = document.getElementById('textToImageTab');
const imageToImageTab = document.getElementById('imageToImageTab');
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');
imageToImageTab.classList.remove('text-primary', 'border-primary');
imageToImageTab.classList.add('text-gray-500', 'border-transparent');
textToImagePanel.classList.remove('hidden');
imageToImagePanel.classList.add('hidden');
});
imageToImageTab.addEventListener('click', function () {
imageToImageTab.classList.add('text-primary', 'border-primary');
imageToImageTab.classList.remove('text-gray-500', 'border-transparent');
textToImageTab.classList.remove('text-primary', 'border-primary');
textToImageTab.classList.add('text-gray-500', 'border-transparent');
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');
browseImage.addEventListener('click', function () {
imageUpload.click();
});
imageUploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
imageUploadArea.classList.add('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]);
}
}
removeImage.addEventListener('click', function () {
imagePreview.classList.add('hidden');
imageUpload.value = '';
});
imageStrength.addEventListener('input', function () {
strengthValue.textContent = this.value;
});
// 生成按钮点击事件
const generateBtn = document.getElementById('generateBtn');
generateBtn.addEventListener('click', generateImage);
// 重新生成按钮点击事件
const regenerateBtn = document.getElementById('regenerateBtn');
regenerateBtn.addEventListener('click', function () {
// 隐藏结果区域
document.getElementById('resultSection').classList.add('hidden');
// 显示状态区域
document.getElementById('statusSection').classList.remove('hidden');
// 重新生成
generateImage();
});
// 放大按钮点击事件
const upscaleBtn = document.getElementById('upscaleBtn');
upscaleBtn.addEventListener('click', function () {
alert('图像放大功能即将实现');
});
// 变体按钮点击事件
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;
}
} 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]);
return;
}
// 获取表单元素并进行空值检查
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('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 => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
queryCount++;
// 获取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';
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
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)}%`;
statusMessage.innerHTML += ` (已等待${elapsedTime}秒)`;
// 任务完成
if (data.status === 'completed') {
statusMessage.innerHTML = '<i class="fa fa-check-circle"></i> 图像生成完成!';
statusMessage.style.color = '#10b981';
// 确保进度为100%
progressFill.style.width = '100%';
progressText.textContent = '100%';
// 显示结果
setTimeout(() => {
showResult(data.image_url, taskId, prompt);
}, 1000);
// 清除定时器
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
// 任务失败
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(checkStatus, queryInterval);
}
})
.catch(error => {
console.error('检查任务状态失败:', error);
// 在错误情况下仍继续轮询直到达到最大次数
if (queryCount < maxQueries) {
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>生成图像';
}
});
}
// 立即开始第一次检查
checkStatus();
}
// 显示结果函数
function showResult(imageUrl, taskId, prompt) {
// 隐藏状态区域
document.getElementById('statusSection').classList.add('hidden');
// 显示结果区域
const resultSection = document.getElementById('resultSection');
resultSection.classList.remove('hidden');
// 设置结果图像
const resultImage = document.getElementById('resultImage');
resultImage.src = imageUrl;
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
// 保存到历史记录
saveToHistory(taskId, prompt, imageUrl);
// 更新历史记录网格
updateHistoryGrid();
}
// 保存到历史记录函数
function saveToHistory(taskId, prompt, imageUrl) {
let history = JSON.parse(localStorage.getItem('mjHistory') || '[]');
history.unshift({
id: taskId,
prompt: prompt,
imageUrl: imageUrl,
timestamp: new Date().getTime()
});
// 只保留最近20条记录
if (history.length > 20) {
history = history.slice(0, 20);
}
localStorage.setItem('mjHistory', JSON.stringify(history));
}
// 更新历史记录网格函数
function updateHistoryGrid() {
const historyGrid = document.getElementById('historyGrid');
const history = JSON.parse(localStorage.getItem('mjHistory') || '[]');
if (history.length === 0) {
historyGrid.innerHTML = `
<div class="col-span-full text-center py-12 text-gray-500 dark:text-gray-400">
<i class="fa fa-history text-4xl mb-4"></i>
<p>暂无历史记录</p>
<p class="text-sm mt-2">开始生成图像以保存到历史记录</p>
</div>
`;
return;
}
historyGrid.innerHTML = '';
history.forEach(item => {
const date = new Date(item.timestamp);
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
const historyItem = document.createElement('div');
historyItem.className = 'overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group bg-white dark:bg-gray-800';
historyItem.innerHTML = `
<div class="relative h-48 overflow-hidden">
<img src="${item.imageUrl}" alt="生成的图像" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-4">
<div class="text-white text-sm truncate w-full">${item.prompt}</div>
</div>
</div>
<div class="p-4 flex justify-between items-center">
<span class="text-xs text-gray-500 dark:text-gray-400">${formattedDate}</span>
<button class="view-history-item text-primary hover:text-primary/80 transition-colors" data-image-url="${item.imageUrl}">
<i class="fa fa-eye"></i>
</button>
</div>
`;
historyGrid.appendChild(historyItem);
});
// 添加历史记录项点击事件
document.querySelectorAll('.view-history-item').forEach(button => {
button.addEventListener('click', function () {
const imageUrl = this.getAttribute('data-image-url');
// 显示结果区域
const resultSection = document.getElementById('resultSection');
resultSection.classList.remove('hidden');
// 设置结果图像
const resultImage = document.getElementById('resultImage');
resultImage.src = imageUrl;
});
});
// 初始化历史记录网格
updateHistoryGrid();
}
// 添加 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 = [
"一只穿着太空服的柯基犬在火星表面行走,背景是红色星球和远处的地球,科幻风格,高清细节",
"中国传统水墨画风格的山间小屋,雾气缭绕,远处有瀑布,意境悠远,留白艺术",
"蒸汽朋克风格的城市景观空中漂浮着飞艇机械结构细节丰富黄昏光影8K分辨率",
"可爱的卡通熊猫宇航员在太空中吃竹子,周围有星星和行星,迪士尼风格,明亮色彩",
"赛博朋克风格的东京雨夜,霓虹灯闪烁,全息广告,湿润的街道反射, Blade Runner 氛围",
"中世纪奇幻城堡坐落在山顶,周围有龙和魔法光芒,日落时分,史诗电影场景",
"极简主义风格的室内设计,白色为主色调,原木家具,大窗户,自然光,北欧风格",
"未来科技感的跑车设计,流线型车身,透明车顶,悬浮效果,银色金属质感,未来城市背景",
"水彩风格的森林动物聚会,兔子、狐狸和松鼠围坐在蘑菇旁,童话风格,柔和色彩",
"复古80年代风格的街机游戏厅霓虹灯光老式游戏机像素艺术怀旧氛围"
];
// 图生图示例提示词库
const imagePromptExamples = [
"将图片转换为梵高风格的油画,星月夜笔触,鲜艳色彩",
"变成卡通风格,线条简洁,色彩明亮,适合儿童绘本",
"转换为赛博朋克风格,添加霓虹灯效果和未来科技元素",
"模拟水墨画效果,黑白灰三色,强调意境和笔触",
"变成3D渲染风格添加立体效果和真实光影",
"转换为像素艺术8-bit风格适合复古游戏场景",
"模拟水彩画效果,柔和的色彩过渡和纹理",
"变成低多边形风格,几何图形构成,简约美学",
"转换为蒸汽朋克风格,添加齿轮、铜制元素和机械结构",
"模拟素描效果,铅笔线条,强调轮廓和阴影"
];
// 页面加载完成后绑定事件
document.addEventListener('DOMContentLoaded', function () {
// 文生图随机按钮
const randomPromptBtn = document.getElementById('randomPromptBtn');
const promptInput = document.getElementById('prompt');
// 图生图随机按钮
const randomImagePromptBtn = document.getElementById('randomImagePromptBtn');
const imagePromptInput = document.getElementById('imagePrompt');
// 文生图随机功能
if (randomPromptBtn && promptInput) {
randomPromptBtn.addEventListener('click', function () {
const randomIndex = Math.floor(Math.random() * textPromptExamples.length);
promptInput.value = textPromptExamples[randomIndex];
// 添加输入框焦点效果
promptInput.focus();
setTimeout(() => promptInput.blur(), 100);
});
}
// 图生图随机功能
if (randomImagePromptBtn && imagePromptInput) {
randomImagePromptBtn.addEventListener('click', function () {
const randomIndex = Math.floor(Math.random() * imagePromptExamples.length);
imagePromptInput.value = imagePromptExamples[randomIndex];
// 添加输入框焦点效果
imagePromptInput.focus();
setTimeout(() => imagePromptInput.blur(), 100);
});
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,506 @@
// DOM加载完成后执行
document.addEventListener('DOMContentLoaded', function () {
// 主题切换功能
const themeToggle = document.getElementById('themeToggle');
themeToggle.addEventListener('click', function () {
document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
});
// 检查本地存储的主题偏好
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
// 移动端菜单切换
const menuToggle = document.getElementById('menuToggle');
const mobileMenu = document.getElementById('mobileMenu');
menuToggle.addEventListener('click', function () {
mobileMenu.classList.toggle('hidden');
});
// 选项卡切换
const textToImageTab = document.getElementById('textToImageTab');
const imageToImageTab = document.getElementById('imageToImageTab');
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');
imageToImageTab.classList.remove('text-primary', 'border-primary');
imageToImageTab.classList.add('text-gray-500', 'border-transparent');
textToImagePanel.classList.remove('hidden');
imageToImagePanel.classList.add('hidden');
});
imageToImageTab.addEventListener('click', function () {
imageToImageTab.classList.add('text-primary', 'border-primary');
imageToImageTab.classList.remove('text-gray-500', 'border-transparent');
textToImageTab.classList.remove('text-primary', 'border-primary');
textToImageTab.classList.add('text-gray-500', 'border-transparent');
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');
browseImage.addEventListener('click', function () {
imageUpload.click();
});
imageUploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
imageUploadArea.classList.add('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]);
}
}
removeImage.addEventListener('click', function () {
imagePreview.classList.add('hidden');
imageUpload.value = '';
});
imageStrength.addEventListener('input', function () {
strengthValue.textContent = this.value;
});
// 随机提示词功能 - 移到顶层DOMContentLoaded回调中
const randomPromptBtn = document.getElementById('randomPromptBtn');
const randomImagePromptBtn = document.getElementById('randomImagePromptBtn');
const promptInput = document.getElementById('prompt');
const imagePromptInput = document.getElementById('imagePrompt');
if (randomPromptBtn && promptInput) {
randomPromptBtn.addEventListener('click', function () {
const randomPrompt = textPromptExamples[Math.floor(Math.random() * textPromptExamples.length)];
promptInput.value = randomPrompt;
});
}
if (randomImagePromptBtn && imagePromptInput) {
randomImagePromptBtn.addEventListener('click', function () {
const randomPrompt = imagePromptExamples[Math.floor(Math.random() * imagePromptExamples.length)];
imagePromptInput.value = randomPrompt;
});
}
// 生成按钮点击事件
const generateBtn = document.getElementById('generateBtn');
generateBtn.addEventListener('click', generateImage);
// 重新生成按钮点击事件
const regenerateBtn = document.getElementById('regenerateBtn');
regenerateBtn.addEventListener('click', function () {
// 隐藏结果区域
document.getElementById('resultSection').classList.add('hidden');
// 显示状态区域
document.getElementById('statusSection').classList.remove('hidden');
// 重新生成
generateImage();
});
// 放大按钮点击事件
const upscaleBtn = document.getElementById('upscaleBtn');
upscaleBtn.addEventListener('click', function () {
alert('图像放大功能即将实现');
});
// 变体按钮点击事件
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);
});
// 生成图像函数
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]);
return;
}
// 获取表单元素并进行空值检查
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');
// 验证必填字段
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('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 => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
queryCount++;
// 获取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';
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
return;
}
// 更新状态信息
statusMessage.innerHTML = `<i class="fa fa-spinner fa-spin"></i> AI 正在创作中...`;
statusMessage.style.color = ''; // 重置颜色
// 更新进度条
let progress = data.progress || 0;
// 正确解析进度值,处理"32%"这样的字符串
if (typeof progress === 'string' && progress.endsWith('%')) {
progress = parseFloat(progress);
} else if (typeof progress === 'string') {
progress = parseFloat(progress) || 0;
}
// 添加NaN检查
if (isNaN(progress) || progress < 0) 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)}%`;
statusMessage.innerHTML += ` (已等待${elapsedTime}秒)`;
// 任务完成
if (data.status === 'completed') {
statusMessage.innerHTML = '<i class="fa fa-check-circle"></i> 图像生成完成!';
statusMessage.style.color = '#10b981';
// 确保进度为100%
progressFill.style.width = '100%';
progressText.textContent = '100%';
// 显示结果
setTimeout(() => {
showResult(data.image_url, taskId, prompt);
}, 1000);
// 清除定时器
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
// 任务失败
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(checkStatus, queryInterval);
}
})
.catch(error => {
console.error('检查任务状态失败:', error);
// 在错误情况下仍继续轮询直到达到最大次数
if (queryCount < maxQueries) {
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>生成图像';
}
});
}
// 立即开始第一次检查
checkStatus();
}
// 显示结果函数
function showResult(imageUrl, taskId, prompt) {
// 隐藏状态区域
document.getElementById('statusSection').classList.add('hidden');
// 显示结果区域
const resultSection = document.getElementById('resultSection');
resultSection.classList.remove('hidden');
// 设置结果图像
const resultImage = document.getElementById('resultImage');
resultImage.src = imageUrl;
// 恢复生成按钮
generateBtn.disabled = false;
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
}
// 添加 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 = [
"一只穿着太空服的柯基犬在火星表面行走,背景是红色星球和远处的地球,科幻风格,高清细节",
"中国传统水墨画风格的山间小屋,雾气缭绕,远处有瀑布,意境悠远,留白艺术",
"蒸汽朋克风格的城市景观空中漂浮着飞艇机械结构细节丰富黄昏光影8K分辨率",
"可爱的卡通熊猫宇航员在太空中吃竹子,周围有星星和行星,迪士尼风格,明亮色彩",
"赛博朋克风格的东京雨夜,霓虹灯闪烁,全息广告,湿润的街道反射, Blade Runner 氛围",
"中世纪奇幻城堡坐落在山顶,周围有龙和魔法光芒,日落时分,史诗电影场景",
"极简主义风格的室内设计,白色为主色调,原木家具,大窗户,自然光,北欧风格",
"未来科技感的跑车设计,流线型车身,透明车顶,悬浮效果,银色金属质感,未来城市背景",
"水彩风格的森林动物聚会,兔子、狐狸和松鼠围坐在蘑菇旁,童话风格,柔和色彩",
"复古80年代风格的街机游戏厅霓虹灯光老式游戏机像素艺术怀旧氛围"
];
// 图生图示例提示词库
const imagePromptExamples = [
"将图片转换为梵高风格的油画,星月夜笔触,鲜艳色彩",
"变成卡通风格,线条简洁,色彩明亮,适合儿童绘本",
"转换为赛博朋克风格,添加霓虹灯效果和未来科技元素",
"模拟水墨画效果,黑白灰三色,强调意境和笔触",
"变成3D渲染风格添加立体效果和真实光影",
"转换为像素艺术8-bit风格适合复古游戏场景",
"模拟水彩画效果,柔和的色彩过渡和纹理",
"变成低多边形风格,几何图形构成,简约美学",
"转换为蒸汽朋克风格,添加齿轮、铜制元素和机械结构",
"模拟素描效果,铅笔线条,强调轮廓和阴影"
];
// 页面加载完成后绑定事件
document.addEventListener('DOMContentLoaded', function () {
// 文生图随机按钮
const randomPromptBtn = document.getElementById('randomPromptBtn');
const promptInput = document.getElementById('prompt');
// 图生图随机按钮
const randomImagePromptBtn = document.getElementById('randomImagePromptBtn');
const imagePromptInput = document.getElementById('imagePrompt');
// 文生图随机功能
if (randomPromptBtn && promptInput) {
randomPromptBtn.addEventListener('click', function () {
const randomIndex = Math.floor(Math.random() * textPromptExamples.length);
promptInput.value = textPromptExamples[randomIndex];
// 添加输入框焦点效果
promptInput.focus();
setTimeout(() => promptInput.blur(), 100);
});
}
// 图生图随机功能
if (randomImagePromptBtn && imagePromptInput) {
randomImagePromptBtn.addEventListener('click', function () {
const randomIndex = Math.floor(Math.random() * imagePromptExamples.length);
imagePromptInput.value = imagePromptExamples[randomIndex];
// 添加输入框焦点效果
imagePromptInput.focus();
setTimeout(() => imagePromptInput.blur(), 100);
});
}
// 初始化历史记录网格
updateHistoryGrid();
});
});