[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:
谢宇宁
2026-10-16 11:33:31 +07:00
co-authored by Claude Opus 5.5
commit d2ddb1c321
423 changed files with 52152 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { MobileTabbar } from '../src/components/layout/MobileTabbar';
const render = (requestPath?: string, favorites?: unknown[]) => renderToStaticMarkup(createElement(MobileTabbar, { data: { requestPath, favorites } }));
test('mobile navigation follows the Figma page whitelist and current route', () => {
for (const [path, current] of [['/', '/'], ["/dramas/", "/dramas/"], ["/dramas/all/page/2/", "/dramas/"], ['/series/', '/series/'], ['/series/anime/12/2/', '/series/']] as const) {
const html = render(path, []);
assert.equal((html.match(/data-navigation-key=/g) || []).length, 3, path);
assert.doesNotMatch(html, /mobile_mine|我的|\/account\//, path);
assert.equal((html.match(/aria-current="page"/g) || []).length, 1, path);
assert.ok(html.includes(`href="${current}" aria-current="page"`), path);
assert.match(html, /mobile-tabbar\/\w+-active\.svg/);
}
for (const path of ["/account/", "/account/favorites/", "/account/login/", "/account/register/", "/discover/search/", "/drama/1/", "/series/watch/1/", "/account/recharge/", "/account/history/", "/account/purchases/", "/account/settings/", '/about/', '/help/', '/terms/', '/privacy/', '/feedback/', "/collections/", "/rankings/", '/404/']) {
assert.doesNotMatch(render(path), /<nav/, path);
}
assert.doesNotMatch(render(), /<nav/);
});
test('mobile navigation uses the unmodified active and muted Figma icon exports', async () => {
const manifest = JSON.parse(await readFile(new URL('../scripts/figma-icon-manifest.json', import.meta.url), 'utf8')) as { assets: { file: string; sha256: string }[] };
const assets = manifest.assets.filter(asset => asset.file.includes('/mobile-tabbar/'));
assert.equal(assets.length, 8);
for (const asset of assets) {
const bytes = await readFile(new URL('../' + asset.file, import.meta.url));
assert.equal(createHash('sha256').update(bytes).digest('hex'), asset.sha256, asset.file);
assert.ok(bytes.toString().includes(asset.file.includes('-active') ? '#FF0F53' : '#7E818B'), asset.file);
}
});