
之前很多小伙伴问我,能不能分享gpt-image2的生图源码,今天它来啦✨ 不管你是编程爱好者、开发者,还是想搭建自己的生图网站,这份源码直接安排,零保留分享!
✅ 源码完整可直接部署:无需二次开发,下载后按照教程操作,就能快速搭建属于自己的AI生图网站,省去从零开发的麻烦,小白开发者也能轻松上手
✅ 核心功能无删减:保留原网站全部核心能力——零门槛文字生图、高清无水印输出、多风格适配、快速出图,所有功能均可正常使用、自由修改
✅ 开源可定制:源码完全开源,支持自由修改功能、调整界面风格、添加专属模块,满足不同开发者的个性化需求,商用、自用都可(合规范围内);补充说明:源码接口调用的是第三方API,单次调用成本约0.04,成本透明,可根据自身需求调整调用频次;大家只需从指定链接注册,获取APIKey后放到网站对应配置处,即可直接使用,操作简单无门槛
✅ 附带详细部署教程:担心不会操作?配套完整图文教程,从下载、配置到部署,一步一步讲解,全程无难点,跟着做就能成功搭建
不管你是想练手编程、搭建个人项目,还是想批量部署生图网站,这份gpt-image2源码都能帮你少走弯路,省去大量开发时间!
无需转发、无需集赞,直接获取源码👇
【api接口地址】:接口地址
温馨提示:源码可自由修改,但禁止用于违规用途哦~ 拿到源码的小伙伴,欢迎评论区反馈部署效果,互相交流学习!🎉
gptimage2源码 #AI生图源码 #开源源码 #编程福利 #开发者必备 #生图网站搭建 #源码分享
源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>多米AI · 图像工坊</title>
<script src="https://cdn.tailwindcss.com">
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif;
background: #f5f7fb;
color: #1e1e2f;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ===== 滚动条 ===== */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, .2);
border-radius: 20px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(99, 102, 241, .4);
}
/* ===== 顶部导航 ===== */
.navbar-light {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(18px) saturate(1.6);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
/* ===== 卡片通用 ===== */
.card-light {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 24px;
transition: border-color 0.25s, box-shadow 0.3s;
}
.card-light:hover {
border-color: rgba(99, 102, 241, 0.15);
box-shadow: 0 12px 40px -12px rgba(0, 0, 0, 0.06);
}
/* ===== 输入框 ===== */
.input-light {
background: #ffffff;
border: 1px solid #e2e4ea;
border-radius: 14px;
padding: 12px 18px;
color: #1e1e2f;
font-size: 0.9rem;
transition: all 0.25s;
outline: none;
width: 100%;
}
.input-light:focus {
border-color: #818cf8;
box-shadow: 0 0 0 4px rgba(129, 140, 248, 0.10);
}
.input-light::placeholder {
color: #a0a3b1;
font-weight: 300;
}
/* ===== 文本域 ===== */
.textarea-light {
background: #ffffff;
border: 1px solid #e2e4ea;
border-radius: 18px;
padding: 18px 22px;
color: #1e1e2f;
font-size: 0.95rem;
line-height: 1.7;
transition: all 0.25s;
outline: none;
resize: vertical;
min-height: 100px;
width: 100%;
font-family: inherit;
}
.textarea-light:focus {
border-color: #818cf8;
box-shadow: 0 0 0 4px rgba(129, 140, 248, 0.08);
}
.textarea-light::placeholder {
color: #a0a3b1;
font-weight: 300;
}
/* ===== 按钮主色 ===== */
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border: none;
border-radius: 18px;
padding: 16px 32px;
font-weight: 600;
font-size: 1.05rem;
color: #ffffff;
cursor: pointer;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 28px -8px rgba(99, 102, 241, 0.35);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
letter-spacing: 0.2px;
}
.btn-primary:hover {
transform: scale(1.008);
box-shadow: 0 12px 40px -8px rgba(99, 102, 241, 0.45);
filter: brightness(1.04);
}
.btn-primary:active {
transform: scale(0.98);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
filter: none;
box-shadow: none;
}
/* ===== 比例按钮 ===== */
.ratio-btn {
background: #ffffff;
border: 1px solid #e2e4ea;
border-radius: 14px;
padding: 10px 8px;
text-align: center;
cursor: pointer;
transition: all 0.25s;
user-select: none;
}
.ratio-btn:hover {
border-color: #a5b4fc;
background: #f8f9ff;
}
.ratio-btn.active {
background: #6366f1;
border-color: #6366f1;
color: #ffffff;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.20);
}
.ratio-btn.active .desc {
color: rgba(255, 255, 255, 0.7);
}
.ratio-btn .label {
font-size: 1rem;
font-weight: 700;
font-family: 'Inter', monospace;
letter-spacing: 0.3px;
}
.ratio-btn .desc {
font-size: 0.65rem;
color: #9ca3af;
margin-top: 1px;
}
.ratio-btn.active .desc {
color: rgba(255, 255, 255, 0.7);
}
/* ===== 灵感标签 ===== */
.prompt-tag {
background: #f8f9ff;
border: 1px solid #e8eaf0;
border-radius: 100px;
padding: 8px 16px;
font-size: 0.8rem;
color: #4b4d5c;
cursor: pointer;
transition: all 0.25s;
user-select: none;
display: inline-block;
line-height: 1.4;
}
.prompt-tag:hover {
background: #eef0ff;
border-color: #a5b4fc;
color: #4338ca;
transform: translateY(-1px);
}
/* ===== 历史卡片 ===== */
.history-card {
border-radius: 14px;
overflow: hidden;
background: #ffffff;
border: 1px solid #f0f1f5;
transition: all 0.3s;
cursor: pointer;
}
.history-card:hover {
border-color: #d0d5f0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
transform: translateY(-2px);
}
.history-card img {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
display: block;
background: #f5f6fa;
}
.history-card .info {
padding: 10px 12px 12px;
}
.history-card .info p {
font-size: 0.75rem;
color: #6b7280;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.5;
}
/* ===== 结果卡片 ===== */
.result-card {
border-radius: 18px;
overflow: hidden;
background: #ffffff;
border: 1px solid #f0f1f5;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.result-card:hover {
transform: translateY(-4px) scale(1.01);
border-color: #c7d2fe;
box-shadow: 0 16px 48px -12px rgba(99, 102, 241, 0.10);
}
.result-card img {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
display: block;
background: #f5f6fa;
}
.result-card .actions {
padding: 12px 14px 14px;
display: flex;
gap: 8px;
}
.result-card .actions button {
flex: 1;
padding: 8px 0;
border-radius: 12px;
border: none;
background: #f5f6fa;
color: #4b4d5c;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.result-card .actions button:hover {
background: #eef0ff;
color: #4338ca;
}
/* ===== 进度条 ===== */
.progress-track {
height: 4px;
background: #f0f1f5;
border-radius: 20px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #6366f1, #8b5cf6, #22d3ee);
border-radius: 20px;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
width: 0%;
}
/* ===== Toast ===== */
.toast {
position: fixed;
bottom: 32px;
right: 32px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(16px);
border: 1px solid rgba(99, 102, 241, 0.12);
border-radius: 18px;
padding: 14px 24px;
color: #1e1e2f;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.12);
opacity: 0;
transform: translateY(12px) scale(0.96);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 999;
}
.toast.show {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.toast i {
font-size: 1.1rem;
color: #6366f1;
}
/* ===== 状态指示器 ===== */
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
background: #6366f1;
animation: pulse-dot 1.6s ease-in-out infinite;
}
@keyframes pulse-dot {
0%,
100% {
opacity: 0.4;
transform: scale(0.9);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
/* ===== 辅助 ===== */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===== 响应式 ===== */
@media (max-width: 768px) {
.navbar-light .row {
flex-direction: column;
gap: 10px;
}
.btn-primary {
font-size: 0.95rem;
padding: 14px 18px;
}
.ratio-btn .label {
font-size: 0.9rem;
}
.toast {
bottom: 16px;
right: 16px;
left: 16px;
border-radius: 14px;
padding: 12px 18px;
font-size: 0.8rem;
}
.result-card .actions button {
font-size: 0.7rem;
padding: 6px 0;
}
}
/* ===== 选择框 ===== */
select.input-light {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 16px center;
padding-right: 40px;
cursor: pointer;
}
select.input-light option {
background: #ffffff;
color: #1e1e2f;
}
/* ===== 徽章 ===== */
.badge-soft {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 500;
background: #f0f1ff;
color: #6366f1;
}
</style>
</head>
<body>
<!-- ===== 顶部导航 ===== -->
<header class="navbar-light sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-5 md:px-8 py-4 flex items-center justify-between gap-4 row">
<div class="flex items-center gap-4 shrink-0">
<div class="flex items-center gap-2.5">
<span class="w-9 h-9 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-sm shadow-indigo-200">✦</span>
<span class="text-lg font-semibold tracking-tight text-gray-800">多米<span class="text-indigo-500">AI</span></span>
</div>
<span class="hidden sm:inline text-xs text-gray-400 font-light border-l border-gray-200/60 pl-4">图像工坊</span>
</div>
<div class="flex items-center gap-3 w-full max-w-md">
<div class="relative flex-1">
<i class="fas fa-key absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
<input id="apiKey" type="password" placeholder="API Key · 一次保存,永久使用" class="input-light pl-10 pr-4 text-sm" />
</div>
<button onclick="toggleKeyVisibility()" class="w-10 h-10 rounded-xl bg-gray-100 hover:bg-gray-200 flex items-center justify-center shrink-0 transition-colors border border-gray-200/60">
<i id="keyEye" class="fas fa-eye text-gray-500 text-sm"></i>
</button>
</div>
</div>
</header>
<!-- ===== 主布局 ===== -->
<div class="max-w-7xl mx-auto px-5 md:px-8 py-8 flex gap-8">
<!-- ===== 侧边栏 · 历史 ===== -->
<aside class="hidden xl:block w-64 shrink-0">
<div class="card-light p-5 sticky top-28 max-h-[calc(100vh-10rem)] overflow-y-auto">
<div class="flex items-center justify-between mb-5">
<h3 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
<i class="fas fa-clock-rotate-left text-indigo-400 text-xs"></i> 生成历史
</h3>
<button onclick="clearHistory()" class="text-xs text-gray-400 hover:text-red-500 transition-colors">
<i class="fas fa-trash-can"></i>
</button>
</div>
<div id="history" class="space-y-3"></div>
<div id="historyEmpty" class="text-center py-10 text-gray-400 text-xs">
<i class="fas fa-image text-xl mb-3 block opacity-50"></i>
暂无记录
</div>
</div>
</aside>
<!-- ===== 主内容 ===== -->
<main class="flex-1 min-w-0">
<!-- Hero -->
<div class="text-center mb-10">
<h1 class="text-4xl md:text-5xl font-extrabold tracking-tight text-gray-900 leading-tight">
AI <span class="text-indigo-500">爆款</span>工坊
</h1>
<p class="mt-3 text-gray-500 text-sm md:text-base flex items-center justify-center gap-2 flex-wrap">
<span class="badge-soft">
<i class="fas fa-sparkles text-indigo-400 text-[10px]"></i> image2 4K
</span>
<span class="text-gray-300">·</span>
<span>多种比例</span>
<span class="text-gray-300">·</span>
<span>海量灵感</span>
</p>
</div>
<!-- ===== 生成卡片 ===== -->
<div class="card-light p-6 md:p-8 shadow-sm">
<!-- 提示词 -->
<textarea id="prompt" class="textarea-light" placeholder="描述你的爆款图片,或点击下方灵感标签一键使用 …"></textarea>
<!-- 灵感标签库 -->
<div class="mt-7">
<div class="flex items-center justify-between mb-4">
<span class="text-xs text-gray-500 flex items-center gap-2">
<i class="fas fa-lightbulb text-amber-500/80 text-[10px]"></i> 精选灵感 · 点击即用
</span>
<button onclick="shufflePrompts()" class="text-xs px-4 py-1.5 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-500 transition-colors flex items-center gap-1.5 border border-gray-200/60">
<i class="fas fa-arrows-rotate text-[10px]"></i> 换一批
</button>
</div>
<div id="promptLibrary" class="flex flex-wrap gap-2 max-h-44 overflow-y-auto pr-1"></div>
</div>
<!-- 比例 -->
<div class="mt-7">
<span class="text-xs text-gray-500 flex items-center gap-2 mb-3">
<i class="fas fa-crop text-[10px]"></i> 输出比例
</span>
<div id="ratioButtons" class="grid grid-cols-5 gap-2.5"></div>
</div>
<!-- 参数行 -->
<div class="mt-7 grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs text-gray-500 mb-1.5 font-medium">模型</label>
<select id="model" class="input-light">
<option value="gpt-image-2">image2 4K · 推荐</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1.5 font-medium">生成数量</label>
<select id="n" class="input-light">
<option value="1">1 张</option>
<option value="2" selected>2 张</option>
<option value="4">4 张</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1.5 font-medium">参考图像(可选)</label>
<input id="refImage" type="text" placeholder="https://..." class="input-light text-sm" />
</div>
</div>
<!-- 生成按钮 -->
<button onclick="generateImage()" id="generateBtn" class="btn-primary mt-8">
<i class="fas fa-bolt"></i>
<span id="btnText">生成爆款图片</span>
</button>
</div>
<!-- ===== 结果区域 ===== -->
<div id="resultArea" class="mt-10 hidden">
<div class="card-light p-6 md:p-8 shadow-sm">
<!-- 状态头 -->
<div class="flex items-center justify-between mb-5">
<div class="flex items-center gap-3">
<span id="statusIcon" class="status-dot"></span>
<div>
<div id="statusText" class="text-base font-semibold text-gray-800">任务提交中 …</div>
<div id="taskIdDisplay" class="text-[11px] text-gray-400 font-mono mt-0.5"></div>
</div>
</div>
<button onclick="cancelTask()" class="text-xs px-4 py-2 rounded-xl bg-gray-100 hover:bg-red-100 text-gray-500 hover:text-red-600 transition-colors border border-gray-200/60">
<i class="fas fa-xmark mr-1.5"></i>取消
</button>
</div>
<!-- 进度 -->
<div class="progress-track mb-8">
<div id="progress" class="progress-bar"></div>
</div>
<!-- 图片网格 -->
<div id="images" class="grid grid-cols-1 sm:grid-cols-2 gap-5"></div>
</div>
</div>
</main>
</div>
<!-- ===== Toast ===== -->
<div id="toast" class="toast">
<i id="toastIcon" class="fas fa-check-circle"></i>
<span id="toastMsg">已完成</span>
</div>
<script>
// ============================================================
// 状态
// ============================================================
let currentTaskId = null;
let pollingInterval = null;
let history = JSON.parse(localStorage.getItem('duomi_history') || '[]');
let promptLibrary = [
"赛博朋克东京夜景,雨夜霓虹灯反射在湿漉漉的街道上,飞行汽车穿梭,超现实主义,电影级光影,4K",
"梦幻治愈系森林小屋,夕阳洒进窗户,飘浮的萤火虫,温暖色调,吉卜力风格,超详细",
"一位身穿汉服的古代女子站在长城上眺望,晨雾缭绕,金色阳光,史诗级电影感,写实摄影",
"蒸汽朋克风格的巨大飞艇在天空中航行,云海翻腾,铜管机械细节丰富,复古色彩",
"极简日式禅意庭院,枯山水,晨雾弥漫,极简构图,宁静氛围,4K超清",
"水下梦幻珊瑚城堡,彩色鱼群游弋,光线从海面穿透,魔幻现实主义",
"未来赛博格少女在霓虹城市中漫步,反射在玻璃上的全息投影,cyberpunk 2077风格",
"中国古风山水画意境,墨色淡彩,山川云雾,飞鸟点缀,国风插画大师级",
"一只戴着王冠的胖橘猫坐在宝座上,宫廷背景,搞笑奢华,卡通写实混合",
"黄金时刻的薰衣草花田,一位少女奔跑其中,长发飞扬,电影画面,温暖光影",
"黑暗奇幻:黑龙盘踞在古老城堡废墟上,月光照耀,史诗级概念艺术",
"极简产品渲染:透明玻璃香水瓶,漂浮在水面,反射梦幻光斑,工作室灯光",
"80年代复古霓虹街机厅,赛博朋克少女玩游戏,胶片颗粒,怀旧氛围",
"魔法森林中的精灵少女,长发飘逸,萤火虫环绕,梦幻光效,吉卜力+写实",
"雪山湖泊日出,镜面反射,极致风景摄影,4K国家地理级别"
];
const ratios = [
{ label: "9:16", value: "1024x1792", desc: "竖版" },
{ label: "3:4", value: "1152x1536", desc: "主图" },
{ label: "1:1", value: "1024x1024", desc: "正方" },
{ label: "16:9", value: "1792x1024", desc: "横版" },
{ label: "21:9", value: "1792x768", desc: "宽屏" }
];
// ============================================================
// API Key 持久化
// ============================================================
function loadApiKey() {
const saved = localStorage.getItem('duomi_api_key');
if (saved) {
document.getElementById('apiKey').value = saved;
}
}
function saveApiKey() {
const key = document.getElementById('apiKey').value.trim();
if (key) {
localStorage.setItem('duomi_api_key', key);
} else {
localStorage.removeItem('duomi_api_key');
}
}
// 输入时自动保存
document.addEventListener('DOMContentLoaded', () => {
loadApiKey();
document.getElementById('apiKey').addEventListener('input', saveApiKey);
});
// 切换 Key 可见性
function toggleKeyVisibility() {
const el = document.getElementById('apiKey');
const eye = document.getElementById('keyEye');
if (el.type === 'password') {
el.type = 'text';
eye.className = 'fas fa-eye-slash text-gray-500 text-sm';
} else {
el.type = 'password';
eye.className = 'fas fa-eye text-gray-500 text-sm';
}
}
// ============================================================
// 渲染函数
// ============================================================
function renderPromptLibrary() {
const container = document.getElementById('promptLibrary');
container.innerHTML = '';
promptLibrary.forEach(text => {
const tag = document.createElement('span');
tag.className = 'prompt-tag';
tag.textContent = text.length > 58 ? text.slice(0, 55) + '…' : text;
tag.title = text;
tag.onclick = () => {
document.getElementById('prompt').value = text;
showToast('灵感已加载', 'fa-lightbulb');
};
container.appendChild(tag);
});
}
function shufflePrompts() {
promptLibrary = promptLibrary.sort(() => Math.random() - 0.5);
renderPromptLibrary();
showToast('灵感已刷新', 'fa-arrows-rotate');
}
function renderRatios() {
const container = document.getElementById('ratioButtons');
container.innerHTML = '';
ratios.forEach((r, i) => {
const btn = document.createElement('div');
btn.className = `ratio-btn ${i === 3 ? 'active' : ''}`;
btn.innerHTML = `<div class="label">${r.label}</div><div class="desc">${r.desc}</div>`;
btn.onclick = () => {
document.querySelectorAll('.ratio-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
};
container.appendChild(btn);
});
}
function renderHistory() {
const container = document.getElementById('history');
const empty = document.getElementById('historyEmpty');
container.innerHTML = '';
if (!history.length) {
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
history.slice(0, 10).forEach(item => {
const div = document.createElement('div');
div.className = 'history-card';
div.innerHTML = `
<img src="${item.images[0]}" alt="历史" loading="lazy" />
<div class="info">
<p>${item.prompt}</p>
</div>
`;
div.onclick = () => showResult(item);
container.appendChild(div);
});
}
// ============================================================
// 核心生成
// ============================================================
async function generateImage() {
const apiKey = document.getElementById('apiKey').value.trim();
if (!apiKey) {
showToast('请先输入 API Key', 'fa-key');
document.getElementById('apiKey').focus();
return;
}
// 再次保存
localStorage.setItem('duomi_api_key', apiKey);
let prompt = document.getElementById('prompt').value.trim();
if (!prompt) {
showToast('请输入或选择提示词', 'fa-pencil');
return;
}
const btn = document.getElementById('generateBtn');
btn.disabled = true;
document.getElementById('btnText').innerHTML = `<i class="fas fa-spinner fa-spin"></i> 提交中 …`;
// 显示结果区
const area = document.getElementById('resultArea');
area.classList.remove('hidden');
document.getElementById('statusText').textContent = '任务已提交,正在生成 …';
document.getElementById('statusIcon').className = 'status-dot';
document.getElementById('progress').style.width = '20%';
document.getElementById('images').innerHTML = '';
// 获取当前比例
const activeRatio = document.querySelector('.ratio-btn.active');
const size = activeRatio ? activeRatio.querySelector('.label').textContent : '16:9';
const sizeMap = {
'9:16': '1024x1792',
'3:4': '1152x1536',
'1:1': '1024x1024',
'16:9': '1792x1024',
'21:9': '1792x768'
};
const finalSize = sizeMap[size] || '1792x1024';
const payload = {
model: 'gpt-image-2',
prompt: prompt,
size: finalSize,
n: parseInt(document.getElementById('n').value)
};
try {
const res = await fetch('https://duomiapi.com/v1/images/generations?async=true', {
method: 'POST',
headers: {
'Authorization': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.id) {
currentTaskId = data.id;
document.getElementById('taskIdDisplay').textContent = `Task: ${currentTaskId}`;
document.getElementById('progress').style.width = '40%';
startPolling(apiKey);
} else {
showToast('提交失败,请检查 API Key', 'fa-circle-exclamation');
area.classList.add('hidden');
}
} catch (e) {
showToast('网络错误,请重试', 'fa-wifi-slash');
area.classList.add('hidden');
} finally {
btn.disabled = false;
document.getElementById('btnText').innerHTML = `<i class="fas fa-bolt"></i> 生成爆款图片`;
}
}
function startPolling(apiKey) {
if (pollingInterval) clearInterval(pollingInterval);
let attempts = 0;
pollingInterval = setInterval(async () => {
attempts++;
try {
const res = await fetch(`https://duomiapi.com/v1/tasks/${currentTaskId}`, {
headers: { 'Authorization': apiKey }
});
const data = await res.json();
if (data.state === 'succeeded') {
clearInterval(pollingInterval);
pollingInterval = null;
const images = data.data.images.map(i => i.url);
const resultData = {
prompt: document.getElementById('prompt').value,
images,
ratio: '16:9'
};
showResult(resultData);
saveToHistory(resultData);
document.getElementById('progress').style.width = '100%';
showToast('4K 爆款生成完成 🎉', 'fa-sparkles');
document.getElementById('statusIcon').className = 'w-3 h-3 rounded-full bg-emerald-500';
document.getElementById('statusText').innerHTML =
'生成完成 <span class="text-emerald-600 font-semibold">✓ 4K</span>';
} else if (data.state === 'failed') {
clearInterval(pollingInterval);
pollingInterval = null;
showToast('生成失败,请重试', 'fa-circle-exclamation');
document.getElementById('statusIcon').className = 'w-3 h-3 rounded-full bg-red-400';
document.getElementById('statusText').textContent = '生成失败';
} else {
const p = Math.min(40 + attempts * 6, 92);
document.getElementById('progress').style.width = p + '%';
document.getElementById('statusText').textContent = `正在生成 … ${Math.round(p)}%`;
}
} catch (e) {
// silent
}
}, 2600);
}
function showResult(data) {
const container = document.getElementById('images');
container.innerHTML = '';
data.images.forEach(url => {
const card = document.createElement('div');
card.className = 'result-card';
card.innerHTML = `
<img src="${url}" alt="生成结果" loading="lazy" />
<div class="actions">
<button onclick="downloadImage('${url}');event.stopImmediatePropagation()">
<i class="fas fa-download"></i> 下载
</button>
<button onclick="copyImageUrl('${url}');event.stopImmediatePropagation()">
<i class="fas fa-link"></i> 复制
</button>
</div>
`;
card.onclick = () => window.open(url, '_blank');
container.appendChild(card);
});
document.getElementById('resultArea').classList.remove('hidden');
}
function saveToHistory(data) {
history.unshift(data);
if (history.length > 20) history.pop();
localStorage.setItem('duomi_history', JSON.stringify(history));
renderHistory();
}
function clearHistory() {
if (!confirm('确定清空所有历史记录?')) return;
history = [];
localStorage.setItem('duomi_history', JSON.stringify(history));
renderHistory();
showToast('历史已清空', 'fa-trash-can');
}
// ============================================================
// 工具函数
// ============================================================
function downloadImage(url) {
const a = document.createElement('a');
a.href = url;
a.download = `domi-${Date.now()}.jpg`;
a.click();
showToast('下载已开始', 'fa-download');
}
function copyImageUrl(url) {
navigator.clipboard.writeText(url).then(() => {
showToast('链接已复制', 'fa-link');
}).catch(() => {
showToast('复制失败', 'fa-triangle-exclamation');
});
}
function cancelTask() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
document.getElementById('resultArea').classList.add('hidden');
showToast('已取消', 'fa-ban');
}
let toastTimer = null;
function showToast(msg, icon = 'fa-check-circle') {
const toast = document.getElementById('toast');
document.getElementById('toastIcon').className = `fas ${icon}`;
document.getElementById('toastMsg').textContent = msg;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.remove('show'), 2800);
}
// ============================================================
// 初始化
// ============================================================
window.onload = () => {
renderRatios();
renderPromptLibrary();
renderHistory();
console.log('%c✦ 多米AI 图像工坊 ✦', 'color:#6366f1;font-size:18px;font-weight:600;');
console.log('%cAPI Key 已本地持久化,刷新无需重新输入', 'color:#22d3ee;font-size:12px;');
};
</script>
</body>
</html>

