Files changed (1) hide show
  1. style.css +847 -299
style.css CHANGED
@@ -1,299 +1,847 @@
1
- /* style.css */
2
- :root {
3
- --bg-color: #0f172a;
4
- --panel-bg: rgba(30, 41, 59, 0.75);
5
- --panel-border: rgba(148, 163, 184, 0.15);
6
- --accent-color: #3b82f6;
7
- --text-main: #f1f5f9;
8
- --text-sub: #94a3b8;
9
- --btn-shadow: #1e3a8a;
10
- }
11
-
12
- * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
13
- body {
14
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
15
- background: var(--bg-color) radial-gradient(circle at 50% 0%, #1e293b 0%, var(--bg-color) 70%);
16
- color: var(--text-main);
17
- height: 100vh;
18
- display: flex;
19
- flex-direction: column;
20
- overflow: hidden;
21
- }
22
-
23
- /* --- 通用组件 --- */
24
- .glass-panel {
25
- background: var(--panel-bg);
26
- backdrop-filter: blur(16px);
27
- -webkit-backdrop-filter: blur(16px);
28
- border: 1px solid var(--panel-border);
29
- }
30
-
31
- .btn-3d {
32
- background: var(--accent-color);
33
- color: white;
34
- border: none;
35
- border-radius: 12px;
36
- font-weight: 600;
37
- box-shadow: 0 4px 0 var(--btn-shadow), 0 5px 10px rgba(0,0,0,0.2);
38
- transition: transform 0.1s, box-shadow 0.1s;
39
- cursor: pointer;
40
- display: inline-flex;
41
- align-items: center;
42
- justify-content: center;
43
- }
44
- .btn-3d:active { transform: translateY(4px); box-shadow: 0 0 0 var(--btn-shadow); }
45
- .btn-3d:disabled { background: #475569; box-shadow: none; transform: none; opacity: 0.7; }
46
-
47
- /* --- 布局 --- */
48
- .app-container {
49
- max-width: 1200px;
50
- margin: 0 auto;
51
- width: 100%;
52
- height: 100%;
53
- display: flex;
54
- flex-direction: column;
55
- position: relative;
56
- }
57
-
58
- header {
59
- padding: 15px 20px;
60
- display: flex;
61
- justify-content: space-between;
62
- align-items: center;
63
- flex-shrink: 0;
64
- }
65
- header h2 { font-size: 1.2rem; font-weight: 700; letter-spacing: 0.5px; }
66
-
67
- /* --- 历史记录区域 --- */
68
- .history-container {
69
- flex: 1;
70
- overflow-y: auto;
71
- padding: 10px 20px;
72
- padding-bottom: 180px; /* 给底部输入栏留空间 */
73
- }
74
-
75
- .grid-layout {
76
- display: grid;
77
- grid-template-columns: repeat(2, 1fr); /* 默认两列 */
78
- gap: 16px;
79
- }
80
-
81
- .history-item {
82
- position: relative;
83
- aspect-ratio: 16 / 9;
84
- border-radius: 12px;
85
- overflow: hidden;
86
- border: 1px solid var(--panel-border);
87
- background: rgba(0,0,0,0.2);
88
- cursor: pointer;
89
- }
90
- .history-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; }
91
- .history-item:hover img { transform: scale(1.05); }
92
-
93
- .item-badge {
94
- position: absolute;
95
- top: 8px; left: 8px;
96
- background: rgba(59, 130, 246, 0.9);
97
- font-size: 10px; padding: 2px 6px;
98
- border-radius: 4px; z-index: 2;
99
- }
100
-
101
- .item-actions {
102
- position: absolute;
103
- bottom: 0; right: 0; left: 0;
104
- padding: 10px;
105
- background: linear-gradient(transparent, rgba(0,0,0,0.8));
106
- display: flex;
107
- justify-content: flex-end;
108
- gap: 8px;
109
- opacity: 0;
110
- transition: opacity 0.2s;
111
- }
112
- .history-item:hover .item-actions { opacity: 1; }
113
- /* 移动端默认显示操作烂 */
114
- @media (hover: none) { .item-actions { opacity: 1; } }
115
-
116
- .icon-btn {
117
- width: 32px; height: 32px;
118
- border-radius: 8px;
119
- background: rgba(255,255,255,0.15);
120
- color: white;
121
- border: none;
122
- display: flex; align-items: center; justify-content: center;
123
- backdrop-filter: blur(4px);
124
- }
125
-
126
- /* --- 输入区域 (新设计) --- */
127
- .input-section {
128
- position: absolute; /* 浮动在底部 */
129
- bottom: 20px;
130
- left: 50%;
131
- transform: translateX(-50%);
132
- width: calc(100% - 40px);
133
- max-width: 800px;
134
- display: flex;
135
- flex-direction: column;
136
- border-radius: 20px;
137
- overflow: hidden;
138
- box-shadow: 0 10px 30px rgba(0,0,0,0.3);
139
- z-index: 100;
140
- }
141
-
142
- /* 1. 预览层 (位于输入框上方) */
143
- .preview-bar {
144
- padding: 0;
145
- max-height: 0;
146
- overflow-x: auto;
147
- overflow-y: hidden;
148
- background: rgba(15, 23, 42, 0.4);
149
- display: flex;
150
- gap: 8px;
151
- transition: all 0.3s ease;
152
- white-space: nowrap;
153
- }
154
- .preview-bar.visible {
155
- padding: 12px;
156
- max-height: 100px; /* 展开 */
157
- border-bottom: 1px solid var(--panel-border);
158
- }
159
-
160
- .thumb-wrapper {
161
- position: relative;
162
- width: 64px; height: 64px;
163
- flex-shrink: 0;
164
- border-radius: 8px;
165
- border: 1px solid rgba(255,255,255,0.1);
166
- overflow: hidden;
167
- }
168
- .thumb-wrapper img { width: 100%; height: 100%; object-fit: cover; }
169
- .thumb-remove {
170
- position: absolute; top: 2px; right: 2px;
171
- width: 16px; height: 16px;
172
- background: rgba(239,68,68,0.9);
173
- border-radius: 50%;
174
- display: flex; align-items: center; justify-content: center;
175
- font-size: 12px; cursor: pointer;
176
- }
177
-
178
- /* 2. 控制层 (按钮+输入框) */
179
- .control-bar {
180
- display: flex;
181
- align-items: flex-end;
182
- padding: 12px;
183
- gap: 10px;
184
- }
185
-
186
- .upload-trigger {
187
- width: 44px; height: 44px;
188
- border-radius: 12px;
189
- background: rgba(255,255,255,0.05);
190
- border: 1px dashed rgba(255,255,255,0.3);
191
- color: var(--text-sub);
192
- display: flex; align-items: center; justify-content: center;
193
- font-size: 20px;
194
- flex-shrink: 0;
195
- transition: all 0.2s;
196
- }
197
- .upload-trigger:hover, .upload-trigger.active {
198
- background: rgba(59, 130, 246, 0.2);
199
- border-color: var(--accent-color);
200
- color: var(--accent-color);
201
- }
202
-
203
- .main-input {
204
- flex: 1;
205
- background: transparent;
206
- border: none;
207
- color: white;
208
- font-size: 16px;
209
- padding: 10px 5px;
210
- resize: none;
211
- max-height: 120px;
212
- outline: none;
213
- line-height: 1.5;
214
- }
215
- .main-input::placeholder { color: rgba(255,255,255,0.3); }
216
-
217
- .send-btn {
218
- height: 44px;
219
- padding: 0 20px;
220
- font-size: 15px;
221
- flex-shrink: 0;
222
- }
223
-
224
- /* 加载动画 */
225
- .loader {
226
- width: 18px; height: 18px;
227
- border: 2px solid rgba(255,255,255,0.3);
228
- border-top-color: white;
229
- border-radius: 50%;
230
- animation: spin 0.8s linear infinite;
231
- display: none;
232
- }
233
- .loading .loader { display: block; }
234
- .loading span { display: none; }
235
- @keyframes spin { to { transform: rotate(360deg); } }
236
-
237
- /* --- 弹窗 --- */
238
- .modal {
239
- display: none;
240
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
241
- background: rgba(0,0,0,0.9);
242
- z-index: 1000;
243
- justify-content: center; align-items: center;
244
- backdrop-filter: blur(5px);
245
- }
246
- .modal-content {
247
- background: #1e293b;
248
- width: 95%; max-width: 1000px; height: 90%;
249
- border-radius: 16px;
250
- display: flex; flex-direction: column;
251
- position: relative;
252
- border: 1px solid var(--panel-border);
253
- }
254
- .close-modal {
255
- position: absolute; top: 15px; right: 15px;
256
- width: 36px; height: 36px;
257
- border-radius: 50%; background: rgba(0,0,0,0.5);
258
- color: white; border: 1px solid rgba(255,255,255,0.2);
259
- z-index: 10; font-size: 20px;
260
- }
261
- .modal-img-area { flex: 1; display: flex; justify-content: center; align-items: center; background: #000; overflow: hidden; border-radius: 16px 16px 0 0; }
262
- .modal-img-area img { max-width: 100%; max-height: 100%; object-fit: contain; }
263
- .modal-footer { padding: 15px; background: #1e293b; border-top: 1px solid var(--panel-border); display: flex; flex-direction: column; gap: 10px; }
264
- .input-refs { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 5px; }
265
- .ref-thumb { width: 40px; height: 40px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.2); cursor: pointer; }
266
- .prompt-display { background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; color: #cbd5e1; font-size: 0.9rem; max-height: 80px; overflow-y: auto; }
267
-
268
- /* --- 手机响应式设计 --- */
269
- @media (max-width: 768px) {
270
- .app-container { padding: 0; }
271
- .history-container { padding: 10px; }
272
-
273
- /* 历史记录一行一个 */
274
- .grid-layout {
275
- grid-template-columns: 1fr;
276
- gap: 20px;
277
- }
278
-
279
- /* 输入框贴底全宽 */
280
- .input-section {
281
- bottom: 0;
282
- width: 100%;
283
- max-width: 100%;
284
- border-radius: 20px 20px 0 0;
285
- border-bottom: none;
286
- }
287
-
288
- .history-container { padding-bottom: 160px; } /* 增加底部留白防止遮挡 */
289
-
290
- /* 调整按钮点击区域 */
291
- .send-btn { padding: 0 15px; min-width: 80px; }
292
- .preview-bar.visible { max-height: 80px; }
293
- .thumb-wrapper { width: 50px; height: 50px; }
294
- }
295
-
296
- /* 登录层 */
297
- #login-overlay { position: fixed; inset: 0; z-index: 5000; display: flex; justify-content: center; align-items: center; background: #0f172a; }
298
- .login-card { width: 300px; padding: 30px; text-align: center; }
299
- .login-card input { width: 100%; padding: 12px; margin: 15px 0; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: white; border-radius: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Banana Pro AI</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <style>
9
+ /* 调整输入框位置 - 向上移动 */
10
+ .input-section {
11
+ bottom: 60px !important; /* 向上移动到 60px */
12
+ }
13
+
14
+ /* 相应地调整历史容器的底部间距,避免内容被遮挡 */
15
+ .history-container {
16
+ padding-bottom: 220px !important; /* 增加底部间距 */
17
+ }
18
+
19
+ /* 移动端保持贴底设计 */
20
+ @media (max-width: 768px) {
21
+ .input-section {
22
+ bottom: 0 !important; /* 移动端依然贴底 */
23
+ }
24
+ .history-container {
25
+ padding-bottom: 160px !important; /* 移动端保持原样 */
26
+ }
27
+ }
28
+ </style>
29
+ </head>
30
+ <body>
31
+
32
+ <!-- 登录界面 -->
33
+ <div id="login-overlay">
34
+ <div class="login-card glass-panel">
35
+ <h1 style="font-size: 2rem;">🍌</h1>
36
+ <h3 style="margin: 10px 0; color: #cbd5e1;">Banana Pro</h3>
37
+ <input type="password" id="pwd" placeholder="Password" onkeypress="if(event.key==='Enter')doLogin()">
38
+ <button class="btn-3d" onclick="doLogin()" style="width: 100%; padding: 10px;">Unlock</button>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- 主应用 -->
43
+ <div class="app-container" id="app" style="filter: blur(10px); pointer-events: none;">
44
+ <header>
45
+ <div>
46
+ <h2>创意画板</h2>
47
+ <div style="font-size: 12px; color: var(--text-sub);" id="status-bar">Ready</div>
48
+ </div>
49
+ </header>
50
+
51
+ <!-- 历史画廊 -->
52
+ <div class="history-container">
53
+ <div class="grid-layout" id="gallery"></div>
54
+ </div>
55
+
56
+ <!-- 底部输入与预览组合区 -->
57
+ <div class="input-section glass-panel" id="drop-zone">
58
+ <!-- 上部分:预览条 -->
59
+ <div class="preview-bar" id="preview-bar"></div>
60
+
61
+ <!-- 下部分:操作栏 -->
62
+ <div class="control-bar">
63
+ <button class="upload-trigger" id="upload-btn" title="上传参考图">📷</button>
64
+ <textarea id="prompt" class="main-input" rows="1" placeholder="描述画面... (支持拖拽图片)"></textarea>
65
+ <button class="btn-3d send-btn" id="send-btn">
66
+ <span>生成</span>
67
+ <div class="loader"></div>
68
+ </button>
69
+ </div>
70
+ </div>
71
+
72
+ <input type="file" id="file-input" multiple accept="image/*" hidden>
73
+ </div>
74
+
75
+ <!-- 详情弹窗 -->
76
+ <div class="modal" id="modal">
77
+ <div class="modal-content glass-panel">
78
+ <button class="close-modal" onclick="closeModal()">×</button>
79
+ <div class="modal-img-area">
80
+ <img id="m-img" src="">
81
+ </div>
82
+ <div class="modal-footer">
83
+ <div class="input-refs" id="m-refs"></div>
84
+ <div class="prompt-display" id="m-prompt"></div>
85
+ <button class="btn-3d" id="m-reuse" style="width: 100%; padding: 12px;">
86
+ 🎨 复用参数与图片
87
+ </button>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <script>
93
+ // ============================================
94
+ // 应用配置
95
+ // ============================================
96
+ const CONFIG = {
97
+ DB_NAME: 'BananaProDB_v3',
98
+ DB_VERSION: 1,
99
+ STORE_NAME: 'artworks',
100
+ MAX_IMAGES: 16,
101
+ ERROR_IMAGE: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="%23333" width="100" height="100"/><text fill="%23666" x="50%" y="50%" text-anchor="middle" dy=".3em">Error</text></svg>'
102
+ };
103
+
104
+ // ============================================
105
+ // 全局状态管理
106
+ // ============================================
107
+ class AppState {
108
+ constructor() {
109
+ this.db = null;
110
+ this.currentImages = [];
111
+ this.galleryData = [];
112
+ this.currentModalItem = null;
113
+ this._listeners = new Map();
114
+ }
115
+
116
+ // 简单的事件系统
117
+ on(event, callback) {
118
+ if (!this._listeners.has(event)) {
119
+ this._listeners.set(event, new Set());
120
+ }
121
+ this._listeners.get(event).add(callback);
122
+ }
123
+
124
+ emit(event, data) {
125
+ if (this._listeners.has(event)) {
126
+ this._listeners.get(event).forEach(callback => callback(data));
127
+ }
128
+ }
129
+ }
130
+
131
+ const appState = new AppState();
132
+
133
+ // ============================================
134
+ // 工具函数
135
+ // ============================================
136
+ const Utils = {
137
+ // 防抖函数
138
+ debounce(func, wait) {
139
+ let timeout;
140
+ return function executedFunction(...args) {
141
+ const later = () => {
142
+ clearTimeout(timeout);
143
+ func(...args);
144
+ };
145
+ clearTimeout(timeout);
146
+ timeout = setTimeout(later, wait);
147
+ };
148
+ },
149
+
150
+ // 创建元素的辅助函数
151
+ createElement(tag, attrs = {}, children = []) {
152
+ const el = document.createElement(tag);
153
+ Object.entries(attrs).forEach(([key, value]) => {
154
+ if (key === 'className') {
155
+ el.className = value;
156
+ } else if (key === 'textContent') {
157
+ el.textContent = value;
158
+ } else if (key.startsWith('on')) {
159
+ el.addEventListener(key.slice(2).toLowerCase(), value);
160
+ } else {
161
+ el.setAttribute(key, value);
162
+ }
163
+ });
164
+ children.forEach(child => {
165
+ if (typeof child === 'string') {
166
+ el.appendChild(document.createTextNode(child));
167
+ } else {
168
+ el.appendChild(child);
169
+ }
170
+ });
171
+ return el;
172
+ }
173
+ };
174
+
175
+ // ============================================
176
+ // IndexedDB 模块
177
+ // ============================================
178
+ class Database {
179
+ static async init() {
180
+ return new Promise((resolve, reject) => {
181
+ const request = indexedDB.open(CONFIG.DB_NAME, CONFIG.DB_VERSION);
182
+
183
+ request.onerror = () => reject(new Error('数据库打开失败'));
184
+ request.onsuccess = () => {
185
+ appState.db = request.result;
186
+ resolve();
187
+ };
188
+
189
+ request.onupgradeneeded = (event) => {
190
+ const db = event.target.result;
191
+ if (!db.objectStoreNames.contains(CONFIG.STORE_NAME)) {
192
+ const store = db.createObjectStore(CONFIG.STORE_NAME, {
193
+ keyPath: 'id',
194
+ autoIncrement: true
195
+ });
196
+ store.createIndex('timestamp', 'timestamp', { unique: false });
197
+ }
198
+ };
199
+ });
200
+ }
201
+
202
+ static async transaction(storeName, mode, operation) {
203
+ return new Promise((resolve, reject) => {
204
+ const tx = appState.db.transaction([storeName], mode);
205
+ const store = tx.objectStore(storeName);
206
+
207
+ tx.oncomplete = () => resolve(result);
208
+ tx.onerror = () => reject(new Error('事务执行失败'));
209
+
210
+ let result;
211
+ try {
212
+ result = operation(store);
213
+ } catch (error) {
214
+ reject(error);
215
+ }
216
+ });
217
+ }
218
+
219
+ static async save(item) {
220
+ return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
221
+ const record = {
222
+ ...item,
223
+ timestamp: Date.now()
224
+ };
225
+ return store.add(record);
226
+ });
227
+ }
228
+
229
+ static async getAll() {
230
+ return new Promise((resolve, reject) => {
231
+ const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
232
+ const store = tx.objectStore(CONFIG.STORE_NAME);
233
+ const request = store.getAll();
234
+
235
+ request.onsuccess = () => {
236
+ const results = request.result.sort((a, b) => b.timestamp - a.timestamp);
237
+ resolve(results);
238
+ };
239
+ request.onerror = () => reject(new Error('读取数据失败'));
240
+ });
241
+ }
242
+
243
+ static async delete(id) {
244
+ return this.transaction(CONFIG.STORE_NAME, 'readwrite', (store) => {
245
+ return store.delete(id);
246
+ });
247
+ }
248
+
249
+ static async getById(id) {
250
+ return new Promise((resolve, reject) => {
251
+ const tx = appState.db.transaction([CONFIG.STORE_NAME], 'readonly');
252
+ const store = tx.objectStore(CONFIG.STORE_NAME);
253
+ const request = store.get(id);
254
+
255
+ request.onsuccess = () => resolve(request.result);
256
+ request.onerror = () => reject(new Error('获取数据失败'));
257
+ });
258
+ }
259
+ }
260
+
261
+ // ============================================
262
+ // 图片处理模块
263
+ // ============================================
264
+ class ImageHandler {
265
+ static fileToBase64(file) {
266
+ return new Promise((resolve, reject) => {
267
+ const reader = new FileReader();
268
+ reader.onload = () => resolve(reader.result);
269
+ reader.onerror = reject;
270
+ reader.readAsDataURL(file);
271
+ });
272
+ }
273
+
274
+ static async processFiles(files) {
275
+ const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
276
+ const promises = [];
277
+
278
+ for (const file of imageFiles) {
279
+ if (appState.currentImages.length >= CONFIG.MAX_IMAGES) {
280
+ alert(`最多只能上传 ${CONFIG.MAX_IMAGES} 张图片`);
281
+ break;
282
+ }
283
+ promises.push(this.fileToBase64(file));
284
+ }
285
+
286
+ try {
287
+ const results = await Promise.all(promises);
288
+ appState.currentImages.push(...results);
289
+ appState.emit('imagesChanged');
290
+ } catch (err) {
291
+ console.error('图片读取失败:', err);
292
+ alert('部分图片读取失败');
293
+ }
294
+ }
295
+
296
+ static removeAt(index) {
297
+ appState.currentImages.splice(index, 1);
298
+ appState.emit('imagesChanged');
299
+ }
300
+
301
+ static clear() {
302
+ appState.currentImages = [];
303
+ appState.emit('imagesChanged');
304
+ }
305
+
306
+ static setImages(images) {
307
+ appState.currentImages = images ? [...images] : [];
308
+ appState.emit('imagesChanged');
309
+ }
310
+ }
311
+
312
+ // ============================================
313
+ // UI组件基类
314
+ // ============================================
315
+ class Component {
316
+ constructor(container) {
317
+ this.container = typeof container === 'string'
318
+ ? document.getElementById(container)
319
+ : container;
320
+ }
321
+
322
+ render() {
323
+ throw new Error('render method must be implemented');
324
+ }
325
+ }
326
+
327
+ // ============================================
328
+ // 预览条管理模块
329
+ // ============================================
330
+ class PreviewManager extends Component {
331
+ constructor() {
332
+ super('preview-bar');
333
+ this.uploadBtn = document.getElementById('upload-btn');
334
+ this.statusBar = document.getElementById('status-bar');
335
+
336
+ // 监听图片变化
337
+ appState.on('imagesChanged', () => this.render());
338
+ }
339
+
340
+ render() {
341
+ const images = appState.currentImages;
342
+
343
+ // 使用 DocumentFragment 减少重排
344
+ const fragment = document.createDocumentFragment();
345
+
346
+ if (images.length === 0) {
347
+ this.container.classList.remove('visible');
348
+ this.uploadBtn.classList.remove('active');
349
+ this.statusBar.textContent = 'Ready';
350
+ this.container.innerHTML = '';
351
+ return;
352
+ }
353
+
354
+ this.container.classList.add('visible');
355
+ this.uploadBtn.classList.add('active');
356
+ this.statusBar.textContent = `已选择 ${images.length}/${CONFIG.MAX_IMAGES} 张图片`;
357
+
358
+ images.forEach((imgData, index) => {
359
+ const wrapper = Utils.createElement('div', { className: 'thumb-wrapper' }, [
360
+ Utils.createElement('img', { src: imgData }),
361
+ Utils.createElement('div', {
362
+ className: 'thumb-remove',
363
+ textContent: '×',
364
+ onClick: (e) => {
365
+ e.stopPropagation();
366
+ ImageHandler.removeAt(index);
367
+ }
368
+ })
369
+ ]);
370
+ fragment.appendChild(wrapper);
371
+ });
372
+
373
+ this.container.innerHTML = '';
374
+ this.container.appendChild(fragment);
375
+ }
376
+ }
377
+
378
+ // ============================================
379
+ // 画廊管理模块
380
+ // ============================================
381
+ class GalleryManager extends Component {
382
+ constructor() {
383
+ super('gallery');
384
+ this.renderDebounced = Utils.debounce(() => this.render(), 100);
385
+ }
386
+
387
+ async load() {
388
+ try {
389
+ appState.galleryData = await Database.getAll();
390
+ this.render();
391
+ } catch (err) {
392
+ console.error('加载画廊失败:', err);
393
+ alert('加载画廊失败,请刷新页面重试');
394
+ }
395
+ }
396
+
397
+ render() {
398
+ if (appState.galleryData.length === 0) {
399
+ this.container.innerHTML = `
400
+ <div style="grid-column: 1/-1; text-align: center; color: var(--text-sub); padding: 60px 20px;">
401
+ <div style="font-size: 48px; margin-bottom: 10px;">🎨</div>
402
+ <div>暂无作品,开始创作吧!</div>
403
+ </div>
404
+ `;
405
+ return;
406
+ }
407
+
408
+ const fragment = document.createDocumentFragment();
409
+ appState.galleryData.forEach(item => {
410
+ fragment.appendChild(this.createCard(item));
411
+ });
412
+
413
+ this.container.innerHTML = '';
414
+ this.container.appendChild(fragment);
415
+ }
416
+
417
+ createCard(item) {
418
+ const card = Utils.createElement('div', {
419
+ className: 'history-item',
420
+ 'data-id': item.id,
421
+ onClick: () => ModalManager.open(item)
422
+ });
423
+
424
+ // 参考图标记
425
+ if (item.inputImages?.length > 0) {
426
+ card.appendChild(Utils.createElement('div', {
427
+ className: 'item-badge',
428
+ textContent: `📎 ${item.inputImages.length}`
429
+ }));
430
+ }
431
+
432
+ // 主图片
433
+ const img = Utils.createElement('img', {
434
+ loading: 'lazy',
435
+ src: item.image,
436
+ onError: function() {
437
+ this.src = CONFIG.ERROR_IMAGE;
438
+ console.error('图片加载失败, ID:', item.id);
439
+ }
440
+ });
441
+ card.appendChild(img);
442
+
443
+ // 操作按钮
444
+ const actions = Utils.createElement('div', { className: 'item-actions' }, [
445
+ Utils.createElement('button', {
446
+ className: 'icon-btn',
447
+ innerHTML: '⬇',
448
+ onClick: (e) => {
449
+ e.stopPropagation();
450
+ this.downloadImage(item);
451
+ }
452
+ }),
453
+ Utils.createElement('button', {
454
+ className: 'icon-btn',
455
+ style: 'background: rgba(239,68,68,0.8)',
456
+ innerHTML: '🗑',
457
+ onClick: (e) => {
458
+ e.stopPropagation();
459
+ this.deleteItem(item.id);
460
+ }
461
+ })
462
+ ]);
463
+ card.appendChild(actions);
464
+
465
+ return card;
466
+ }
467
+
468
+ downloadImage(item) {
469
+ const link = Utils.createElement('a', {
470
+ href: item.image,
471
+ download: `banana-pro-${item.id}-${Date.now()}.png`
472
+ });
473
+ document.body.appendChild(link);
474
+ link.click();
475
+ document.body.removeChild(link);
476
+ }
477
+
478
+ async deleteItem(id) {
479
+ if (!confirm('确定要删除这张图片吗?')) return;
480
+
481
+ try {
482
+ await Database.delete(id);
483
+ await this.load();
484
+ } catch (err) {
485
+ console.error('删除失败:', err);
486
+ alert('删除失败,请重试');
487
+ }
488
+ }
489
+ }
490
+
491
+ // ============================================
492
+ // 弹窗管理模块
493
+ // ============================================
494
+ class ModalManager {
495
+ static init() {
496
+ this.modal = document.getElementById('modal');
497
+ this.imgEl = document.getElementById('m-img');
498
+ this.promptEl = document.getElementById('m-prompt');
499
+ this.refsEl = document.getElementById('m-refs');
500
+ this.reuseBtn = document.getElementById('m-reuse');
501
+
502
+ // 事件绑定
503
+ this.modal.addEventListener('click', (e) => {
504
+ if (e.target === this.modal) this.close();
505
+ });
506
+
507
+ document.addEventListener('keydown', (e) => {
508
+ if (e.key === 'Escape' && this.modal.style.display === 'flex') {
509
+ this.close();
510
+ }
511
+ });
512
+
513
+ this.reuseBtn.addEventListener('click', () => this.reuse());
514
+ }
515
+
516
+ static open(item) {
517
+ appState.currentModalItem = item;
518
+
519
+ this.imgEl.src = item.image;
520
+ this.promptEl.textContent = item.prompt;
521
+
522
+ // 渲染参考图
523
+ const fragment = document.createDocumentFragment();
524
+ if (item.inputImages?.length > 0) {
525
+ item.inputImages.forEach(imgData => {
526
+ const thumb = Utils.createElement('img', {
527
+ className: 'ref-thumb',
528
+ src: imgData,
529
+ onClick: () => window.open(imgData, '_blank')
530
+ });
531
+ fragment.appendChild(thumb);
532
+ });
533
+ }
534
+ this.refsEl.innerHTML = '';
535
+ this.refsEl.appendChild(fragment);
536
+
537
+ this.modal.style.display = 'flex';
538
+ }
539
+
540
+ static close() {
541
+ this.modal.style.display = 'none';
542
+ appState.currentModalItem = null;
543
+ }
544
+
545
+ static reuse() {
546
+ const item = appState.currentModalItem;
547
+ if (!item) return;
548
+
549
+ const textarea = document.getElementById('prompt');
550
+ textarea.value = item.prompt;
551
+ this.adjustTextareaHeight(textarea);
552
+
553
+ ImageHandler.setImages(item.inputImages || []);
554
+
555
+ this.close();
556
+ textarea.focus();
557
+ }
558
+
559
+ static adjustTextareaHeight(textarea) {
560
+ textarea.style.height = 'auto';
561
+ textarea.style.height = textarea.scrollHeight + 'px';
562
+ }
563
+ }
564
+
565
+ // ============================================
566
+ // 生成请求模块
567
+ // ============================================
568
+ class Generator {
569
+ constructor() {
570
+ this.sendBtn = document.getElementById('send-btn');
571
+ this.textarea = document.getElementById('prompt');
572
+ this.isGenerating = false;
573
+
574
+ this.bindEvents();
575
+ }
576
+
577
+ bindEvents() {
578
+ this.sendBtn.addEventListener('click', () => this.generate());
579
+
580
+ // 输入框自动高度
581
+ this.textarea.addEventListener('input', () => {
582
+ ModalManager.adjustTextareaHeight(this.textarea);
583
+ });
584
+
585
+ // 回车发送
586
+ this.textarea.addEventListener('keydown', (e) => {
587
+ if (e.key === 'Enter' && !e.shiftKey && !this.isGenerating) {
588
+ e.preventDefault();
589
+ this.generate();
590
+ }
591
+ });
592
+ }
593
+
594
+ setLoading(loading) {
595
+ this.isGenerating = loading;
596
+ this.sendBtn.classList.toggle('loading', loading);
597
+ this.sendBtn.disabled = loading;
598
+ }
599
+
600
+ async generate() {
601
+ const prompt = this.textarea.value.trim();
602
+ if (!prompt) {
603
+ alert('请输入提示词');
604
+ this.textarea.focus();
605
+ return;
606
+ }
607
+
608
+ this.setLoading(true);
609
+
610
+ try {
611
+ const response = await fetch('/api/generate', {
612
+ method: 'POST',
613
+ headers: { 'Content-Type': 'application/json' },
614
+ body: JSON.stringify({
615
+ prompt: prompt,
616
+ images: appState.currentImages
617
+ })
618
+ });
619
+
620
+ if (!response.ok) {
621
+ throw new Error(`HTTP error! status: ${response.status}`);
622
+ }
623
+
624
+ const data = await response.json();
625
+
626
+ if (!data.success || !data.image?.startsWith('data:image')) {
627
+ throw new Error(data.message || '生成失败:返回的图片数据无效');
628
+ }
629
+
630
+ // 保存到数据库
631
+ await Database.save({
632
+ prompt: prompt,
633
+ image: data.image,
634
+ inputImages: [...appState.currentImages]
635
+ });
636
+
637
+ // 刷新画廊
638
+ await galleryManager.load();
639
+
640
+ // 清空输入
641
+ this.textarea.value = '';
642
+ this.textarea.style.height = 'auto';
643
+ ImageHandler.clear();
644
+
645
+ } catch (err) {
646
+ console.error('生成失败:', err);
647
+ alert(`生成失败: ${err.message}`);
648
+ } finally {
649
+ this.setLoading(false);
650
+ }
651
+ }
652
+ }
653
+
654
+ // ============================================
655
+ // 认证模块
656
+ // ============================================
657
+ class Auth {
658
+ static async check() {
659
+ try {
660
+ const res = await fetch('/api/check-auth');
661
+ const data = await res.json();
662
+ return data.authenticated === true;
663
+ } catch {
664
+ return false;
665
+ }
666
+ }
667
+
668
+ static async login(password) {
669
+ try {
670
+ const res = await fetch('/api/login', {
671
+ method: 'POST',
672
+ headers: { 'Content-Type': 'application/json' },
673
+ body: JSON.stringify({ password })
674
+ });
675
+ const data = await res.json();
676
+ return data.success === true;
677
+ } catch {
678
+ return false;
679
+ }
680
+ }
681
+
682
+ static unlock() {
683
+ document.getElementById('login-overlay').style.display = 'none';
684
+ const app = document.getElementById('app');
685
+ app.style.filter = 'none';
686
+ app.style.pointerEvents = 'all';
687
+ }
688
+ }
689
+
690
+ // ============================================
691
+ // 拖拽上传模块
692
+ // ============================================
693
+ class DragDrop {
694
+ constructor(dropZone) {
695
+ this.dropZone = document.getElementById(dropZone);
696
+ this.dragCounter = 0;
697
+ this.bindEvents();
698
+ }
699
+
700
+ bindEvents() {
701
+ const events = ['dragenter', 'dragover', 'dragleave', 'drop'];
702
+
703
+ events.forEach(event => {
704
+ this.dropZone.addEventListener(event, this.preventDefaults);
705
+ });
706
+
707
+ this.dropZone.addEventListener('dragenter', () => {
708
+ this.dragCounter++;
709
+ this.highlight();
710
+ });
711
+
712
+ this.dropZone.addEventListener('dragleave', () => {
713
+ this.dragCounter--;
714
+ if (this.dragCounter === 0) {
715
+ this.unhighlight();
716
+ }
717
+ });
718
+
719
+ this.dropZone.addEventListener('drop', async (e) => {
720
+ this.dragCounter = 0;
721
+ this.unhighlight();
722
+
723
+ const files = e.dataTransfer.files;
724
+ if (files.length > 0) {
725
+ await ImageHandler.processFiles(files);
726
+ }
727
+ });
728
+ }
729
+
730
+ preventDefaults(e) {
731
+ e.preventDefault();
732
+ e.stopPropagation();
733
+ }
734
+
735
+ highlight() {
736
+ this.dropZone.style.borderColor = 'var(--accent-color)';
737
+ this.dropZone.style.background = 'rgba(59, 130, 246, 0.1)';
738
+ }
739
+
740
+ unhighlight() {
741
+ this.dropZone.style.borderColor = '';
742
+ this.dropZone.style.background = '';
743
+ }
744
+ }
745
+
746
+ // ============================================
747
+ // 文件选择模块
748
+ // ============================================
749
+ class FileSelector {
750
+ constructor() {
751
+ this.input = document.getElementById('file-input');
752
+ this.btn = document.getElementById('upload-btn');
753
+
754
+ this.bindEvents();
755
+ }
756
+
757
+ bindEvents() {
758
+ this.btn.addEventListener('click', () => this.input.click());
759
+
760
+ this.input.addEventListener('change', async () => {
761
+ if (this.input.files.length > 0) {
762
+ await ImageHandler.processFiles(this.input.files);
763
+ }
764
+ this.input.value = '';
765
+ });
766
+ }
767
+ }
768
+
769
+ // ============================================
770
+ // 全局实例
771
+ // ============================================
772
+ let previewManager, galleryManager, generator, dragDrop, fileSelector;
773
+
774
+ // ============================================
775
+ // 全局函数
776
+ // ============================================
777
+ async function doLogin() {
778
+ const pwd = document.getElementById('pwd').value.trim();
779
+ if (!pwd) {
780
+ document.getElementById('pwd').focus();
781
+ return;
782
+ }
783
+
784
+ const button = event.target;
785
+ button.disabled = true;
786
+
787
+ try {
788
+ const success = await Auth.login(pwd);
789
+ if (success) {
790
+ Auth.unlock();
791
+ await galleryManager.load();
792
+ } else {
793
+ alert('密码错误');
794
+ document.getElementById('pwd').value = '';
795
+ document.getElementById('pwd').focus();
796
+ }
797
+ } finally {
798
+ button.disabled = false;
799
+ }
800
+ }
801
+
802
+ function closeModal() {
803
+ ModalManager.close();
804
+ }
805
+
806
+ // ============================================
807
+ // 应用初始化
808
+ // ============================================
809
+ async function initApp() {
810
+ try {
811
+ // 初始化数据库
812
+ await Database.init();
813
+
814
+ // 初始化各模块
815
+ previewManager = new PreviewManager();
816
+ galleryManager = new GalleryManager();
817
+ ModalManager.init();
818
+ generator = new Generator();
819
+ dragDrop = new DragDrop('drop-zone');
820
+ fileSelector = new FileSelector();
821
+
822
+ // 检查认证状态
823
+ const isAuth = await Auth.check();
824
+ if (isAuth) {
825
+ Auth.unlock();
826
+ await galleryManager.load();
827
+ } else {
828
+ // 聚焦到密码输入框
829
+ document.getElementById('pwd').focus();
830
+ }
831
+
832
+ console.log('App initialized successfully');
833
+ } catch (err) {
834
+ console.error('App initialization failed:', err);
835
+ alert('应用初始化失败,请刷新页面重试');
836
+ }
837
+ }
838
+
839
+ // 启动应用
840
+ if (document.readyState === 'loading') {
841
+ document.addEventListener('DOMContentLoaded', initApp);
842
+ } else {
843
+ initApp();
844
+ }
845
+ </script>
846
+ </body>
847
+ </html>