admin-dressgen.html 7.65 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>新建人衣图任务</title>
<link rel="stylesheet" href="style.css" />
<style>
  .admin-wrap { padding: 18px 24px; max-width: 1400px; }
  .upload-section-label { font-weight: 600; margin: 14px 0 6px; font-size: 13px; }
  .upload-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 800px; }
  .upload-grid.garment { grid-template-columns: 1fr; max-width: 260px; }
  .upload-slot { border: 1px dashed var(--border); border-radius: 6px; padding: 8px; text-align: center; }
  .upload-slot label { display: block; font-weight: 600; margin-bottom: 6px; font-size: 12px; }
  .upload-slot img { width: 100%; height: 140px; object-fit: cover; border-radius: 4px; margin-top: 6px; display: none; }
  .hint { color: var(--muted); font-size: 12px; }
  .prompt-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-top: 8px; }
  .prompt-grid .field label { font-weight: 600; font-size: 12px; }
  .prompt-grid textarea { min-height: 130px; font-size: 12px; }
</style>
</head>
<body>
<header class="topbar">
  <h1>新建人衣图任务</h1>
  <div class="spacer"></div>
  <a href="admin-dressgenlist.html">人衣图任务列表</a>
  <a href="admin.html">视频库</a>
  <a href="admin-characters.html">人物库</a>
  <a href="admin-garments.html">衣服库</a>
  <a href="admin-batch-gen.html">批量生成</a>
  <a href="admin-dance-refs.html">参考跳舞视频库</a>
  <a href="admin-dance-library.html">跳舞直播间片库</a>
  <a href="admin-dance-qc.html">质检</a>
  <a href="live.html">← 返回直播频道</a>
</header>

<main class="admin-wrap">
  <div class="card">
    <div class="field"><label>人物定妆照(有几张传几张,至少 1 张)+ 新衣服 → 依次生成穿新衣服的全身正/侧/背 3 张图</label></div>

    <div class="upload-section-label">人物 · 全身(可选)</div>
    <div class="upload-grid">
      <div class="upload-slot"><label>全身正</label><input type="file" data-slot="bodyFront" accept="image/*" /><img data-preview="bodyFront" /></div>
      <div class="upload-slot"><label>全身侧</label><input type="file" data-slot="bodySide" accept="image/*" /><img data-preview="bodySide" /></div>
      <div class="upload-slot"><label>全身背</label><input type="file" data-slot="bodyBack" accept="image/*" /><img data-preview="bodyBack" /></div>
    </div>

    <div class="upload-section-label">人物 · 脸部(可选)</div>
    <div class="upload-grid">
      <div class="upload-slot"><label>脸正</label><input type="file" data-slot="faceFront" accept="image/*" /><img data-preview="faceFront" /></div>
      <div class="upload-slot"><label>脸侧</label><input type="file" data-slot="faceSide" accept="image/*" /><img data-preview="faceSide" /></div>
      <div class="upload-slot"><label>脸背</label><input type="file" data-slot="faceBack" accept="image/*" /><img data-preview="faceBack" /></div>
    </div>
    <div class="hint">全身/脸部 6 张不用都传,传几张算几张,至少上传 1 张人物参考图。</div>

    <div class="upload-section-label">新衣服(必传)</div>
    <div class="upload-grid garment">
      <div class="upload-slot"><label>衣服图</label><input type="file" data-slot="garmentImage" accept="image/*" /><img data-preview="garmentImage" /></div>
    </div>

    <div class="upload-section-label">生成 prompt(每个视角各一条,已填默认值,改不改都能直接提交)</div>
    <div class="prompt-grid">
      <div class="field"><label>正面 prompt</label><textarea id="promptFront"></textarea></div>
      <div class="field"><label>侧面 prompt</label><textarea id="promptSide"></textarea></div>
      <div class="field"><label>背面 prompt</label><textarea id="promptBack"></textarea></div>
    </div>

    <div class="field" style="margin-top:12px;max-width:320px;">
      <label>生成供应商</label>
      <select id="providerSelect">
        <option value="image2">GPT Image 2(万界方舟,默认,60-180秒/张;失败自动改用 Seedream v4)</option>
        <option value="wavespeed">Seedream v4(wavespeed.ai,约30秒/张,更便宜)</option>
      </select>
    </div>

    <button id="submitBtn" style="margin-top:12px;">生成人衣图(提交后台任务)</button>
    <span id="submitHint" class="hint"></span>
    <div id="submitError" class="error-box" hidden></div>
  </div>
</main>

<script>
const _fetch = window.fetch;
window.fetch = async (...args) => {
  const res = await _fetch(...args);
  if (res.status === 401 && String(args[0]).startsWith("/api/admin/") && !String(args[0]).includes("/login")) {
    location.href = "/admin-login.html?next=" + encodeURIComponent(location.pathname);
  }
  return res;
};

const PERSON_SLOTS = ["bodyFront", "bodySide", "bodyBack", "faceFront", "faceSide", "faceBack"];

document.querySelectorAll("[data-slot]").forEach((input) => {
  input.addEventListener("change", () => {
    const preview = document.querySelector(`[data-preview="${input.dataset.slot}"]`);
    const file = input.files[0];
    if (!file) {
      preview.style.display = "none";
      return;
    }
    preview.src = URL.createObjectURL(file);
    preview.style.display = "block";
  });
});

async function loadDefaultPrompts() {
  const res = await fetch("/api/admin/dressgen/prompts");
  const prompts = await res.json();
  document.getElementById("promptFront").value = prompts.front;
  document.getElementById("promptSide").value = prompts.side;
  document.getElementById("promptBack").value = prompts.back;
}

async function uploadImage(file) {
  const fd = new FormData();
  fd.append("file", file);
  const res = await fetch("/api/admin/upload", { method: "POST", body: fd });
  if (!res.ok) throw new Error((await res.json()).error || "上传失败");
  return (await res.json()).url;
}

document.getElementById("submitBtn").addEventListener("click", async () => {
  const errorBox = document.getElementById("submitError");
  const submitBtn = document.getElementById("submitBtn");
  const submitHint = document.getElementById("submitHint");
  errorBox.hidden = true;

  const garmentFile = document.querySelector('[data-slot="garmentImage"]').files[0];
  if (!garmentFile) {
    errorBox.hidden = false;
    errorBox.textContent = "衣服图必传";
    return;
  }
  const personFiles = PERSON_SLOTS.filter((slot) => document.querySelector(`[data-slot="${slot}"]`).files[0]);
  if (personFiles.length === 0) {
    errorBox.hidden = false;
    errorBox.textContent = "至少上传 1 张人物参考图(全身/脸部任意角度)";
    return;
  }

  submitBtn.disabled = true;
  try {
    submitHint.textContent = "上传中...";
    const urls = { garmentImage: await uploadImage(garmentFile) };
    for (const slot of personFiles) urls[slot] = await uploadImage(document.querySelector(`[data-slot="${slot}"]`).files[0]);

    submitHint.textContent = "已提交后台任务...";
    const res = await fetch("/api/admin/dressgen", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        ...urls,
        provider: document.getElementById("providerSelect").value,
        prompts: {
          front: document.getElementById("promptFront").value,
          side: document.getElementById("promptSide").value,
          back: document.getElementById("promptBack").value,
        },
      }),
    });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error || "提交失败");
    location.href = `admin-dressgendetail.html?id=${data.id}`;
  } catch (e) {
    errorBox.hidden = false;
    errorBox.textContent = e.message;
  } finally {
    submitBtn.disabled = false;
  }
});

loadDefaultPrompts();
</script>
</body>
</html>