You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
18 KiB
460 lines
18 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MV Creator</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f0f0f;
|
|
--card: #1a1a1a;
|
|
--border: #2a2a2a;
|
|
--text: #e0e0e0;
|
|
--text-dim: #888;
|
|
--accent: #f0c040;
|
|
--accent-dim: #a08020;
|
|
--green: #4ade80;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
background: var(--bg); color: var(--text);
|
|
line-height: 1.6; padding: 2rem 2rem 6rem;
|
|
max-width: 1200px; margin: 0 auto;
|
|
}
|
|
|
|
.header { margin-bottom: 2.5rem; text-align: center; }
|
|
.header h1 { font-size: 1.6rem; font-weight: 600; margin-bottom: 0.4rem; }
|
|
.header .sub { font-size: 0.85rem; color: var(--text-dim); }
|
|
|
|
/* --- 风格分区 --- */
|
|
.style-section {
|
|
margin-bottom: 2.5rem; padding: 1.5rem;
|
|
border: 2px solid var(--border); border-radius: 12px;
|
|
background: var(--card); cursor: pointer;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
.style-section:hover { border-color: #444; }
|
|
.style-section.selected {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 1px var(--accent), 0 0 20px rgba(240,192,64,0.1);
|
|
}
|
|
.style-section.selected .style-radio { background: var(--accent); border-color: var(--accent); }
|
|
.style-section.selected .style-radio::after { content: ''; display: block; width: 8px; height: 8px; background: #1a1a1a; border-radius: 50%; }
|
|
|
|
.style-top { display: flex; align-items: center; gap: 14px; margin-bottom: 1rem; }
|
|
.style-radio {
|
|
width: 20px; height: 20px; border-radius: 50%;
|
|
border: 2px solid #555; flex-shrink: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.style-name { font-size: 1.2rem; font-weight: 600; color: var(--accent); }
|
|
.style-desc { font-size: 0.85rem; color: var(--text-dim); margin-bottom: 0.8rem; }
|
|
|
|
.style-tags { margin-bottom: 1rem; }
|
|
.style-tags span {
|
|
display: inline-block; background: #222; border: 1px solid #333;
|
|
border-radius: 4px; padding: 2px 10px; margin: 3px 3px 3px 0;
|
|
font-size: 0.75rem; color: #aaa;
|
|
}
|
|
|
|
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
|
|
.image-grid img {
|
|
width: 100%; height: auto; border-radius: 6px; display: block;
|
|
transition: transform 0.2s;
|
|
}
|
|
.image-grid img:hover { transform: scale(1.03); }
|
|
|
|
.model-rec { margin-top: 0.8rem; font-size: 0.75rem; color: #666; }
|
|
|
|
/* --- 自定义区 --- */
|
|
.custom-section {
|
|
margin-bottom: 2.5rem; padding: 1.5rem;
|
|
border: 2px solid var(--border); border-radius: 12px;
|
|
background: var(--card);
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
.custom-section:hover { border-color: #444; }
|
|
.custom-section.has-content { border-color: var(--green); }
|
|
.custom-section.selected {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 1px var(--accent), 0 0 20px rgba(240,192,64,0.1);
|
|
}
|
|
.custom-section .style-top { display: flex; align-items: center; gap: 14px; margin-bottom: 1rem; }
|
|
.custom-section .style-radio {
|
|
width: 20px; height: 20px; border-radius: 50%;
|
|
border: 2px solid #555; flex-shrink: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.custom-section.selected .style-radio { background: var(--accent); border-color: var(--accent); }
|
|
.custom-section.selected .style-radio::after { content: ''; display: block; width: 8px; height: 8px; background: #1a1a1a; border-radius: 50%; }
|
|
.custom-hint { font-size: 0.8rem; color: #666; margin-bottom: 1.2rem; }
|
|
|
|
.custom-row { display: flex; gap: 1.5rem; align-items: flex-start; }
|
|
.custom-text-col { flex: 1; min-width: 0; }
|
|
.custom-upload-col { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; }
|
|
|
|
.custom-text-col textarea {
|
|
width: 100%; min-height: 120px; padding: 12px; resize: vertical;
|
|
background: #111; border: 1px solid #333; border-radius: 8px;
|
|
color: var(--text); font-size: 0.9rem; line-height: 1.6;
|
|
font-family: inherit;
|
|
}
|
|
.custom-text-col textarea:focus { outline: none; border-color: var(--accent); }
|
|
.custom-text-col textarea::placeholder { color: #555; }
|
|
|
|
.upload-area {
|
|
width: 200px; min-height: 140px; padding: 1rem;
|
|
border: 2px dashed #333; border-radius: 8px;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
cursor: pointer; transition: border-color 0.2s;
|
|
}
|
|
.upload-area:hover { border-color: #555; }
|
|
.upload-area.has-image { border-style: solid; border-color: var(--green); }
|
|
.upload-area .upload-icon { font-size: 1.6rem; color: #555; margin-bottom: 0.4rem; }
|
|
.upload-area .upload-label { font-size: 0.75rem; color: #666; text-align: center; }
|
|
.upload-btn {
|
|
margin-top: 8px; padding: 6px 16px; background: #222; border: 1px solid #444;
|
|
border-radius: 6px; color: var(--text); cursor: pointer; font-size: 0.8rem;
|
|
}
|
|
.upload-btn:hover { background: #333; }
|
|
#upload-preview {
|
|
margin-top: 0.6rem; display: none; text-align: center;
|
|
}
|
|
#upload-preview img {
|
|
max-width: 196px; max-height: 140px; border-radius: 6px;
|
|
}
|
|
#upload-preview .remove-btn {
|
|
margin-top: 6px; padding: 3px 12px; background: #331111;
|
|
border: 1px solid #663333; border-radius: 4px;
|
|
color: #ff6666; cursor: pointer; font-size: 0.75rem;
|
|
}
|
|
|
|
/* --- 提交区 --- */
|
|
.submit-area {
|
|
position: fixed; bottom: 0; left: 0; right: 0;
|
|
background: rgba(15,15,15,0.95); backdrop-filter: blur(8px);
|
|
border-top: 1px solid var(--border); padding: 1rem 2rem;
|
|
display: flex; align-items: center; justify-content: center; gap: 16px;
|
|
}
|
|
.submit-area .selected-label { font-size: 0.9rem; color: var(--text-dim); }
|
|
.submit-area .selected-name { color: var(--accent); font-weight: 600; }
|
|
.submit-btn {
|
|
padding: 10px 32px; background: var(--accent); color: #1a1a1a;
|
|
border: none; border-radius: 8px; font-size: 1rem; font-weight: 600;
|
|
cursor: pointer; transition: background 0.2s;
|
|
}
|
|
.submit-btn:hover { background: #e0b030; }
|
|
.submit-btn:disabled { background: #555; color: #999; cursor: not-allowed; }
|
|
|
|
#status-msg { font-size: 0.85rem; color: var(--green); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1>美术风格选择</h1>
|
|
<p class="sub">选择一种内置风格,或自定义风格描述 / 上传参考图</p>
|
|
</div>
|
|
|
|
<div id="presets-container"></div>
|
|
|
|
<div class="custom-section" id="custom-section" onclick="onCustomSectionClick(event)">
|
|
<div class="style-top">
|
|
<div class="style-radio"></div>
|
|
<span class="style-name">自定义风格</span>
|
|
</div>
|
|
<p class="custom-hint">输入风格描述文字、上传参考图,或两者兼用</p>
|
|
<div class="custom-row">
|
|
<div class="custom-text-col">
|
|
<textarea id="custom-text" placeholder="描述你想要的美术风格,例如: 赛博朋克霓虹风,高饱和紫蓝色调,暗夜城市背景, 强烈明暗对比,发光粒子和全息投影元素…" oninput="onCustomInput()"></textarea>
|
|
</div>
|
|
<div class="custom-upload-col">
|
|
<div class="upload-area" id="upload-area" onclick="document.getElementById('file-input').click()">
|
|
<div class="upload-icon">+</div>
|
|
<div class="upload-label"><span id="upload-label-text">上传参考图</span><br><span class="upload-formats" style="font-size:0.65rem;color:#555">PNG / JPG / WebP</span></div>
|
|
</div>
|
|
<input type="file" id="file-input" accept="image/*" style="display:none">
|
|
<div id="upload-preview">
|
|
<img id="upload-img" alt="uploaded">
|
|
<br>
|
|
<button class="remove-btn" onclick="removeUpload(event)">移除</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="submit-area">
|
|
<span class="selected-label"><span id="selected-label-text">已选:</span><span class="selected-name" id="selected-display">未选择</span></span>
|
|
<button class="submit-btn" id="submit-btn" disabled onclick="submitChoice()">提交选择</button>
|
|
<span id="status-msg"></span>
|
|
</div>
|
|
|
|
<script>
|
|
const PREVIEW_TYPE = null; /* __PREVIEW_TYPE__ */
|
|
const PREVIEW_DATA = null; /* __PREVIEW_DATA__ */
|
|
|
|
const PREVIEW_LANG = null; /* __PREVIEW_LANG__ */
|
|
const LANG = PREVIEW_LANG || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
|
const I18N = {
|
|
zh: {
|
|
pageTitle: '美术风格选择',
|
|
pageSub: '选择一种内置风格,或自定义风格描述 / 上传参考图',
|
|
customStyle: '自定义风格',
|
|
customHint: '输入风格描述文字、上传参考图,或两者兼用',
|
|
customPlaceholder: '描述你想要的美术风格,例如:\n赛博朋克霓虹风,高饱和紫蓝色调,暗夜城市背景,\n强烈明暗对比,发光粒子和全息投影元素…',
|
|
uploadRef: '上传参考图',
|
|
uploadLabel: '上传参考图',
|
|
uploadFormats: 'PNG / JPG / WebP',
|
|
removeBtn: '移除',
|
|
selectedLabel: '已选:',
|
|
notSelected: '未选择',
|
|
submitBtn: '提交选择',
|
|
recommendModel: '推荐生图模型:',
|
|
enterDesc: '请输入风格描述或上传参考图',
|
|
submitFailed: '提交失败,请重试',
|
|
confirmNext: '已提交,Hub 将继续下一步',
|
|
pageClosing: '页面将自动关闭...',
|
|
canClose: '可以关闭此页面了',
|
|
feedbackStyleCustom: '风格:自定义',
|
|
feedbackDesc: '描述:',
|
|
feedbackRef: '参考图:',
|
|
feedbackStyle: '风格:',
|
|
},
|
|
en: {
|
|
pageTitle: 'Art Style Selection',
|
|
pageSub: 'Choose a built-in style, or customize with description / reference image',
|
|
customStyle: 'Custom Style',
|
|
customHint: 'Enter a style description, upload a reference image, or both',
|
|
customPlaceholder: 'Describe the art style you want, e.g.:\nCyberpunk neon, high saturation purple-blue tones, dark city backdrop,\nstrong contrast, glowing particles and holographic elements…',
|
|
uploadRef: 'Upload Reference',
|
|
uploadLabel: 'Upload Reference',
|
|
uploadFormats: 'PNG / JPG / WebP',
|
|
removeBtn: 'Remove',
|
|
selectedLabel: 'Selected: ',
|
|
notSelected: 'None',
|
|
submitBtn: 'Submit Selection',
|
|
recommendModel: 'Recommended model: ',
|
|
enterDesc: 'Please enter a style description or upload a reference image',
|
|
submitFailed: 'Submit failed, please retry',
|
|
confirmNext: 'Submitted. Hub will proceed to next step.',
|
|
pageClosing: 'Page will close automatically...',
|
|
canClose: 'You can close this page now.',
|
|
feedbackStyleCustom: 'Style: Custom',
|
|
feedbackDesc: 'Description: ',
|
|
feedbackRef: 'Reference: ',
|
|
feedbackStyle: 'Style: ',
|
|
}
|
|
};
|
|
const t = (key) => (I18N[LANG] || I18N.en)[key] || key;
|
|
document.documentElement.lang = LANG === 'zh' ? 'zh-CN' : 'en';
|
|
|
|
let selectedStyle = null;
|
|
let uploadedPath = null;
|
|
let customText = '';
|
|
|
|
function init() {
|
|
// i18n static text
|
|
document.querySelector('.header h1').textContent = t('pageTitle');
|
|
document.querySelector('.header .sub').textContent = t('pageSub');
|
|
document.querySelector('#custom-section .style-name').textContent = t('customStyle');
|
|
document.querySelector('.custom-hint').textContent = t('customHint');
|
|
document.getElementById('custom-text').placeholder = t('customPlaceholder');
|
|
document.querySelector('.upload-label').innerHTML = t('uploadLabel') + '<br><span style="font-size:0.65rem;color:#555">' + t('uploadFormats') + '</span>';
|
|
document.querySelector('.remove-btn').textContent = t('removeBtn');
|
|
document.getElementById('selected-display').textContent = t('notSelected');
|
|
document.getElementById('selected-label-text').textContent = t('selectedLabel');
|
|
document.getElementById('submit-btn').textContent = t('submitBtn');
|
|
document.title = 'MV Creator - ' + t('pageTitle');
|
|
|
|
const data = typeof PREVIEW_DATA === 'string' ? JSON.parse(PREVIEW_DATA) : PREVIEW_DATA;
|
|
if (!data || !data.presets) return;
|
|
|
|
const container = document.getElementById('presets-container');
|
|
const ids = Object.keys(data.presets);
|
|
|
|
ids.forEach((id, idx) => {
|
|
const p = data.presets[id];
|
|
const label = String.fromCharCode(65 + idx); // A, B, C...
|
|
const section = document.createElement('div');
|
|
section.className = 'style-section';
|
|
section.dataset.styleId = id;
|
|
section.onclick = () => selectStyle(id);
|
|
|
|
let imagesHtml = '';
|
|
p.images.forEach(img => {
|
|
imagesHtml += `<img src="${img.url}" alt="${img.filename}" loading="lazy">`;
|
|
});
|
|
|
|
let tagsHtml = '';
|
|
p.tags.forEach(tag => { tagsHtml += `<span>${tag}</span>`; });
|
|
|
|
section.innerHTML = `
|
|
<div class="style-top">
|
|
<div class="style-radio"></div>
|
|
<span class="style-name">${label}. ${p.name}</span>
|
|
</div>
|
|
<div class="style-desc">${p.description}</div>
|
|
<div class="style-tags">${tagsHtml}</div>
|
|
<div class="image-grid">${imagesHtml}</div>
|
|
<div class="model-rec">${t('recommendModel')}${p.recommend}</div>
|
|
`;
|
|
container.appendChild(section);
|
|
});
|
|
|
|
// 已有用户上传
|
|
if (data.user_refs && data.user_refs.length > 0) {
|
|
showUploadedImage(data.user_refs[0].url);
|
|
}
|
|
}
|
|
|
|
function selectStyle(id) {
|
|
selectedStyle = id;
|
|
document.querySelectorAll('.style-section').forEach(el => {
|
|
el.classList.toggle('selected', el.dataset.styleId === id);
|
|
});
|
|
document.getElementById('custom-section').classList.remove('selected');
|
|
const data = typeof PREVIEW_DATA === 'string' ? JSON.parse(PREVIEW_DATA) : PREVIEW_DATA;
|
|
const p = data.presets[id];
|
|
document.getElementById('selected-display').textContent = p ? p.name : id;
|
|
document.getElementById('submit-btn').disabled = false;
|
|
}
|
|
|
|
function selectCustomStyle() {
|
|
selectedStyle = '__custom__';
|
|
document.querySelectorAll('.style-section').forEach(el => {
|
|
el.classList.remove('selected');
|
|
});
|
|
document.getElementById('custom-section').classList.add('selected');
|
|
document.getElementById('selected-display').textContent = t('customStyle');
|
|
document.getElementById('submit-btn').disabled = false;
|
|
}
|
|
|
|
function hasCustomContent() {
|
|
return !!(uploadedPath || customText.trim());
|
|
}
|
|
|
|
function onCustomInput() {
|
|
customText = document.getElementById('custom-text').value;
|
|
if (selectedStyle !== '__custom__' && hasCustomContent()) {
|
|
selectCustomStyle();
|
|
}
|
|
}
|
|
|
|
function showUploadedImage(url) {
|
|
const preview = document.getElementById('upload-preview');
|
|
const img = document.getElementById('upload-img');
|
|
const area = document.getElementById('upload-area');
|
|
img.src = url;
|
|
preview.style.display = 'block';
|
|
area.classList.add('has-image');
|
|
}
|
|
|
|
function removeUpload(e) {
|
|
if (e) e.stopPropagation();
|
|
uploadedPath = null;
|
|
document.getElementById('upload-preview').style.display = 'none';
|
|
document.getElementById('upload-area').classList.remove('has-image');
|
|
document.getElementById('file-input').value = '';
|
|
}
|
|
|
|
document.getElementById('file-input').addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = async () => {
|
|
const base64 = reader.result.split(',')[1];
|
|
try {
|
|
const resp = await fetch('/upload', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ data: base64, filename: file.name })
|
|
});
|
|
const result = await resp.json();
|
|
if (result.ok) {
|
|
uploadedPath = result.path;
|
|
showUploadedImage(result.url);
|
|
selectCustomStyle();
|
|
}
|
|
} catch (err) {
|
|
console.error('Upload failed:', err);
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
function onCustomSectionClick(e) {
|
|
if (e.target.closest('button') || e.target.closest('input') || e.target.closest('textarea') || e.target.closest('.upload-area')) return;
|
|
selectCustomStyle();
|
|
}
|
|
|
|
async function submitChoice() {
|
|
if (!selectedStyle) return;
|
|
|
|
if (selectedStyle === '__custom__' && !hasCustomContent()) {
|
|
document.getElementById('status-msg').textContent = t('enterDesc');
|
|
return;
|
|
}
|
|
|
|
let feedback;
|
|
if (selectedStyle === '__custom__') {
|
|
const parts = [t('feedbackStyleCustom')];
|
|
if (customText.trim()) {
|
|
parts.push(`${t('feedbackDesc')}${customText.trim()}`);
|
|
}
|
|
if (uploadedPath) {
|
|
parts.push(`${t('feedbackRef')}${uploadedPath}`);
|
|
}
|
|
feedback = parts.join('\n');
|
|
} else {
|
|
const data = typeof PREVIEW_DATA === 'string' ? JSON.parse(PREVIEW_DATA) : PREVIEW_DATA;
|
|
const p = data.presets[selectedStyle];
|
|
feedback = `${t('feedbackStyle')}${selectedStyle} (${p ? p.name : ''})`;
|
|
if (uploadedPath) {
|
|
feedback += `\n${t('feedbackRef')}${uploadedPath}`;
|
|
}
|
|
if (customText.trim()) {
|
|
feedback += `\n${t('feedbackDesc')}${customText.trim()}`;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch('/feedback', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ feedback })
|
|
});
|
|
const result = await resp.json();
|
|
if (result.ok) {
|
|
showDone(t('confirmNext'));
|
|
}
|
|
} catch (err) {
|
|
document.getElementById('status-msg').textContent = t('submitFailed');
|
|
}
|
|
}
|
|
|
|
init();
|
|
function showDone(msg) {
|
|
document.body.innerHTML = `
|
|
<div style="display:flex;align-items:center;justify-content:center;height:100vh;background:#0f0f0f;">
|
|
<div style="text-align:center;color:#e0e0e0;font-family:-apple-system,sans-serif;">
|
|
<div style="font-size:3rem;color:#4ade80;margin-bottom:1rem;">✓</div>
|
|
<div style="font-size:1.2rem;font-weight:600;margin-bottom:0.5rem;">${msg}</div>
|
|
<div class="done-sub" style="font-size:0.85rem;color:#888;">${t('pageClosing')}</div>
|
|
</div>
|
|
</div>`;
|
|
setTimeout(() => {
|
|
window.close();
|
|
setTimeout(() => {
|
|
const sub = document.querySelector('.done-sub');
|
|
if (sub) sub.textContent = t('canClose');
|
|
}, 2000);
|
|
}, 800);
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|