[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
+33
View File
@@ -0,0 +1,33 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFile, stat } from 'node:fs/promises';
import test from 'node:test';
import { IMAGE_PLACEHOLDER } from '../src/lib/view-helpers';
import { IMAGE_PLACEHOLDER as REACT_IMAGE_PLACEHOLDER } from '../src/lib/media/images';
import { coverImage } from '../src/server/services/model';
test('the shared image fallback preserves the complete Figma brand export', async () => {
const svg = await readFile(new URL('../public/static/brand/placeholder.svg', import.meta.url));
const brand = await readFile(new URL('../public/static/brand/footer.svg', import.meta.url));
const manifest = JSON.parse(await readFile(new URL('../public/static/brand/placeholder.manifest.json', import.meta.url), 'utf8')) as {
width: number; height: number; sha256: string;
composition: { embeddedImage: { sha256: string; unmodified: boolean; width: number; height: number } };
};
const embedded = svg.toString().match(/href="data:image\/svg\+xml;base64,([^"]+)"/);
assert.ok(embedded);
assert.deepEqual(Buffer.from(embedded[1], 'base64'), brand);
assert.match(svg.toString(), /width="480" height="480" viewBox="0 0 480 480"/);
assert.match(svg.toString(), /<rect width="480" height="480" fill="#26272f"\/>/);
assert.match(svg.toString(), /<image\b[^>]*\bopacity="0\.2"/);
assert.equal(manifest.width, 480);
assert.equal(manifest.height, 480);
assert.equal(manifest.sha256, createHash('sha256').update(svg).digest('hex'));
assert.equal(manifest.composition.embeddedImage.sha256, createHash('sha256').update(brand).digest('hex'));
assert.equal(manifest.composition.embeddedImage.unmodified, true);
assert.equal(manifest.composition.embeddedImage.width, 240);
assert.ok(Math.abs(manifest.composition.embeddedImage.height - 240 * 44 / 124) < 0.000001);
assert.equal(IMAGE_PLACEHOLDER, '/static/brand/placeholder.svg');
assert.equal(coverImage(undefined), IMAGE_PLACEHOLDER);
assert.equal(new URL(REACT_IMAGE_PLACEHOLDER, 'https://site.example').pathname, IMAGE_PLACEHOLDER);
await assert.rejects(stat(new URL('../public/static/images/placeholder.png', import.meta.url)), { code: 'ENOENT' });
});