chore: 初始化仓库

中华文明全图鉴——文物全图系统(PC Web 地图 + NestJS API + 管理后台)。
含三大 IP(文物南迁北归 / 国宝海外回归 / 博物馆手艺人)、AI 文物对话、
文物地图与详情、以及 demo-video-kit 演示视频生成工具。
This commit is contained in:
selfrelease
2026-06-13 20:55:44 +08:00
commit 2d847e154f
161 changed files with 22629 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
// WenwuMap 演示视频配置(使用 tools/demo-video-kit
// 运行:node tools/demo-video-kit/src/cli.mjs e2e/demo.config.mjs --out e2e/videos-kit
import { NARRATION } from "./narration.mjs";
const N = (k) => NARRATION[k] || k;
export default {
baseUrl: "http://localhost:3000/map",
viewport: { width: 1440, height: 900 },
brand: "中华文明全图鉴 · 功能演示",
outDir: "e2e/videos-kit",
voice: { name: "Cherry", model: "qwen-tts", apiKeyEnv: "AI_API_KEY", fallbackVoice: "Tingting" },
intro: { narration: N("__intro"), sub: "中华文明全图鉴 · Cultural Heritage Atlas" },
outro: { narration: N("__outro") },
steps: [
{
label: "搜索",
narration: N("搜索“青铜”"),
run: async ({ page, pause }) => {
const input = page.getByPlaceholder("搜索文物、机构、城市…");
await input.click();
await input.pressSequentially("青铜", { delay: 120 });
await pause(1500);
await input.fill("");
await pause(500);
},
},
{
label: "城市分布翻页",
narration: N("城市分布翻页"),
run: async ({ page, pause }) => {
const next = page.locator("aside").first().getByRole("button", { name: "下一页" });
for (let i = 0; i < 2; i++) {
await next.click({ timeout: 4000 });
await pause(900);
}
},
},
{
label: "门类筛选:青铜器",
narration: N("门类筛选:青铜器"),
run: async ({ page, pause }) => {
const b = page.getByRole("button", { name: "青铜器" }).first();
await b.click({ timeout: 5000 });
await pause(1500);
await b.click({ timeout: 5000 });
await pause(600);
},
},
{
label: "年代筛选:唐代",
narration: N("年代筛选:唐代"),
run: async ({ page, pause }) => {
const b = page.getByRole("button", { name: "唐代", exact: true }).first();
await b.click({ timeout: 5000 });
await pause(1500);
await b.click({ timeout: 5000 });
await pause(600);
},
},
{
label: "左栏收起/展开",
narration: N("左栏收起/展开"),
run: async ({ page, pause }) => {
const t = page.getByTitle(/收起左栏|展开左栏/);
await t.click({ timeout: 5000 });
await pause(1100);
await t.click({ timeout: 5000 });
await pause(800);
},
},
{
label: "右栏收起/展开",
narration: N("右栏收起/展开"),
run: async ({ page, pause }) => {
const t = page.getByTitle(/收起右栏|展开右栏/);
await t.click({ timeout: 5000 });
await pause(1100);
await t.click({ timeout: 5000 });
await pause(800);
},
},
{
label: "点击地图标记 · 查看馆藏",
narration: N("点击地图标记 · 查看馆藏"),
run: async ({ page, pause }) => {
const markers = page.locator("div.cursor-pointer.flex-col");
await markers.first().waitFor({ state: "attached", timeout: 25000 });
await pause(1200);
const aside = page.locator("aside").last();
const total = Math.min(await markers.count(), 8);
for (let i = 0; i < total; i++) {
const box = await markers.nth(i).boundingBox();
if (!box) continue;
await page.mouse.click(box.x + box.width / 2, box.y + box.height - 3);
await pause(1300);
if (
(await aside.getByText("机构藏品").count()) > 0 ||
(await page.getByRole("button", { name: /文物信息/ }).count()) > 0
)
break;
}
},
},
{
label: "进入文物详情",
narration: N("进入文物详情"),
run: async ({ page, pause }) => {
const aside = page.locator("aside").last();
if ((await aside.getByText("机构藏品").count()) > 0) {
await aside.locator("button:has(div.truncate)").first().click({ timeout: 4000 });
await pause(1600);
}
await page.getByRole("button", { name: /文物信息/ }).waitFor({ timeout: 8000 });
},
},
{
label: "展开同一机构列表",
narration: N("展开同一机构列表"),
run: async ({ page, pause }) => {
await page.getByText(/还有 \d+ 件/).first().click({ timeout: 4000 });
await pause(1500);
},
},
{
label: "文物信息折叠/展开",
narration: N("文物信息折叠/展开"),
run: async ({ page, pause }) => {
const btn = page.getByRole("button", { name: /文物信息/ });
await btn.click({ timeout: 4000 });
await pause(1000);
await btn.click({ timeout: 4000 });
await pause(800);
},
},
{
label: "AI 文物对话 · 流式回答与追问建议",
narration: N("AI 文物对话 · 流式回答与追问建议"),
run: async ({ page, pause }) => {
const aside = page.locator("aside").last();
await page.getByRole("button", { name: "讲解员" }).first().click({ timeout: 4000 });
await pause(800);
await page.getByText("它为什么珍贵?").first().click({ timeout: 4000 });
await page.locator("div.md-chat").first().waitFor({ timeout: 30000 });
await pause(3500);
await aside.getByText("下一步 · 你可以问").waitFor({ timeout: 20000 });
await aside.locator('button:has-text("")').first().click({ timeout: 4000 });
await page.locator("div.md-chat").nth(1).waitFor({ timeout: 30000 });
await pause(3500);
},
},
{
label: "图片放大与无极缩放(马踏飞燕)",
narration: N("图片放大与无极缩放(马踏飞燕)"),
run: async ({ page, pause }) => {
const input = page.getByPlaceholder("搜索文物、机构、城市…");
await input.fill("马踏飞燕");
await page
.waitForFunction(() => document.querySelectorAll("div.cursor-pointer.flex-col").length <= 2, null, {
timeout: 8000,
})
.catch(() => {});
await pause(900);
const markers = page.locator("div.cursor-pointer.flex-col");
await markers.first().waitFor({ state: "attached", timeout: 15000 });
const box = await markers.first().boundingBox();
await page.mouse.click(box.x + box.width / 2, box.y + box.height - 3);
await pause(1700);
const aside = page.locator("aside").last();
if ((await aside.getByText("机构藏品").count()) > 0) {
await aside.locator("button:has(div.truncate)").first().click({ timeout: 4000 });
await pause(1500);
}
const cover = page.locator("img.cursor-zoom-in").first();
await cover.waitFor({ state: "visible", timeout: 6000 });
const cb = await cover.boundingBox();
await page.mouse.click(cb.x + cb.width / 2, cb.y + cb.height / 2);
await pause(1400);
await page.mouse.move(720, 450);
for (let i = 0; i < 6; i++) {
await page.mouse.wheel(0, -320);
await pause(420);
}
await pause(1200);
await page.keyboard.press("Escape");
await pause(900);
},
},
{
label: "文物南迁北归 · 重走万里守护之路",
narration: N("文物南迁北归 · 重走万里守护之路"),
run: async ({ page, pause }) => {
await page.getByPlaceholder("搜索文物、机构、城市…").fill("");
await pause(700);
await page.getByRole("button", { name: "文物南迁北归之路" }).click({ timeout: 6000 });
await pause(2600);
await page.getByRole("button", { name: "播放" }).click({ timeout: 6000 });
await pause(12000);
await page.getByRole("button", { name: "退出路线" }).click({ timeout: 6000 });
await pause(1000);
},
},
{
label: "国宝海外回归 · 选择文物,重走回家之路",
narration: N("国宝海外回归 · 选择文物,重走回家之路"),
run: async ({ page, pause }) => {
await page.getByRole("button", { name: "国宝海外回归" }).click({ timeout: 6000 });
await page.getByText("国宝海外回归 · 选择文物").first().waitFor({ timeout: 6000 });
await pause(1600);
await page.locator('button:has-text("现藏")').first().click({ timeout: 6000 });
await pause(2600);
await page.getByRole("button", { name: "播放" }).click({ timeout: 6000 });
await pause(7000);
await page.getByRole("button", { name: "退出路线" }).click({ timeout: 6000 });
await pause(1000);
},
},
],
};