Files
crawler-plugin/app/web_source/index.html
铭坤 a5da06537e modified: blueprints/__pycache__/__init__.cpython-39.pyc
modified:   blueprints/__pycache__/admin.cpython-39.pyc
	modified:   blueprints/__pycache__/auth.cpython-39.pyc
	modified:   blueprints/__pycache__/brand.cpython-311.pyc
	modified:   blueprints/__pycache__/brand.cpython-39.pyc
	modified:   blueprints/brand.py
	new file:   brand_spider/__pycache__/main.cpython-311.pyc
	modified:   brand_spider/__pycache__/main.cpython-39.pyc
	new file:   brand_spider/__pycache__/web_dec.cpython-311.pyc
	modified:   brand_spider/main.py
	modified:   config.py
	modified:   web_source/admin.html
	modified:   web_source/brand.html
	modified:   web_source/home.html
	modified:   web_source/index.html
	modified:   web_source/login.html
	new file:   web_source/templates_backup/admin.html
	new file:   web_source/templates_backup/brand.html
	new file:   web_source/templates_backup/home.html
	new file:   web_source/templates_backup/index.html
	new file:   web_source/templates_backup/login.html
2026-03-26 16:39:39 +08:00

5834 lines
263 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", "Noto Serif SC", "SimSun", "Songti SC", "Times New Roman", serif;
background: rgba(13, 13, 13, 1);
color: #e0e0e0;
overflow: hidden;
height: 100vh;
user-select: none;
font-weight: bold;
}
/* 顶部栏 */
.top-bar {
height: 56px;
/*background: #1a1a1a;*/
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid #2a2a2a;
-webkit-app-region: drag;
app-region: drag;
}
.logo-area {
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 36px;
height: 36px;
border-radius: 8px;
object-fit: cover; /* 或 contain根据需要选择 */
}
.app-name {
font-size: 18px;
font-weight: 600;
color: #fff;
}
.btn-home {
font-size: 13px;
color: #999;
text-decoration: none;
padding: 8px 12px;
margin-right: 8px;
border-radius: 6px;
transition: all 0.2s;
-webkit-app-region: no-drag;
app-region: no-drag;
}
.btn-home:hover {
color: #fff;
background: #2a2a2a;
font-size: medium;
}
.nav-tabs {
display: flex;
align-items: center;
gap: 0;
-webkit-app-region: no-drag;
app-region: no-drag;
}
.nav-tab-group {
display: flex;
align-items: center;
gap: 6px;
background: rgba(77, 72, 72, 0.99);
border-radius: 10px;
}
.nav-tab-sep {
width: 1px;
height: 20px;
background: linear-gradient(to bottom, transparent, #444 15%, #444 85%, transparent);
margin: 0 10px;
flex-shrink: 0;
opacity: 0.9;
}
.nav-tab {
padding: 8px 14px;
font-size: 13px;
color: #999;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.nav-tab:hover {
color: #fff;
background: #2a2a2a;
}
.nav-tab.active {
color: #3498db;
background: rgba(52, 152, 219, 0.2);
}
.top-right {
display: flex;
align-items: center;
gap: 16px;
-webkit-app-region: no-drag;
app-region: no-drag;
}
.vx-text {
font-size: 12px;
color: #888;
}
.icon-btn {
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s;
}
.icon-btn:hover {
color: #fff;
background: #2a2a2a;
}
.window-controls {
display: flex;
gap: 2px;
}
.window-btn {
width: 46px;
height: 32px;
background: transparent;
border: none;
color: #999;
cursor: pointer;
font-size: 10px;
transition: all 0.2s;
}
.window-btn:hover {
background: #2a2a2a;
color: #fff;
}
.window-btn.close:hover {
background: #e74c3c;
color: #fff;
}
.btn-settings {
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s;
}
.btn-settings:hover {
color: #3498db;
background: rgba(52, 152, 219, 0.15);
}
/* 设置弹窗 */
.settings-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 10000;
align-items: center;
justify-content: center;
}
.settings-overlay.show { display: flex; }
.settings-modal {
background: #1e1e1e;
border-radius: 12px;
width: 90%;
max-width: 480px;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.settings-modal-header {
padding: 16px 20px;
border-bottom: 1px solid #2a2a2a;
display: flex;
align-items: center;
justify-content: space-between;
}
.settings-modal-title {
font-size: 18px;
font-weight: 600;
color: #fff;
}
.settings-modal-close {
width: 32px;
height: 32px;
background: transparent;
border: none;
color: #999;
cursor: pointer;
font-size: 20px;
line-height: 1;
border-radius: 6px;
transition: all 0.2s;
}
.settings-modal-close:hover {
color: #fff;
background: #2a2a2a;
}
.settings-modal-body {
padding: 20px;
overflow-y: auto;
}
.settings-section {
margin-bottom: 24px;
}
.settings-section:last-child { margin-bottom: 0; }
.settings-section-title {
font-size: 14px;
color: #ddd;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.settings-section-title .icon { font-size: 16px; opacity: 0.9; }
.settings-section-desc {
font-size: 12px;
color: #888;
margin-bottom: 10px;
}
.settings-input-wrap {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 6px;
}
.settings-input {
flex: 1;
padding: 10px 12px;
font-size: 13px;
border: 1px solid #3a3a3a;
border-radius: 8px;
background: #252525;
color: #e0e0e0;
}
.settings-input::placeholder { color: #666; }
.settings-input:focus {
outline: none;
border-color: #3498db;
}
.settings-path-display {
flex: 1;
padding: 10px 12px;
font-size: 13px;
border: 1px solid #3a3a3a;
border-radius: 8px;
background: #252525;
color: #888;
min-height: 40px;
}
.settings-path-display.has-path { color: #b0b0b0; }
.btn-choose-folder {
padding: 10px 16px;
font-size: 13px;
background: #2a2a2a;
color: #e0e0e0;
border: 1px solid #3a3a3a;
border-radius: 8px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.btn-choose-folder:hover {
background: #333;
border-color: #3498db;
color: #3498db;
}
.settings-update-block {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #252525;
border-radius: 8px;
border: 1px solid #3a3a3a;
}
.settings-version-text {
font-size: 13px;
color: #aaa;
}
.btn-check-update {
padding: 8px 14px;
font-size: 13px;
background: #2a2a2a;
color: #3498db;
border: 1px solid #3498db;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-check-update:hover {
background: rgba(52, 152, 219, 0.2);
}
.settings-modal-footer {
padding: 16px 20px;
border-top: 1px solid #2a2a2a;
display: flex;
justify-content: flex-end;
}
.btn-settings-close {
padding: 10px 20px;
font-size: 13px;
background: #3498db;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.btn-settings-close:hover {
background: #2980b9;
}
/* 主内容区 */
.main-content {
display: flex;
height: calc(100vh - 56px);
}
/* 左侧面板 */
.left-panel {
width: 320px;
background: #1e1e1e;
padding: 20px;
overflow-y: auto;
border-right: 1px solid #2a2a2a;
}
.section-title {
font-size: 13px;
color: #bbb;
margin-bottom: 10px;
}
.section-title .title-icon {
margin-left: 4px;
font-size: 12px;
opacity: 0.9;
}
.option-group {
margin-bottom: 20px;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.btn-group-ratio-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.btn-group-ratio-4col {
grid-template-columns: repeat(4, 1fr);
}
.opt-btn {
padding: 8px 14px;
font-size: 13px;
color: #ccc;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.opt-btn:hover {
color: #fff;
background: #333;
}
.opt-btn.active {
background: #3498db;
border-color: #2980b9;
color: #fff;
box-shadow: 0 0 0 1px #2980b9;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 10px 12px;
font-size: 13px;
color: #e0e0e0;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
font-family: inherit;
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #666;
}
.form-textarea {
min-height: 80px;
resize: vertical;
}
.form-select {
cursor: pointer;
appearance: none;
/*background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L1 3h10z'/%3E%3C/svg%3E");*/
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
.btn-paste-alone {
width: 100%;
padding: 10px 14px;
font-size: 13px;
color: #fff;
background: #27ae60;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 8px;
transition: all 0.2s;
}
.btn-paste-alone:hover {
background: #2ecc71;
}
.upload-zone {
margin-top: 10px;
padding: 24px 16px;
border: 2px dashed #3a3a3a;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #252525;
}
.upload-zone:hover {
border-color: #3498db;
background: #2a2a2a;
}
.upload-zone-icon {
font-size: 24px;
color: #666;
margin-bottom: 8px;
}
.upload-zone-text {
font-size: 13px;
color: #999;
}
.upload-zone-hint {
font-size: 11px;
color: #555;
margin-top: 4px;
}
.product-detail-preview {
margin-top: 10px;
min-height: 0;
}
.hint-text {
font-size: 11px;
color: #666;
margin-top: 8px;
line-height: 1.5;
}
.section-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.section-title-row .section-title {
margin-bottom: 0;
}
.btn-paste-right {
width: auto;
padding: 8px 14px;
margin-top: 0;
}
.btn-add-color-slot {
width: 32px;
height: 32px;
background: #27ae60;
border: none;
border-radius: 6px;
color: #fff;
font-size: 20px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-add-color-slot:hover {
background: #2ecc71;
}
.btn-add-color-slot:disabled {
background: #3a3a3a;
color: #666;
cursor: not-allowed;
}
.color-slots-container {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 12px;
}
.color-slot-item {
background: #252525;
border: 1px solid #3a3a3a;
border-radius: 8px;
padding: 12px;
}
.color-slot-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.color-slot-item input[type="text"].color-name-input {
flex: 1;
padding: 6px 10px;
font-size: 12px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: #e0e0e0;
}
.color-slot-item input[type="text"].color-name-input::placeholder {
color: #666;
}
.btn-remove-color-slot {
width: 24px;
height: 24px;
background: #c0392b;
border: none;
border-radius: 4px;
color: #fff;
font-size: 14px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.btn-remove-color-slot:hover {
background: #e74c3c;
}
.color-slot-upload {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
min-height: 40px;
padding: 4px;
border-radius: 4px;
border: 1px solid transparent;
transition: border-color 0.2s;
}
.color-slot-upload-item {
aspect-ratio: 1;
background: #2a2a2a;
border-radius: 6px;
overflow: hidden;
position: relative;
border: 2px dashed #3a3a3a;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.color-slot-upload-item:hover {
border-color: #3498db;
}
.color-slot-upload-item.add-btn {
color: #666;
font-size: 18px;
}
.color-slot-upload-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.color-slot-upload-item .remove-btn {
position: absolute;
top: 2px;
right: 2px;
width: 18px;
height: 18px;
background: rgba(231, 76, 60, 0.9);
border: none;
border-radius: 4px;
color: #fff;
font-size: 11px;
cursor: pointer;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.feature-example-label {
font-size: 13px;
color: #bbb;
margin-bottom: 8px;
}
.feature-mode-content {
margin-top: 10px;
}
.feature-mode-tabs {
margin-bottom: 0;
}
/* 指定文案 - 第X屏文案 */
.specify-screen-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 280px;
overflow-y: auto;
}
.specify-screen-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.specify-screen-item label {
font-size: 12px;
color: #999;
}
.specify-screen-item .form-input {
margin: 0;
}
.btn-add-screen {
margin-top: 8px;
padding: 8px 14px;
font-size: 13px;
color: #7ab8ff;
background: transparent;
border: 1px dashed #3a3a3a;
border-radius: 6px;
cursor: pointer;
}
.btn-add-screen:hover {
border-color: #5a5a5a;
color: #9ac8ff;
}
/* 默认面板 - 分批提示词:每行 提示词 + 生成张数 */
.batch-prompt-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 280px;
overflow-y: auto;
}
.batch-prompt-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.batch-prompt-row .batch-prompt-input {
flex: 1;
min-width: 120px;
margin: 0;
}
.batch-prompt-row .batch-count-input {
width: 72px;
margin: 0;
padding: 6px 8px;
}
.batch-prompt-row .batch-prompt-label {
font-size: 12px;
color: #999;
min-width: 40px;
}
.batch-prompt-row .batch-remove-btn {
padding: 6px 10px;
font-size: 14px;
color: #999;
background: transparent;
border: 1px solid #3a3a3a;
border-radius: 6px;
cursor: pointer;
}
.batch-prompt-row .batch-remove-btn:hover {
color: #e74c3c;
border-color: #5a3a3a;
}
.textarea-wrap {
position: relative;
}
.prompt-textarea {
width: 100%;
min-height: 80px;
padding: 10px 12px 24px;
font-size: 13px;
color: #e0e0e0;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
resize: vertical;
font-family: inherit;
}
.prompt-textarea::placeholder {
color: #666;
}
.prompt-textarea-large {
min-height: 120px;
}
.char-count {
position: absolute;
bottom: 8px;
right: 12px;
font-size: 11px;
color: #666;
}
.char-count-top-right {
bottom: auto;
top: 8px;
right: 12px;
}
.upload-btns {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn-paste {
flex: 1;
padding: 10px;
font-size: 13px;
color: #fff;
background: #27ae60;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-paste:hover {
background: #2ecc71;
}
.btn-clear {
flex: 1;
padding: 10px;
font-size: 13px;
color: #fff;
background: #c0392b;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-clear:hover {
background: #e74c3c;
}
.btn-upload {
flex: 1;
padding: 10px;
font-size: 13px;
color: #fff;
background: #3498db;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-upload:hover {
background: #2980b9;
}
.upload-preview {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 10px;
min-height: 60px;
}
.upload-preview-item {
aspect-ratio: 1;
background: #2a2a2a;
border-radius: 6px;
overflow: hidden;
position: relative;
border: 2px solid #3a3a3a;
}
.upload-preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-preview-item .remove-btn {
position: absolute;
top: 2px;
right: 2px;
width: 20px;
height: 20px;
background: rgba(231, 76, 60, 0.9);
border: none;
border-radius: 4px;
color: #fff;
font-size: 12px;
cursor: pointer;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.upload-option-hidden {
display: none;
}
.btn-generate {
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 600;
color: #fff;
background: #3498db;
border: none;
border-radius: 8px;
cursor: pointer;
margin-top: 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.btn-generate:hover {
background: #2980b9;
}
.btn-generate.generating {
background: #e74c3c;
}
.btn-generate.generating:hover {
background: #c0392b;
}
/* 中央区域 - 工作台 */
.center-area {
flex: 1;
background: #0d0d0d;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.center-toolbar {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #2a2a2a;
background: #1a1a1a;
flex-shrink: 0;
}
.btn-manual-stitch {
padding: 8px 16px;
font-size: 13px;
color: #e0e0e0;
background: #2a2a2a;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-manual-stitch:hover {
background: #333;
color: #fff;
}
.btn-refresh {
padding: 8px 16px;
font-size: 13px;
color: #fff;
background: #e67e22;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-refresh:hover {
background: #d35400;
}
.btn-download {
padding: 8px 16px;
font-size: 13px;
color: #fff;
background: #3498db;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-download:hover {
background: #2980b9;
}
.center-toolbar-spacer {
flex: 1;
}
.btn-close-preview {
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: #999;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.btn-close-preview:hover {
background: #2a2a2a;
color: #fff;
}
/* 工作台主体:左图列表 + 右拼接预览 */
.workspace-body {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
position: relative;
}
/* 拼接中蒙版:覆盖工作台左右区域,禁止操作 */
.workspace-stitch-loading {
position: absolute;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.75);
pointer-events: auto;
}
.workspace-stitch-loading .workspace-stitch-loading-content {
padding: 32px 48px;
text-align: center;
color: #fff;
background: rgba(0, 0, 0, 0.88);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.workspace-stitch-loading .spinner {
display: inline-block;
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #3498db;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.workspace-stitch-loading .loading-text {
margin-top: 16px;
font-size: 16px;
font-weight: 600;
color: #fff;
letter-spacing: 0.5px;
animation: loading-pulse 1.2s ease-in-out infinite;
}
.workspace-left {
flex: 1;
min-width: 0;
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
background: #0d0d0d;
}
.workspace-left-inner {
column-count: 2;
column-gap: 16px;
min-height: 200px;
}
@media (min-width: 1200px) {
.workspace-left-inner { column-count: 3; }
}
.workspace-thumb {
break-inside: avoid;
margin-bottom: 16px;
width: 100%;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
position: relative;
}
.workspace-thumb:hover {
border-color: #3498db;
}
.workspace-thumb-in-stitch {
border-color: #3498db;
}
.workspace-thumb-badge {
position: absolute;
left: 8px;
top: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background: #3498db;
color: #fff;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.workspace-stitch-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-shrink: 0;
}
.workspace-stitch-count {
font-size: 14px;
color: #ccc;
}
.workspace-stitch-cancel {
padding: 6px 14px;
font-size: 13px;
color: #fff;
background: #e74c3c;
border: none;
border-radius: 6px;
cursor: pointer;
}
.workspace-stitch-cancel:hover {
background: rgba(202, 81, 67, 0.99);
}
.workspace-stitch-start {
padding: 6px 14px;
font-size: 13px;
color: #fff;
background: #27ae60;
border: none;
border-radius: 6px;
cursor: pointer;
}
.workspace-stitch-start:hover {
background: #219a52;
}
.workspace-thumb-loading {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.7);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 2;
color: #fff;
font-size: 12px;
}
.workspace-thumb-loading .spinner {
width: 28px;
height: 28px;
border: 3px solid rgba(255,255,255,0.3);
border-top-color: #3498db;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.workspace-thumb-loading .txt {
margin-top: 10px;
font-size: 14px;
font-weight: 600;
color: #fff;
}
.workspace-thumb img {
width: 100%;
height: auto;
display: block;
object-fit: contain;
vertical-align: top;
}
.workspace-left-empty {
color: #555;
font-size: 14px;
align-self: center;
padding: 40px;
}
.workspace-right {
width: 38%;
min-width: 260px;
border-left: 1px solid #2a2a2a;
padding: 20px;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
background: #0d0d0d;
}
.workspace-stitched-wrap {
width: 100%;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.workspace-stitched-wrap:hover {
border-color: #3498db;
}
.workspace-stitched-wrap img {
width: 100%;
height: auto;
display: block;
vertical-align: top;
}
.workspace-right-empty {
color: #555;
font-size: 14px;
text-align: center;
padding: 40px;
}
/* 手动拼接顺序弹窗 */
.stitch-order-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 999;
align-items: center;
justify-content: center;
}
.stitch-order-overlay.show {
display: flex;
}
.stitch-order-modal {
background: #1e1e1e;
border-radius: 12px;
padding: 24px;
max-width: 90vw;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid #2a2a2a;
}
.stitch-order-title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin-bottom: 16px;
}
.stitch-order-list {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.stitch-order-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: #2a2a2a;
border-radius: 8px;
cursor: grab;
}
.stitch-order-item:active {
cursor: grabbing;
}
.stitch-order-item .thumb {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
}
.stitch-order-item .idx {
font-size: 13px;
color: #999;
min-width: 24px;
}
.stitch-order-item .drag-handle {
color: #666;
font-size: 18px;
}
.stitch-order-item .stitch-remove-btn {
margin-left: auto;
padding: 4px 10px;
font-size: 12px;
color: #e74c3c;
background: transparent;
border: 1px solid #e74c3c;
border-radius: 4px;
cursor: pointer;
}
.stitch-order-item .stitch-remove-btn:hover {
background: rgba(231, 76, 60, 0.2);
}
.stitch-order-add-title {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.stitch-order-add-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.stitch-order-add-item {
width: 56px;
height: 56px;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
border: 2px solid #2a2a2a;
transition: border-color 0.2s;
}
.stitch-order-add-item:hover {
border-color: #3498db;
}
.stitch-order-add-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.stitch-order-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.stitch-order-actions button {
padding: 8px 20px;
font-size: 13px;
border: none;
border-radius: 6px;
cursor: pointer;
}
.stitch-order-cancel {
background: #333;
color: #e0e0e0;
}
.stitch-order-ok {
background: #3498db;
color: #fff;
}
.preview-placeholder {
display: none;
}
.preview-image {
display: none;
}
/* 反推词 - 提示词卡片(工作台区域可纵向滚动,避免多卡片显示不全) */
.preview-prompt-cards {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 700px;
position: absolute;
top: 56px;
left: 0;
right: 0;
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
}
.prompt-card {
background: #1e1e1e;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 14px 16px;
display: flex;
align-items: flex-start;
gap: 12px;
}
.prompt-card-content {
flex: 1;
font-size: 13px;
color: #e0e0e0;
line-height: 1.6;
word-break: break-word;
}
.prompt-card-copy {
flex-shrink: 0;
padding: 6px 12px;
font-size: 12px;
color: #3498db;
background: rgba(52, 152, 219, 0.15);
border: 1px solid #3498db;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.prompt-card-copy:hover {
background: rgba(52, 152, 219, 0.25);
}
.prompt-card-copy.copied {
color: #27ae60;
border-color: #27ae60;
background: rgba(39, 174, 96, 0.15);
}
/* 生成中蒙版:铺满整个工作台,最高层,禁止操作 */
.preview-loading {
position: absolute;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.75);
pointer-events: auto;
}
.preview-loading .preview-loading-content {
padding: 32px 48px;
text-align: center;
color: #fff;
background: rgba(0, 0, 0, 0.88);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.preview-loading .spinner {
display: inline-block;
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #3498db;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.preview-loading .preview-loading-content .loading-text {
margin-top: 16px;
font-size: 16px;
font-weight: 600;
color: #fff;
letter-spacing: 0.5px;
animation: loading-pulse 1.2s ease-in-out infinite;
}
@keyframes loading-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 右侧面板 */
.right-panel {
width: 280px;
background: #1e1e1e;
display: flex;
flex-direction: column;
border-left: 1px solid #2a2a2a;
}
.history-header {
padding: 16px;
border-bottom: 1px solid #2a2a2a;
}
.history-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
color: #fff;
}
.history-tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.history-tab {
padding: 6px 12px;
font-size: 12px;
color: #999;
background: #2a2a2a;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.history-tab:hover {
color: #fff;
}
.history-tab.active {
color: #3498db;
background: rgba(52, 152, 219, 0.2);
}
.btn-clean {
width: 100%;
padding: 8px;
font-size: 12px;
color: #999;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-clean:hover {
color: #fff;
background: #333;
}
.history-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.history-grid {
display: flex;
flex-direction: column;
gap: 0;
}
.history-load-more {
padding: 12px;
text-align: center;
}
.btn-load-more {
padding: 8px 20px;
font-size: 13px;
color: #999;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-load-more:hover {
color: #fff;
background: #333;
}
/* 历史图库条目:时间叠在图片右下角半透明,长图缩小显示全图 */
.history-item {
padding: 10px 0;
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
transition: background 0.2s;
}
.history-item:last-child {
border-bottom: none;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.03);
}
.history-item-img-wrap {
position: relative;
width: 100%;
max-height: 360px;
aspect-ratio: 3 / 4;
border-radius: 8px;
overflow: hidden;
background: #2a2a2a;
}
.history-item:hover .history-item-img-wrap {
outline: 1px solid rgba(52, 152, 219, 0.5);
}
.history-item img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.history-item-time {
position: absolute;
right: 6px;
bottom: 6px;
padding: 2px 6px;
font-size: 12px;
color: #fff;
background: rgba(0, 0, 0, 0.5);
border-radius: 4px;
}
/* 无边框窗口样式 - Windows */
.no-drag {
-webkit-app-region: no-drag;
app-region: no-drag;
}
/* 图片查看模态框 */
.image-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 1000;
align-items: center;
justify-content: center;
}
.image-modal-overlay.show {
display: flex;
}
.image-modal {
background: #1e1e1e;
border-radius: 12px;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #3a3a3a;
}
.image-modal-header {
padding: 16px 24px;
text-align: center;
border-bottom: 1px solid #2a2a2a;
}
.image-modal-title {
font-size: 18px;
font-weight: 600;
color: #fff;
margin-bottom: 4px;
}
.image-modal-subtitle {
font-size: 13px;
color: #999;
}
.image-modal-body {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
min-height: 300px;
position: relative;
}
.image-modal-body img {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
border-radius: 8px;
}
.image-modal-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
background: rgba(42, 42, 42, 0.9);
border: 1px solid #3a3a3a;
border-radius: 8px;
color: #fff;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.image-modal-arrow:hover {
background: #3498db;
border-color: #2980b9;
}
.image-modal-arrow.prev {
left: 16px;
}
.image-modal-arrow.next {
right: 16px;
}
.image-modal-footer {
padding: 16px 24px;
border-top: 1px solid #2a2a2a;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.image-modal-btn {
padding: 10px 18px;
font-size: 13px;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.image-modal-btn.restore {
background: #27ae60;
color: #fff;
}
.image-modal-btn.restore:hover {
background: #2ecc71;
}
.image-modal-btn.batch-download {
background: #9b59b6;
color: #fff;
}
.image-modal-btn.batch-download:hover {
background: #8e44ad;
}
.image-modal-btn.copy,
.image-modal-btn.download-long {
background: #3498db;
color: #fff;
}
.image-modal-btn.copy:hover,
.image-modal-btn.download-long:hover {
background: #2980b9;
}
.image-modal-btn.close {
background: #555;
color: #fff;
}
.image-modal-btn.close:hover {
background: #666;
}
.image-modal-btn.workspace-download,
.image-modal-btn.workspace-edit,
.image-modal-btn.workspace-regenerate {
background: #3498db;
color: #fff;
}
.image-modal-btn.workspace-edit { background: #27ae60; }
.image-modal-btn.workspace-edit:hover { background: #2ecc71; }
.image-modal-btn.workspace-regenerate { background: #e67e22; }
.image-modal-btn.workspace-regenerate:hover { background: #d35400; }
/* 图片编辑弹窗(工作台大图内“图片编辑”触发) */
.image-edit-modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 1001;
align-items: center;
justify-content: center;
}
.image-edit-modal-overlay.show { display: flex; }
.image-edit-modal {
background: #1e1e1e;
border-radius: 12px;
max-width: 90vw;
max-height: 90vh;
width: 720px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #3a3a3a;
}
.image-edit-modal .edit-image-wrap {
padding: 16px;
background: #252525;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 280px;
position: relative;
}
.image-edit-modal .edit-image-wrap img {
max-width: 100%;
max-height: 50vh;
object-fit: contain;
border-radius: 8px;
}
.image-edit-modal .edit-canvas-wrap {
position: absolute;
top: 16px; left: 16px; right: 16px; bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.image-edit-modal .edit-canvas-wrap canvas {
pointer-events: auto;
max-width: 100%;
max-height: 100%;
}
.image-edit-toolbar {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
border-bottom: 1px solid #2a2a2a;
background: #1e1e1e;
}
.image-edit-toolbar .tool-btn {
padding: 8px 14px;
font-size: 12px;
border: 1px solid #27ae60;
background: rgba(39, 174, 96, 0.2);
color: #27ae60;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
}
.image-edit-toolbar .tool-btn.active {
background: #27ae60;
color: #fff;
}
.image-edit-toolbar .tool-btn.secondary {
border-color: #555;
background: #2a2a2a;
color: #999;
}
.image-edit-toolbar .brush-label {
font-size: 12px;
color: #999;
margin-left: 8px;
}
.image-edit-toolbar .brush-slider {
width: 120px;
height: 6px;
-webkit-appearance: none;
background: #333;
border-radius: 3px;
}
.image-edit-toolbar .brush-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #3498db;
border-radius: 50%;
cursor: pointer;
}
.image-edit-hints {
padding: 8px 16px;
font-size: 11px;
color: #666;
line-height: 1.5;
border-bottom: 1px solid #2a2a2a;
}
.image-edit-prompt-row {
padding: 12px 16px;
display: flex;
gap: 12px;
align-items: flex-start;
border-bottom: 1px solid #2a2a2a;
}
.image-edit-prompt-row .prompt-textarea {
flex: 1;
min-height: 80px;
padding: 10px 12px;
font-size: 13px;
color: #fff;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
resize: vertical;
}
.image-edit-prompt-row .prompt-textarea::placeholder {
color: #666;
}
.image-edit-prompt-row .btn-ref-image {
flex-shrink: 0;
padding: 10px 16px;
font-size: 13px;
color: #fff;
background: #3498db;
border: none;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
}
.image-edit-prompt-row .btn-ref-image:hover { background: #2980b9; }
.image-edit-modal-footer {
padding: 16px 24px;
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #2a2a2a;
}
.image-edit-modal-footer .btn-cancel {
padding: 10px 20px;
font-size: 13px;
color: #999;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
cursor: pointer;
}
.image-edit-modal-footer .btn-cancel:hover { color: #fff; background: #333; }
.image-edit-modal-footer .btn-confirm {
padding: 10px 20px;
font-size: 13px;
color: #fff;
background: #3498db;
border: none;
border-radius: 6px;
cursor: pointer;
}
.image-edit-modal-footer .btn-confirm:hover { background: #2980b9; }
/* 重新生成弹窗(查看大图内“重新生成”触发) */
.regenerate-modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1002;
align-items: center;
justify-content: center;
}
.regenerate-modal-overlay.show { display: flex; }
.regenerate-modal {
background: #fff;
border-radius: 12px;
max-width: 90vw;
max-height: 90vh;
width: 560px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.regenerate-modal-top {
background: #f5f0e8;
padding: 20px 24px 24px;
flex-shrink: 0;
}
.regenerate-preview-title {
font-size: 18px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 8px;
line-height: 1.3;
}
.regenerate-preview-subtitle {
font-size: 13px;
color: #333;
margin-bottom: 16px;
line-height: 1.4;
}
.regenerate-preview-img-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
background: #ebe5dc;
border-radius: 8px;
overflow: hidden;
}
.regenerate-preview-img-wrap img {
max-width: 100%;
max-height: 42vh;
object-fit: contain;
}
.regenerate-modal-bottom {
background: #2c2c2c;
padding: 16px 24px 20px;
color: #e0e0e0;
flex-shrink: 0;
}
.regenerate-desc {
font-size: 12px;
line-height: 1.6;
margin-bottom: 8px;
color: #ccc;
}
.regenerate-style {
font-size: 12px;
margin-bottom: 12px;
color: #aaa;
}
.regenerate-prompt-row {
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 10px;
}
.regenerate-prompt-label {
font-size: 13px;
color: #ccc;
flex-shrink: 0;
}
.regenerate-prompt-input {
flex: 1;
padding: 8px 12px;
font-size: 13px;
color: #fff;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 6px;
}
.regenerate-prompt-input::placeholder { color: #888; }
.regenerate-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 14px;
}
.regenerate-btn {
padding: 10px 20px;
font-size: 13px;
border: none;
border-radius: 6px;
cursor: pointer;
}
.regenerate-btn.cancel {
background: #555;
color: #fff;
}
.regenerate-btn.cancel:hover { background: #666; }
.regenerate-btn.confirm {
background: #3498db;
color: #fff;
}
.regenerate-btn.confirm:hover { background: #2980b9; }
.regenerate-footer-actions {
display: flex;
align-items: center;
gap: 12px;
}
.regenerate-footer-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
font-size: 13px;
color: #fff;
background: transparent;
border: none;
cursor: pointer;
border-radius: 4px;
}
.regenerate-footer-btn:hover { background: rgba(255,255,255,0.1); }
.regenerate-footer-btn.close { margin-left: auto; }
</style>
</head>
<body>
<!-- 顶部栏 -->
<header class="top-bar">
<div class="logo-area">
<img class="logo" src="../logo.jpg"></img>
<span class="app-name">数富AI</span>
<a href="/home" class="btn-home" title="返回首页">← 返回首页</a>
</div>
<nav class="nav-tabs">
<span class="nav-tab-group">
<button class="nav-tab" data-max-images="0" data-panel="textToImage">反推词</button>
<button class="nav-tab" data-max-images="8" data-panel="productMainImage">产品主图</button>
<button class="nav-tab active" data-max-images="8" data-panel="buyerShow">买家秀</button>
</span>
<span class="nav-tab-sep" aria-hidden="true"></span>
<span class="nav-tab-group">
<button class="nav-tab" data-max-images="6" data-panel="productPoster">产品海报</button>
<button class="nav-tab" data-max-images="6" data-panel="clonePoster">克隆海报</button>
<button class="nav-tab" data-max-images="6" data-panel="randomPoster" style="display:none">随机海报</button>
</span>
<span class="nav-tab-sep" aria-hidden="true"></span>
<!-- <span class="nav-tab-group">-->
<!-- </span>-->
<!-- <span class="nav-tab-sep" aria-hidden="true"></span>-->
<span class="nav-tab-group">
<button class="nav-tab" data-max-images="8" data-panel="clothingDetail">服装详情</button>
<button class="nav-tab" data-max-images="8" data-panel="productDetail" style="display:none">产品详情</button>
<button class="nav-tab" data-max-images="8" data-panel="extremeDetail">极致详情</button>
<button class="nav-tab" data-max-images="8" data-panel="cloneDetail">克隆详情</button>
</span>
<!-- <button class="nav-tab" data-max-images="0">实用工具</button>-->
</nav>
<div class="top-right">
<button type="button" class="btn-settings" id="btnSettings" title="设置"></button>
</div>
</header>
<div class="main-content">
<!-- 左侧面板 -->
<aside class="left-panel">
<!-- 反推词面板 -->
<div class="panel-content" id="textToImagePanel" style="display: none;">
<div class="option-group">
<div class="section-title">提示词</div>
<div class="textarea-wrap">
<textarea class="prompt-textarea prompt-textarea-large" id="textToImagePromptInput" placeholder="输入自定义指令,用于反推提示词..."></textarea>
<span class="char-count char-count-top-right" id="textToImageCharCount">0 字符</span>
</div>
</div>
<div class="option-group">
<div class="section-title">上传图片(最多8张)</div>
<div class="hint-text">支持JPG PNG BMP JPEG</div>
<input type="file" id="fileInputReverseRef" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneReverseRef">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="reverseRefCount">0</span>/8)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewReverseRef"></div>
</div>
<div class="option-group">
<div class="section-title">上传视频(只支持1个)</div>
<div class="hint-text">支持 MP4 等视频格式</div>
<input type="file" id="fileInputReverseVideo" accept="video/*,.mp4,.webm" hidden>
<div class="upload-zone" id="uploadZoneReverseVideo">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传视频</div>
<div class="upload-zone-hint">仅支持1个视频</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewReverseVideo" style="display: none;"></div>
</div>
<button class="btn-generate" id="btnTextToImageGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 默认面板(图片编辑等) -->
<div class="panel-content" id="defaultPanel">
<div class="option-group">
<div class="section-title">生成数量</div>
<div class="btn-group">
<button class="opt-btn active" data-value="1">1张</button>
<button class="opt-btn" data-value="2">2张</button>
<button class="opt-btn" data-value="3">3张</button>
<button class="opt-btn" data-value="4">4张</button>
<button class="opt-btn" data-value="5">5张</button>
</div>
</div>
<div class="option-group">
<div class="section-title">提示词模式</div>
<div class="btn-group">
<button class="opt-btn active" data-mode="unified">统一提示词</button>
<button class="opt-btn" data-mode="batch">分批提示词</button>
</div>
<div class="hint-text">● 统一提示词:所有图片使用相同的提示词生成;分批提示词:每行一个提示词</div>
</div>
<div class="option-group" id="defaultUnifiedPromptGroup">
<div class="section-title">统一提示词</div>
<div class="textarea-wrap">
<textarea class="prompt-textarea" id="promptInput" placeholder="精美产品海报"></textarea>
<span class="char-count" id="charCount">0字符</span>
</div>
</div>
<div class="option-group" id="defaultBatchPromptGroup" style="display: none;">
<div class="section-title">分批提示词</div>
<!-- <div class="hint-text">根据上方「生成数量」显示对应行数,每行:提示词 + 生成张数,传参 text:[]</div>-->
<div class="batch-prompt-list" id="batchPromptList"></div>
</div>
<div class="option-group">
<div class="section-title">画幅比例</div>
<div class="btn-group">
<button class="opt-btn" data-ratio="auto">auto</button>
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
<div class="hint-text">● 图片编辑模式建议使用auto自动比例</div>
</div>
<div class="option-group">
<div class="section-title">分辨率(RESOLUTION)</div>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group" id="uploadOptionGroup">
<div class="section-title" id="uploadSectionTitle">上传图片(最多8张)</div>
<div class="hint-text">支持JPG PNG BMP JPEG</div>
<div class="upload-preview" id="uploadPreview"></div>
<div class="upload-btns">
<input type="file" id="fileInput" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<button class="btn-upload" id="btnSelectFiles">选择文件</button>
<button class="btn-paste" id="btnPaste">粘图片</button>
<button class="btn-clear" id="btnClear">清空上传</button>
</div>
</div>
<button class="btn-generate" id="btnImageEditGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 产品海报面板 -->
<div class="panel-content" id="productPosterPanel" style="display: none;">
<div class="option-group">
<label class="section-title">主标题</label>
<input type="text" class="form-input" id="productPosterMainTitle" placeholder="如: 口红吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">副标题</label>
<textarea class="form-textarea" id="productPosterSubtitle" placeholder="如: 1:保湿效果好 2: 适合敏感肌&#10;3:价格实惠" rows="4"></textarea>
</div>
<div class="option-group">
<label class="section-title">品牌名 (可选)</label>
<input type="text" class="form-input" id="productPosterBrand" placeholder="(可选)">
</div>
<div class="option-group">
<label class="section-title">成分 (INGREDIENTS)</label>
<input type="text" class="form-input" id="productPosterIngredients" placeholder="例如: 纯棉、透明质酸...(可选)">
</div>
<div class="option-group">
<label class="section-title">活动 (ACTIVITY)</label>
<input type="text" class="form-input" id="productPosterActivity" placeholder="例如: 买一送一、限时折扣...(可选)">
</div>
<div class="option-group">
<label class="section-title">画幅比例</label>
<div class="btn-group">
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
</div>
<div class="option-group">
<label class="section-title">分辨率 (RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">上传图片 (最多6张)</label>
<div class="section-title-row" style="margin-bottom: 8px;">
<span class="hint-text" style="margin: 0;">支持多选与拖拽排序</span>
<button type="button" class="btn-paste-alone btn-paste-right" id="btnPasteProductPoster">
<span>📋</span>
粘贴图片
</button>
</div>
<input type="file" id="fileInputProductPoster" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneProductPoster">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="productPosterImagesCount">0</span>/6)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewProductPoster"></div>
</div>
<button class="btn-generate" id="btnProductPosterGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 随机海报面板 -->
<div class="panel-content" id="randomPosterPanel" style="display: none;">
<div class="option-group">
<label class="section-title">主标题</label>
<input type="text" class="form-input" id="randomPosterMainTitle" placeholder="如: 口红 吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">副标题</label>
<textarea class="form-textarea" id="randomPosterSubtitle" placeholder="如: 1: 保湿效果好 2: 适合敏感肌 3: 价格实惠" rows="4"></textarea>
</div>
<div class="option-group">
<label class="section-title">画幅比例</label>
<div class="btn-group btn-group-ratio-grid">
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
</div>
<div class="option-group">
<label class="section-title">分辨率(RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">上传图片(最多6张)</label>
<div class="section-title-row" style="margin-bottom: 8px;">
<span class="hint-text" style="margin: 0;">支持多选与拖拽排序</span>
<button class="btn-paste-alone btn-paste-right" id="btnPasteRandomPoster">
<span>📋</span>
粘贴图片
</button>
</div>
<input type="file" id="fileInputRandomPoster" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneRandomPoster">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="randomPosterImagesCount">0</span>/6)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewRandomPoster"></div>
</div>
<button class="btn-generate" id="btnRandomPosterGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 克隆海报面板 -->
<div class="panel-content" id="clonePosterPanel" style="display: none;">
<div class="option-group">
<label class="section-title">主标题</label>
<input type="text" class="form-input" id="clonePosterMainTitle" placeholder="如: 口红 吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">副标题</label>
<textarea class="form-textarea" id="clonePosterSubtitle" placeholder="如: 1: 保湿效果好 2: 适合敏感肌 3: 价格实惠" rows="4"></textarea>
</div>
<div class="option-group">
<label class="section-title">画幅比例</label>
<div class="btn-group btn-group-ratio-grid btn-group-ratio-4col">
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
</div>
<div class="option-group">
<label class="section-title">分辨率 (RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">上传版式 (1张)</label>
<input type="file" id="fileInputClonePosterLayout" accept=".jpg,.jpeg,.png,.bmp" hidden>
<div class="upload-zone" id="uploadZoneClonePosterLayout">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">上传版式</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewClonePosterLayout" style="display: none;"></div>
</div>
<div class="option-group">
<label class="section-title">上传图片 (最多6张)</label>
<input type="file" id="fileInputClonePosterImages" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneClonePosterImages">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">上传图片 (<span id="clonePosterImagesCount">0</span>/6)</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewClonePosterImages"></div>
</div>
<button class="btn-generate" id="btnClonePosterGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 服装详情面板 -->
<div class="panel-content" id="clothingDetailPanel" style="display: none;">
<div class="option-group">
<label class="section-title">产品名称(必填)</label>
<input type="text" class="form-input" id="clothingProductName" placeholder="如:口红吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">功能特点组 (FEATURE GROUP)</label>
<textarea class="form-textarea" id="clothingFeatures" placeholder="如:1:保湿效果好 2:适合敏感肌 3:价格实惠" rows="4"></textarea>
</div>
<div class="option-group">
<label class="section-title">画幅比例</label>
<div class="btn-group">
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
</div>
<div class="option-group">
<label class="section-title">分辨率 (RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">风格选择 <span class="title-icon"></span></label>
<!-- <select class="form-select" id="clothingStyle">-->
<!-- <option value="">&#45;&#45; 选择风格 &#45;&#45;</option>-->
<!-- </select>-->
<input class="form-select" id="clothingStyle" list="styleList_clothes" placeholder="-- 选择或输入风格 --">
<datalist id="styleList_clothes">
<option value="极简高级">
<option value="时尚潮流">
<option value="复古颗粒">
<option value="网红外拍">
<option value="手机自拍">
</datalist>
</div>
<div class="option-group">
<label class="section-title">输出文案语言 (OUTPUT LANGUAGE)</label>
<input type="text" class="form-input" id="clothingLanguage" placeholder="例如:中文输出、英文输出、中英混合">
</div>
<div class="option-group">
<label class="section-title">生成张数 <span class="title-icon">🖼</span></label>
<div class="btn-group">
<button class="opt-btn" data-count="3">3张</button>
<button class="opt-btn" data-count="5">5张</button>
<button class="opt-btn active" data-count="9">9张</button>
<button class="opt-btn" data-count="14">14张</button>
</div>
</div>
<div class="option-group">
<label class="section-title">服装细节(至少1张)</label>
<button class="btn-paste-alone" id="btnPasteClothingDetail">
<span>📋</span>
粘贴图片
</button>
<input type="file" id="fileInputClothingDetail" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneClothingDetail">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="clothingDetailCount">0</span>/8)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewClothingDetail"></div>
</div>
<button class="btn-generate" id="btnClothingDetailGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 产品详情面板 -->
<div class="panel-content" id="productDetailPanel" style="display: none;">
<div class="option-group">
<label class="section-title">产品名称(必填)</label>
<input type="text" class="form-input" id="productName" placeholder="如: 口红 吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">功能特点组 (FEATURE GROUP)</label>
<textarea class="form-textarea" id="productFeatures" placeholder="如: 1: 保湿效果好 2: 适合敏感肌 3: 价格实惠" rows="4"></textarea>
</div>
<div class="option-group">
<label class="section-title">品牌名(可选)</label>
<input type="text" class="form-input" id="productBrand" placeholder="(可选)">
</div>
<div class="option-group">
<label class="section-title">成分(INGREDIENTS)</label>
<input type="text" class="form-input" id="productIngredients" placeholder="例如: 纯棉、透明质酸... (可选)">
</div>
<div class="option-group">
<label class="section-title">活动(ACTIVITY)</label>
<input type="text" class="form-input" id="productActivity" placeholder="例如: 买一送一、限时折扣... (可选)">
</div>
<div class="option-group">
<label class="section-title">画幅比例</label>
<div class="btn-group">
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
</div>
<div class="option-group">
<label class="section-title">分辨率(RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">风格选择</label>
<!-- <select class="form-select" id="productStyle">-->
<!-- <option value="">&#45;&#45; 选择风格 &#45;&#45;</option>-->
<!-- </select>-->
<input class="form-select" id="productStyle" list="styleList" placeholder="-- 选择或输入风格 --">
<datalist id="styleList">
<option value="极简高级">
<option value="科技未来">
<option value="自然有机">
<option value="轻奢质感">
<option value="工业硬朗">
</datalist>
</div>
<div class="option-group">
<label class="section-title">输出文案语言(OUTPUT LANGUAGE)</label>
<input type="text" class="form-input" id="productLanguage" placeholder="例如: 中文输出、英文输出、中英混合">
</div>
<div class="option-group">
<label class="section-title">生成张数</label>
<div class="btn-group">
<button class="opt-btn" data-count="3">3张</button>
<button class="opt-btn" data-count="5">5张</button>
<button class="opt-btn active" data-count="9">9张</button>
<button class="opt-btn" data-count="14">14张</button>
</div>
</div>
<div class="option-group">
<label class="section-title">参考图片(最多8张)</label>
<div class="hint-text">建议包含正面及多角度图</div>
<button class="btn-paste-alone" id="btnPasteProductDetail">
<span>📋</span>
粘贴图片
</button>
<input type="file" id="fileInputProductDetail" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneProductDetail">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片(<span id="productDetailCount">0</span>/8)</div>
<div class="upload-zone-hint">支持多选 / 拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewProductDetail"></div>
</div>
<button class="btn-generate" id="btnProductDetailGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 极致详情面板 -->
<div class="panel-content" id="extremeDetailPanel" style="display: none;">
<div class="option-group">
<label class="section-title">产品名称(必填)</label>
<input type="text" class="form-input" id="extremeProductName" placeholder="如: 口红 吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">功能特点组 (FEATURE GROUP)</label>
<div class="feature-mode-tabs btn-group">
<button class="opt-btn" data-feature-mode="auto">自动模式</button>
<button class="opt-btn active" data-feature-mode="manual">手动模式</button>
<button class="opt-btn" data-feature-mode="specify">指定文案</button>
</div>
<!-- 手动模式:示例列表 -->
<div class="feature-mode-content" id="extremeFeatureManual">
<div class="feature-example-label">示例2:</div>
<textarea class="form-textarea" id="extremeFeatureManualInput" rows="10" placeholder="每行一个特点格式如1. A类母婴级纯棉: 软糯透气, 呵护娇嫩敏感肌">1. A类母婴级纯棉: 软糯透气, 呵护娇嫩敏感肌
2. 加宽腰头无痕边: 不勒肚不卡档, 自在无束缚
3. 0荧光0甲醛: 严守安全标准, 贴身穿着才安心
4. 萌趣卡通印花: 色彩清新柔和, 孩子一眼就爱
5. 立体透气抑菌档: 吸湿排汗强, 干爽舒适不闷
6. 高弹面料不松垮: 耐洗不易变形, 久穿不起球
7. 2-12岁全尺码: 剪裁贴合身形, 舒适贴合不紧
8. 弹力平角不卡腿: 穿脱顺畅方便, 孩子自己穿
9. 多种花色随心选: 满足日常替换, 天天不重样</textarea>
</div>
<!-- 指定文案模式第X屏文案与生成张数绑定传参 text: ["xxx","xxx"] -->
<div class="feature-mode-content" id="extremeFeatureSpecify" style="display: none;">
<div class="specify-screen-list" id="extremeSpecifyScreenList" data-panel="extremeDetail"></div>
</div>
<!-- 自动模式:无额外输入 -->
</div>
<div class="option-group">
<label class="section-title">画幅比例 (ASPECT RATIO)</label>
<div class="btn-group">
<button class="opt-btn active" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
</div>
<div class="option-group">
<label class="section-title">风格和功能卖点描述</label>
<input type="text" class="form-input" id="extremeStyleDesc" placeholder="例如:高级质感、极简主义、奢华轻奢">
</div>
<div class="option-group">
<label class="section-title">输出文案语言 (OUTPUT LANGUAGE)</label>
<input type="text" class="form-input" id="extremeOutputLang" placeholder="例如:使用中文输出、使用英文输出">
</div>
<div class="option-group">
<label class="section-title">分辨率 (RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">生成张数</label>
<div class="btn-group">
<button class="opt-btn" data-count="3">3张</button>
<button class="opt-btn" data-count="5">5张</button>
<button class="opt-btn active" data-count="9">9张</button>
<button class="opt-btn" data-count="14">14张</button>
</div>
</div>
<div class="option-group">
<div class="section-title-row">
<label class="section-title">参考图片 (最多8张)</label>
<button class="btn-paste-alone btn-paste-right" id="btnPasteExtremeDetail">
<span>📋</span>
粘贴图片
</button>
</div>
<div class="hint-text">建议包含正面及多角度图</div>
<input type="file" id="fileInputExtremeDetail" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneExtremeDetail">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="extremeDetailCount">0</span>/8)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewExtremeDetail"></div>
</div>
<button class="btn-generate" id="btnExtremeGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 产品主图面板 -->
<div class="panel-content" id="productMainImagePanel" style="display: none;">
<div class="option-group">
<label class="section-title">产品名称(必填)</label>
<input type="text" class="form-input" id="productMainName" placeholder="如:口红 吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">功能特点组 (FEATURE GROUP)</label>
<div class="feature-mode-tabs btn-group">
<button class="opt-btn active" data-feature-mode="auto">自动模式</button>
<button class="opt-btn" data-feature-mode="specify">指定文案</button>
</div>
<!-- 指定文案模式第X屏文案与生成张数绑定传参 text: ["xxx","xxx"] -->
<div class="feature-mode-content" id="productMainFeatureSpecify" style="display: none;">
<div class="specify-screen-list" id="productMainSpecifyScreenList" data-panel="productMainImage"></div>
</div>
</div>
<div class="option-group">
<label class="section-title">画幅比例</label>
<div class="btn-group">
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn active" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
</div>
</div>
<div class="option-group">
<label class="section-title">分辨率 (RESOLUTION)</label>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">风格描述 (可选)</label>
<input type="text" class="form-input" id="productMainStyle" placeholder="如:白底图 浅色调 墨绿色调 户外 居家">
</div>
<div class="option-group">
<label class="section-title">输出文案语言 (OUTPUT LANGUAGE)</label>
<input type="text" class="form-input" id="productMainLanguage" placeholder="例如:中文输出、英文输出、中英混合">
</div>
<div class="option-group">
<label class="section-title">生成张数</label>
<div class="btn-group">
<button class="opt-btn active" data-count="1">1张</button>
<button class="opt-btn" data-count="3">3张</button>
<button class="opt-btn" data-count="5">5张</button>
<button class="opt-btn" data-count="7">7张</button>
<button class="opt-btn" data-count="9">9张</button>
</div>
</div>
<div class="option-group">
<div class="section-title-row">
<label class="section-title">产品图片 (最多8张)</label>
<button class="btn-paste-alone btn-paste-right" id="btnPasteProductMain">
<span>📋</span>
粘贴图片
</button>
</div>
<div class="hint-text">建议包含正面及多角度图</div>
<input type="file" id="fileInputProductMain" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneProductMain">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="productMainCount">0</span>/8)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewProductMain"></div>
</div>
<div class="option-group" style="display: none">
<div class="section-title-row">
<label class="section-title">参考颜色图</label>
<button class="btn-add-color-slot" id="btnAddColorSlot" type="button" title="添加颜色上传栏">
<span>+</span>
</button>
</div>
<div class="hint-text">点击 + 添加指定颜色的图片上传栏最多10组每组最多8张</div>
<div class="color-slots-container" id="colorSlotsContainer">
<!-- 默认无栏位,点击+号添加 -->
</div>
</div>
<button class="btn-generate" id="btnProductMainGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 克隆详情面板 -->
<div class="panel-content" id="cloneDetailPanel" style="display: none;">
<div class="option-group">
<label class="section-title">产品名称 (必填)</label>
<input type="text" class="form-input" id="cloneProductName" placeholder="如: 口红 吹风机 美容仪 美妆包">
</div>
<div class="option-group">
<label class="section-title">功能特点组 (FEATURE GROUP)</label>
<textarea class="form-textarea" id="cloneFeatures" placeholder="如: 1:保湿效果好 2:适合敏感肌 3:价格实惠" rows="4"></textarea>
</div>
<div class="option-group">
<div class="section-title">克隆模式</div>
<div class="btn-group">
<button class="opt-btn active" data-clone-mode="domestic">国内模式</button>
<button class="opt-btn" data-clone-mode="amazon">亚马逊模式</button>
<button class="opt-btn" data-clone-mode="specify">指定文案</button>
</div>
</div>
<!-- 指定文案下方:输入框与克隆参考图张数绑定,传参 text: [] -->
<div class="option-group" id="cloneSpecifyWrap" style="display: none;">
<label class="section-title">每张参考图对应文案 (可选)</label>
<div class="specify-screen-list" id="cloneSpecifyScreenList" data-panel="cloneDetail"></div>
</div>
<div class="option-group">
<div class="section-title">画幅比例</div>
<div class="btn-group btn-group-ratio-grid">
<button class="opt-btn active" data-ratio="auto">auto</button>
<button class="opt-btn" data-ratio="3:4">3:4</button>
<button class="opt-btn" data-ratio="1:1">1:1</button>
<button class="opt-btn" data-ratio="16:9">16:9</button>
<button class="opt-btn" data-ratio="9:16">9:16</button>
<button class="opt-btn" data-ratio="4:3">4:3</button>
<button class="opt-btn" data-ratio="2:3">2:3</button>
<button class="opt-btn" data-ratio="3:2">3:2</button>
<button class="opt-btn" data-ratio="21:9">21:9</button>
</div>
<div class="hint-text">💡 克隆模式强烈建议使用auto自动比例</div>
</div>
<div class="option-group">
<div class="section-title">分辨率 (RESOLUTION)</div>
<div class="btn-group">
<button class="opt-btn active" data-res="2k">2K</button>
<button class="opt-btn" data-res="4k">4K</button>
</div>
</div>
<div class="option-group">
<label class="section-title">输出文案语言 (OUTPUT LANGUAGE)</label>
<input type="text" class="form-input" id="cloneOutputLang" placeholder="例如: 中文输出、英文输出、中英混合">
</div>
<div class="option-group">
<div class="section-title-row">
<label class="section-title">产品实拍图</label>
<button class="btn-paste-alone btn-paste-right" id="btnPasteCloneProduct">
<span>📋</span>
粘贴图片
</button>
</div>
<input type="file" id="fileInputCloneProduct" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneCloneProduct">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="cloneProductCount">0</span>/6)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewCloneProduct"></div>
</div>
<div class="option-group">
<div class="section-title-row">
<label class="section-title">克隆参考图</label>
<button class="btn-paste-alone btn-paste-right" id="btnPasteCloneRef">
<span>📋</span>
粘贴图片
</button>
</div>
<input type="file" id="fileInputCloneRef" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneCloneRef">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传图片 (<span id="cloneRefCount">0</span>/14)</div>
<div class="upload-zone-hint">支持多选/拖拽上传</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewCloneRef"></div>
</div>
<button class="btn-generate" id="btnCloneDetailGenerate">
<span></span>
立即生成
</button>
</div>
<!-- 多模特图上传(除反推词外所有栏目显示,由 JS 插入到当前面板「立即生成」上方) -->
<div class="panel-content" id="modelImagesPanel" style="display: none;">
<div class="option-group">
<div class="section-title">多模特图上传(最多5张)</div>
<div class="hint-text">支持JPG PNG BMP JPEG</div>
<input type="file" id="fileInputModelImages" accept=".jpg,.jpeg,.png,.bmp" multiple hidden>
<div class="upload-zone" id="uploadZoneModelImages">
<div class="upload-zone-icon"></div>
<div class="upload-zone-text">点击上传 (<span id="modelImagesCount">0</span>/5)</div>
</div>
<div class="upload-preview product-detail-preview" id="uploadPreviewModelImages"></div>
</div>
</div>
</aside>
<!-- 中央区域 - 工作台 -->
<main class="center-area">
<div class="center-toolbar">
<button class="btn-manual-stitch" id="btnManualStitch" title="选择图片拼接顺序">
<span></span> 手动拼接
</button>
<!-- <button class="btn-refresh" id="btnBatchRefresh" title="批量重新获取">-->
<!-- <span>↻</span> 批量重新获取-->
<!-- </button>-->
<button class="btn-download" id="btnBatchDownload" title="批量下载原图">
<span></span> 批量下载原图
</button>
<div class="center-toolbar-spacer"></div>
<button class="btn-close-preview" id="btnClosePreview" title="清空工作台">X</button>
</div>
<div class="workspace-body">
<div class="workspace-left">
<div class="workspace-stitch-bar" id="workspaceStitchBar" style="display: none;">
<span class="workspace-stitch-count">已选 <span id="workspaceStitchCount">0</span></span>
<button type="button" class="workspace-stitch-cancel" id="workspaceStitchCancel">取消选择</button>
<button type="button" class="workspace-stitch-start" id="workspaceStitchStart">开始拼接</button>
</div>
<div class="workspace-left-inner" id="workspaceLeftInner"></div>
<div class="workspace-left-empty" id="workspaceLeftEmpty">生成或恢复到工作台的图片将显示在此处,点击可查看大图</div>
</div>
<div class="workspace-right">
<div class="workspace-stitched-wrap" id="workspaceStitchedWrap" style="display: none;">
<img id="workspaceStitchedImg" src="" alt="拼接效果">
</div>
<div class="workspace-right-empty" id="workspaceRightEmpty">右侧显示所有图片竖向拼接后的效果,点击可查看大图并保存</div>
</div>
<div class="workspace-stitch-loading" id="workspaceStitchLoading" style="display: none;">
<div class="workspace-stitch-loading-content">
<div class="spinner"></div>
<div class="loading-text">拼接中,请稍候...</div>
</div>
</div>
</div>
<div class="preview-loading" id="previewLoading" style="display: none;">
<div class="preview-loading-content">
<div class="spinner"></div>
<div class="loading-text">生成中,请稍候...</div>
</div>
</div>
<div class="preview-prompt-cards" id="previewPromptCards" style="display: none; position: absolute; padding: 20px;"></div>
</main>
<!-- 设置弹窗 -->
<div class="settings-overlay" id="settingsOverlay">
<div class="settings-modal">
<div class="settings-modal-header">
<span class="settings-modal-title">设置</span>
<button type="button" class="settings-modal-close" id="settingsModalClose">×</button>
</div>
<div class="settings-modal-body">
<div class="settings-section">
<div class="settings-section-title"><span class="icon"></span> API Key</div>
<div class="settings-section-desc">用于生成接口鉴权,必须设置后才能进行生成。仅保存在本机,不会上传。</div>
<div class="settings-input-wrap">
<input type="password" class="settings-input" id="settingsApiKey" placeholder="请输入 API Key" autocomplete="off">
</div>
</div>
<div class="settings-section">
<div class="settings-section-title"><span class="icon"></span> 自动保存位置</div>
<div class="settings-section-desc">设置后,生图完成后将自动保存副本到此文件夹。</div>
<div class="settings-input-wrap">
<input type="text" class="settings-input settings-path-input" id="settingsAutoSavePath" placeholder="未设置 (图片将仅显示在软件中),可手动输入或点击下方选择文件夹" style="width:100%;margin-bottom:8px;">
<input type="file" id="inputChooseFolder" webkitdirectory directory multiple style="display:none" />
<button type="button" class="btn-choose-folder" id="btnChooseFolder">选择文件夹</button>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title"><span class="icon"></span> 软件更新</div>
<div class="settings-update-block">
<span class="settings-version-text" id="settingsVersionText">当前版本: v1.0.0</span>
<button type="button" class="btn-check-update" id="btnCheckUpdate"><span></span> 检测更新</button>
</div>
<div class="settings-section-desc" style="margin-top: 8px;" id="settingsUpdateHint"></div>
<div class="settings-input-wrap" style="margin-top: 8px;">
<button type="button" class="btn-choose-folder" id="btnDownloadUpdate" style="display: none;">下载更新</button>
</div>
</div>
</div>
<div class="settings-modal-footer">
<button type="button" class="btn-settings-close" id="settingsSaveClose">关闭</button>
</div>
</div>
</div>
<!-- 手动拼接顺序弹窗 -->
<div class="stitch-order-overlay" id="stitchOrderOverlay">
<div class="stitch-order-modal">
<div class="stitch-order-title">选择参与拼接的图片及顺序(拖拽调整顺序,× 移出拼接)</div>
<div class="stitch-order-list" id="stitchOrderList"></div>
<div class="stitch-order-add-title" id="stitchOrderAddTitle">点击下方图片可加入拼接顺序:</div>
<div class="stitch-order-add-list" id="stitchOrderAddList"></div>
<div class="stitch-order-actions">
<button class="stitch-order-cancel" id="stitchOrderCancel">取消</button>
<button class="stitch-order-ok" id="stitchOrderOk">确定</button>
</div>
</div>
</div>
<!-- 右侧面板 -->
<aside class="right-panel">
<div class="history-header">
<div class="history-title">历史图库</div>
<div class="history-tabs" id="historyCategoryTabs">
<button type="button" class="history-tab active" data-panel-type="">全部</button>
<button type="button" class="history-tab" data-panel-type="textToImage">反推词</button>
<button type="button" class="history-tab" data-panel-type="randomPoster" style="display:none">随机海报</button>
<button type="button" class="history-tab" data-panel-type="clonePoster">克隆海报</button>
<button type="button" class="history-tab" data-panel-type="clothingDetail">服装详情</button>
<button type="button" class="history-tab" data-panel-type="buyerShow">买家秀</button>
<button type="button" class="history-tab" data-panel-type="productPoster">产品海报</button>
<button type="button" class="history-tab" data-panel-type="productDetail" style="display:none">产品详情</button>
<button type="button" class="history-tab" data-panel-type="extremeDetail">极致详情</button>
<button type="button" class="history-tab" data-panel-type="productMainImage">产品主图</button>
<button type="button" class="history-tab" data-panel-type="cloneDetail">克隆详情</button>
</div>
</div>
<div class="history-content">
<div class="history-grid" id="historyGrid"></div>
<div class="history-load-more" id="historyLoadMore" style="display: none;">
<button class="btn-load-more" id="btnHistoryLoadMore">加载更多</button>
</div>
<div class="history-empty" id="historyEmpty" style="display: none; padding: 20px; color: #666; font-size: 13px; text-align: center;">暂无历史记录</div>
</div>
</aside>
</div>
<!-- 图片查看模态框 -->
<div class="image-modal-overlay" id="imageModalOverlay">
<div class="image-modal">
<div class="image-modal-header">
<!-- <div class="image-modal-title" id="imageModalTitle">碗</div>-->
<div class="image-modal-subtitle" id="imageModalSubtitle">查看大图</div>
</div>
<div class="image-modal-body">
<button class="image-modal-arrow prev" id="modalPrevBtn"></button>
<img id="imageModalImg" src="" alt="">
<button class="image-modal-arrow next" id="modalNextBtn"></button>
</div>
<div class="image-modal-footer">
<button class="image-modal-btn restore" id="modalRestoreBtn" style="display: none;">
<span></span> 恢复到工作台
</button>
<button class="image-modal-btn batch-download" id="modalBatchDownloadBtn" style="display: none;">
<span></span> 批量下载全部
</button>
<button class="image-modal-btn download-long" id="modalDownloadLongBtn" style="display: none;">
<span></span> 下载长图
</button>
<button class="image-modal-btn workspace-download" id="modalWorkspaceDownloadBtn" style="display: none;">
<span></span> 下载原图
</button>
<button class="image-modal-btn workspace-edit" id="modalWorkspaceEditBtn" style="display: none;">
<span></span> 图片编辑
</button>
<button class="image-modal-btn workspace-regenerate" id="modalWorkspaceRegenerateBtn" style="display: none;">
<span></span> 重新生成
</button>
<button class="image-modal-btn close" id="modalCloseBtn">
<span>×</span> 关闭
</button>
</div>
</div>
</div>
<!-- 图片编辑弹窗(由查看大图内“图片编辑”触发) -->
<div class="image-edit-modal-overlay" id="imageEditModalOverlay">
<div class="image-edit-modal">
<div class="edit-image-wrap" id="imageEditImageWrap">
<img id="imageEditImg" src="" alt="">
<div class="edit-canvas-wrap" id="imageEditCanvasWrap">
<canvas id="imageEditCanvas"></canvas>
</div>
</div>
<div class="image-edit-toolbar">
<button type="button" class="tool-btn active" id="imageEditSmearBtn" title="涂抹模式">🖌 涂抹模式 ON</button>
<button type="button" class="tool-btn active" id="imageEditMarkerBtn" title="数字标记">📑 数字标记 ON</button>
<span class="brush-label">画笔: <span id="imageEditBrushSize">20</span>px</span>
<input type="range" class="brush-slider" id="imageEditBrushSlider" min="5" max="60" value="20">
<button type="button" class="tool-btn secondary" id="imageEditUndoBtn" title="撤销"></button>
<button type="button" class="tool-btn secondary" id="imageEditClearBtn" title="清除">🗑</button>
</div>
<div class="image-edit-hints">
*提示: 红色区域将被重新绘制<br>
*提示: 点击图片添加标记,右键删除标记
</div>
<div class="image-edit-prompt-row">
<textarea class="prompt-textarea" id="imageEditPromptInput" placeholder="如: 移除框选的内容&#10;如: 移除框选的内容 改成**&#10;如: 把**改成**&#10;如果上传了参考图提示词应该写:把图片一的**改成图片二的**..."></textarea>
<button type="button" class="btn-ref-image" id="imageEditRefImageBtn">参考图 □</button>
</div>
<div class="image-edit-modal-footer">
<button type="button" class="btn-cancel" id="imageEditCancelBtn">取消</button>
<button type="button" class="btn-confirm" id="imageEditConfirmBtn">确认生成</button>
</div>
</div>
</div>
<!-- 重新生成弹窗(由查看大图内“重新生成”触发) -->
<div class="regenerate-modal-overlay" id="regenerateModalOverlay">
<div class="regenerate-modal">
<div class="regenerate-modal-top">
<div class="regenerate-preview-title" id="regeneratePreviewTitle">Just the Right Capacity, Easily Holds Daily Essentials.</div>
<div class="regenerate-preview-subtitle" id="regeneratePreviewSubtitle">Phone, long wallet, keys, lipstick—all fit in without looking bulky.</div>
<div class="regenerate-preview-img-wrap">
<img id="regeneratePreviewImg" src="" alt="">
</div>
</div>
<div class="regenerate-modal-bottom">
<!-- <div class="regenerate-desc">上传的产品图片为严格外观依据,保持产品结构、颜色、五金细节100%一致,仅对光影和环境进行适度美化提升质感。</div>-->
<!-- <div class="regenerate-style">Visual Style: 经典百搭. Ensure this style is applied.</div>-->
<div class="regenerate-prompt-row">
<label class="regenerate-prompt-label">提示词</label>
<input type="text" class="regenerate-prompt-input" id="regeneratePromptInput" placeholder="请输入提示词">
</div>
<div class="regenerate-actions">
<button type="button" class="regenerate-btn cancel" id="regenerateCancelBtn">取消</button>
<button type="button" class="regenerate-btn confirm" id="regenerateConfirmBtn">确认生成</button>
</div>
<div class="regenerate-footer-actions">
<!-- <button type="button" class="regenerate-footer-btn" id="regenerateCopyBtn"><span>复制</span></button>-->
<button type="button" class="regenerate-footer-btn" id="regenerateSaveBtn"><span>保存</span></button>
<button type="button" class="regenerate-footer-btn close" id="regenerateCloseBtn"><span>×</span> 关闭</button>
</div>
</div>
</div>
</div>
<script>
// 选项按钮切换
document.querySelectorAll('.opt-btn[data-value]').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
document.querySelectorAll('.opt-btn[data-mode]').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const panel = this.closest('#defaultPanel');
if (panel) {
const mode = this.dataset.mode;
document.getElementById('defaultUnifiedPromptGroup').style.display = mode === 'unified' ? 'block' : 'none';
const batchGroup = document.getElementById('defaultBatchPromptGroup');
batchGroup.style.display = mode === 'batch' ? 'block' : 'none';
if (mode === 'batch') syncBatchPromptListFromCount();
}
});
});
// 根据生成数量同步分批提示词列表:显示与张数相同的 N 行,每行 提示词 + 生成张数(参考 syncSpecifyScreenList
function syncBatchPromptList(count) {
const list = document.getElementById('batchPromptList');
if (!list) return;
const countNum = Math.max(1, parseInt(count, 10) || 1);
const rows = list.querySelectorAll('.batch-prompt-row');
const prompts = Array.from(rows).map(r => (r.querySelector('.batch-prompt-input')?.value ?? '').trim());
const counts = Array.from(rows).map(r => (parseInt(r.querySelector('.batch-count-input')?.value, 10) || 1));
list.innerHTML = '';
for (let i = 0; i < countNum; i++) {
const idx = i + 1;
const div = document.createElement('div');
div.className = 'batch-prompt-row';
div.innerHTML = `<label class="batch-prompt-label">第${idx}批</label><input type="text" class="form-input batch-prompt-input" data-batch-index="${idx}" placeholder="提示词" value="">`;
//<input type="number" class="form-input batch-count-input" min="1" value="1" title="生成张数">
const promptInput = div.querySelector('.batch-prompt-input');
// const countInput = div.querySelector('.batch-count-input');
if (prompts[i] !== undefined) promptInput.value = prompts[i];
// if (counts[i] !== undefined) countInput.value = counts[i];
list.appendChild(div);
}
}
function syncBatchPromptListFromCount() {
const count = document.querySelector('#defaultPanel .opt-btn[data-value].active')?.dataset.value || '1';
syncBatchPromptList(count);
}
document.getElementById('defaultPanel').addEventListener('click', function(e) {
const countBtn = e.target.closest('.opt-btn[data-value]');
if (countBtn && document.querySelector('#defaultPanel .opt-btn[data-mode].active')?.dataset.mode === 'batch') {
syncBatchPromptList(countBtn.dataset.value);
}
});
document.querySelectorAll('.opt-btn[data-ratio]').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
document.querySelectorAll('.opt-btn[data-res]').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
// 上传图片状态
let uploadedImages = [];
const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp'];
//保存历史图库请求获取到的所有数据,用于工作台中渲染可以直接获取链接不用进行拼接一次太慢
let history_data = {}
// 工作台:当前栏目下的图片(生成 + 恢复到工作台)
let workspaceImages = [];
/** 与 workspaceImages 一一对应的历史记录 id用于重新生成时更新该条记录无则 null/undefined */
let workspaceImageIds = [];
let long_image_ls = []; //存储所有的长图,用于恢复到工作台时去除
/** 按栏目存储工作台状态:未操作过的栏目为空,操作过的保留上次内容 */
let workspaceByPanel = {};
function getCurrentPanelId() {
return document.querySelector('.nav-tab.active')?.dataset.panel || 'imageEdit';
}
function getWorkspaceState(panelId) {
if (!workspaceByPanel[panelId]) {
workspaceByPanel[panelId] = { images: [], imageIds: [], stitchOrder: null, manualStitchBarVisible: false, prompts: [], longImageUrl: null };
}
return workspaceByPanel[panelId];
}
function saveCurrentWorkspaceToState() {
const id = getCurrentPanelId();
const s = getWorkspaceState(id);
const imagesChanged = JSON.stringify(s.images) !== JSON.stringify(workspaceImages);
const orderChanged = JSON.stringify(s.stitchOrder || []) !== JSON.stringify(stitchOrder || []);
if (imagesChanged || orderChanged) s.longImageUrl = null; // 用户修改工作台后不再使用接口返回的长图
s.images = workspaceImages.slice();
s.imageIds = (workspaceImageIds && workspaceImageIds.length) ? workspaceImageIds.slice() : [];
s.stitchOrder = stitchOrder ? stitchOrder.slice() : null;
s.manualStitchBarVisible = manualStitchBarVisible;
}
function loadWorkspaceFromState(panelId) {
const s = getWorkspaceState(panelId);
workspaceImages = s.images.slice();
workspaceImageIds = (s.imageIds && s.imageIds.length) ? s.imageIds.slice() : [];
stitchOrder = s.stitchOrder ? s.stitchOrder.slice() : null;
manualStitchBarVisible = s.manualStitchBarVisible;
}
/** 正在生成时,对应的工作台卡片下标(用于在卡片上显示“生成中”;-1 表示无) */
let generatingWorkspaceIndex = -1;
/** 发起本次生成的栏目 id用于生成完成后写回该栏目状态 */
let generatingPanelId = null;
/** 当前生成请求的 AbortController用于取消生成 */
let currentAbortController = null;
/** 是否处于生成中(用于将「立即生成」切换为「取消生成」) */
let isGenerating = false;
function setGeneratingUI(show) {
isGenerating = show;
document.querySelectorAll('.btn-generate').forEach(btn => {
const span = btn.querySelector('span');
const textNode = span && span.nextSibling;
if (textNode) textNode.textContent = show ? '取消生成' : '立即生成';
if (span) span.textContent = show ? '✕' : '✦';
btn.classList.toggle('generating', show);
});
}
/** 手动拼接顺序null 表示按 workspaceImages 顺序,否则为参与拼接的下标数组(可为子集) */
let stitchOrder = null;
/** 是否已点击过「手动拼接」,为 true 时才显示左侧「已选N张/取消选择/开始拼接」条 */
let manualStitchBarVisible = false;
/** 拼接图缓存key 为 getOrderedWorkspaceUrls 的 JSONvalue 为 dataUrl */
let stitchCache = {};
const STITCH_CACHE_MAX = 5;
let stitchDebounceTimer = null;
const STITCH_DEBOUNCE_MS = 150;
// 设置API Key / 自动保存路径localStorage
const STORAGE_API_KEY = 'maixiang_api_key';
const STORAGE_AUTO_SAVE_PATH = 'maixiang_auto_save_path';
function getApiKey() { return (localStorage.getItem(STORAGE_API_KEY) || '').trim(); }
function getAutoSavePath() { return (localStorage.getItem(STORAGE_AUTO_SAVE_PATH) || '').trim(); }
function openSettings() {
document.getElementById('settingsApiKey').value = getApiKey();
const path = getAutoSavePath();
const pathEl = document.getElementById('settingsAutoSavePath');
pathEl.value = path || '';
document.getElementById('settingsOverlay').classList.add('show');
}
function closeSettings() {
const key = document.getElementById('settingsApiKey').value.trim();
localStorage.setItem(STORAGE_API_KEY, key);
document.getElementById('settingsOverlay').classList.remove('show');
}
document.getElementById('btnSettings').onclick = openSettings;
document.getElementById('settingsModalClose').onclick = closeSettings;
document.getElementById('settingsSaveClose').onclick = closeSettings;
document.getElementById('settingsOverlay').addEventListener('click', function(e) {
if (e.target === this) closeSettings();
});
document.getElementById('settingsApiKey').addEventListener('input', function() {
localStorage.setItem(STORAGE_API_KEY, this.value);
});
(function() {
var inputFolder = document.getElementById('inputChooseFolder');
var pathInputEl = document.getElementById('settingsAutoSavePath');
function savePathAndShow(path) {
path = (path || '').trim();
localStorage.setItem(STORAGE_AUTO_SAVE_PATH, path);
pathInputEl.value = path;
}
// 手动输入:失焦或输入时同步保存
pathInputEl.addEventListener('input', function() {
var path = this.value.trim();
localStorage.setItem(STORAGE_AUTO_SAVE_PATH, path);
});
pathInputEl.addEventListener('blur', function() {
localStorage.setItem(STORAGE_AUTO_SAVE_PATH, this.value.trim());
});
// 仅用于选择文件夹路径并保存,不上传任何文件
inputFolder.addEventListener('change', function() {
var files = this.files;
this.value = '';
if (!files || files.length === 0) return;
var f = files[0];
var path = '';
if (f.path) {
path = f.path.replace(/\\/g, '/').replace(/\/[^/]+$/, '');
}
if (path) {
savePathAndShow(path);
} else {
var pathInput = prompt('当前环境无法直接获取文件夹路径,请手动输入保存路径(留空则取消):');
if (pathInput !== null && pathInput.trim()) {
savePathAndShow(pathInput.trim());
}
}
});
document.getElementById('btnChooseFolder').onclick = function() {
var api = window.pywebview && window.pywebview.api;
if (api && api.select_folder) {
api.select_folder().then(savePathAndShow).catch(function(e) { console.warn('选择文件夹失败', e); });
} else {
inputFolder.click();
}
};
})();
const settingsVersionText = document.getElementById('settingsVersionText');
const settingsUpdateHint = document.getElementById('settingsUpdateHint');
const btnDownloadUpdate = document.getElementById('btnDownloadUpdate');
let currentVersion = '1.0.0';
let latestVersionInfo = null;
settingsVersionText.textContent = '当前版本: v' + currentVersion;
document.getElementById('btnCheckUpdate').onclick = async function() {
settingsUpdateHint.textContent = '正在检测更新...';
btnDownloadUpdate.style.display = 'none';
try {
const resp = await fetch('/api/version', { credentials: 'same-origin' }).catch(() => null);
if (resp && resp.ok) {
const data = await resp.json();
latestVersionInfo = data;
currentVersion = (data.version || currentVersion).replace(/^v/i, '');
settingsVersionText.textContent = '当前版本: v' + currentVersion;
if (data.has_update && data.latest_version) {
settingsUpdateHint.textContent = '发现新版本 v' + data.latest_version + (data.desc ? '' + data.desc : '');
btnDownloadUpdate.style.display = 'inline-block';
btnDownloadUpdate.textContent = '立即更新';
btnDownloadUpdate.onclick = async function() {
if (!confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return;
if (!data.file_url) {
settingsUpdateHint.textContent = '暂无下载地址,请关注官方渠道。';
return;
}
settingsUpdateHint.textContent = '正在下载并准备更新,程序将自动退出...';
btnDownloadUpdate.disabled = true;
try {
const updateResp = await fetch('/api/update/do', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_url: data.file_url })
});
const result = await updateResp.json().catch(() => ({}));
if (result.success) {
settingsUpdateHint.textContent = '更新已启动,程序即将退出...';
} else {
settingsUpdateHint.textContent = result.error || '更新启动失败';
btnDownloadUpdate.disabled = false;
}
} catch (e) {
settingsUpdateHint.textContent = '请求更新失败,请重试';
btnDownloadUpdate.disabled = false;
}
};
} else {
settingsUpdateHint.textContent = '已是最新版本';
}
} else {
settingsUpdateHint.textContent = '无法连接更新服务,请稍后重试。';
}
} catch (e) {
settingsUpdateHint.textContent = '检测更新失败';
}
};
btnDownloadUpdate.style.display = 'none';
// 点击「取消生成」时中止当前请求(捕获阶段优先于各按钮的 onclick
document.addEventListener('click', function(e) {
const btn = e.target.closest('.btn-generate');
if (!btn || !isGenerating) return;
e.preventDefault();
e.stopPropagation();
if (currentAbortController) currentAbortController.abort();
document.getElementById('previewLoading').style.display = 'none';
generatingWorkspaceIndex = -1;
renderWorkspaceLeft();
setGeneratingUI(false);
currentAbortController = null;
}, true);
// 生成图片 API 调用与结果展示(通过 Flask API支持 OSS 上传及历史记录)
async function callGenerate(params) {
if (!getApiKey()) {
alert('请先在设置中配置 API Key 后再进行生成。');
openSettings();
return;
}
params.panel_type = document.querySelector('.nav-tab.active')?.dataset.panel || 'imageEdit';
generatingPanelId = getCurrentPanelId();
const api = window.pywebview?.api;
const loading = document.getElementById('previewLoading');
hidePromptCards();
const cardIndex = params.generatingCardIndex;
const useCardLoading = cardIndex !== undefined && cardIndex >= 0;
currentAbortController = new AbortController();
setGeneratingUI(true);
if (useCardLoading) {
generatingWorkspaceIndex = cardIndex;
renderWorkspaceLeft();
} else {
loading.style.display = 'flex';
}
try {
let result;
const sendParams = { ...params };
delete sendParams.generatingCardIndex;
sendParams.api_key = getApiKey();
try {
const resp = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(sendParams),
signal: currentAbortController.signal,
});
result = await resp.json();
if (resp.status === 401) {
window.location.href = '/login';
return;
}
} catch (fetchErr) {
if (fetchErr.name === 'AbortError') throw fetchErr;
if (api?.generate_images) {
result = await api.generate_images(sendParams);
} else {
throw fetchErr;
}
}
loading.style.display = 'none';
generatingWorkspaceIndex = -1;
const panelId = generatingPanelId != null ? generatingPanelId : getCurrentPanelId();
if (result.success) {
if (params.menu === 1 && result.prompts && result.prompts.length > 0) {
showPromptCards(result.prompts);
} else if (result.urls && result.urls.length > 0) {
const state = getWorkspaceState(panelId);
const isEditReplace = params.menu === 11 && cardIndex !== undefined && cardIndex >= 0;
if (isEditReplace) {
// 重新生成:只替换一张图。若栏目 state 尚未保存images 为空),先用当前工作台列表初始化
if (state.images.length === 0 && getCurrentPanelId() === panelId) {
state.images = workspaceImages.slice();
state.imageIds = (workspaceImageIds && workspaceImageIds.length) ? workspaceImageIds.slice() : [];
}
if (cardIndex < state.images.length) {
state.images[cardIndex] = result.long_image_url !== "" ? result.long_image_url : result.urls[0];
if (result.history_id != null) {
if (!state.imageIds) state.imageIds = [];
while (state.imageIds.length <= cardIndex) state.imageIds.push(null);
state.imageIds[cardIndex] = result.history_id;
}
}
state.stitchOrder = state.stitchOrder ? state.stitchOrder.slice() : null;
} else {
state.images = result.urls.slice();
if (result.history_id != null) {
state.imageIds = state.images.map(function() { return result.history_id; });
}
state.stitchOrder = null;
state.longImageUrl = (result.long_image_url && result.long_image_url !== '') ? result.long_image_url : null;
}
if (getCurrentPanelId() === panelId) {
workspaceImages = state.images.slice();
workspaceImageIds = (state.imageIds && state.imageIds.length) ? state.imageIds.slice() : [];
stitchOrder = state.stitchOrder ? state.stitchOrder.slice() : null;
renderWorkspaceLeft();
if (!isEditReplace) renderStitchedPreview();
}
// 正常生成:历史图库只增加一条,显示长图(无长图时用首图)
if (!isEditReplace) {
const displayUrl = (result.long_image_url && result.long_image_url !== '') ? result.long_image_url : (result.urls && result.urls[0]);
if (displayUrl) appendToHistoryGrid([displayUrl], true);
}
// 自动保存到设置中的文件夹(重新生成不自动保存、不刷新历史)
const autoPath = getAutoSavePath();
if (!isEditReplace && autoPath && result.urls.length > 0) {
const saveApi = window.pywebview?.api;
if (saveApi?.save_image_to_folder) {
const ts = new Date().toISOString().replace(/[-:]/g, '').slice(0, 15);
result.urls.forEach((url, i) => {
const name = `生成_${ts}_${i + 1}.png`;
saveApi.save_image_to_folder(url, autoPath, name).catch(() => {});
});
}
}
} else {
alert(result.error || '生成失败');
}
} else {
alert(result.error || '生成失败');
}
} catch (err) {
loading.style.display = 'none';
generatingWorkspaceIndex = -1;
renderWorkspaceLeft();
if (err.name === 'AbortError') {
// 用户点击了「取消生成」,不提示
} else {
alert('生成失败: ' + (err.message || err));
}
} finally {
setGeneratingUI(false);
currentAbortController = null;
generatingPanelId = null;
}
}
function hidePromptCards() {
const promptCards = document.getElementById('previewPromptCards');
if (promptCards) {
promptCards.style.display = 'none';
}
}
function showPromptCards(prompts) {
const promptCards = document.getElementById('previewPromptCards');
promptCards.style.display = 'flex';
promptCards.innerHTML = prompts.map((text, i) => {
const id = 'prompt-card-' + i;
return `<div class="prompt-card">
<div class="prompt-card-content" id="${id}">${escapeHtml(String(text || ''))}</div>
<button class="prompt-card-copy" data-copy-id="${id}">复制</button>
</div>`;
}).join('');
promptCards.querySelectorAll('.prompt-card-copy').forEach(btn => {
btn.onclick = function() {
const el = document.getElementById(btn.dataset.copyId);
const text = el ? el.textContent : '';
navigator.clipboard.writeText(text).then(() => {
btn.textContent = '已复制';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 1500);
});
};
});
// 保存反推词结果到当前栏目状态,便于切换栏目后再切回时恢复
const panelId = getCurrentPanelId();
if (panelId === 'textToImage') getWorkspaceState(panelId).prompts = prompts;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function formatHistoryTime(created_at) {
if (!created_at) return '';
if (created_at.length >= 16) return created_at.slice(11, 16);
if (created_at.length >= 5) return created_at.slice(-5);
return created_at;
}
function appendToHistoryGrid(urls, isNew) {
const historyGrid = document.getElementById('historyGrid');
const timeStr = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
urls.forEach(url => {
const item = document.createElement('div');
item.className = 'history-item';
item.innerHTML = `<div class="history-item-img-wrap"><img src="${url}" alt=""><span class="history-item-time">${isNew ? '新' : timeStr}</span></div>`;
item.onclick = function(e) {
if (!e.target.closest('.remove-btn')) openImageModal(urls, urls.indexOf(url), { source: 'history' });
};
historyGrid.insertBefore(item, historyGrid.firstChild);
});
}
let historyPage = 0;
let historyHasMore = true;
let historyLoading = false;
/** 当前历史图库栏目筛选:空字符串为全部 */
let historyPanelType = '';
async function loadHistoryPage(append) {
if (historyLoading) return;
historyLoading = true;
const page = append ? historyPage + 1 : 1;
const url = `/api/history?page=${page}&page_size=20` + (historyPanelType ? `&panel_type=${encodeURIComponent(historyPanelType)}` : '');
try {
const resp = await fetch(url, { credentials: 'same-origin' });
const data = await resp.json();
if (resp.status === 401) {
window.location.href = '/login';
return;
}
if (!data.success) throw new Error(data.error || '加载失败');
const historyGrid = document.getElementById('historyGrid');
const loadMoreDiv = document.getElementById('historyLoadMore');
const emptyDiv = document.getElementById('historyEmpty');
if (!append) historyGrid.innerHTML = '';
const items = data.items || [];
items.forEach(rec => {
history_data[rec.id] = rec
const urls = rec.result_urls || [];
if (urls.length === 0) return;
const longUrl = rec.long_image_url || null;
long_image_ls.push(longUrl)
const thumbUrl = longUrl || urls[0];
const modalUrls = longUrl ? [longUrl].concat(urls) : urls;
const modalStartIndex = 0;
const timeStr = formatHistoryTime(rec.created_at);
const item = document.createElement('div');
item.className = 'history-item';
item.innerHTML = `<div class="history-item-img-wrap"><img src="${thumbUrl}" alt=""><span class="history-item-time">${timeStr}</span></div>`;
item.history_id = rec.id
item.onclick = function(e) {
if (!e.target.closest('.remove-btn')) openImageModal(modalUrls, modalStartIndex, { source: 'history', historyId: rec.id });
};
historyGrid.appendChild(item);
});
historyPage = data.page;
historyHasMore = (data.page * data.page_size) < (data.total || 0);
loadMoreDiv.style.display = historyHasMore ? 'block' : 'none';
emptyDiv.style.display = !append && items.length === 0 ? 'block' : 'none';
} catch (err) {
console.warn('加载历史失败:', err);
} finally {
historyLoading = false;
}
}
function renderWorkspaceLeft() {
const inner = document.getElementById('workspaceLeftInner');
const empty = document.getElementById('workspaceLeftEmpty');
const stitchBar = document.getElementById('workspaceStitchBar');
const stitchCountEl = document.getElementById('workspaceStitchCount');
if (workspaceImages.length === 0) {
inner.innerHTML = '';
if (stitchBar) stitchBar.style.display = 'none';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
if (stitchBar) stitchBar.style.display = manualStitchBarVisible ? 'flex' : 'none';
const orderMap = (stitchOrder && stitchOrder.length) ? (() => { const m = {}; stitchOrder.forEach((idx, pos) => { m[idx] = pos + 1; }); return m; })() : {};
const displayCount = (stitchOrder && stitchOrder.length) ? stitchOrder.length : workspaceImages.length;
if (stitchCountEl) stitchCountEl.textContent = displayCount;
inner.innerHTML = workspaceImages.map((url, i) => {
const isLoading = generatingWorkspaceIndex === i;
const loadingHtml = isLoading
? '<div class="workspace-thumb-loading"><div class="spinner"></div><div class="txt">生成中,请稍候...</div></div>'
: '';
const inStitch = orderMap[i];
const badgeHtml = inStitch ? `<span class="workspace-thumb-badge">${inStitch}</span>` : '';
const stitchClass = inStitch ? ' workspace-thumb-in-stitch' : '';
return `<div class="workspace-thumb${stitchClass}" data-idx="${i}">${badgeHtml}<img src="${url}" alt="">${loadingHtml}</div>`;
}).join('');
inner.querySelectorAll('.workspace-thumb').forEach(el => {
const i = parseInt(el.dataset.idx, 10);
if (generatingWorkspaceIndex === i) return;
el.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
// 未触发手动拼接时:单击查看大图;已触发时:单击选择/取消选择序号
if (manualStitchBarVisible) {
toggleStitchOrder(i);
} else {
openImageModal(workspaceImages, i, { source: 'workspace' });
}
};
el.ondblclick = function(e) {
e.preventDefault();
e.stopPropagation();
openImageModal(workspaceImages, i, { source: 'workspace' });
};
});
}
function toggleStitchOrder(idx) {
if (!stitchOrder || stitchOrder.length === 0) {
stitchOrder = [idx];
} else {
const pos = stitchOrder.indexOf(idx);
if (pos >= 0) stitchOrder = stitchOrder.filter(i => i !== idx);
else stitchOrder = stitchOrder.concat([idx]);
}
if (stitchOrder.length === 0) stitchOrder = null;
renderWorkspaceLeft();
}
function getOrderedWorkspaceUrls() {
if (!stitchOrder || stitchOrder.length === 0) return workspaceImages.slice();
return stitchOrder.map(idx => workspaceImages[idx]).filter(Boolean);
}
function renderStitchedPreview() {
if (stitchDebounceTimer) clearTimeout(stitchDebounceTimer);
stitchDebounceTimer = setTimeout(renderStitchedPreviewNow, STITCH_DEBOUNCE_MS);
}
function renderStitchedPreviewNow() {
stitchDebounceTimer = null;
const wrap = document.getElementById('workspaceStitchedWrap');
const imgEl = document.getElementById('workspaceStitchedImg');
const empty = document.getElementById('workspaceRightEmpty');
const urls = getOrderedWorkspaceUrls();
if (urls.length === 0) {
wrap.style.display = 'none';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
wrap.style.display = 'block';
const state = getWorkspaceState(getCurrentPanelId());
const orderedFromState = state.stitchOrder ? state.stitchOrder.map(i => state.images[i]).filter(Boolean) : state.images.slice();
if (state.longImageUrl && JSON.stringify(orderedFromState) === JSON.stringify(urls)) {
imgEl.src = state.longImageUrl;
imgEl.dataset.stitchedDataUrl = state.longImageUrl;
const cacheKey = getCurrentPanelId() + '\n' + JSON.stringify(urls);
stitchCache[cacheKey] = state.longImageUrl;
return;
}
const cacheKey = getCurrentPanelId() + '\n' + JSON.stringify(urls);
if (stitchCache[cacheKey]) {
imgEl.src = stitchCache[cacheKey];
imgEl.dataset.stitchedDataUrl = stitchCache[cacheKey];
return;
}
buildStitchedDataUrl(urls).then(dataUrl => {
if (Object.keys(stitchCache).length >= STITCH_CACHE_MAX) {
const first = Object.keys(stitchCache)[0];
delete stitchCache[first];
}
stitchCache[cacheKey] = dataUrl;
imgEl.src = dataUrl;
imgEl.dataset.stitchedDataUrl = dataUrl;
}).catch(() => {
imgEl.src = urls[0];
imgEl.dataset.stitchedDataUrl = '';
});
}
function buildStitchedDataUrl(urls) {
return new Promise((resolve, reject) => {
if (urls.length === 0) { reject(new Error('no images')); return; }
const load = (src) => new Promise((res, rej) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => res(img);
img.onerror = rej;
img.src = src;
});
Promise.all(urls.map(load)).then(imgs => {
let maxW = 0, totalH = 0;
imgs.forEach(img => {
if (img.naturalWidth && img.naturalHeight) {
if (img.naturalWidth > maxW) maxW = img.naturalWidth;
totalH += img.naturalHeight;
} else {
if (img.width > maxW) maxW = img.width;
totalH += img.height;
}
});
const canvas = document.createElement('canvas');
canvas.width = maxW;
canvas.height = totalH;
const ctx = canvas.getContext('2d');
let y = 0;
imgs.forEach(img => {
const w = img.naturalWidth || img.width;
const h = img.naturalHeight || img.height;
ctx.drawImage(img, 0, y, w, h);
y += h;
});
try {
resolve(canvas.toDataURL('image/png'));
} catch (e) {
reject(e);
}
}).catch(reject);
});
}
function getMaxImages() {
const activeTab = document.querySelector('.nav-tab.active');
return parseInt(activeTab?.dataset.maxImages || '8', 10);
}
function updateUploadUI() {
const maxImages = getMaxImages();
const group = document.getElementById('uploadOptionGroup');
const titleEl = document.getElementById('uploadSectionTitle');
if (maxImages === 0) {
group.classList.add('upload-option-hidden');
return;
}
group.classList.remove('upload-option-hidden');
titleEl.textContent = `上传图片(最多${maxImages}张)`;
if (uploadedImages.length > maxImages) {
uploadedImages = uploadedImages.slice(0, maxImages);
}
renderUploadPreview();
}
function renderUploadPreview() {
const container = document.getElementById('uploadPreview');
container.innerHTML = '';
uploadedImages.forEach((dataUrl, idx) => {
const item = document.createElement('div');
item.className = 'upload-preview-item';
item.innerHTML = `<img src="${dataUrl}" alt="预览${idx + 1}"><button class="remove-btn" data-idx="${idx}">×</button>`;
container.appendChild(item);
});
container.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
const idx = parseInt(btn.dataset.idx, 10);
uploadedImages.splice(idx, 1);
renderUploadPreview();
};
});
}
function addImages(files) {
const maxImages = getMaxImages();
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, maxImages - uploadedImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (uploadedImages.length < maxImages) {
uploadedImages.push(e.target.result);
renderUploadPreview();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnSelectFiles').onclick = () => document.getElementById('fileInput').click();
document.getElementById('fileInput').onchange = e => {
if (e.target.files?.length) {
addImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnPaste').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addImages(images);
} catch (err) {
console.warn('粘贴图片失败:', err);
}
};
document.getElementById('btnClear').onclick = () => {
uploadedImages = [];
renderUploadPreview();
};
// 导航标签与面板切换
function switchLeftPanel() {
const activeTab = document.querySelector('.nav-tab.active');
const panel = activeTab?.dataset.panel || '';
const isTextToImage = panel === 'textToImage';
const modelImagesPanel = document.getElementById('modelImagesPanel');
document.getElementById('defaultPanel').style.display = (panel === '' || panel === 'imageEdit') ? 'block' : 'none';
document.getElementById('productPosterPanel').style.display = panel === 'productPoster' ? 'block' : 'none';
const textToImagePanel = document.getElementById('textToImagePanel');
if (textToImagePanel) textToImagePanel.style.display = isTextToImage ? 'block' : 'none';
document.getElementById('randomPosterPanel').style.display = panel === 'randomPoster' ? 'block' : 'none';
document.getElementById('clonePosterPanel').style.display = panel === 'clonePoster' ? 'block' : 'none';
document.getElementById('clothingDetailPanel').style.display = panel === 'clothingDetail' ? 'block' : 'none';
document.getElementById('productDetailPanel').style.display = panel === 'productDetail' ? 'block' : 'none';
document.getElementById('extremeDetailPanel').style.display = panel === 'extremeDetail' ? 'block' : 'none';
document.getElementById('productMainImagePanel').style.display = panel === 'productMainImage' || panel === 'buyerShow' ? 'block' : 'none';
document.getElementById('cloneDetailPanel').style.display = panel === 'cloneDetail' ? 'block' : 'none';
// 模特图放在当前面板最底(「立即生成」上方)
if (isTextToImage) {
modelImagesPanel.style.display = 'none';
} else {
let panelElId = (panel === '' || panel === 'imageEdit') ? 'defaultPanel' : panel + 'Panel';
if (panel === 'buyerShow') panelElId = 'productMainImagePanel'; // 买家秀与产品主图共用同一面板
const panelEl = document.getElementById(panelElId);
const btnGenerate = panelEl ? panelEl.querySelector('.btn-generate') : null;
if (panelEl && btnGenerate) {
panelEl.insertBefore(modelImagesPanel, btnGenerate);
}
modelImagesPanel.style.display = 'block';
}
}
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.addEventListener('click', function() {
const nextPanel = this.dataset.panel || 'imageEdit';
saveCurrentWorkspaceToState();
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
loadWorkspaceFromState(nextPanel);
renderWorkspaceLeft();
renderStitchedPreview();
// 切回反推词时恢复已保存的提示词卡片,否则隐藏
const nextState = getWorkspaceState(nextPanel);
if (nextPanel === 'textToImage' && nextState.prompts && nextState.prompts.length > 0) {
showPromptCards(nextState.prompts);
} else {
hidePromptCards();
}
switchLeftPanel();
updateUploadUI();
});
});
switchLeftPanel(); // 初始化面板显示
loadWorkspaceFromState(getCurrentPanelId()); // 工作台显示当前栏目状态(初始为空)
renderWorkspaceLeft();
renderStitchedPreview();
// 多模特图上传(最多5张) - 除反推词外所有栏目
let modelImages = [];
const MAX_MODEL_IMAGES = 5;
const uploadZoneModelImages = document.getElementById('uploadZoneModelImages');
const fileInputModelImages = document.getElementById('fileInputModelImages');
const previewModelImages = document.getElementById('uploadPreviewModelImages');
const countElModelImages = document.getElementById('modelImagesCount');
function updateModelImagesUI() {
if (!countElModelImages) return;
countElModelImages.textContent = modelImages.length;
if (modelImages.length > 0) {
previewModelImages.style.display = 'grid';
previewModelImages.innerHTML = modelImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewModelImages.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
modelImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateModelImagesUI();
};
});
uploadZoneModelImages.style.display = modelImages.length >= MAX_MODEL_IMAGES ? 'none' : 'block';
} else {
uploadZoneModelImages.style.display = 'block';
previewModelImages.style.display = 'none';
previewModelImages.innerHTML = '';
}
}
function addModelImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_MODEL_IMAGES - modelImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = ev => {
if (modelImages.length < MAX_MODEL_IMAGES) {
modelImages.push(ev.target.result);
updateModelImagesUI();
}
};
reader.readAsDataURL(file);
});
}
if (uploadZoneModelImages) {
uploadZoneModelImages.onclick = () => fileInputModelImages.click();
uploadZoneModelImages.ondragover = e => { e.preventDefault(); uploadZoneModelImages.style.borderColor = '#3498db'; };
uploadZoneModelImages.ondragleave = () => { uploadZoneModelImages.style.borderColor = '#3a3a3a'; };
uploadZoneModelImages.ondrop = e => {
e.preventDefault();
uploadZoneModelImages.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addModelImages(e.dataTransfer.files);
};
}
if (fileInputModelImages) {
fileInputModelImages.onchange = e => {
if (e.target.files?.length) {
addModelImages(e.target.files);
e.target.value = '';
}
};
}
function getModelImagesParam() {
if (modelImages.length === 0) return {};
return { model_images: modelImages };
}
// 反推词 - 字数统计
const textToImagePromptInput = document.getElementById('textToImagePromptInput');
const textToImageCharCount = document.getElementById('textToImageCharCount');
if (textToImagePromptInput && textToImageCharCount) {
textToImagePromptInput.addEventListener('input', () => {
textToImageCharCount.textContent = textToImagePromptInput.value.length + ' 字符';
});
}
// 反推词 - 上传图片(最多8张)
let reverseRefImages = [];
const uploadZoneReverseRef = document.getElementById('uploadZoneReverseRef');
const fileInputReverseRef = document.getElementById('fileInputReverseRef');
const previewReverseRef = document.getElementById('uploadPreviewReverseRef');
const countElReverseRef = document.getElementById('reverseRefCount');
const MAX_REVERSE_REF = 8;
function updateReverseRefUI() {
countElReverseRef.textContent = reverseRefImages.length;
if (reverseRefImages.length > 0) {
previewReverseRef.style.display = 'grid';
previewReverseRef.innerHTML = reverseRefImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewReverseRef.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
reverseRefImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateReverseRefUI();
};
});
uploadZoneReverseRef.style.display = reverseRefImages.length >= MAX_REVERSE_REF ? 'none' : 'block';
} else {
uploadZoneReverseRef.style.display = 'block';
previewReverseRef.style.display = 'none';
previewReverseRef.innerHTML = '';
}
}
function addReverseRefImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_REVERSE_REF - reverseRefImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = ev => {
if (reverseRefImages.length < MAX_REVERSE_REF) {
reverseRefImages.push(ev.target.result);
updateReverseRefUI();
}
};
reader.readAsDataURL(file);
});
}
uploadZoneReverseRef.onclick = () => fileInputReverseRef.click();
uploadZoneReverseRef.ondragover = e => { e.preventDefault(); uploadZoneReverseRef.style.borderColor = '#3498db'; };
uploadZoneReverseRef.ondragleave = () => { uploadZoneReverseRef.style.borderColor = '#3a3a3a'; };
uploadZoneReverseRef.ondrop = e => {
e.preventDefault();
uploadZoneReverseRef.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addReverseRefImages(e.dataTransfer.files);
};
fileInputReverseRef.onchange = e => {
if (e.target.files?.length) {
addReverseRefImages(e.target.files);
e.target.value = '';
}
};
// 反推词 - 上传视频(只支持1个)
let reverseVideoData = null;
const uploadZoneReverseVideo = document.getElementById('uploadZoneReverseVideo');
const fileInputReverseVideo = document.getElementById('fileInputReverseVideo');
const previewReverseVideo = document.getElementById('uploadPreviewReverseVideo');
function updateReverseVideoUI() {
if (reverseVideoData) {
uploadZoneReverseVideo.style.display = 'none';
previewReverseVideo.style.display = 'block';
previewReverseVideo.innerHTML = `<div class="upload-preview-item" style="grid-column: 1/-1;"><span style="color:#888;">已选视频</span><button class="remove-btn" data-clear-video="1">×</button></div>`;
previewReverseVideo.querySelector('.remove-btn').onclick = () => {
reverseVideoData = null;
updateReverseVideoUI();
};
} else {
uploadZoneReverseVideo.style.display = 'block';
previewReverseVideo.style.display = 'none';
previewReverseVideo.innerHTML = '';
}
}
uploadZoneReverseVideo.onclick = () => fileInputReverseVideo.click();
fileInputReverseVideo.onchange = e => {
const file = e.target.files?.[0];
if (file && file.type.startsWith('video/')) {
const reader = new FileReader();
reader.onload = ev => { reverseVideoData = ev.target.result; updateReverseVideoUI(); };
reader.readAsDataURL(file);
}
e.target.value = '';
};
document.getElementById('btnTextToImageGenerate').onclick = function() {
const prompt = document.getElementById('textToImagePromptInput').value.trim();
const params = { menu: 1, prompt: prompt || '', ref_images: reverseRefImages };
if (reverseVideoData) params.video = reverseVideoData;
callGenerate(params);
};
// 克隆海报面板 - 画幅、分辨率按钮
document.getElementById('clonePosterPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
// 随机海报面板 - 画幅、分辨率按钮
document.getElementById('randomPosterPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
// 随机海报 - 上传图片(最多6张)
let randomPosterImages = [];
const uploadZoneRP = document.getElementById('uploadZoneRandomPoster');
const fileInputRP = document.getElementById('fileInputRandomPoster');
const previewRP = document.getElementById('uploadPreviewRandomPoster');
const countElRP = document.getElementById('randomPosterImagesCount');
const MAX_RANDOM_POSTER_IMAGES = 6;
function updateRandomPosterUploadUI() {
countElRP.textContent = randomPosterImages.length;
if (randomPosterImages.length > 0) {
previewRP.style.display = 'grid';
previewRP.innerHTML = randomPosterImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewRP.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
randomPosterImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateRandomPosterUploadUI();
};
});
uploadZoneRP.style.display = randomPosterImages.length >= MAX_RANDOM_POSTER_IMAGES ? 'none' : 'block';
} else {
uploadZoneRP.style.display = 'block';
previewRP.style.display = 'none';
previewRP.innerHTML = '';
}
}
function addRandomPosterImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_RANDOM_POSTER_IMAGES - randomPosterImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = ev => {
if (randomPosterImages.length < MAX_RANDOM_POSTER_IMAGES) {
randomPosterImages.push(ev.target.result);
updateRandomPosterUploadUI();
}
};
reader.readAsDataURL(file);
});
}
uploadZoneRP.onclick = () => fileInputRP.click();
uploadZoneRP.ondragover = e => { e.preventDefault(); uploadZoneRP.style.borderColor = '#3498db'; };
uploadZoneRP.ondragleave = () => { uploadZoneRP.style.borderColor = '#3a3a3a'; };
uploadZoneRP.ondrop = e => {
e.preventDefault();
uploadZoneRP.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addRandomPosterImages(e.dataTransfer.files);
};
fileInputRP.onchange = e => {
if (e.target.files?.length) {
addRandomPosterImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnPasteRandomPoster').onclick = async () => {
try {
const items = await navigator.clipboard.read().catch(() => []);
const images = [];
for (const item of items) {
if (item.types.includes('image/png') || item.types.includes('image/jpeg')) {
const blob = await item.getType('image/png').catch(() => item.getType('image/jpeg'));
if (blob) images.push(blob);
}
}
if (images.length) addRandomPosterImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
document.getElementById('btnRandomPosterGenerate').onclick = function() {
const mainTitle = document.getElementById('randomPosterMainTitle').value.trim();
const subtitle = document.getElementById('randomPosterSubtitle').value.trim();
const ratio = document.querySelector('#randomPosterPanel .opt-btn[data-ratio].active')?.dataset.ratio || '16:9';
const res = document.querySelector('#randomPosterPanel .opt-btn[data-res].active')?.dataset.res || '2k';
if (randomPosterImages.length === 0) { alert('请上传图片(最多6张)'); return; }
callGenerate({ menu: 3, name: mainTitle || '产品', desc: subtitle, ratio, resolution: res, count: 1, ref_images: randomPosterImages, ...getModelImagesParam() });
};
// 产品海报面板 - 画幅、分辨率按钮
document.getElementById('productPosterPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
// 产品海报 - 上传图片(最多6张)
let productPosterImages = [];
const uploadZoneProductPoster = document.getElementById('uploadZoneProductPoster');
const fileInputProductPoster = document.getElementById('fileInputProductPoster');
const previewProductPoster = document.getElementById('uploadPreviewProductPoster');
const countElProductPoster = document.getElementById('productPosterImagesCount');
const MAX_PRODUCT_POSTER_IMAGES = 6;
function updateProductPosterUploadUI() {
countElProductPoster.textContent = productPosterImages.length;
if (productPosterImages.length > 0) {
previewProductPoster.style.display = 'grid';
previewProductPoster.innerHTML = productPosterImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewProductPoster.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
productPosterImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateProductPosterUploadUI();
};
});
uploadZoneProductPoster.style.display = productPosterImages.length >= MAX_PRODUCT_POSTER_IMAGES ? 'none' : 'block';
} else {
uploadZoneProductPoster.style.display = 'block';
previewProductPoster.style.display = 'none';
previewProductPoster.innerHTML = '';
}
}
function addProductPosterImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_PRODUCT_POSTER_IMAGES - productPosterImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = ev => {
if (productPosterImages.length < MAX_PRODUCT_POSTER_IMAGES) {
productPosterImages.push(ev.target.result);
updateProductPosterUploadUI();
}
};
reader.readAsDataURL(file);
});
}
uploadZoneProductPoster.onclick = () => fileInputProductPoster.click();
uploadZoneProductPoster.ondragover = e => { e.preventDefault(); uploadZoneProductPoster.style.borderColor = '#3498db'; };
uploadZoneProductPoster.ondragleave = () => { uploadZoneProductPoster.style.borderColor = '#3a3a3a'; };
uploadZoneProductPoster.ondrop = e => {
e.preventDefault();
uploadZoneProductPoster.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addProductPosterImages(e.dataTransfer.files);
};
fileInputProductPoster.onchange = e => {
if (e.target.files?.length) {
addProductPosterImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnPasteProductPoster').onclick = async () => {
try {
const items = await navigator.clipboard.read().catch(() => []);
const images = [];
for (const item of items) {
if (item.types.includes('image/png') || item.types.includes('image/jpeg')) {
const blob = await item.getType('image/png').catch(() => item.getType('image/jpeg'));
if (blob) images.push(blob);
}
}
if (images.length) addProductPosterImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
document.getElementById('btnProductPosterGenerate').onclick = function() {
const mainTitle = document.getElementById('productPosterMainTitle').value.trim();
const subtitle = document.getElementById('productPosterSubtitle').value.trim();
const brandName = document.getElementById('productPosterBrand').value.trim();
const ingredients = document.getElementById('productPosterIngredients').value.trim();
const activity = document.getElementById('productPosterActivity').value.trim();
const ratio = document.querySelector('#productPosterPanel .opt-btn[data-ratio].active')?.dataset.ratio || '16:9';
const res = document.querySelector('#productPosterPanel .opt-btn[data-res].active')?.dataset.res || '2k';
if (productPosterImages.length === 0) { alert('请上传图片(最多6张)'); return; }
callGenerate({
menu: 6,
name: mainTitle || '产品',
desc: subtitle,
brand_name: brandName,
Ingredients: ingredients,
activity: activity,
ratio,
resolution: res,
count: 1,
ref_images: productPosterImages,
...getModelImagesParam()
});
};
// 克隆海报 - 上传版式(1张)
let clonePosterLayoutImage = null;
const uploadZoneClonePosterLayout = document.getElementById('uploadZoneClonePosterLayout');
const fileInputClonePosterLayout = document.getElementById('fileInputClonePosterLayout');
const previewClonePosterLayout = document.getElementById('uploadPreviewClonePosterLayout');
function updateClonePosterLayoutUploadUI() {
if (clonePosterLayoutImage) {
uploadZoneClonePosterLayout.style.display = 'none';
previewClonePosterLayout.style.display = 'grid';
previewClonePosterLayout.innerHTML = `<div class="upload-preview-item"><img src="${clonePosterLayoutImage}" alt=""><button class="remove-btn" data-clear-layout="1">×</button></div>`;
previewClonePosterLayout.querySelector('.remove-btn').onclick = () => {
clonePosterLayoutImage = null;
updateClonePosterLayoutUploadUI();
};
} else {
uploadZoneClonePosterLayout.style.display = 'block';
previewClonePosterLayout.style.display = 'none';
previewClonePosterLayout.innerHTML = '';
}
}
uploadZoneClonePosterLayout.onclick = () => fileInputClonePosterLayout.click();
uploadZoneClonePosterLayout.ondragover = e => { e.preventDefault(); uploadZoneClonePosterLayout.style.borderColor = '#3498db'; };
uploadZoneClonePosterLayout.ondragleave = () => { uploadZoneClonePosterLayout.style.borderColor = '#3a3a3a'; };
uploadZoneClonePosterLayout.ondrop = e => {
e.preventDefault();
uploadZoneClonePosterLayout.style.borderColor = '#3a3a3a';
const file = e.dataTransfer.files?.[0];
if (file && ALLOWED_TYPES.includes(file.type)) {
const reader = new FileReader();
reader.onload = ev => { clonePosterLayoutImage = ev.target.result; updateClonePosterLayoutUploadUI(); };
reader.readAsDataURL(file);
}
};
fileInputClonePosterLayout.onchange = e => {
const file = e.target.files?.[0];
if (file && ALLOWED_TYPES.includes(file.type)) {
const reader = new FileReader();
reader.onload = ev => { clonePosterLayoutImage = ev.target.result; updateClonePosterLayoutUploadUI(); };
reader.readAsDataURL(file);
}
e.target.value = '';
};
// 克隆海报 - 上传图片(最多6张)
let clonePosterImages = [];
const uploadZoneClonePosterImages = document.getElementById('uploadZoneClonePosterImages');
const fileInputClonePosterImages = document.getElementById('fileInputClonePosterImages');
const previewClonePosterImages = document.getElementById('uploadPreviewClonePosterImages');
const countElClonePoster = document.getElementById('clonePosterImagesCount');
const MAX_CLONE_POSTER_IMAGES = 6;
function updateClonePosterImagesUploadUI() {
countElClonePoster.textContent = clonePosterImages.length;
if (clonePosterImages.length > 0) {
previewClonePosterImages.style.display = 'grid';
previewClonePosterImages.innerHTML = clonePosterImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewClonePosterImages.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
clonePosterImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateClonePosterImagesUploadUI();
};
});
uploadZoneClonePosterImages.style.display = clonePosterImages.length >= MAX_CLONE_POSTER_IMAGES ? 'none' : 'block';
} else {
uploadZoneClonePosterImages.style.display = 'block';
previewClonePosterImages.style.display = 'none';
previewClonePosterImages.innerHTML = '';
}
}
function addClonePosterImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_CLONE_POSTER_IMAGES - clonePosterImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = ev => {
if (clonePosterImages.length < MAX_CLONE_POSTER_IMAGES) {
clonePosterImages.push(ev.target.result);
updateClonePosterImagesUploadUI();
}
};
reader.readAsDataURL(file);
});
}
uploadZoneClonePosterImages.onclick = () => fileInputClonePosterImages.click();
uploadZoneClonePosterImages.ondragover = e => { e.preventDefault(); uploadZoneClonePosterImages.style.borderColor = '#3498db'; };
uploadZoneClonePosterImages.ondragleave = () => { uploadZoneClonePosterImages.style.borderColor = '#3a3a3a'; };
uploadZoneClonePosterImages.ondrop = e => {
e.preventDefault();
uploadZoneClonePosterImages.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addClonePosterImages(e.dataTransfer.files);
};
fileInputClonePosterImages.onchange = e => {
if (e.target.files?.length) {
addClonePosterImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnClonePosterGenerate').onclick = function() {
const mainTitle = document.getElementById('clonePosterMainTitle').value.trim();
const subtitle = document.getElementById('clonePosterSubtitle').value.trim();
const ratio = document.querySelector('#clonePosterPanel .opt-btn[data-ratio].active')?.dataset.ratio || '16:9';
const res = document.querySelector('#clonePosterPanel .opt-btn[data-res].active')?.dataset.res || '2k';
if (!clonePosterLayoutImage) { alert('请上传版式图片(1张)'); return; }
if (clonePosterImages.length === 0) { alert('请上传图片(最多6张)'); return; }
callGenerate({ menu: 4, name: mainTitle || '产品', desc: subtitle, ratio, resolution: res, count: 1, layout_image: clonePosterLayoutImage, ref_images: clonePosterImages, ...getModelImagesParam() });
};
// 默认面板(图片编辑) - 立即生成
document.getElementById('btnImageEditGenerate').onclick = function() {
const promptMode = document.querySelector('#defaultPanel .opt-btn[data-mode].active')?.dataset.mode || 'unified';
const prompt = document.getElementById('promptInput').value.trim();
const count = parseInt(document.querySelector('#defaultPanel .opt-btn[data-value].active')?.dataset.value || '1', 10);
const ratio = document.querySelector('#defaultPanel .opt-btn[data-ratio].active')?.dataset.ratio || '16:9';
const res = document.querySelector('#defaultPanel .opt-btn[data-res].active')?.dataset.res || '2k';
if (uploadedImages.length === 0) { alert('请上传图片'); return; }
if (promptMode === 'batch') {
const rows = document.querySelectorAll('#batchPromptList .batch-prompt-row');
const text = Array.from(rows).map(r => (r.querySelector('.batch-prompt-input').value || '').trim());
const totalCount = Array.from(rows).reduce((sum, r) => sum + (parseInt(r.querySelector('.batch-count-input').value, 10) || 0), 0);
if (!text.some(t => t.length > 0)) { alert('请至少填写一行提示词'); return; }
callGenerate({ menu: 2, text: text, count: totalCount || text.length, ratio, resolution: res, ref_images: uploadedImages, ...getModelImagesParam() });
} else {
if (!prompt) { alert('请输入提示词'); return; }
callGenerate({ menu: 2, prompt, ratio, resolution: res, count, ref_images: uploadedImages, ...getModelImagesParam() });
}
};
// 服装详情面板 - 画幅、分辨率、生成张数按钮
document.getElementById('clothingDetailPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res], .opt-btn[data-count]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
// 服装详情 - 服装细节图片上传
let clothingDetailImages = [];
const uploadZoneCD = document.getElementById('uploadZoneClothingDetail');
const fileInputCD = document.getElementById('fileInputClothingDetail');
const previewCD = document.getElementById('uploadPreviewClothingDetail');
const countElCD = document.getElementById('clothingDetailCount');
const MAX_CLOTHING_DETAIL = 8;
function updateClothingDetailUploadUI() {
countElCD.textContent = clothingDetailImages.length;
if (clothingDetailImages.length > 0) {
previewCD.style.display = 'grid';
previewCD.innerHTML = clothingDetailImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewCD.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
clothingDetailImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateClothingDetailUploadUI();
};
});
uploadZoneCD.style.display = clothingDetailImages.length >= MAX_CLOTHING_DETAIL ? 'none' : 'block';
} else {
uploadZoneCD.style.display = 'block';
previewCD.style.display = 'none';
previewCD.innerHTML = '';
}
}
function addClothingDetailImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, 8 - clothingDetailImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (clothingDetailImages.length < 8) {
clothingDetailImages.push(e.target.result);
updateClothingDetailUploadUI();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnPasteClothingDetail').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addClothingDetailImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
uploadZoneCD.onclick = () => fileInputCD.click();
uploadZoneCD.ondragover = e => { e.preventDefault(); uploadZoneCD.style.borderColor = '#3498db'; };
uploadZoneCD.ondragleave = () => { uploadZoneCD.style.borderColor = '#3a3a3a'; };
uploadZoneCD.ondrop = e => {
e.preventDefault();
uploadZoneCD.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addClothingDetailImages(e.dataTransfer.files);
};
fileInputCD.onchange = e => {
if (e.target.files?.length) {
addClothingDetailImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnClothingDetailGenerate').onclick = function() {
const name = document.getElementById('clothingProductName').value.trim();
const desc = document.getElementById('clothingFeatures').value.trim();
const ratio = document.querySelector('#clothingDetailPanel .opt-btn[data-ratio].active')?.dataset.ratio || '16:9';
const res = document.querySelector('#clothingDetailPanel .opt-btn[data-res].active')?.dataset.res || '2k';
const count = parseInt(document.querySelector('#clothingDetailPanel .opt-btn[data-count].active')?.dataset.count || '9', 10);
const style = document.getElementById('clothingStyle').value || document.getElementById('clothingStyle').options[0]?.value || '极简高级';
const language = document.getElementById('clothingLanguage').value.trim() || '中文';
if (!name) { alert('请输入产品名称'); return; }
if (clothingDetailImages.length === 0) { alert('请上传服装细节图片'); return; }
callGenerate({ menu: 5, name, desc, ratio, resolution: res, count, style, language, ref_images: clothingDetailImages, ...getModelImagesParam() });
};
// 产品详情面板 - 生成张数、画幅、分辨率按钮
document.getElementById('productDetailPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res], .opt-btn[data-count]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
// 从指定文案「第X屏」列表收集 text 数组,传参 text: ["xxx","xxx"]
function getSpecifyScreenTexts(listId) {
const list = document.getElementById(listId);
if (!list) return [];
const inputs = list.querySelectorAll('.specify-screen-item input.form-input');
return Array.from(inputs).map(inp => inp.value.trim()).filter(Boolean);
}
// 根据生成张数同步指定文案列表:只显示与张数相同的 N 个文案输入框
function syncSpecifyScreenList(listId, count) {
const list = document.getElementById(listId);
if (!list) return;
const countNum = Math.max(1, parseInt(count, 10) || 1);
const items = list.querySelectorAll('.specify-screen-item');
const values = Array.from(items).map(el => (el.querySelector('input.form-input')?.value ?? '').trim());
list.innerHTML = '';
for (let i = 0; i < countNum; i++) {
const idx = i + 1;
const div = document.createElement('div');
div.className = 'specify-screen-item';
div.innerHTML = `<label>第${idx}屏文案 (可选)</label><input type="text" class="form-input" data-screen-index="${idx}" placeholder="如:高颜值外观,一眼心动" value="">`;
const input = div.querySelector('input');
if (values[i] !== undefined) input.value = values[i];
list.appendChild(div);
}
}
function syncExtremeSpecifyFromCount() {
const count = document.querySelector('#extremeDetailPanel .opt-btn[data-count].active')?.dataset.count || '9';
syncSpecifyScreenList('extremeSpecifyScreenList', count);
}
function syncProductMainSpecifyFromCount() {
const count = document.querySelector('#productMainImagePanel .opt-btn[data-count].active')?.dataset.count || '1';
syncSpecifyScreenList('productMainSpecifyScreenList', count);
}
// 克隆详情 - 指定文案与克隆参考图张数绑定
function syncCloneSpecifyFromRefCount() {
const list = document.getElementById('cloneSpecifyScreenList');
if (!list) return;
const count = cloneRefImages.length;
if (count === 0) {
list.innerHTML = '';
return;
}
syncSpecifyScreenList('cloneSpecifyScreenList', count);
}
// 极致详情面板 - 功能特点组模式切换
document.querySelectorAll('#extremeDetailPanel .opt-btn[data-feature-mode]').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const mode = this.dataset.featureMode;
document.getElementById('extremeFeatureManual').style.display = mode === 'manual' ? 'block' : 'none';
document.getElementById('extremeFeatureSpecify').style.display = mode === 'specify' ? 'block' : 'none';
if (mode === 'specify') syncExtremeSpecifyFromCount();
});
});
// 极致详情面板 - 画幅、分辨率、生成张数按钮
document.getElementById('extremeDetailPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res], .opt-btn[data-count]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (btn.dataset.count) syncExtremeSpecifyFromCount();
}
});
// 极致详情 - 参考图片上传
let extremeDetailImages = [];
const uploadZoneED = document.getElementById('uploadZoneExtremeDetail');
const fileInputED = document.getElementById('fileInputExtremeDetail');
const previewED = document.getElementById('uploadPreviewExtremeDetail');
const countElED = document.getElementById('extremeDetailCount');
const MAX_EXTREME_DETAIL = 8;
function updateExtremeDetailUploadUI() {
countElED.textContent = extremeDetailImages.length;
if (extremeDetailImages.length > 0) {
previewED.style.display = 'grid';
previewED.innerHTML = extremeDetailImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewED.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
extremeDetailImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateExtremeDetailUploadUI();
};
});
uploadZoneED.style.display = extremeDetailImages.length >= MAX_EXTREME_DETAIL ? 'none' : 'block';
} else {
uploadZoneED.style.display = 'block';
previewED.style.display = 'none';
previewED.innerHTML = '';
}
}
function addExtremeDetailImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, 8 - extremeDetailImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (extremeDetailImages.length < 8) {
extremeDetailImages.push(e.target.result);
updateExtremeDetailUploadUI();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnPasteExtremeDetail').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addExtremeDetailImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
uploadZoneED.onclick = () => fileInputED.click();
uploadZoneED.ondragover = e => { e.preventDefault(); uploadZoneED.style.borderColor = '#3498db'; };
uploadZoneED.ondragleave = () => { uploadZoneED.style.borderColor = '#3a3a3a'; };
uploadZoneED.ondrop = e => {
e.preventDefault();
uploadZoneED.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addExtremeDetailImages(e.dataTransfer.files);
};
fileInputED.onchange = e => {
if (e.target.files?.length) {
addExtremeDetailImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnExtremeGenerate').onclick = function() {
const name = document.getElementById('extremeProductName').value.trim();
const featureMode = document.querySelector('#extremeDetailPanel .opt-btn[data-feature-mode].active')?.dataset.featureMode || 'manual';
let desc = '';
let textArr = [];
if (featureMode === 'manual') {
desc = document.getElementById('extremeFeatureManualInput').value.trim();
} else if (featureMode === 'specify') {
textArr = getSpecifyScreenTexts('extremeSpecifyScreenList');
desc = textArr.length ? '' : '';
}
const style = document.getElementById('extremeStyleDesc').value.trim() || '极简高级';
const language = document.getElementById('extremeOutputLang').value.trim() || '中文';
const ratio = document.querySelector('#extremeDetailPanel .opt-btn[data-ratio].active')?.dataset.ratio || '3:4';
const res = document.querySelector('#extremeDetailPanel .opt-btn[data-res].active')?.dataset.res || '2k';
const count = parseInt(document.querySelector('#extremeDetailPanel .opt-btn[data-count].active')?.dataset.count || '9', 10);
if (!name) { alert('请输入产品名称'); return; }
if (extremeDetailImages.length === 0) { alert('请上传参考图片'); return; }
const modeMap = { auto: '1', manual: '1', specify: '3' };
const params = { menu: 8, name, desc, style, language, ratio, resolution: res, count, mode: modeMap[featureMode] || '1', ref_images: extremeDetailImages, ...getModelImagesParam() };
if (featureMode === 'specify') params.text = textArr;
callGenerate(params);
};
// 产品详情 - 参考图片
let productDetailImages = [];
const uploadZone = document.getElementById('uploadZoneProductDetail');
const fileInputPD = document.getElementById('fileInputProductDetail');
const previewPD = document.getElementById('uploadPreviewProductDetail');
const countEl = document.getElementById('productDetailCount');
const MAX_PRODUCT_DETAIL = 8;
function updateProductDetailUploadUI() {
countEl.textContent = productDetailImages.length;
if (productDetailImages.length > 0) {
previewPD.style.display = 'grid';
previewPD.innerHTML = productDetailImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewPD.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
productDetailImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateProductDetailUploadUI();
};
});
uploadZone.style.display = productDetailImages.length >= MAX_PRODUCT_DETAIL ? 'none' : 'block';
} else {
uploadZone.style.display = 'block';
previewPD.style.display = 'none';
previewPD.innerHTML = '';
}
}
function addProductDetailImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, 8 - productDetailImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (productDetailImages.length < 8) {
productDetailImages.push(e.target.result);
updateProductDetailUploadUI();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnPasteProductDetail').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addProductDetailImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
uploadZone.onclick = () => fileInputPD.click();
uploadZone.ondragover = e => { e.preventDefault(); uploadZone.style.borderColor = '#3498db'; };
uploadZone.ondragleave = () => { uploadZone.style.borderColor = '#3a3a3a'; };
uploadZone.ondrop = e => {
e.preventDefault();
uploadZone.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addProductDetailImages(e.dataTransfer.files);
};
fileInputPD.onchange = e => {
if (e.target.files?.length) {
addProductDetailImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnProductDetailGenerate').onclick = function() {
const name = document.getElementById('productName').value.trim();
const desc = document.getElementById('productFeatures').value.trim();
const brand = document.getElementById('productBrand').value.trim();
const ingredients = document.getElementById('productIngredients').value.trim();
const activity = document.getElementById('productActivity').value.trim();
const ratio = document.querySelector('#productDetailPanel .opt-btn[data-ratio].active')?.dataset.ratio || '16:9';
const res = document.querySelector('#productDetailPanel .opt-btn[data-res].active')?.dataset.res || '2k';
const count = parseInt(document.querySelector('#productDetailPanel .opt-btn[data-count].active')?.dataset.count || '9', 10);
const style = document.getElementById('productStyle').value || '极简高级';
const language = document.getElementById('productLanguage').value.trim() || '中文';
if (!name) { alert('请输入产品名称'); return; }
if (productDetailImages.length === 0) { alert('请上传参考图片'); return; }
callGenerate({ menu: 7, name, desc, brand_name: brand, Ingredients: ingredients, activity, ratio, resolution: res, count, style, language, ref_images: productDetailImages, ...getModelImagesParam() });
};
// 产品主图面板
const productMainImagePanel = document.getElementById('productMainImagePanel');
productMainImagePanel.addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-ratio], .opt-btn[data-res], .opt-btn[data-count]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (btn.dataset.count) syncProductMainSpecifyFromCount();
}
});
// 产品主图 - 功能特点组模式切换
document.querySelectorAll('#productMainImagePanel .opt-btn[data-feature-mode]').forEach(btn => {
btn.addEventListener('click', function() {
this.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const mode = this.dataset.featureMode;
document.getElementById('productMainFeatureSpecify').style.display = mode === 'specify' ? 'block' : 'none';
if (mode === 'specify') syncProductMainSpecifyFromCount();
});
});
// 产品主图 - 产品图片上传
let productMainImages = [];
const uploadZonePM = document.getElementById('uploadZoneProductMain');
const fileInputPM = document.getElementById('fileInputProductMain');
const previewPM = document.getElementById('uploadPreviewProductMain');
const countElPM = document.getElementById('productMainCount');
const MAX_PRODUCT_MAIN = 8;
function updateProductMainUploadUI() {
countElPM.textContent = productMainImages.length;
if (productMainImages.length > 0) {
previewPM.style.display = 'grid';
previewPM.innerHTML = productMainImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewPM.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = (ev) => { ev.stopPropagation(); productMainImages.splice(parseInt(btn.dataset.idx, 10), 1); updateProductMainUploadUI(); };
});
uploadZonePM.style.display = productMainImages.length >= MAX_PRODUCT_MAIN ? 'none' : 'block';
} else {
uploadZonePM.style.display = 'block';
previewPM.style.display = 'none';
previewPM.innerHTML = '';
}
}
function addProductMainImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, 8 - productMainImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (productMainImages.length < 8) {
productMainImages.push(e.target.result);
updateProductMainUploadUI();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnPasteProductMain').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addProductMainImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
uploadZonePM.onclick = () => fileInputPM.click();
uploadZonePM.ondragover = e => { e.preventDefault(); uploadZonePM.style.borderColor = '#3498db'; };
uploadZonePM.ondragleave = () => { uploadZonePM.style.borderColor = '#3a3a3a'; };
uploadZonePM.ondrop = e => {
e.preventDefault();
uploadZonePM.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addProductMainImages(e.dataTransfer.files);
};
fileInputPM.onchange = e => {
if (e.target.files?.length) {
addProductMainImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnProductMainGenerate').onclick = function() {
const name = document.getElementById('productMainName').value.trim();
const featureMode = document.querySelector('#productMainImagePanel .opt-btn[data-feature-mode].active')?.dataset.featureMode || 'auto';
const textArr = featureMode === 'specify' ? getSpecifyScreenTexts('productMainSpecifyScreenList') : [];
const desc = '';
const style = document.getElementById('productMainStyle').value.trim() || '白底图';
const language = document.getElementById('productMainLanguage').value.trim() || '中文';
const ratio = document.querySelector('#productMainImagePanel .opt-btn[data-ratio].active')?.dataset.ratio || '3:4';
const res = document.querySelector('#productMainImagePanel .opt-btn[data-res].active')?.dataset.res || '2k';
const count = parseInt(document.querySelector('#productMainImagePanel .opt-btn[data-count].active')?.dataset.count || '1', 10);
if (!name) { alert('请输入产品名称'); return; }
if (productMainImages.length === 0) { alert('请上传产品图片'); return; }
const mode = featureMode === 'specify' ? '3' : '1';
// menu 如果是买家秀,则是 12
const activeTab = document.querySelector('.nav-tab.active');
const panel = activeTab?.dataset.panel || '';
const menu = panel === "buyerShow" ? 12 : 9
const params = { menu: menu, name, desc, style, language, ratio, resolution: res, count, mode, ref_images: productMainImages, ...getModelImagesParam() };
if (featureMode === 'specify') params.text = textArr;
callGenerate(params);
};
// 克隆详情面板 - 克隆模式、画幅、分辨率按钮
document.getElementById('cloneDetailPanel').addEventListener('click', function(e) {
const btn = e.target.closest('.opt-btn[data-clone-mode], .opt-btn[data-ratio], .opt-btn[data-res]');
if (btn) {
btn.parentElement.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (btn.dataset.cloneMode) {
const isSpecify = btn.dataset.cloneMode === 'specify';
const wrap = document.getElementById('cloneSpecifyWrap');
if (wrap) wrap.style.display = isSpecify ? 'block' : 'none';
if (isSpecify) syncCloneSpecifyFromRefCount();
}
}
});
// 克隆详情 - 产品实拍图 (最多6张)
let cloneProductImages = [];
const uploadZoneCloneProduct = document.getElementById('uploadZoneCloneProduct');
const fileInputCloneProduct = document.getElementById('fileInputCloneProduct');
const previewCloneProduct = document.getElementById('uploadPreviewCloneProduct');
const countElCloneProduct = document.getElementById('cloneProductCount');
const MAX_CLONE_PRODUCT = 6;
function updateCloneProductUploadUI() {
countElCloneProduct.textContent = cloneProductImages.length;
if (cloneProductImages.length > 0) {
previewCloneProduct.style.display = 'grid';
previewCloneProduct.innerHTML = cloneProductImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewCloneProduct.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
cloneProductImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateCloneProductUploadUI();
};
});
uploadZoneCloneProduct.style.display = cloneProductImages.length >= MAX_CLONE_PRODUCT ? 'none' : 'block';
} else {
uploadZoneCloneProduct.style.display = 'block';
previewCloneProduct.style.display = 'none';
previewCloneProduct.innerHTML = '';
}
}
function addCloneProductImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_CLONE_PRODUCT - cloneProductImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (cloneProductImages.length < MAX_CLONE_PRODUCT) {
cloneProductImages.push(e.target.result);
updateCloneProductUploadUI();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnPasteCloneProduct').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addCloneProductImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
uploadZoneCloneProduct.onclick = () => fileInputCloneProduct.click();
uploadZoneCloneProduct.ondragover = e => { e.preventDefault(); uploadZoneCloneProduct.style.borderColor = '#3498db'; };
uploadZoneCloneProduct.ondragleave = () => { uploadZoneCloneProduct.style.borderColor = '#3a3a3a'; };
uploadZoneCloneProduct.ondrop = e => {
e.preventDefault();
uploadZoneCloneProduct.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addCloneProductImages(e.dataTransfer.files);
};
fileInputCloneProduct.onchange = e => {
if (e.target.files?.length) {
addCloneProductImages(e.target.files);
e.target.value = '';
}
};
// 克隆详情 - 克隆参考图 (最多14张)
let cloneRefImages = [];
const uploadZoneCloneRef = document.getElementById('uploadZoneCloneRef');
const fileInputCloneRef = document.getElementById('fileInputCloneRef');
const previewCloneRef = document.getElementById('uploadPreviewCloneRef');
const countElCloneRef = document.getElementById('cloneRefCount');
const MAX_CLONE_REF = 14;
function updateCloneRefUploadUI() {
countElCloneRef.textContent = cloneRefImages.length;
if (cloneRefImages.length > 0) {
previewCloneRef.style.display = 'grid';
previewCloneRef.innerHTML = cloneRefImages.map((dataUrl, idx) =>
`<div class="upload-preview-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-idx="${idx}">×</button></div>`
).join('');
previewCloneRef.querySelectorAll('.remove-btn').forEach(btn => {
btn.onclick = () => {
cloneRefImages.splice(parseInt(btn.dataset.idx, 10), 1);
updateCloneRefUploadUI();
};
});
uploadZoneCloneRef.style.display = cloneRefImages.length >= MAX_CLONE_REF ? 'none' : 'block';
} else {
uploadZoneCloneRef.style.display = 'block';
previewCloneRef.style.display = 'none';
previewCloneRef.innerHTML = '';
}
syncCloneSpecifyFromRefCount();
}
function addCloneRefImages(files) {
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_CLONE_REF - cloneRefImages.length);
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (cloneRefImages.length < MAX_CLONE_REF) {
cloneRefImages.push(e.target.result);
updateCloneRefUploadUI();
}
};
reader.readAsDataURL(file);
});
}
document.getElementById('btnPasteCloneRef').onclick = async () => {
try {
const items = await navigator.clipboard.read();
const images = [];
for (const item of items) {
if (item.types.includes('image/png')) images.push(await item.getType('image/png'));
else if (item.types.includes('image/jpeg')) images.push(await item.getType('image/jpeg'));
}
if (images.length) addCloneRefImages(images);
} catch (err) { console.warn('粘贴图片失败:', err); }
};
uploadZoneCloneRef.onclick = () => fileInputCloneRef.click();
uploadZoneCloneRef.ondragover = e => { e.preventDefault(); uploadZoneCloneRef.style.borderColor = '#3498db'; };
uploadZoneCloneRef.ondragleave = () => { uploadZoneCloneRef.style.borderColor = '#3a3a3a'; };
uploadZoneCloneRef.ondrop = e => {
e.preventDefault();
uploadZoneCloneRef.style.borderColor = '#3a3a3a';
if (e.dataTransfer.files?.length) addCloneRefImages(e.dataTransfer.files);
};
fileInputCloneRef.onchange = e => {
if (e.target.files?.length) {
addCloneRefImages(e.target.files);
e.target.value = '';
}
};
document.getElementById('btnCloneDetailGenerate').onclick = function() {
const name = document.getElementById('cloneProductName').value.trim();
const desc = document.getElementById('cloneFeatures').value.trim();
const cloneMode = document.querySelector('#cloneDetailPanel .opt-btn[data-clone-mode].active')?.dataset.cloneMode || 'domestic';
const modeMap = { domestic: '1', amazon: '2', specify: '3' };
const ratio = document.querySelector('#cloneDetailPanel .opt-btn[data-ratio].active')?.dataset.ratio || 'auto';
const res = document.querySelector('#cloneDetailPanel .opt-btn[data-res].active')?.dataset.res || '2k';
const language = document.getElementById('cloneOutputLang').value.trim() || '中文';
if (!name) { alert('请输入产品名称'); return; }
if (cloneProductImages.length === 0) { alert('请上传产品实拍图'); return; }
if (cloneRefImages.length === 0) { alert('请上传克隆参考图'); return; }
const params = { menu: 10, name, desc, ratio, resolution: res, language, mode: modeMap[cloneMode] || '1', proc_images: cloneProductImages, ref_images: cloneRefImages, ...getModelImagesParam() };
if (cloneMode === 'specify') params.text = getSpecifyScreenTexts('cloneSpecifyScreenList');
callGenerate(params);
};
// 参考颜色图 - 最多10个栏位每栏最多8张
const MAX_COLOR_SLOTS = 10;
const MAX_IMAGES_PER_COLOR = 8;
let colorSlots = []; // [{ colorName: '', images: [] }]
const colorSlotsContainer = document.getElementById('colorSlotsContainer');
const btnAddColorSlot = document.getElementById('btnAddColorSlot');
function renderColorSlots() {
colorSlotsContainer.innerHTML = '';
colorSlots.forEach((slot, slotIdx) => {
const div = document.createElement('div');
div.className = 'color-slot-item';
div.dataset.slotIdx = slotIdx;
const imgsHtml = slot.images.map((dataUrl, imgIdx) =>
`<div class="color-slot-upload-item"><img src="${dataUrl}" alt=""><button class="remove-btn" data-slot="${slotIdx}" data-img="${imgIdx}">×</button></div>`
).join('');
const canAdd = slot.images.length < MAX_IMAGES_PER_COLOR;
const addBtnHtml = canAdd ? `<div class="color-slot-upload-item add-btn" data-slot="${slotIdx}" data-add="1" title="点击上传或拖拽图片">+</div>` : '';
div.innerHTML = `
<div class="color-slot-header">
<input type="text" class="color-name-input" placeholder="指定颜色名称(如:红色 蓝色)" data-slot="${slotIdx}" value="${(slot.colorName || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}">
<button class="btn-remove-color-slot" data-slot="${slotIdx}" type="button" title="移除该颜色栏">×</button>
</div>
<div class="color-slot-upload" data-slot="${slotIdx}">
${imgsHtml}
${addBtnHtml}
</div>
`;
colorSlotsContainer.appendChild(div);
});
bindColorSlotEvents();
}
function bindColorSlotEvents() {
colorSlotsContainer.querySelectorAll('.color-name-input').forEach(input => {
input.oninput = () => { colorSlots[parseInt(input.dataset.slot, 10)].colorName = input.value; };
});
colorSlotsContainer.querySelectorAll('.btn-remove-color-slot').forEach(btn => {
btn.onclick = () => {
colorSlots.splice(parseInt(btn.dataset.slot, 10), 1);
renderColorSlots();
updateAddColorSlotBtn();
};
});
colorSlotsContainer.querySelectorAll('.color-slot-upload-item .remove-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const slotIdx = parseInt(btn.dataset.slot, 10);
const imgIdx = parseInt(btn.dataset.img, 10);
colorSlots[slotIdx].images.splice(imgIdx, 1);
renderColorSlots();
};
});
colorSlotsContainer.querySelectorAll('.color-slot-upload-item.add-btn').forEach(item => {
item.onclick = () => {
const slotIdx = parseInt(item.dataset.slot, 10);
const input = document.createElement('input');
input.type = 'file';
input.accept = '.jpg,.jpeg,.png,.bmp';
input.multiple = true;
input.onchange = e => {
if (e.target.files?.length) {
addImagesToColorSlot(slotIdx, e.target.files);
e.target.value = '';
}
};
input.click();
};
});
colorSlotsContainer.querySelectorAll('.color-slot-upload').forEach(uploadDiv => {
const slotIdx = parseInt(uploadDiv.dataset.slot, 10);
uploadDiv.ondragover = e => { e.preventDefault(); uploadDiv.style.borderColor = '#3498db'; };
uploadDiv.ondragleave = () => { uploadDiv.style.borderColor = ''; };
uploadDiv.ondrop = e => {
e.preventDefault();
uploadDiv.style.borderColor = '';
if (e.dataTransfer.files?.length) addImagesToColorSlot(slotIdx, e.dataTransfer.files);
};
});
}
function addImagesToColorSlot(slotIdx, files) {
const slot = colorSlots[slotIdx];
if (!slot || slot.images.length >= MAX_IMAGES_PER_COLOR) return;
const toAdd = Array.from(files).filter(f => ALLOWED_TYPES.includes(f.type));
const remaining = Math.max(0, MAX_IMAGES_PER_COLOR - slot.images.length);
let added = 0;
toAdd.slice(0, remaining).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
if (slot.images.length < MAX_IMAGES_PER_COLOR) {
slot.images.push(e.target.result);
added++;
if (added <= remaining) renderColorSlots();
}
};
reader.readAsDataURL(file);
});
}
function updateAddColorSlotBtn() {
btnAddColorSlot.disabled = colorSlots.length >= MAX_COLOR_SLOTS;
btnAddColorSlot.title = colorSlots.length >= MAX_COLOR_SLOTS ? '已达到最多10组' : '添加颜色上传栏';
}
btnAddColorSlot.onclick = () => {
if (colorSlots.length >= MAX_COLOR_SLOTS) return;
colorSlots.push({ colorName: '', images: [] });
renderColorSlots();
updateAddColorSlotBtn();
};
updateAddColorSlotBtn();
// 历史图库:页面加载时自动加载第一页
loadHistoryPage(false);
document.getElementById('btnHistoryLoadMore').onclick = () => loadHistoryPage(true);
// document.querySelector('.btn-clean').onclick = () => {
// historyPage = 0;
// historyHasMore = true;
// loadHistoryPage(false);
// };
// 历史图库栏目分类:点击切换筛选
document.querySelectorAll('#historyCategoryTabs .history-tab').forEach(tab => {
tab.addEventListener('click', function() {
this.parentElement.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
historyPanelType = (this.dataset.panelType || '').trim();
historyPage = 0;
historyHasMore = true;
loadHistoryPage(false);
});
});
// 提示词字数统计
const promptInput = document.getElementById('promptInput');
const charCount = document.getElementById('charCount');
promptInput.addEventListener('input', () => {
charCount.textContent = promptInput.value.length + '字符';
});
updateUploadUI();
switchLeftPanel();
// 图片查看模态框
const imageModalOverlay = document.getElementById('imageModalOverlay');
const imageModalImg = document.getElementById('imageModalImg');
// const imageModalTitle = document.getElementById('imageModalTitle');
// const imageModalSubtitle = document.getElementById('imageModalSubtitle');
const modalPrevBtn = document.getElementById('modalPrevBtn');
const modalNextBtn = document.getElementById('modalNextBtn');
const modalRestoreBtn = document.getElementById('modalRestoreBtn');
const modalBatchDownloadBtn = document.getElementById('modalBatchDownloadBtn');
const modalDownloadLongBtn = document.getElementById('modalDownloadLongBtn');
const modalWorkspaceDownloadBtn = document.getElementById('modalWorkspaceDownloadBtn');
const modalWorkspaceEditBtn = document.getElementById('modalWorkspaceEditBtn');
const modalWorkspaceRegenerateBtn = document.getElementById('modalWorkspaceRegenerateBtn');
const modalCloseBtn = document.getElementById('modalCloseBtn');
let modalImages = [];
let modalCurrentIndex = 0;
/** 'workspace' | 'history' | 'stitched':来源,用于控制底部按钮 */
let modalSource = 'history';
/** 从历史打开时对应的历史记录 id恢复到工作台后用于 workspaceImageIds */
let modalHistoryId = null;
function openImageModal(images, index, opts) {
opts = opts || {};
modalImages = Array.isArray(images) ? images : [images];
modalCurrentIndex = index >= 0 ? index : 0;
modalSource = opts.source || 'history';
modalHistoryId = opts.historyId != null ? opts.historyId : null;
if (modalImages.length === 0) return;
const img = modalImages[modalCurrentIndex];
const src = typeof img === 'string' ? img : (img.src || img);
imageModalImg.src = src;
imageModalOverlay.classList.add('show');
updateModalArrows();
updateModalFooterBySource();
}
function closeImageModal() {
imageModalOverlay.classList.remove('show');
}
function updateModalFooterBySource() {
modalRestoreBtn.style.display = modalSource === 'history' ? 'flex' : 'none';
modalBatchDownloadBtn.style.display = modalSource === 'history' && modalImages.length > 1 ? 'flex' : 'none';
modalDownloadLongBtn.style.display = (modalSource === 'history' && modalImages.length > 0) || modalSource === 'stitched' ? 'flex' : 'none';
modalWorkspaceDownloadBtn.style.display = modalSource === 'workspace' ? 'flex' : 'none';
modalWorkspaceEditBtn.style.display = modalSource === 'workspace' ? 'flex' : 'none';
modalWorkspaceRegenerateBtn.style.display = modalSource === 'workspace' ? 'flex' : 'none';
}
function updateModalArrows() {
modalPrevBtn.style.visibility = modalImages.length > 1 ? 'visible' : 'hidden';
modalNextBtn.style.visibility = modalImages.length > 1 ? 'visible' : 'hidden';
}
function showModalPrev() {
if (modalImages.length <= 1) return;
modalCurrentIndex = (modalCurrentIndex - 1 + modalImages.length) % modalImages.length;
const img = modalImages[modalCurrentIndex];
imageModalImg.src = typeof img === 'string' ? img : (img.src || img);
}
function showModalNext() {
if (modalImages.length <= 1) return;
modalCurrentIndex = (modalCurrentIndex + 1) % modalImages.length;
const img = modalImages[modalCurrentIndex];
imageModalImg.src = typeof img === 'string' ? img : (img.src || img);
}
function restoreToWorkspace() {
if (modalImages.length === 0) return;
hidePromptCards();
if (modalSource === 'history') {
// 将当前弹窗中的图片 src 提取出来
const srcs = modalImages.map(img => typeof img === 'string' ? img : (img.src || img));
// 优先查找是否存在已经生成好的长图(在 long_image_ls 中)
let longImageUrl = null;
for (const src of srcs) {
if (long_image_ls.indexOf(src) !== -1) {
longImageUrl = src;
break;
}
}
// 工作台左侧仍然只保留原图,去掉长图
workspaceImages = srcs.filter(e => long_image_ls.indexOf(e) === -1);
workspaceImageIds = workspaceImages.map(function() { return modalHistoryId; });
stitchOrder = null;
renderWorkspaceLeft();
if (longImageUrl) {
// 如果已经存在长图,右侧直接使用,不再进行前端拼接
const wrap = document.getElementById('workspaceStitchedWrap');
const imgEl = document.getElementById('workspaceStitchedImg');
const empty = document.getElementById('workspaceRightEmpty');
if (wrap && imgEl && empty) {
empty.style.display = 'none';
wrap.style.display = 'block';
imgEl.src = longImageUrl;
imgEl.dataset.stitchedDataUrl = longImageUrl;
}
} else {
// 不存在长图时再触发拼接
renderStitchedPreview();
}
closeImageModal();
return;
}
const img = modalImages[modalCurrentIndex];
const src = typeof img === 'string' ? img : (img.src || img);
workspaceImages.push(src);
workspaceImageIds.push(null);
stitchOrder = null;
renderWorkspaceLeft();
renderStitchedPreview();
closeImageModal();
}
async function downloadImage(src, filename) {
const fn = filename || 'image.png';
const api = window.pywebview?.api;
if (api?.save_image) {
// pywebview 模式:调用 Python 弹窗选择保存位置
let urlOrData = src;
if (src.startsWith('blob:')) {
const res = await fetch(src);
const blob = await res.blob();
urlOrData = await new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(r.result);
r.onerror = reject;
r.readAsDataURL(blob);
});
}
const ret = await api.save_image(urlOrData, fn);
if (!ret?.success) {
if (ret?.error !== '用户取消') alert('下载失败: ' + (ret?.error || '未知错误'));
}
return;
}
// 浏览器模式降级:通过后端代理下载
if (src.startsWith('http://') || src.startsWith('https://')) {
const proxyUrl = '/api/download?url=' + encodeURIComponent(src) + '&filename=' + encodeURIComponent(fn);
const res = await fetch(proxyUrl, { credentials: 'same-origin' });
if (!res.ok) throw new Error('下载失败');
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fn;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
return;
}
try {
const res = await fetch(src);
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fn;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch (err) {
console.warn('下载失败:', err);
alert('下载失败: ' + (err.message || err));
}
}
modalPrevBtn.onclick = showModalPrev;
modalNextBtn.onclick = showModalNext;
modalRestoreBtn.onclick = restoreToWorkspace;
modalCloseBtn.onclick = closeImageModal;
modalWorkspaceDownloadBtn.onclick = async () => {
const img = modalImages[modalCurrentIndex];
const src = typeof img === 'string' ? img : (img.src || img);
await downloadImage(src, `原图_${modalCurrentIndex + 1}.png`);
};
/** 将图片 URL 转为 data URLAPI 需要 data URL 上传) */
async function urlToDataUrl(src) {
if (typeof src !== 'string') return '';
if (src.startsWith('data:')) return src;
try {
const res = await fetch(src, { mode: 'cors' });
const blob = await res.blob();
return await new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(r.result);
r.onerror = reject;
r.readAsDataURL(blob);
});
} catch (e) {
console.warn('urlToDataUrl failed:', e);
return src;
}
}
modalWorkspaceEditBtn.onclick = async function() {
const img = modalImages[modalCurrentIndex];
const src = typeof img === 'string' ? img : (img.src || img);
const dataUrl = await urlToDataUrl(src);
imageEditWorkspaceIndex = modalSource === 'workspace' ? modalCurrentIndex : -1;
closeImageModal();
openImageEditModal(dataUrl);
};
// 重新生成弹窗:当前图片、工作台下标(用于替换)、历史记录 id用于后端更新该条记录
let regenerateCurrentSrc = '';
let regenerateWorkspaceIndex = -1;
let regenerateHistoryId = null;
const regenerateModalOverlay = document.getElementById('regenerateModalOverlay');
const regeneratePreviewImg = document.getElementById('regeneratePreviewImg');
const regeneratePromptInput = document.getElementById('regeneratePromptInput');
const regenerateCancelBtn = document.getElementById('regenerateCancelBtn');
const regenerateConfirmBtn = document.getElementById('regenerateConfirmBtn');
// const regenerateCopyBtn = document.getElementById('regenerateCopyBtn');
const regenerateSaveBtn = document.getElementById('regenerateSaveBtn');
const regenerateCloseBtn = document.getElementById('regenerateCloseBtn');
function openRegenerateModal(src, workspaceIndex, historyId) {
regenerateCurrentSrc = src;
regenerateWorkspaceIndex = workspaceIndex >= 0 ? workspaceIndex : -1;
regenerateHistoryId = (historyId != null && historyId !== '') ? historyId : null;
regeneratePreviewImg.src = src;
regeneratePromptInput.value = '';
regenerateModalOverlay.classList.add('show');
}
function closeRegenerateModal() {
regenerateModalOverlay.classList.remove('show');
}
modalWorkspaceRegenerateBtn.onclick = async function() {
const img = modalImages[modalCurrentIndex];
const src = typeof img === 'string' ? img : (img.src || img);
const fromWorkspace = modalSource === 'workspace';
const historyId = fromWorkspace ? (workspaceImageIds[modalCurrentIndex] ?? null) : null;
closeImageModal();
openRegenerateModal(src, fromWorkspace ? modalCurrentIndex : -1, historyId);
};
regenerateCancelBtn.onclick = closeRegenerateModal;
regenerateCloseBtn.onclick = closeRegenerateModal;
regenerateConfirmBtn.onclick = async function() {
const prompt = (regeneratePromptInput.value || '').trim();
const dataUrl = await urlToDataUrl(regenerateCurrentSrc);
if (!dataUrl) {
alert('当前图片无法用于生成');
return;
}
const params = {
ref_images: [dataUrl],
menu: 11,
prompt: prompt || '提示词'
};
if (regenerateWorkspaceIndex >= 0)
params.generatingCardIndex = regenerateWorkspaceIndex;
if (regenerateHistoryId != null) {
params.history_id = regenerateHistoryId;
params.history_index = regenerateWorkspaceIndex >= 0 ? regenerateWorkspaceIndex : 0;
}
closeRegenerateModal();
await callGenerate(params);
};
// regenerateCopyBtn.onclick = function() {
// const text = document.querySelector('.regenerate-desc').textContent + '\n' + document.querySelector('.regenerate-style').textContent;
// navigator.clipboard.writeText(text).then(function() {
// const span = regenerateCopyBtn.querySelector('span');
// if (span) { const t = span.textContent; span.textContent = '已复制'; setTimeout(function() { span.textContent = t; }, 1500); }
// });
// };
regenerateSaveBtn.onclick = function() {
downloadImage(regenerateCurrentSrc, '重新生成预览.png');
};
// 图片编辑弹窗:当前图 data URL、参考图 data URL默认与当前图相同
let imageEditCurrentDataUrl = '';
let imageEditRefDataUrl = '';
let imageEditRefDataUrl_regenerate = '';
/** 从工作台第几张图进入编辑(用于生成时在该卡片上显示“生成中”;-1 表示非工作台进入) */
let imageEditWorkspaceIndex = -1;
const imageEditModalOverlay = document.getElementById('imageEditModalOverlay');
const imageEditImg = document.getElementById('imageEditImg');
const imageEditPromptInput = document.getElementById('imageEditPromptInput');
const imageEditBrushSize = document.getElementById('imageEditBrushSize');
const imageEditBrushSlider = document.getElementById('imageEditBrushSlider');
const imageEditSmearBtn = document.getElementById('imageEditSmearBtn');
const imageEditMarkerBtn = document.getElementById('imageEditMarkerBtn');
const imageEditCanvas = document.getElementById('imageEditCanvas');
/** 涂抹笔画(显示坐标) */
let imageEditStrokes = [];
/** 数字标记 { x, y, num }(显示坐标) */
let imageEditMarkers = [];
/** 撤销栈:'stroke' | 'marker' */
let imageEditUndoStack = [];
/** 图片原始宽高,用于合成时全分辨率 */
let imageEditNaturalW = 0, imageEditNaturalH = 0;
const IMAGE_EDIT_GREEN = 'rgba(100, 220, 120, 0.65)';
function imageEditGetCanvasXY(e) {
const r = imageEditCanvas.getBoundingClientRect();
const scaleX = imageEditCanvas.width / r.width;
const scaleY = imageEditCanvas.height / r.height;
return {
x: (e.clientX - r.left) * scaleX,
y: (e.clientY - r.top) * scaleY
};
}
function imageEditRedraw() {
const ctx = imageEditCanvas.getContext('2d');
ctx.clearRect(0, 0, imageEditCanvas.width, imageEditCanvas.height);
const brushSize = parseInt(imageEditBrushSlider.value, 10) || 20;
ctx.strokeStyle = IMAGE_EDIT_GREEN;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
imageEditStrokes.forEach(function(s) {
ctx.lineWidth = s.brushSize || brushSize;
ctx.beginPath();
s.points.forEach(function(p, i) {
if (i === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
});
ctx.stroke();
});
imageEditMarkers.forEach(function(m) {
const radius = Math.max(14, brushSize * 0.8);
ctx.fillStyle = IMAGE_EDIT_GREEN;
ctx.beginPath();
ctx.arc(m.x, m.y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#1e1e1e';
ctx.font = 'bold ' + Math.round(radius * 1.1) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(m.num), m.x, m.y);
});
}
/** 将原图 + 涂抹 + 数字标记 合成为一张图 data URL用于 ref_images */
function imageEditGetCompositedDataUrl() {
const w = imageEditNaturalW || imageEditImg.naturalWidth || imageEditCanvas.width;
const h = imageEditNaturalH || imageEditImg.naturalHeight || imageEditCanvas.height;
const scaleX = w / imageEditCanvas.width;
const scaleY = h / imageEditCanvas.height;
const off = document.createElement('canvas');
off.width = w;
off.height = h;
const ctx = off.getContext('2d');
const img = new Image();
return new Promise(function(resolve, reject) {
img.onload = function() {
ctx.drawImage(img, 0, 0, w, h);
ctx.strokeStyle = IMAGE_EDIT_GREEN;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
imageEditStrokes.forEach(function(s) {
const bw = (s.brushSize || 20) * Math.max(scaleX, scaleY);
ctx.lineWidth = bw;
ctx.beginPath();
s.points.forEach(function(p, i) {
const px = p.x * scaleX, py = p.y * scaleY;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.stroke();
});
const brushSize = parseInt(imageEditBrushSlider.value, 10) || 20;
const radiusScale = Math.max(scaleX, scaleY);
imageEditMarkers.forEach(function(m) {
const radius = Math.max(14, brushSize * 0.8) * radiusScale;
ctx.fillStyle = IMAGE_EDIT_GREEN;
ctx.beginPath();
ctx.arc(m.x * scaleX, m.y * scaleY, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = Math.max(2, radiusScale);
ctx.stroke();
ctx.fillStyle = '#1e1e1e';
ctx.font = 'bold ' + Math.round(radius * 1.1) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(m.num), m.x * scaleX, m.y * scaleY);
});
try {
resolve(off.toDataURL('image/png'));
} catch (e) {
reject(e);
}
};
img.onerror = function() { resolve(imageEditCurrentDataUrl); };
img.crossOrigin = 'anonymous';
img.src = imageEditCurrentDataUrl;
});
}
let imageEditDrawing = false;
let imageEditCurrentStroke = null;
function openImageEditModal(currentImageDataUrl) {
imageEditCurrentDataUrl = currentImageDataUrl || '';
imageEditRefDataUrl = imageEditCurrentDataUrl;
imageEditStrokes = [];
imageEditMarkers = [];
imageEditUndoStack = [];
imageEditImg.src = imageEditCurrentDataUrl;
imageEditImg.onload = function() {
const c = imageEditCanvas;
imageEditNaturalW = imageEditImg.naturalWidth;
imageEditNaturalH = imageEditImg.naturalHeight;
c.width = imageEditImg.offsetWidth;
c.height = imageEditImg.offsetHeight;
c.style.width = c.width + 'px';
c.style.height = c.height + 'px';
imageEditRedraw();
};
imageEditPromptInput.value = '';
imageEditModalOverlay.classList.add('show');
imageEditBrushSlider.value = 20;
imageEditBrushSize.textContent = '20';
imageEditSmearBtn.classList.add('active');
imageEditSmearBtn.textContent = '🖌 涂抹模式 ON';
imageEditMarkerBtn.classList.add('active');
imageEditMarkerBtn.textContent = '📑 数字标记 ON';
}
function closeImageEditModal() {
imageEditModalOverlay.classList.remove('show');
}
document.getElementById('imageEditCancelBtn').onclick = closeImageEditModal;
document.getElementById('imageEditConfirmBtn').onclick = async function() {
const prompt = imageEditPromptInput.value.trim();
// if (!prompt) {
// alert('请输入提示词');
// return;
// }
if (!imageEditCurrentDataUrl) {
alert('当前图片无效');
return;
}
let refImageDataUrl = imageEditCurrentDataUrl;
if (imageEditStrokes.length > 0 || imageEditMarkers.length > 0) {
try {
refImageDataUrl = await imageEditGetCompositedDataUrl();
} catch (e) {
console.warn('合成编辑图失败,使用原图', e);
}
}
closeImageEditModal();
const cardIndex = imageEditWorkspaceIndex >= 0 ? imageEditWorkspaceIndex : undefined;
let param = {
ref_images: [refImageDataUrl],
menu: 11,
prompt: prompt,
// proc_images: [imageEditRefDataUrl],
generatingCardIndex: cardIndex
}
if (imageEditRefDataUrl_regenerate!== ""){
param["proc_images"] = [imageEditRefDataUrl_regenerate]
}
callGenerate(param);
imageEditWorkspaceIndex = -1;
};
imageEditModalOverlay.addEventListener('click', function(e) {
if (e.target === imageEditModalOverlay) closeImageEditModal();
});
imageEditBrushSlider.addEventListener('input', function() {
const v = this.value;
imageEditBrushSize.textContent = v;
});
imageEditSmearBtn.addEventListener('click', function() {
this.classList.toggle('active');
this.textContent = this.classList.contains('active') ? '🖌 涂抹模式 ON' : '🖌 涂抹模式 OFF';
});
imageEditMarkerBtn.addEventListener('click', function() {
this.classList.toggle('active');
this.textContent = this.classList.contains('active') ? '📑 数字标记 ON' : '📑 数字标记 OFF';
});
(function initImageEditCanvas() {
const canvas = imageEditCanvas;
const brushSizeEl = imageEditBrushSize;
canvas.addEventListener('mousedown', function(e) {
if (e.button !== 0 && e.button !== 2) return;
const p = imageEditGetCanvasXY(e);
if (e.button === 2) {
e.preventDefault();
const r = (parseInt(imageEditBrushSlider.value, 10) || 20) * 1.2;
const idx = imageEditMarkers.findIndex(function(m) {
return Math.hypot(m.x - p.x, m.y - p.y) <= r;
});
if (idx >= 0) {
imageEditMarkers.splice(idx, 1);
imageEditRedraw();
}
return;
}
if (imageEditSmearBtn.classList.contains('active')) {
imageEditDrawing = true;
imageEditCurrentStroke = { points: [p], brushSize: parseInt(imageEditBrushSlider.value, 10) || 20 };
imageEditStrokes.push(imageEditCurrentStroke);
imageEditUndoStack.push('stroke');
} else if (imageEditMarkerBtn.classList.contains('active')) {
const num = imageEditMarkers.length + 1;
imageEditMarkers.push({ x: p.x, y: p.y, num: num });
imageEditUndoStack.push('marker');
imageEditRedraw();
}
});
canvas.addEventListener('mousemove', function(e) {
if (!imageEditDrawing || !imageEditCurrentStroke) return;
const p = imageEditGetCanvasXY(e);
imageEditCurrentStroke.points.push(p);
imageEditRedraw();
});
canvas.addEventListener('mouseup', function(e) {
if (e.button !== 0) return;
imageEditDrawing = false;
imageEditCurrentStroke = null;
});
canvas.addEventListener('mouseleave', function() {
imageEditDrawing = false;
imageEditCurrentStroke = null;
});
canvas.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
document.getElementById('imageEditUndoBtn').onclick = function() {
if (imageEditUndoStack.length === 0) return;
const type = imageEditUndoStack.pop();
if (type === 'stroke') imageEditStrokes.pop();
else if (type === 'marker') imageEditMarkers.pop();
imageEditRedraw();
};
document.getElementById('imageEditClearBtn').onclick = function() {
imageEditStrokes = [];
imageEditMarkers = [];
imageEditUndoStack = [];
imageEditRedraw();
};
})();
const imageEditRefImageInput = document.createElement('input');
imageEditRefImageInput.type = 'file';
imageEditRefImageInput.accept = '.jpg,.jpeg,.png,.bmp';
imageEditRefImageInput.style.display = 'none';
document.body.appendChild(imageEditRefImageInput);
imageEditRefImageInput.onchange = function() {
const file = this.files && this.files[0];
if (!file) return;
console.log("imageEditRefImageInput修改触发")
const reader = new FileReader();
reader.onload = function(e) {
imageEditRefDataUrl_regenerate = e.target.result;
};
reader.readAsDataURL(file);
this.value = '';
};
document.getElementById('imageEditRefImageBtn').onclick = function() {
console.log("触发imageEditRefImageBtn点击")
imageEditRefImageInput.click();
};
modalBatchDownloadBtn.onclick = async () => {
for (let i = 0; i < modalImages.length; i++) {
const img = modalImages[i];
const src = typeof img === 'string' ? img : (img.src || img);
await downloadImage(src, `image_${i + 1}.png`);
if (i < modalImages.length - 1) await new Promise(r => setTimeout(r, 200));
}
};
// modalCopyBtn.onclick = async () => {
// try {
// const res = await fetch(imageModalImg.src);
// const blob = await res.blob();
// await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
// } catch (err) { console.warn('复制失败:', err); }
// };
modalDownloadLongBtn.onclick = async () => {
await downloadImage(imageModalImg.src, '长图.png');
};
// 工作台:左侧空状态与右侧拼接图点击
(function initWorkspace() {
const leftEmpty = document.getElementById('workspaceLeftEmpty');
const rightEmpty = document.getElementById('workspaceRightEmpty');
const leftInner = document.getElementById('workspaceLeftInner');
if (leftInner && leftInner.children.length) leftEmpty.style.display = 'none';
const stitchedWrap = document.getElementById('workspaceStitchedWrap');
const stitchedImg = document.getElementById('workspaceStitchedImg');
if (stitchedWrap && stitchedImg) {
stitchedWrap.addEventListener('click', function() {
const dataUrl = stitchedImg.dataset.stitchedDataUrl || stitchedImg.src;
if (!dataUrl) return;
openImageModal([dataUrl], 0, { source: 'stitched' });
});
}
// 手动拼接点击后才显示左侧「已选N张/取消选择/开始拼接」条,并在左侧直接选择顺序
document.getElementById('btnManualStitch').onclick = function() {
if (workspaceImages.length === 0) { alert('工作台暂无图片'); return; }
manualStitchBarVisible = true;
renderWorkspaceLeft();
document.querySelector('.workspace-left')?.scrollIntoView({ behavior: 'smooth' });
};
document.getElementById('workspaceStitchCancel').onclick = function() {
stitchOrder = null;
renderWorkspaceLeft();
renderStitchedPreview();
};
document.getElementById('workspaceStitchStart').onclick = async function() {
const urls = getOrderedWorkspaceUrls();
if (!urls || urls.length === 0) { alert('请先在左侧选择要拼接的图片及顺序'); return; }
const btn = this;
const origText = btn.textContent;
const stitchLoading = document.getElementById('workspaceStitchLoading');
btn.disabled = true;
btn.textContent = '拼接中...';
if (stitchLoading) stitchLoading.style.display = 'flex';
try {
const resp = await fetch('/api/stitch/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ urls: urls }),
});
const res = await resp.json().catch(() => ({}));
if (res.success && res.long_image_url) {
const wrap = document.getElementById('workspaceStitchedWrap');
const imgEl = document.getElementById('workspaceStitchedImg');
const empty = document.getElementById('workspaceRightEmpty');
if (wrap && imgEl && empty) {
empty.style.display = 'none';
wrap.style.display = 'block';
imgEl.src = res.long_image_url;
imgEl.dataset.stitchedDataUrl = res.long_image_url;
}
manualStitchBarVisible = false;
renderWorkspaceLeft();
// appendToHistoryGrid([res.long_image_url], true);
} else {
alert(res.error || '拼接保存失败');
}
} catch (e) {
alert('请求失败:' + (e.message || '网络错误'));
} finally {
btn.disabled = false;
btn.textContent = origText;
if (stitchLoading) stitchLoading.style.display = 'none';
}
};
// 批量下载原图
document.getElementById('btnBatchDownload').onclick = async function() {
if (workspaceImages.length === 0) { alert('工作台暂无图片'); return; }
for (let i = 0; i < workspaceImages.length; i++) {
await downloadImage(workspaceImages[i], `原图_${i + 1}.png`);
if (i < workspaceImages.length - 1) await new Promise(r => setTimeout(r, 200));
}
};
document.getElementById('btnClosePreview').onclick = function() {
if (workspaceImages.length === 0) return;
if (confirm('确定清空当前栏目工作台所有图片吗?')) {
workspaceImages = [];
workspaceImageIds = [];
stitchOrder = null;
manualStitchBarVisible = false;
saveCurrentWorkspaceToState();
renderWorkspaceLeft();
renderStitchedPreview();
hidePromptCards();
}
};
})();
imageModalOverlay.addEventListener('click', function(e) {
if (e.target === imageModalOverlay) closeImageModal();
});
// 窗口控制 - 等待 pywebview API 就绪后绑定
function setupWindowControls() {
const api = window.pywebview?.api;
if (!api) return;
}
window.addEventListener('pywebviewready', setupWindowControls);
if (window.pywebview?.api) setupWindowControls();
switchLeftPanel()
</script>
</body>
</html>