diff --git a/admin.html b/admin.html
index 578f4d3..ae298e3 100644
--- a/admin.html
+++ b/admin.html
@@ -70,6 +70,7 @@
名称(英) |
Prompt |
视频地址 |
+ 溯源(人物×衣服×参考视频) |
时长 |
点赞 |
来源 |
@@ -96,6 +97,9 @@ window.fetch = async (...args) => {
};
let library = [];
+let characters = [];
+let garments = [];
+let danceRefs = [];
let editingId = null;
let sortKey = "createdAt";
let sortDir = -1;
@@ -107,11 +111,34 @@ function escapeHtml(s) {
}
async function loadLibrary() {
- const res = await fetch("/api/library?sort=newest");
- library = await res.json();
+ const [libRes, charRes, garmentRes, danceRefRes] = await Promise.all([
+ fetch("/api/library?sort=newest"),
+ fetch("/api/admin/characters"),
+ fetch("/api/admin/garments"),
+ fetch("/api/admin/dance-refs"),
+ ]);
+ library = await libRes.json();
+ characters = await charRes.json();
+ garments = await garmentRes.json();
+ danceRefs = await danceRefRes.json();
render();
}
+function nameOf(list, id) {
+ if (!id) return null;
+ const item = list.find((x) => x.id === id);
+ return item ? item.name : id;
+}
+
+// T10: 一步法下没有 composedImageId/合成图,溯源只有这三个字段(M.7)。
+function traceability(c) {
+ const characterName = nameOf(characters, c.characterId);
+ const garmentName = nameOf(garments, c.garmentId);
+ const danceRefName = nameOf(danceRefs, c.refDanceVideoId);
+ if (!characterName && !garmentName && !danceRefName) return "-";
+ return [characterName, garmentName, danceRefName].map((n) => n || "?").join(" × ");
+}
+
function filteredSorted() {
const q = document.getElementById("searchInput").value.trim().toLowerCase();
const source = document.getElementById("sourceFilter").value;
@@ -148,6 +175,7 @@ function render() {
|
|
|
+ ${escapeHtml(traceability(c))} |
${c.duration ?? "-"}s |
${c.likes || 0} |
${c.source} |
@@ -165,6 +193,7 @@ function render() {
${escapeHtml(c.promptEn || "")} |
${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""} |
${videoHref(c)} |
+ ${escapeHtml(traceability(c))} |
${c.duration ?? "-"}s |
${c.likes || 0} |
${c.source} |
diff --git a/dressup-generate.test.js b/dressup-generate.test.js
index bb821a3..26a2cb1 100644
--- a/dressup-generate.test.js
+++ b/dressup-generate.test.js
@@ -188,6 +188,19 @@ test("generate: full happy path lands a clip with character/garment/dance-ref tr
assert.equal(clip.refDanceVideoId, danceRef.id);
assert.equal(clip.channel, "dance");
+ // T10's admin.html traceability column joins clip.characterId/garmentId/
+ // refDanceVideoId back against the three admin lists to show "谁的脸 +
+ // 谁的衣服 + 哪条参考视频" — confirm that join actually resolves to the
+ // real names, not just matching ids.
+ const [charList, garmentList, danceRefList] = await Promise.all([
+ fetch(`${BASE}/api/admin/characters`, authed()).then((r) => r.json()),
+ fetch(`${BASE}/api/admin/garments`, authed()).then((r) => r.json()),
+ fetch(`${BASE}/api/admin/dance-refs`, authed()).then((r) => r.json()),
+ ]);
+ assert.equal(charList.find((x) => x.id === clip.characterId).name, "小美");
+ assert.equal(garmentList.find((x) => x.id === clip.garmentId).name, "红旗袍");
+ assert.equal(danceRefList.find((x) => x.id === clip.refDanceVideoId).name, "拉丁舞");
+
// Re-submitting the same character x garment combo should hit the M.7
// dedupe cache instead of generating again.
const repeatRes = await fetch(