new file: "brand - \345\211\257\346\234\254.html" new file: "brand-\346\227\247.html" new file: brand.html new file: home.html new file: index.html new file: login.html
5834 lines
263 KiB
HTML
5834 lines
263 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>数富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: 适合敏感肌 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="">-- 选择风格 --</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="">-- 选择风格 --</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="如: 移除框选的内容 如: 移除框选的内容 改成** 如: 把**改成** 如果上传了参考图提示词应该写:把图片一的**改成图片二的**..."></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 的 JSON,value 为 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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>')}">
|
||
<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 URL(API 需要 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>
|