[Ai] feat: 花魁短剧 Next.js 站点初始提交
- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import {
|
||||
DramaCard,
|
||||
PlayRecommendationCard,
|
||||
SeriesCard,
|
||||
} from "../src/components/catalog/Cards";
|
||||
import { Pagination } from "../src/components/catalog/Pagination";
|
||||
import { PromoGrid } from "../src/components/catalog/Promotions";
|
||||
import {
|
||||
CatalogGrid,
|
||||
ListPage,
|
||||
SeriesPage,
|
||||
} from "../src/features/catalog/BrowsePages";
|
||||
import {
|
||||
allListURL,
|
||||
array,
|
||||
cardTags,
|
||||
moreListURL,
|
||||
moreSeriesListURL,
|
||||
searchResultsURL,
|
||||
seriesListURL,
|
||||
tagPageURL,
|
||||
topicDetailURL,
|
||||
} from "../src/features/catalog/model";
|
||||
|
||||
const item = {
|
||||
id: 7,
|
||||
slug: "都市 剧",
|
||||
playName: "作品名",
|
||||
title: "备用标题",
|
||||
cover: "/static/brand/header.svg",
|
||||
totalEpisode: 8,
|
||||
updateStatus: 1,
|
||||
isFeatured: true,
|
||||
tagIds: [2],
|
||||
tagNames: ["爱情"],
|
||||
tagSlugs: ["romance"],
|
||||
themeIds: [3],
|
||||
themeNames: ["爱情", "都市"],
|
||||
themeSlugs: ["love", "city"],
|
||||
};
|
||||
const ads = [
|
||||
{
|
||||
id: 1,
|
||||
images: "/static/brand/header.svg",
|
||||
link: "https://promo.test/",
|
||||
title: "推荐",
|
||||
},
|
||||
];
|
||||
|
||||
test("React card variants preserve watch URLs, episode gates, badges and linked deduplicated tags", () => {
|
||||
const card = renderToStaticMarkup(<DramaCard item={item} />);
|
||||
assert.match(card, /class="hk-drama-card hk-drama-card--list"/);
|
||||
assert.match(
|
||||
card,
|
||||
/href="\/drama\/都市%20剧\/"|href="\/drama\/%E9%83%BD%E5%B8%82%20%E5%89%A7\/"/,
|
||||
);
|
||||
assert.match(
|
||||
card,
|
||||
/<p class="hk-drama-card__meta hk-drama-card__meta--ep"><span class="hk-drama-card__meta-ep" data-status="serial">连载 · 8 集<\/span><span class="hk-drama-card__meta-tags">/,
|
||||
);
|
||||
assert.doesNotMatch(card, /hk-drama-card__ep"/);
|
||||
assert.match(card, /hk-badge hk-badge--hot/);
|
||||
assert.match(card, /href="\/tags\/romance\/"/);
|
||||
assert.deepEqual(
|
||||
cardTags(item).map((tag) => tag.Name),
|
||||
["爱情", "都市"],
|
||||
);
|
||||
const recommendation = renderToStaticMarkup(
|
||||
<PlayRecommendationCard item={item} />,
|
||||
);
|
||||
assert.match(recommendation, /hk-drama-card hk-drama-card--play/);
|
||||
assert.match(recommendation, /hk-drama-card__meta-ep" data-status="serial">连载 · 8 集</);
|
||||
assert.doesNotMatch(recommendation, /hk-badge--hot/);
|
||||
const video = renderToStaticMarkup(
|
||||
<SeriesCard
|
||||
item={{
|
||||
id: 9,
|
||||
slug: "movie",
|
||||
title: "剧集",
|
||||
mediaType: 2,
|
||||
videoCount: 12,
|
||||
topicNames: ["动画"],
|
||||
coverImg: "/static/brand/header.svg",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
assert.match(video, /href="\/series\/watch\/movie\/"/);
|
||||
assert.match(video, /全 12 集/);
|
||||
assert.match(video, /动画/);
|
||||
});
|
||||
|
||||
test("React list advertising keeps independent desktop and mobile row positions and clone attribution", () => {
|
||||
const items = array(
|
||||
Array.from({ length: 25 }, (_, index) => ({ ...item, id: index + 1 })),
|
||||
);
|
||||
const grid = renderToStaticMarkup(<CatalogGrid items={items} ads={ads} />);
|
||||
assert.equal((grid.match(/class="hk-drama-card hk-drama-card--list"/g) || []).length, 25);
|
||||
assert.equal((grid.match(/class="hk-drama-browse-mid-promo hk-desktop-only"/g) || []).length, 2);
|
||||
assert.equal((grid.match(/class="hk-drama-browse-mid-promo hk-mobile-only"/g) || []).length, 4);
|
||||
const videoGrid = renderToStaticMarkup(
|
||||
<CatalogGrid items={items.slice(0, 16)} ads={ads} series />,
|
||||
);
|
||||
assert.equal(
|
||||
(videoGrid.match(/class="hk-series-list-promo hk-desktop-only"/g) || []).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
(videoGrid.match(/class="hk-series-list-promo hk-mobile-only"/g) || []).length,
|
||||
3,
|
||||
);
|
||||
const promo = renderToStaticMarkup(<PromoGrid items={ads} />);
|
||||
assert.equal((promo.match(/data-track-ad="external"/g) || []).length, 2);
|
||||
assert.equal((promo.match(/data-ad-clone="1"/g) || []).length, 1);
|
||||
assert.match(promo, /rel="sponsored nofollow noopener"/);
|
||||
});
|
||||
|
||||
test("React filter and pagination links preserve the source path-only canonical route contracts", () => {
|
||||
assert.equal(allListURL("", 0, 0, 1, 1), "/dramas/");
|
||||
assert.equal(
|
||||
allListURL("都市", 3, 2, 3, 2),
|
||||
"/dramas/%E9%83%BD%E5%B8%82/2/3/page/2/",
|
||||
);
|
||||
assert.equal(seriesListURL(2, 4, 3, 2), "/series/anime/4/3/page/2/");
|
||||
assert.equal(moreListURL("featured", 1, 0, "", 0, 1), "/curated/dramas/featured/");
|
||||
assert.equal(
|
||||
moreSeriesListURL("", 3, 1, "city", 0, 2),
|
||||
"/curated/series/3/1/city/page/2/",
|
||||
);
|
||||
assert.equal(tagPageURL("romance", 1, 2, 1), "/tags/romance/sort/2/");
|
||||
assert.equal(
|
||||
topicDetailURL("city", 1, 3, 2),
|
||||
"/collection/city/sort/3/page/2/",
|
||||
);
|
||||
assert.equal(searchResultsURL("a/b"), "/search/a%2Fb/");
|
||||
const html = renderToStaticMarkup(
|
||||
<Pagination
|
||||
pagination={{
|
||||
Visible: true,
|
||||
HasPrev: false,
|
||||
HasNext: true,
|
||||
NextURL: "/dramas/all/0/1/page/2/",
|
||||
Items: [
|
||||
{ Label: "1", URL: "/dramas/", Active: true },
|
||||
{ Label: "…", Dots: true },
|
||||
],
|
||||
TotalText: "共 3 页",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
assert.match(html, /hk-pagination__btn--prev hk-is-disabled/);
|
||||
assert.match(html, /aria-current="page"/);
|
||||
assert.match(html, /href="\/dramas\/all\/0\/1\/page\/2\/"/);
|
||||
assert.match(html, /hk-pagination__ellipsis/);
|
||||
assert.match(html, /共 3 页/);
|
||||
const list = renderToStaticMarkup(
|
||||
<ListPage
|
||||
data={{
|
||||
list: [],
|
||||
themeList: [{ id: 3, slug: "city", name: "都市" }],
|
||||
themeId: 3,
|
||||
themeSlug: "city",
|
||||
updateStatus: 2,
|
||||
sortType: 3,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
assert.match(list, /href="\/dramas\/city\/2\/3\/"/);
|
||||
assert.match(list, /暂无短剧/);
|
||||
const series = renderToStaticMarkup(
|
||||
<SeriesPage
|
||||
data={{
|
||||
list: [],
|
||||
topicList: [{ id: 4, name: "爱情" }],
|
||||
topicId: 4,
|
||||
mediaType: 2,
|
||||
sortType: 1,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
assert.match(series, /href="\/series\/anime\/4\/1\/"/);
|
||||
assert.match(series, /暂无剧集/);
|
||||
});
|
||||
Reference in New Issue
Block a user