diff --git a/dance.css b/dance.css index d96a548..c0c7abc 100644 --- a/dance.css +++ b/dance.css @@ -72,6 +72,10 @@ .upload-panel button:disabled { opacity: .5; cursor: default; } .upload-panel .upload-error { color: #c0392b; font: bold 12px Tahoma, sans-serif; margin-top: 8px; } .upload-panel .upload-status { font: bold 12px Tahoma, sans-serif; margin-top: 8px; } +/* T37: paid_boost 插队播放跟模式一/二的"生成中"要有视觉区别 */ +.upload-panel .upload-status.boosted { color: #b8860b; background: #fff7d6; border: 2px solid #000; border-radius: 8px; padding: 6px 8px; } +.upload-panel select { width: 100%; font-size: 12px; margin-bottom: 10px; padding: 4px; } +.upload-panel .field-label { font: bold 12px Tahoma, sans-serif; margin-bottom: 4px; display: block; } /* T29: balance pill + topup tier picker (reuses .nickname-modal/.nickname-box) */ .credit-pill { font: bold 12px Tahoma, sans-serif; } diff --git a/dance.html b/dance.html index b8bbd03..f716d34 100644 --- a/dance.html +++ b/dance.html @@ -220,7 +220,7 @@ function renderAuthButton(user) { authBtn.textContent = user ? `${user.nickname || user.email.split("@")[0]} · 💰${user.creditBalance} · 退出` : "登录"; authBtn.dataset.loggedIn = user ? "1" : ""; topupBtn.hidden = !user; - if (leftTab === "garment") renderWardrobe(); // price/balance-gated panel depends on login state + if (leftTab === "garment" || leftTab === "character") renderWardrobe(); // price/balance-gated panels depend on login state } async function refreshAuthState() { @@ -377,8 +377,8 @@ function renderWardrobe() { renderGarmentUploadPanel(); return; } - if (leftTab !== "wardrobe") { - wardrobePanel.innerHTML = `
传人功能开发中,敬请期待
`; + if (leftTab === "character") { + renderCharacterUploadPanel(); return; } if (wardrobe.length === 0) { @@ -457,9 +457,16 @@ async function submitGarmentUpload() { } return; } - statusBox.textContent = data.status === "ready" ? "生成完成,已加入播放队列!" : "生成中,完成后会自动加入播放队列"; - fileInput.value = ""; - await refreshAuthState(); // balance just changed + const message = data.status === "ready" ? "生成完成,已加入播放队列!" : "生成中,完成后会自动加入播放队列"; + // refreshAuthState() re-renders this whole panel (creditBalance changed + // -> renderAuthButton -> renderWardrobe) — setting the message before + // that would just get wiped by the fresh render, so set it after and + // re-query, not on the (now-replaced) statusBox/fileInput above. + await refreshAuthState(); + document.getElementById("garmentFileInput").value = ""; + const freshStatusBox = document.getElementById("garmentUploadStatus"); + freshStatusBox.hidden = false; + freshStatusBox.textContent = message; pollState(); // surface the new queue entry immediately, same as pickGarment } catch { statusBox.hidden = true; @@ -470,6 +477,96 @@ async function submitGarmentUpload() { } } +// ---- T37: 模式三"传人"(付费,需登录+邮箱验证) ---- +function renderCharacterUploadPanel() { + const spendCharacter = pricing.find((p) => p.key === "spend_character_per_segment"); + const costPerSegment = spendCharacter ? Math.ceil(spendCharacter.credits / (spendCharacter.segmentsPerUnit || 1)) : null; + const priceLine = costPerSegment != null ? `上传一张人物照片,每购买 1 个 10 秒片段扣 ${costPerSegment} 积分——买得越多,这个新角色在直播间露出的时长越长` : "加载价格中…"; + const garmentOptions = wardrobe.map((g) => ``).join(""); + wardrobePanel.innerHTML = ` +
+
${priceLine}
+ + + + + + + + +
`; + document.getElementById("characterUploadBtn").addEventListener("click", submitCharacterUpload); +} + +async function submitCharacterUpload() { + const fileInput = document.getElementById("characterFileInput"); + const garmentSelect = document.getElementById("characterGarmentSelect"); + const segmentsInput = document.getElementById("characterSegmentsInput"); + const errorBox = document.getElementById("characterUploadError"); + const statusBox = document.getElementById("characterUploadStatus"); + const btn = document.getElementById("characterUploadBtn"); + errorBox.hidden = true; + statusBox.hidden = true; + statusBox.classList.remove("boosted"); + + // 未登录/未验证先引导,而不是提交后静默失败(同 T28 验收标准) + if (!currentUser) { + openAuthModal("login"); + return; + } + if (!currentUser.emailVerified) { + errorBox.hidden = false; + errorBox.textContent = "请先完成邮箱验证后再上传(查看邮箱中的验证链接)"; + return; + } + const file = fileInput.files[0]; + if (!file) { + errorBox.hidden = false; + errorBox.textContent = "请先选择一张人物照片"; + return; + } + + btn.disabled = true; + statusBox.hidden = false; + statusBox.textContent = "上传中…"; + try { + const fd = new FormData(); + fd.append("file", file); + if (garmentSelect.value) fd.append("garmentId", garmentSelect.value); + fd.append("segments", segmentsInput.value || "3"); + const res = await fetch("/api/dance/upload-character", { method: "POST", body: fd }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + statusBox.hidden = true; + errorBox.hidden = false; + errorBox.textContent = res.status === 402 ? "积分不足,请先充值" : data.error || "上传失败"; + return; + } + // 付费插队跟模式一/二的普通"排队"视觉上要有区别(T37 验收标准)—— + // 不管此刻是"生成中"还是缓存命中的"ready",都是走 priorityInbox + // 插队通道,不进投票队列. Message set after refreshAuthState(), same + // reasoning as submitGarmentUpload — it re-renders this panel + // (creditBalance changed), which would wipe anything set beforehand. + const message = data.status === "ready" ? "🎉 生成完成,已插队播放!" : "🎉 生成中,完成后将直接插队播放(不用排队投票)"; + await refreshAuthState(); + document.getElementById("characterFileInput").value = ""; + const freshStatusBox = document.getElementById("characterUploadStatus"); + freshStatusBox.hidden = false; + freshStatusBox.classList.add("boosted"); + freshStatusBox.textContent = message; + pollState(); // surface the new priority entry immediately + } catch { + statusBox.hidden = true; + errorBox.hidden = false; + errorBox.textContent = "网络错误,请重试"; + } finally { + btn.disabled = false; + } +} + document.querySelectorAll("#wardrobeTabs button").forEach((b) => b.addEventListener("click", () => { leftTab = b.dataset.tab; diff --git a/pricing.json b/pricing.json index 66e4bc7..4cb6cee 100644 --- a/pricing.json +++ b/pricing.json @@ -25,5 +25,14 @@ "segmentsPerUnit": null, "isActive": true, "updatedAt": 1788356862386 + }, + { + "key": "spend_character_per_segment", + "isActive": true, + "kind": "spend", + "credits": 20, + "priceCents": null, + "segmentsPerUnit": 1, + "updatedAt": 1788359892483 } ] \ No newline at end of file