[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
+87
View File
@@ -0,0 +1,87 @@
import "xgplayer/dist/index.min.css";
import "@/styles/components/video-player.css";
import "@/styles/foundations/base.css";
import "@/styles/components/site-header.css";
import "@/styles/components/site-footer.css";
import "@/styles/components/age-gate.css";
import "@/styles/components/advertisement-dialog.css";
import "@/styles/components/app-download-dialog.css";
import "@/styles/components/installation-guide.css";
import "@/styles/components/toast.css";
import "@/styles/components/mobile-tabbar.css";
import "@/styles/components/pagination.css";
import "@/styles/components/promotional-banner.css";
import "@/styles/components/promotional-grid.css";
import "@/styles/components/advertising-compat.css";
import "@/styles/components/playback-links.css";
import "@/styles/pages/home.css";
import "@/styles/pages/drama-list.css";
import "@/styles/pages/recently-added.css";
import "@/styles/pages/popular-recommendations.css";
import "@/styles/pages/tag-detail.css";
import "@/styles/pages/expanded-list.css";
import "@/styles/pages/series-list.css";
import "@/styles/pages/rankings.css";
import "@/styles/pages/topics.css";
import "@/styles/pages/topic-detail.css";
import "@/styles/pages/search-results.css";
import "@/styles/pages/about.css";
import "@/styles/pages/about-responsive.css";
import "@/styles/pages/help.css";
import "@/styles/pages/feedback.css";
import "@/styles/pages/legal.css";
import "@/styles/pages/not-found.css";
import "@/styles/pages/mobile-search.css";
import "@/styles/pages/site-navigation.css";
import "@/styles/pages/home-road.css";
import "@/styles/pages/playback.css";
import "@/styles/overrides/pwa-safe-area.css";
import type { ReactNode } from "react";
import { PwaProvider } from "@/components/pwa/PwaProvider";
import { pwaViewport } from "@/lib/pwa/config";
import { getRequestPage } from "@/server/react/request-page";
import { toNextMetadata } from "@/server/seo";
import { presentations } from "@/server/react/presentation";
export async function generateMetadata() {
return toNextMetadata((await getRequestPage()).data);
}
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const viewport = pwaViewport;
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
const { view, data } = await getRequestPage(),
presentation = presentations[view] || presentations["404"];
const styleTokens = presentation.styleTokens.join(" ");
return (
<html lang="zh-CN" data-hk-page-styles={styleTokens}>
<head>
{["play", "seriesPlay"].includes(view) && (
<>
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="x5-orientation" content="portrait" />
<meta name="x5-page-mode" content="app" />
<meta name="browsermode" content="application" />
<meta name="imagemode" content="force" />
</>
)}
{data.jsonLD && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: String(data.jsonLD)
.replace(/</g, "\\u003c")
.replace(/>/g, "\\u003e"),
}}
/>
)}
</head>
<body className={presentation.bodyClass}><PwaProvider />{children}</body>
</html>
);
}