964 lines
48 KiB
HTML
964 lines
48 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Midjourney AI 绘图工具</title>
|
||
<!-- 引入 Tailwind CSS -->
|
||
<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 配置 -->
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
primary: '#6366f1',
|
||
secondary: '#8b5cf6',
|
||
dark: '#1e293b',
|
||
light: '#f8fafc',
|
||
accent: '#ec4899'
|
||
},
|
||
fontFamily: {
|
||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||
},
|
||
animation: {
|
||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||
}
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
<style type="text/tailwindcss">
|
||
@layer utilities {
|
||
.content-auto {
|
||
content-visibility: auto;
|
||
}
|
||
|
||
.glass-effect {
|
||
backdrop-filter: blur(10px);
|
||
background-color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.glass-effect-dark {
|
||
backdrop-filter: blur(10px);
|
||
background-color: rgba(15, 23, 42, 0.7);
|
||
}
|
||
|
||
.text-shadow {
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.transition-all-300 {
|
||
transition: all 300ms ease-in-out;
|
||
}
|
||
}
|
||
</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">
|
||
<!-- 导航栏 -->
|
||
<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">
|
||
<div class="flex items-center space-x-2">
|
||
<i class="fa fa-paint-brush text-primary text-2xl"></i>
|
||
<h1 class="text-xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||
Midjourney AI</h1>
|
||
</div>
|
||
<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>
|
||
</nav>
|
||
<div class="flex items-center space-x-3">
|
||
<button id="themeToggle"
|
||
class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||
<i class="fa fa-moon-o dark:hidden"></i>
|
||
<i class="fa fa-sun-o hidden dark:inline-block"></i>
|
||
</button>
|
||
<button class="md:hidden p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||
id="menuToggle">
|
||
<i class="fa fa-bars"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- 移动端菜单 -->
|
||
<div id="mobileMenu"
|
||
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>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container mx-auto px-4 py-8">
|
||
<!-- 英雄区 -->
|
||
<section class="text-center mb-12">
|
||
<h2 class="text-[clamp(2rem,5vw,3.5rem)] font-bold mb-4 text-shadow">
|
||
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">释放您的创意</span>
|
||
</h2>
|
||
<p class="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-8">
|
||
使用 Midjourney AI 生成令人惊叹的图像,只需输入文字描述或上传参考图片
|
||
</p>
|
||
</section>
|
||
|
||
<!-- 生成器区域 -->
|
||
<section id="generator"
|
||
class="mb-16 bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden transform transition-all duration-500 hover:shadow-2xl">
|
||
<!-- 选项卡 -->
|
||
<div class="flex border-b border-gray-200 dark:border-gray-700">
|
||
<button id="textToImageTab"
|
||
class="flex-1 py-4 px-6 text-center font-medium text-primary border-b-2 border-primary dark:text-primary dark:border-primary transition-all-300">
|
||
<i class="fa fa-file-text-o mr-2"></i>文生图
|
||
</button>
|
||
<button id="imageToImageTab"
|
||
class="flex-1 py-4 px-6 text-center font-medium text-gray-500 dark:text-gray-400 border-b-2 border-transparent hover:text-primary dark:hover:text-primary transition-all-300">
|
||
<i class="fa fa-picture-o mr-2"></i>图生图
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 文生图面板 -->
|
||
<div id="textToImagePanel" class="p-6 md:p-8 space-y-6">
|
||
<div class="space-y-4">
|
||
<label for="prompt" class="block text-sm font-medium text-gray-700 dark:text-gray-300">提示词 <span
|
||
class="text-red-500">*</span></label>
|
||
<div class="flex space-x-2">
|
||
<textarea id="prompt" rows="4"
|
||
class="flex-1 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-primary resize-none transition-all-300"
|
||
placeholder="描述您想要生成的图像..."></textarea>
|
||
<button id="randomPromptBtn"
|
||
class="p-3 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-all-300"
|
||
title="随机示例">
|
||
<i class="fa fa-random text-primary mr-1"></i>随机
|
||
</button>
|
||
</div>
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||
提示越详细,生成的图像越符合您的预期。尝试指定风格、细节、光照等。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
<div class="space-y-2">
|
||
<label for="style" class="block text-sm font-medium text-gray-700 dark:text-gray-300">风格</label>
|
||
<select id="style"
|
||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-primary transition-all-300">
|
||
<option value="default">默认</option>
|
||
<option value="realistic">写实</option>
|
||
<option value="cartoon">卡通</option>
|
||
<option value="anime">动漫</option>
|
||
<option value="watercolor">水彩</option>
|
||
<option value="oil">油画</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<label for="aspectRatio"
|
||
class="block text-sm font-medium text-gray-700 dark:text-gray-300">比例</label>
|
||
<select id="aspectRatio"
|
||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-primary transition-all-300">
|
||
<option value="1:1">1:1 (正方形)</option>
|
||
<option value="16:9">16:9 (宽屏)</option>
|
||
<option value="9:16">9:16 (竖屏)</option>
|
||
<option value="4:3">4:3 (标准)</option>
|
||
<option value="3:2">3:2 (照片)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<label for="quality" class="block text-sm font-medium text-gray-700 dark:text-gray-300">质量</label>
|
||
<select id="quality"
|
||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-primary transition-all-300">
|
||
<option value="standard">标准</option>
|
||
<option value="high">高清</option>
|
||
<option value="ultra">超高清</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图生图面板 -->
|
||
<div id="imageToImagePanel" class="p-6 md:p-8 space-y-6 hidden">
|
||
<div class="space-y-4">
|
||
<label for="imagePrompt" class="block text-sm font-medium text-gray-700 dark:text-gray-300">提示词 <span
|
||
class="text-red-500">*</span></label>
|
||
<div class="flex space-x-2">
|
||
<textarea id="imagePrompt" rows="2"
|
||
class="flex-1 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-primary resize-none transition-all-300"
|
||
placeholder="描述您想要生成的图像..."></textarea>
|
||
<button id="randomImagePromptBtn"
|
||
class="p-3 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-all-300"
|
||
title="随机示例">
|
||
<i class="fa fa-random text-primary mr-1"></i>随机
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-4">
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">上传参考图片 <span
|
||
class="text-red-500">*</span></label>
|
||
<div id="imageUploadArea"
|
||
class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center hover:border-primary dark:hover:border-primary transition-all-300 cursor-pointer bg-gray-50 dark:bg-gray-900">
|
||
<input type="file" id="imageUpload" accept="image/*" class="hidden">
|
||
<i class="fa fa-cloud-upload text-4xl text-gray-400 mb-3"></i>
|
||
<p class="text-gray-500 dark:text-gray-400 mb-2">拖放图片到此处,或</p>
|
||
<button id="browseImage"
|
||
class="px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg transition-all-300">
|
||
浏览文件
|
||
</button>
|
||
</div>
|
||
<div id="imagePreview" class="hidden space-y-2">
|
||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">预览:</p>
|
||
<img id="previewImage" src="" alt="预览图片"
|
||
class="max-h-64 object-contain rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||
<button id="removeImage"
|
||
class="text-red-500 hover:text-red-600 text-sm flex items-center transition-colors">
|
||
<i class="fa fa-times-circle mr-1"></i> 移除图片
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
<div class="space-y-2">
|
||
<label for="imageStyle"
|
||
class="block text-sm font-medium text-gray-700 dark:text-gray-300">风格</label>
|
||
<select id="imageStyle"
|
||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-primary transition-all-300">
|
||
<option value="default">默认</option>
|
||
<option value="realistic">写实</option>
|
||
<option value="cartoon">卡通</option>
|
||
<option value="anime">动漫</option>
|
||
<option value="watercolor">水彩</option>
|
||
<option value="oil">油画</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<label for="imageAspectRatio"
|
||
class="block text-sm font-medium text-gray-700 dark:text-gray-300">比例</label>
|
||
<select id="imageAspectRatio"
|
||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-primary transition-all-300">
|
||
<option value="1:1">1:1 (正方形)</option>
|
||
<option value="16:9">16:9 (宽屏)</option>
|
||
<option value="9:16">9:16 (竖屏)</option>
|
||
<option value="4:3">4:3 (标准)</option>
|
||
<option value="3:2">3:2 (照片)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<label for="imageStrength" class="block text-sm font-medium text-gray-700 dark:text-gray-300">参考强度
|
||
(0-100)</label>
|
||
<input type="range" id="imageStrength" min="0" max="100" value="70"
|
||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-primary">
|
||
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
||
<span>更创意</span>
|
||
<span id="strengthValue">70</span>
|
||
<span>更相似</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 生成按钮 -->
|
||
<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>
|
||
</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">
|
||
<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>
|
||
</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">
|
||
<h3 class="text-xl font-semibold mb-2">生成结果</h3>
|
||
<p class="text-gray-600 dark:text-gray-300">您的图像已成功生成</p>
|
||
</div>
|
||
|
||
<div class="p-6 md:p-8 flex flex-col items-center">
|
||
<div id="resultImageContainer" class="relative mb-8 max-w-3xl mx-auto">
|
||
<img id="resultImage" src="" alt="生成的图像"
|
||
class="max-w-full h-auto rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg transform transition-all duration-500 hover:shadow-xl">
|
||
<div class="absolute top-4 right-4 flex space-x-2">
|
||
<button id="downloadBtn"
|
||
class="p-2 bg-white dark:bg-gray-800 rounded-full shadow-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-all-300"
|
||
title="下载图片">
|
||
<i class="fa fa-download"></i>
|
||
</button>
|
||
<button id="regenerateBtn"
|
||
class="p-2 bg-white dark:bg-gray-800 rounded-full shadow-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-all-300"
|
||
title="重新生成">
|
||
<i class="fa fa-refresh"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap justify-center gap-4 w-full max-w-2xl">
|
||
<button id="variationBtn"
|
||
class="flex-1 px-4 py-2 border border-primary text-primary hover:bg-primary/10 rounded-lg transition-all-300 font-medium flex items-center justify-center">
|
||
<i class="fa fa-random mr-2"></i>生成变体
|
||
</button>
|
||
<button id="upscaleBtn"
|
||
class="flex-1 px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg transition-all-300 font-medium flex items-center justify-center">
|
||
<i class="fa fa-search-plus mr-2"></i>放大图像
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
<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="艺术作品"
|
||
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>
|
||
|
||
<footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-8">
|
||
<div class="container mx-auto px-4">
|
||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||
<div class="flex items-center space-x-2 mb-4 md:mb-0">
|
||
<i class="fa fa-paint-brush text-primary text-xl"></i>
|
||
<h2 class="text-lg font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||
Midjourney AI</h2>
|
||
</div>
|
||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-4 md:mb-0">
|
||
© 2023 Midjourney AI. 保留所有权利。
|
||
</div>
|
||
<div class="flex space-x-4">
|
||
<a href="#"
|
||
class="text-gray-500 dark:text-gray-400 hover:text-primary dark:hover:text-primary transition-colors">
|
||
<i class="fa fa-github text-xl"></i>
|
||
</a>
|
||
<a href="#"
|
||
class="text-gray-500 dark:text-gray-400 hover:text-primary dark:hover:text-primary transition-colors">
|
||
<i class="fa fa-twitter text-xl"></i>
|
||
</a>
|
||
<a href="#"
|
||
class="text-gray-500 dark:text-gray-400 hover:text-primary dark:hover:text-primary transition-colors">
|
||
<i class="fa fa-instagram text-xl"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</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');
|
||
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;
|
||
}
|
||
|
||
// 提交生成请求
|
||
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>生成图像';
|
||
}
|
||
}
|
||
|
||
// 轮询任务状态函数
|
||
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
|
||
|
||
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 = '生成超时';
|
||
// 恢复生成按钮
|
||
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(data => {
|
||
queryCount++;
|
||
// 更新状态信息
|
||
const statusMessage = document.getElementById('statusMessage');
|
||
const progressFill = document.getElementById('progressBar');
|
||
const progressText = document.getElementById('progressText');
|
||
|
||
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 正在创作中...`;
|
||
|
||
// 更新进度条
|
||
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%
|
||
progress = 100;
|
||
progressFill.style.width = `${progress}%`;
|
||
progressText.textContent = `${Math.round(progress)}%`;
|
||
|
||
// 显示结果
|
||
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(checkTaskStatus, queryInterval);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('检查任务状态失败:', error);
|
||
// 只有在任务未完成且未超时时才继续查询
|
||
if (queryCount < maxQueries) {
|
||
timerId = setTimeout(checkTaskStatus, queryInterval);
|
||
} else {
|
||
// 恢复生成按钮
|
||
generateBtn.disabled = false;
|
||
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 立即检查一次
|
||
checkTaskStatus();
|
||
})
|
||
|
||
// 显示结果函数
|
||
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();
|
||
}
|
||
</script>
|
||
|
||
<!-- 添加随机示例功能的JavaScript -->
|
||
<script>
|
||
// 文生图示例提示词库
|
||
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> |