From f4556a69d4e1440126bfae1a91fe8e269a731964 Mon Sep 17 00:00:00 2001 From: zwbcc Date: Sat, 28 Mar 2026 20:59:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=88=86=E7=A6=BB=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=BB=A3=E7=A0=81=E5=88=B0=20ui.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +- index.html | 319 +---------------------------------------------------- ui.js | 312 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 321 deletions(-) create mode 100644 ui.js diff --git a/README.md b/README.md index 67527bb..45a8d57 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,12 @@ systemctl --user enable --now image-generator.service ``` image-generator/ -├── index.html # 前端 -├── style.css # 样式 +├── index.html # HTML 页面 +├── style.css # 样式(深色主题) +├── ui.js # 前端交互逻辑 ├── app.js # 服务端(Express) ├── start.sh # 启动脚本 -├── config.json # API Key 存储(自动生成) +├── config.json # API Key 存储(自动生成,不提交) └── CHANGELOG.md ``` diff --git a/index.html b/index.html index 2df5bb7..5bd0141 100644 --- a/index.html +++ b/index.html @@ -219,323 +219,6 @@ - + diff --git a/ui.js b/ui.js new file mode 100644 index 0000000..b97e5fd --- /dev/null +++ b/ui.js @@ -0,0 +1,312 @@ +'use strict'; + +// ── State ────────────────────────────────────────────────────── + +let currentModel = 'image-01'; +let currentImages = []; // [{url, base64}] +let loading = false; + +// ── DOM refs ─────────────────────────────────────────────────── + +const $ = id => document.getElementById(id); + +const promptInput = $('promptInput'); +const charCount = $('charCount'); +const generateForm = $('generateForm'); +const generateBtn = $('generateBtn'); +const btnIcon = generateBtn.querySelector('.btn-icon'); +const btnText = generateBtn.querySelector('.btn-text'); +const spinner = generateBtn.querySelector('.spinner'); +const errorMsg = $('errorMsg'); +const resultArea = $('resultArea'); +const resultMeta = $('resultMeta'); +const imageGrid = $('imageGrid'); +const apiKeyAlert = $('apiKeyAlert'); +const alertSettings = $('alertSettingsBtn'); +const aspectRatio = $('aspectRatio'); +const responseFormat = $('responseFormat'); +const nInput = $('nInput'); +const nMinus = $('nMinus'); +const nPlus = $('nPlus'); +const seedInput = $('seedInput'); +const widthInput = $('widthInput'); +const heightInput = $('heightInput'); +const styleGroup = $('styleGroup'); +const styleType = $('styleType'); +const styleWeight = $('styleWeight'); +const weightHint = $('weightHint'); +const promptOptimizer= $('promptOptimizer'); +const watermark = $('watermark'); +const settingsModal = $('settingsModal'); +const openSettings = $('openSettings'); +const closeSettings = $('closeSettings'); +const cancelSettings = $('cancelSettings'); +const saveSettingsBtn= $('saveSettingsBtn'); +const apiKeyInput = $('apiKeyInput'); +const toggleKey = $('toggleKey'); +const baseUrlInput = $('baseUrlInput'); + +// ── Model tabs ───────────────────────────────────────────────── + +document.querySelectorAll('.model-tab').forEach(tab => { + tab.addEventListener('click', () => { + const model = tab.dataset.model; + setModel(model); + }); +}); + +function setModel(model) { + currentModel = model; + document.querySelectorAll('.model-tab').forEach(t => + t.classList.toggle('active', t.dataset.model === model) + ); + + const isLive = model === 'image-01-live'; + styleGroup.style.display = isLive ? '' : 'none'; + + // 21:9 only for image-01 + const opt21_9 = aspectRatio.querySelector('option[value="21:9"]'); + if (opt21_9) opt21_9.disabled = isLive; + if (isLive && aspectRatio.value === '21:9') aspectRatio.value = '16:9'; +} + +// ── Stepper (n) ──────────────────────────────────────────────── + +nMinus.addEventListener('click', () => { + const v = parseInt(nInput.value) - 1; + if (v >= 1) nInput.value = v; +}); + +nPlus.addEventListener('click', () => { + const v = parseInt(nInput.value) + 1; + if (v <= 9) nInput.value = v; +}); + +// ── Weight slider ────────────────────────────────────────────── + +styleWeight.addEventListener('input', () => { + weightHint.textContent = (styleWeight.value / 10).toFixed(1); +}); + +// ── Helpers ──────────────────────────────────────────────────── + +function setLoading(on) { + loading = on; + generateBtn.disabled = on; + btnIcon.style.display = on ? 'none' : ''; + btnText.style.display = on ? 'none' : ''; + spinner.style.display = on ? 'inline' : 'none'; + generateBtn.classList.toggle('loading', on); +} + +function showError(msg) { + errorMsg.textContent = msg; + errorMsg.style.display = 'flex'; + errorMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); +} + +function clearError() { + errorMsg.style.display = 'none'; +} + +async function api(path, opts = {}) { + const res = await fetch(path, opts); + const json = await res.json(); + if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`); + return json; +} + +function buildPayload() { + const n = parseInt(nInput.value); + const seed = seedInput.value ? parseInt(seedInput.value) : undefined; + const width = widthInput.value ? parseInt(widthInput.value) : undefined; + const height = heightInput.value ? parseInt(heightInput.value) : undefined; + + const payload = { + model: currentModel, + prompt: promptInput.value.trim(), + aspect_ratio: aspectRatio.value, + response_format: responseFormat.value, + n: n, + }; + + if (seed) payload.seed = seed; + if (width && height) { payload.width = width; payload.height = height; } + if (promptOptimizer.checked) payload.prompt_optimizer = true; + if (watermark.checked) payload.aigc_watermark = true; + + if (currentModel === 'image-01-live') { + payload.style = { + style_type: styleType.value, + style_weight: parseFloat((styleWeight.value / 10).toFixed(1)), + }; + } + + return payload; +} + +function renderResults(images, id, successCount, failedCount) { + currentImages = images; + imageGrid.innerHTML = ''; + + if (id) { + resultMeta.innerHTML = ` + 任务 ID:${id} + ${successCount != null ? `成功 ${successCount} 张${failedCount ? `,失败 ${failedCount} 张` : ''}` : ''} + `; + resultMeta.style.display = 'flex'; + } else { + resultMeta.style.display = 'none'; + } + + const fmt = responseFormat.value; + + images.forEach((src, i) => { + const card = document.createElement('div'); + card.className = 'image-card'; + + const img = document.createElement('img'); + img.src = fmt === 'url' ? src : `data:image/jpeg;base64,${src}`; + img.alt = `生成结果 ${i + 1}`; + img.loading = 'lazy'; + + const actions = document.createElement('div'); + actions.className = 'image-actions'; + + const downloadBtn = document.createElement('button'); + downloadBtn.className = 'action-btn'; + downloadBtn.innerHTML = `下载`; + downloadBtn.addEventListener('click', () => downloadImage(src, fmt, i)); + + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = `复制`; + copyBtn.addEventListener('click', () => copySrc(src, fmt, copyBtn)); + + actions.appendChild(downloadBtn); + actions.appendChild(copyBtn); + card.appendChild(img); + card.appendChild(actions); + imageGrid.appendChild(card); + }); + + resultArea.style.display = 'block'; + resultArea.scrollIntoView({ behavior: 'smooth', block: 'start' }); +} + +function downloadImage(src, fmt, index) { + const a = document.createElement('a'); + if (fmt === 'url') { + a.href = src; + } else { + a.href = `data:image/jpeg;base64,${src}`; + } + a.download = `生成-${Date.now()}-${index + 1}.jpg`; + a.target = '_blank'; + a.click(); +} + +async function copySrc(src, fmt, btn) { + try { + await navigator.clipboard.writeText(src); + const orig = btn.innerHTML; + btn.innerHTML = `已复制`; + setTimeout(() => { btn.innerHTML = orig; }, 1500); + } catch { + showError('复制失败'); + } +} + +// ── Init ──────────────────────────────────────────────────────── + +async function init() { + try { + const cfg = await api('/api/config'); + if (!cfg.hasApiKey) apiKeyAlert.style.display = 'flex'; + baseUrlInput.value = cfg.baseUrl || 'https://api.minimaxi.com'; + } catch { /* non-fatal */ } +} + +// ── Generate ──────────────────────────────────────────────────── + +generateForm.addEventListener('submit', async (e) => { + e.preventDefault(); + if (loading) return; + + const prompt = promptInput.value.trim(); + if (!prompt) { showError('请输入图片描述'); return; } + + clearError(); + resultArea.style.display = 'none'; + setLoading(true); + + try { + const payload = buildPayload(); + const data = await api('/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + const images = data.images || []; + if (images.length === 0) { + throw new Error('未返回任何图片'); + } + + renderResults(images, data.id, data.success_count, data.failed_count); + + } catch (err) { + showError(err.message); + } finally { + setLoading(false); + } +}); + +// ── Prompt char count ────────────────────────────────────────── + +promptInput.addEventListener('input', () => { + charCount.textContent = `${promptInput.value.length} / 1500`; +}); + +// ── Settings modal ────────────────────────────────────────────── + +function openModal() { + settingsModal.classList.add('open'); + apiKeyInput.focus(); +} + +function closeModal() { + settingsModal.classList.remove('open'); + clearError(); +} + +openSettings.addEventListener('click', openModal); +alertSettings.addEventListener('click', openModal); +closeSettings.addEventListener('click', closeModal); +cancelSettings.addEventListener('click', closeModal); +settingsModal.addEventListener('click', e => { if (e.target === settingsModal) closeModal(); }); + +toggleKey.addEventListener('click', () => { + apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password'; +}); + +saveSettingsBtn.addEventListener('click', async () => { + clearError(); + const apiKey = apiKeyInput.value.trim(); + const baseUrl = baseUrlInput.value.trim() || 'https://api.minimaxi.com'; + try { + await api('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ apiKey, baseUrl }), + }); + if (apiKey) apiKeyAlert.style.display = 'none'; + closeModal(); + } catch (err) { + showError(err.message); + } +}); + +// ── Start ────────────────────────────────────────────────────── + +init();