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.
516 lines
21 KiB
516 lines
21 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; --green: #4ade80; --blue: #60a5fa; --red: #f87171;
|
|
--purple: #c084fc;
|
|
}
|
|
* { 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 14rem;
|
|
max-width: 1400px; 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); }
|
|
.header .stats {
|
|
margin-top: 0.5rem; font-size: 0.8rem; color: var(--text-dim);
|
|
display: flex; gap: 1.5rem; justify-content: center;
|
|
}
|
|
.header .stats .ok { color: var(--green); }
|
|
.header .stats .miss { color: var(--red); }
|
|
|
|
/* --- 场景卡片 --- */
|
|
.scene-card {
|
|
margin-bottom: 3rem; padding: 1.5rem;
|
|
border: 2px solid var(--border); border-radius: 12px; background: var(--card);
|
|
transition: border-color 0.2s;
|
|
}
|
|
.scene-card:hover { border-color: #3a3a3a; }
|
|
.scene-card.missing { border-color: rgba(248,113,113,0.3); }
|
|
.scene-header {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
margin-bottom: 1rem; padding-bottom: 0.6rem; border-bottom: 1px solid var(--border);
|
|
}
|
|
.scene-header h2 { font-size: 1.2rem; font-weight: 600; color: var(--accent); }
|
|
.scene-time {
|
|
font-size: 0.8rem; color: var(--blue); background: rgba(96,165,250,0.1);
|
|
padding: 2px 10px; border-radius: 4px; font-family: monospace;
|
|
}
|
|
.scene-status {
|
|
font-size: 0.7rem; padding: 2px 10px; border-radius: 4px; font-weight: 600;
|
|
}
|
|
.scene-status.ok { background: rgba(74,222,128,0.15); color: var(--green); }
|
|
.scene-status.miss { background: rgba(248,113,113,0.15); color: var(--red); }
|
|
.scene-at-btn {
|
|
padding: 2px 10px; border-radius: 4px; font-size: 0.75rem;
|
|
cursor: pointer; border: 1px solid #444; background: #222; color: var(--text-dim);
|
|
transition: all 0.15s; margin-left: auto; flex-shrink: 0;
|
|
}
|
|
.scene-at-btn:hover { background: #333; color: var(--accent); border-color: var(--accent); }
|
|
|
|
/* 视频播放器 */
|
|
.video-container {
|
|
margin-bottom: 1rem; border-radius: 8px; overflow: hidden;
|
|
background: #000; position: relative;
|
|
}
|
|
.video-container video {
|
|
width: 100%; display: block; max-height: 480px; object-fit: contain;
|
|
background: #000;
|
|
}
|
|
.video-missing {
|
|
padding: 3rem 2rem; text-align: center; color: var(--red);
|
|
font-size: 0.9rem; background: rgba(248,113,113,0.05);
|
|
border: 1px dashed rgba(248,113,113,0.3); border-radius: 8px;
|
|
}
|
|
|
|
/* 歌词引用 */
|
|
.lyrics-block {
|
|
margin-bottom: 1rem; padding: 0.6rem 1rem;
|
|
border-left: 3px solid var(--purple); background: rgba(192,132,252,0.05);
|
|
font-size: 0.85rem; color: #bbb; font-style: italic;
|
|
}
|
|
.lyrics-block p { margin: 2px 0; }
|
|
|
|
/* 场景信息 */
|
|
.scene-info { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
|
|
@media (max-width: 768px) { .scene-info { grid-template-columns: 1fr; } }
|
|
.info-block h3 { font-size: 0.8rem; color: var(--blue); margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.info-block p { font-size: 0.85rem; color: var(--text-dim); }
|
|
|
|
/* 出场角色标签 */
|
|
.char-tags { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
.char-tag {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 3px 10px; border-radius: 16px; font-size: 0.8rem;
|
|
background: rgba(240,192,64,0.1); border: 1px solid rgba(240,192,64,0.3);
|
|
color: var(--accent); cursor: default; position: relative;
|
|
}
|
|
.char-tag img { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; }
|
|
.char-tag-none { color: var(--text-dim); background: rgba(136,136,136,0.1); border-color: rgba(136,136,136,0.3); }
|
|
.char-ref-popup {
|
|
display: none; position: absolute; top: calc(100% + 8px); left: 0; z-index: 100;
|
|
background: #222; border: 1px solid #444; border-radius: 8px;
|
|
padding: 6px; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
|
}
|
|
.char-ref-popup img { width: 160px; height: auto; border-radius: 6px; display: block; }
|
|
.char-tag:hover .char-ref-popup { display: block; }
|
|
|
|
/* 参考图缩略图 */
|
|
.ref-images { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 1rem; align-items: flex-end; }
|
|
.ref-images .label { font-size: 0.8rem; color: var(--text-dim); width: 100%; margin-bottom: 2px; }
|
|
.ref-thumb {
|
|
width: 80px; height: 52px; border-radius: 6px; object-fit: cover;
|
|
border: 1px solid #333; transition: transform 0.15s;
|
|
}
|
|
.ref-thumb:hover { transform: scale(1.1); border-color: var(--accent); }
|
|
.ref-note { font-size: 0.65rem; color: #666; max-width: 80px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
/* 视频提示词(可折叠) */
|
|
.prompt-toggle {
|
|
display: flex; align-items: center; gap: 8px; cursor: pointer;
|
|
margin-bottom: 0.5rem; user-select: none;
|
|
}
|
|
.prompt-toggle h3 { font-size: 0.8rem; color: var(--blue); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.prompt-toggle .arrow { font-size: 0.7rem; color: #666; transition: transform 0.2s; }
|
|
.prompt-toggle.open .arrow { transform: rotate(90deg); }
|
|
.prompt-block {
|
|
display: none; margin-bottom: 1rem; padding: 0.8rem; background: #111;
|
|
border-radius: 8px; border: 1px solid #222;
|
|
}
|
|
.prompt-block.open { display: block; }
|
|
.prompt-text {
|
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
font-size: 0.8rem; color: #ccc; line-height: 1.5; white-space: pre-wrap; word-break: break-word;
|
|
}
|
|
.prompt-meta { margin-top: 0.5rem; font-size: 0.75rem; color: #666; }
|
|
.prompt-meta span { margin-right: 1.5rem; }
|
|
|
|
/* --- 底部提交区 --- */
|
|
.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;
|
|
}
|
|
.submit-row { max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 10px; }
|
|
.feedback-row { display: flex; align-items: flex-start; gap: 12px; }
|
|
.feedback-textarea {
|
|
flex: 1; min-height: 48px; max-height: 120px; padding: 8px 12px; resize: vertical;
|
|
background: #222; border: 1px solid #444; border-radius: 6px;
|
|
color: var(--text); font-size: 0.85rem; font-family: inherit;
|
|
}
|
|
.feedback-textarea::placeholder { color: #555; }
|
|
.feedback-textarea:focus { outline: none; border-color: var(--accent); }
|
|
.upload-at-btn {
|
|
padding: 6px 14px; background: #222; border: 1px solid #444; border-radius: 5px;
|
|
color: var(--text-dim); cursor: pointer; font-size: 0.8rem; white-space: nowrap;
|
|
flex-shrink: 0; align-self: center;
|
|
}
|
|
.upload-at-btn:hover { background: #333; }
|
|
|
|
.submit-top { display: flex; align-items: center; gap: 12px; }
|
|
.submit-info { font-size: 0.85rem; color: var(--text-dim); }
|
|
.submit-info .count { font-weight: 600; }
|
|
.btn-group { display: flex; gap: 10px; }
|
|
.submit-btn {
|
|
padding: 8px 24px; background: var(--accent); color: #1a1a1a; border: none;
|
|
border-radius: 8px; font-size: 0.9rem; font-weight: 600; cursor: pointer;
|
|
}
|
|
.submit-btn:hover { background: #e0b030; }
|
|
.submit-btn:disabled { background: #555; color: #999; cursor: not-allowed; }
|
|
#status-msg { font-size: 0.85rem; color: var(--green); margin-left: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1>分镜视频预览</h1>
|
|
<p class="sub">审阅各幕视频 · 点击 @ 引用场景到反馈框提交修改或重新生成</p>
|
|
<div class="stats">
|
|
<span><span id="stats-total-label"></span> <span id="total-count">0</span><span id="stats-scenes-unit"></span></span>
|
|
<span class="ok"><span id="stats-generated-label"></span><span id="ok-count">0</span></span>
|
|
<span class="miss"><span id="stats-missing-label"></span><span id="miss-count">0</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="scenes-container"></div>
|
|
|
|
<div class="submit-area">
|
|
<div class="submit-row">
|
|
<div class="feedback-row">
|
|
<textarea class="feedback-textarea" id="feedback-input"
|
|
placeholder="修改建议(选填):点击场景的 @ 引用,描述需要修改的内容或重新生成 如「@scene_03 镜头运动改为缓慢推进,人物表情更悲伤」 如「@scene_05 重新生成,参考 @uploaded/ref.jpg 的构图」 留空 = 确认所有视频,进入剪辑阶段"></textarea>
|
|
<input type="file" id="ref-file" accept="image/*" style="display:none"
|
|
onchange="handleRefUpload(this)">
|
|
<button class="upload-at-btn" onclick="document.getElementById('ref-file').click()">上传参考图 @</button>
|
|
</div>
|
|
<div class="submit-top">
|
|
<span class="submit-info">
|
|
<span class="count" style="color:var(--green)" id="ok-count-bottom">0</span> /
|
|
<span id="total-count-bottom">0</span><span id="generated-count-label"></span>
|
|
</span>
|
|
<button class="submit-btn" id="submit-btn" onclick="submitAll()">提交</button>
|
|
<span id="status-msg"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const PREVIEW_LANG = null; /* __PREVIEW_LANG__ */
|
|
const LANG = PREVIEW_LANG || (navigator.language.startsWith('zh') ? 'zh' : 'en');
|
|
const I18N = {
|
|
zh: {
|
|
pageTitle: '分镜视频预览',
|
|
pageSub: '审阅各幕视频 · 点击 @ 引用场景到反馈框提交修改或重新生成',
|
|
totalLabel: '共',
|
|
scenesUnit: '幕',
|
|
generated: '已生成',
|
|
missing: '缺失',
|
|
statusOk: '已生成',
|
|
statusMiss: '缺失',
|
|
videoMissing: '视频未生成 — 提交反馈后将重新生成此幕',
|
|
videoUnsupported: '浏览器不支持视频播放',
|
|
characters: '出场角色',
|
|
sceneBackground: '场景背景',
|
|
keyProps: '关键物件',
|
|
emptyScene: '空镜',
|
|
sceneDesc: '场景描述',
|
|
videoPrompt: '视频提示词',
|
|
refImages: '参考图:',
|
|
model: '模型:',
|
|
feedbackPlaceholder: '修改建议(选填):点击场景的 @ 引用,描述需要修改的内容或重新生成\n如「@scene_03 镜头运动改为缓慢推进,人物表情更悲伤」\n如「@scene_05 重新生成,参考 @uploaded/ref.jpg 的构图」\n留空 = 确认所有视频,进入剪辑阶段',
|
|
uploadRefBtn: '上传参考图 @',
|
|
generatedCount: '幕已生成',
|
|
submitBtn: '提交',
|
|
submitFailed: '提交失败,请重试',
|
|
confirmNext: '已提交,Hub 将继续下一步',
|
|
pageClosing: '页面将自动关闭...',
|
|
canClose: '可以关闭此页面了',
|
|
citeScene: '引用场景到反馈框',
|
|
charRefAlt: '参考图',
|
|
},
|
|
en: {
|
|
pageTitle: 'Scene Video Preview',
|
|
pageSub: 'Review each scene video · Click @ to cite in feedback for edits or regeneration',
|
|
totalLabel: '',
|
|
scenesUnit: ' scenes',
|
|
generated: 'Generated',
|
|
missing: 'Missing',
|
|
statusOk: 'Generated',
|
|
statusMiss: 'Missing',
|
|
videoMissing: 'Video not generated — will regenerate after feedback submission',
|
|
videoUnsupported: 'Browser does not support video playback',
|
|
characters: 'Characters',
|
|
sceneBackground: 'Scene Background',
|
|
keyProps: 'Key Props',
|
|
emptyScene: 'Empty scene',
|
|
sceneDesc: 'Scene Description',
|
|
videoPrompt: 'Video Prompt',
|
|
refImages: 'References: ',
|
|
model: 'Model: ',
|
|
feedbackPlaceholder: 'Feedback (optional): Click @ on scenes to cite, describe edits or regeneration\ne.g. "@scene_03 change camera to slow push-in, sadder expression"\ne.g. "@scene_05 regenerate, ref @uploaded/ref.jpg composition"\nLeave empty = confirm all videos, proceed to editing phase',
|
|
uploadRefBtn: 'Upload Ref @',
|
|
generatedCount: ' scenes generated',
|
|
submitBtn: 'Submit',
|
|
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.',
|
|
citeScene: 'Cite scene in feedback',
|
|
charRefAlt: 'reference',
|
|
}
|
|
};
|
|
const t = (key) => (I18N[LANG] || I18N.en)[key] || key;
|
|
document.documentElement.lang = LANG === 'zh' ? 'zh-CN' : 'en';
|
|
|
|
const PREVIEW_TYPE = null; /* __PREVIEW_TYPE__ */
|
|
const PREVIEW_DATA = null; /* __PREVIEW_DATA__ */
|
|
|
|
function init() {
|
|
const data = typeof PREVIEW_DATA === 'string' ? JSON.parse(PREVIEW_DATA) : PREVIEW_DATA;
|
|
if (!data || !data.scenes) return;
|
|
|
|
// i18n: update static HTML elements
|
|
document.querySelector('.header h1').textContent = t('pageTitle');
|
|
document.title = 'MV Creator - ' + t('pageTitle');
|
|
document.querySelector('.header .sub').textContent = t('pageSub');
|
|
document.getElementById('feedback-input').placeholder = t('feedbackPlaceholder');
|
|
document.querySelector('.upload-at-btn').textContent = t('uploadRefBtn');
|
|
document.getElementById('submit-btn').textContent = t('submitBtn');
|
|
|
|
const container = document.getElementById('scenes-container');
|
|
const scenes = data.scenes;
|
|
let okCount = 0;
|
|
let missCount = 0;
|
|
|
|
scenes.forEach((scene, si) => {
|
|
if (scene.status === 'generated') okCount++;
|
|
else missCount++;
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'scene-card' + (scene.status !== 'generated' ? ' missing' : '');
|
|
let html = '';
|
|
|
|
// header
|
|
const statusClass = scene.status === 'generated' ? 'ok' : 'miss';
|
|
const statusText = scene.status === 'generated' ? t('statusOk') : t('statusMiss');
|
|
html += `<div class="scene-header">
|
|
<h2>${esc(scene.title)}</h2>
|
|
<span class="scene-time">${esc(scene.time_raw)}</span>
|
|
<span class="scene-status ${statusClass}">${statusText}</span>
|
|
<button class="scene-at-btn" onclick="atScene(${si})" title="${t('citeScene')}">@</button>
|
|
</div>`;
|
|
|
|
// video player or missing placeholder
|
|
if (scene.status === 'generated' && scene.video_url) {
|
|
html += `<div class="video-container">
|
|
<video controls preload="metadata" playsinline>
|
|
<source src="${scene.video_url}" type="video/mp4">
|
|
${t('videoUnsupported')}
|
|
</video>
|
|
</div>`;
|
|
} else {
|
|
html += `<div class="video-missing">${t('videoMissing')}</div>`;
|
|
}
|
|
|
|
// lyrics
|
|
if (scene.lyrics && scene.lyrics.length) {
|
|
html += `<div class="lyrics-block">`;
|
|
scene.lyrics.forEach(l => { html += `<p>${esc(l)}</p>`; });
|
|
html += `</div>`;
|
|
}
|
|
|
|
// scene info grid
|
|
html += `<div class="scene-info">`;
|
|
|
|
// characters
|
|
html += `<div class="info-block"><h3>${t('characters')}</h3><div class="char-tags">`;
|
|
if (scene.character_names && scene.character_names.length) {
|
|
const charRefs = scene.char_refs || [];
|
|
const charNames = scene.character_names || [];
|
|
charNames.forEach(cname => {
|
|
const ref = charRefs.find(r => r.name === cname);
|
|
if (ref && ref.url) {
|
|
html += `<span class="char-tag">${esc(cname)}
|
|
<img src="${ref.url}" alt="${esc(cname)}">
|
|
<div class="char-ref-popup"><img src="${ref.url}" alt="${esc(cname)} ${t('charRefAlt')}"></div>
|
|
</span>`;
|
|
} else {
|
|
html += `<span class="char-tag">${esc(cname)}</span>`;
|
|
}
|
|
});
|
|
} else {
|
|
html += `<span class="char-tag char-tag-none">${t('emptyScene')}</span>`;
|
|
}
|
|
html += `</div></div>`;
|
|
|
|
// scene background
|
|
if (scene.scene_background) {
|
|
html += `<div class="info-block"><h3>${t('sceneBackground')}</h3><p>${esc(scene.scene_background)}</p></div>`;
|
|
}
|
|
|
|
// key props
|
|
if (scene.key_props) {
|
|
html += `<div class="info-block"><h3>${t('keyProps')}</h3><p>${esc(scene.key_props)}</p></div>`;
|
|
}
|
|
|
|
// description
|
|
if (scene.description) {
|
|
html += `<div class="info-block"><h3>${t('sceneDesc')}</h3><p>${esc(scene.description)}</p></div>`;
|
|
}
|
|
html += `</div>`;
|
|
|
|
// reference images
|
|
if (scene.ref_images && scene.ref_images.length) {
|
|
html += `<div class="ref-images"><span class="label">${t('refImages')}</span>`;
|
|
scene.ref_images.forEach(ref => {
|
|
if (ref.url) {
|
|
html += `<div style="text-align:center">
|
|
<img class="ref-thumb" src="${ref.url}" alt="${esc(ref.note || ref.path)}" title="${esc(ref.note || ref.path)}">
|
|
<div class="ref-note">${esc(ref.note || '')}</div>
|
|
</div>`;
|
|
}
|
|
});
|
|
html += `</div>`;
|
|
}
|
|
|
|
// video prompt (collapsible)
|
|
if (scene.prompt) {
|
|
html += `<div class="prompt-toggle" onclick="togglePrompt(this)">
|
|
<span class="arrow">▶</span><h3>${t('videoPrompt')}</h3>
|
|
</div>
|
|
<div class="prompt-block">
|
|
<div class="prompt-text">${esc(scene.prompt)}</div>`;
|
|
if (scene.model) {
|
|
html += `<div class="prompt-meta"><span>${t('model')}${esc(scene.model)}</span></div>`;
|
|
}
|
|
html += `</div>`;
|
|
}
|
|
|
|
card.innerHTML = html;
|
|
container.appendChild(card);
|
|
});
|
|
|
|
// header stats
|
|
document.getElementById('stats-total-label').textContent = t('totalLabel');
|
|
document.getElementById('stats-scenes-unit').textContent = t('scenesUnit');
|
|
document.getElementById('stats-generated-label').textContent = t('generated') + ' ';
|
|
document.getElementById('stats-missing-label').textContent = t('missing') + ' ';
|
|
document.getElementById('total-count').textContent = scenes.length;
|
|
document.getElementById('ok-count').textContent = okCount;
|
|
document.getElementById('miss-count').textContent = missCount;
|
|
|
|
// bottom stats
|
|
document.getElementById('total-count-bottom').textContent = scenes.length;
|
|
document.getElementById('ok-count-bottom').textContent = okCount;
|
|
document.getElementById('generated-count-label').textContent = t('generatedCount');
|
|
|
|
// hide miss stat if all generated
|
|
if (missCount === 0) {
|
|
document.querySelector('.stats .miss').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function togglePrompt(el) {
|
|
el.classList.toggle('open');
|
|
el.nextElementSibling.classList.toggle('open');
|
|
}
|
|
|
|
function atScene(si) {
|
|
const textarea = document.getElementById('feedback-input');
|
|
const val = textarea.value;
|
|
const sceneNum = String(si + 1).padStart(2, '0');
|
|
const insert = `@scene_${sceneNum} `;
|
|
const pos = textarea.selectionStart || val.length;
|
|
textarea.value = val.slice(0, pos) + insert + val.slice(pos);
|
|
textarea.focus();
|
|
textarea.selectionStart = textarea.selectionEnd = pos + insert.length;
|
|
}
|
|
|
|
async function handleRefUpload(input) {
|
|
const file = input.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, clips: true })
|
|
});
|
|
const result = await resp.json();
|
|
if (result.ok) {
|
|
const textarea = document.getElementById('feedback-input');
|
|
const val = textarea.value;
|
|
const insert = `@${result.path} `;
|
|
const pos = textarea.selectionStart || val.length;
|
|
textarea.value = val.slice(0, pos) + insert + val.slice(pos);
|
|
textarea.focus();
|
|
textarea.selectionStart = textarea.selectionEnd = pos + insert.length;
|
|
}
|
|
} catch (err) { console.error('Upload failed:', err); }
|
|
};
|
|
reader.readAsDataURL(file);
|
|
input.value = '';
|
|
}
|
|
|
|
async function submitAll() {
|
|
const feedbackText = document.getElementById('feedback-input').value.trim();
|
|
const feedback = feedbackText || 'LGTM';
|
|
await doSubmit(feedback);
|
|
}
|
|
|
|
async function doSubmit(feedback) {
|
|
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>
|
|
|