Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-11-05 23:07:59 +07:00
co-authored by Claude Opus 5.5
commit f65d10e998
377 changed files with 31301 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import { routeFromSegments } from '@/lib/routing';
import { getPageSeo, metadataFromHead, queryString } from '@/lib/seo-server';
import { SiteFrame } from '@/components/site/SiteFrame';
import { empty } from '@/lib/client/render';
import type { PageView } from '@/lib/page-data';
import { assertPageContent } from '@/lib/page-response';
export const dynamic = 'force-dynamic';
type PageProps={params:Promise<{path:string[]}>;searchParams:Promise<Record<string,string|string[]|undefined>>};
const pathname=(path:string[])=>'/'+path.map(encodeURIComponent).join('/')+'/';
export async function generateMetadata({params,searchParams}:PageProps):Promise<Metadata> {
const {path}=await params;
if(!routeFromSegments(path))notFound();
const result=await getPageSeo(pathname(path),queryString(await searchParams));
assertPageContent(pathname(path),result.bootstrap.content);
return metadataFromHead(result.head);
}
export default async function Page({params,searchParams}:PageProps) {
// Next 16.4 getDynamicParam encodes page props; generateMetadata receives decoded params.
// Decode at this boundary so both calls share the same URL, data and SEO cache entry.
const {path:encodedPath} = await params;
const path=encodedPath.map(decodeURIComponent);
const route=routeFromSegments(path);
if (!route) notFound();
const {bootstrap,view,session,shared}=await getPageSeo(pathname(path),queryString(await searchParams));
assertPageContent(pathname(path),bootstrap.content);
const title=bootstrap.content.title||({'apps':'福利应用','home-road':'回家的路',center:'个人中心',favorites:'我的收藏',history:'观看历史',likes:'我的点赞',purchased:'已购剧集',settings:'账号设置',deposit:'金币充值',feedback:'意见反馈'} as Record<string,string>)[route.key]||'内容列表';
const fallback:PageView={panel:'channel',title,html:empty(bootstrap.content.failed?'内容暂不可用,请稍后重试':'暂无内容',true)};
return <SiteFrame home={{}} homeLoaded={false} view={view||fallback} seo={bootstrap} session={session} shared={shared} />;
}
+4
View File
@@ -0,0 +1,4 @@
// Compatibility alias for older clients; current browser requests use /api/app/*.
export { GET, POST } from '../app/[...path]/route';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
+211
View File
@@ -0,0 +1,211 @@
import { NextRequest, NextResponse } from "next/server";
import { randomUUID } from "node:crypto";
import { isIP } from "node:net";
import { allowedRequest, PUBLIC_PATHS, upstream } from "@/lib/api/server";
import { UPSTREAM_HOP_HEADER } from '@/lib/api/config';
import { ApiError, EXPIRED_CODES } from "@/lib/api/protocol";
import { trackingHeaders } from "@/lib/api/tracking-headers";
import type { Envelope, User } from "@/lib/api/types";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const AUTH = "drama_session";
const BEARER = "drama_bearer";
const DEVICE = "drama_device";
const REGISTERED = "drama_registered";
const CHANNEL = "drama_channel";
const CREDENTIALS = new Set(['auth/login','auth/register','auth/resetPassword']);
const text = (value: unknown) => typeof value === 'string' ? value.trim() : '';
const deviceId = (value: unknown) => /^[a-zA-Z0-9_-]{1,100}$/.test(text(value)) ? text(value) : '';
const expired = (code: number) => EXPIRED_CODES.has(code) || code === 401;
/** Prefer HttpOnly session, then client mirror cookie, then Authorization (jha-1048 style). */
const requestToken = (request: NextRequest) =>
request.cookies.get(AUTH)?.value ||
request.cookies.get(BEARER)?.value ||
text(request.headers.get('Authorization'));
function sessionUser(value: unknown, token = '', registered = false): User {
if (!value || typeof value !== 'object' || !Number.isSafeInteger((value as User).userId) || (value as User).userId <= 0) throw new ApiError('登录响应缺少用户信息,请稍后重试',502);
const user = value as User;
const sessionToken = text(user.token) || token;
if (!sessionToken) throw new ApiError('登录响应缺少凭证,请稍后重试',502);
return {...user, token:sessionToken, isRegister:Boolean(user.isRegister) || user.type === 1 || registered};
}
function userIdOf(value: unknown) {
const userId = value && typeof value === 'object' ? (value as User).userId : 0;
return Number.isSafeInteger(userId) && userId > 0 ? userId : 0;
}
async function handler(request: NextRequest, context: {params: Promise<{path: string[]}>}) {
const path = (await context.params).path.join('/');
const method = request.method;
const headers = {'Cache-Control':'no-store'};
if (request.headers.has(UPSTREAM_HOP_HEADER)) return NextResponse.json({code:502,msg:'上游请求返回本站,请检查 Nginx 的 /api/app/ 后端转发配置',data:null},{status:502,headers:{...headers,[UPSTREAM_HOP_HEADER]:'1'}});
if (!allowedRequest(path,method)) return NextResponse.json({code:404,msg:'接口不存在',data:null},{status:404,headers});
const secure = request.nextUrl.protocol === 'https:';
const cookieOptions = {httpOnly:true,sameSite:'lax' as const,secure,path:'/',maxAge:7*86400};
const clearSession = (response: NextResponse) => {
response.cookies.set(AUTH,'',{...cookieOptions,maxAge:0});
response.cookies.set(BEARER,'',{...cookieOptions,httpOnly:false,maxAge:0});
response.cookies.set(REGISTERED,'',{...cookieOptions,maxAge:0});
};
const writeSession = (response: NextResponse, sessionToken: string, isRegistered: boolean) => {
response.cookies.set(AUTH,sessionToken,cookieOptions);
// Client-visible mirror so ensureGuestSession can skip guest like jha-1048 localStorage token.
response.cookies.set(BEARER,sessionToken,{...cookieOptions,httpOnly:false});
response.cookies.set(REGISTERED,isRegistered ? '1' : '0',cookieOptions);
};
let token = PUBLIC_PATHS.has(path) ? '' : requestToken(request);
const incomingToken = token;
const registered = request.cookies.get(REGISTERED)?.value === '1';
let device = deviceId(request.cookies.get(DEVICE)?.value) || randomUUID();
let channel = text(request.cookies.get(CHANNEL)?.value).slice(0,100);
const establishesDevice = ['auth/guest','auth/login','auth/register'].includes(path);
const userAgent = request.headers.get('user-agent') || '';
const forwardedIp = (request.headers.get('x-real-ip') || request.headers.get('x-forwarded-for')?.split(',')[0] || '').trim();
const clientIp = isIP(forwardedIp) ? forwardedIp : undefined;
const tracking=trackingHeaders(request.headers,path,method);
const identity=trackingHeaders(request.headers,'auth/guest','POST');
const setContextCookies = (response: NextResponse) => {
if (establishesDevice) {
response.cookies.set(DEVICE,device,{...cookieOptions,httpOnly:false,maxAge:365*86400});
if (channel) response.cookies.set(CHANNEL,channel,{...cookieOptions,httpOnly:false,maxAge:365*86400});
}
};
const guest = async () => {
const result = await upstream<User>('auth/guest',{request,method:'POST',body:{devID:device,deviceId:device,channel,mac:''},device,userAgent,clientIp,tracking:identity});
if (result.code !== 200) throw new ApiError(result.tip || result.msg || '游客登录失败',result.code);
return sessionUser(result.data);
};
// Normalize HTTP 401 into the same recovery path as an expired business envelope.
const call = async (callPath: string, options: Parameters<typeof upstream>[1]): Promise<Envelope<unknown>> => {
try { return await upstream(callPath,{...options,request,clientIp,tracking:callPath.split('?')[0]===path?tracking:identity}); }
catch (error) {
if (error instanceof ApiError && error.code === 401) return {code:401,msg:error.message,data:null};
throw error;
}
};
try {
let body: unknown;
if (method === 'POST') {
const size = Number(request.headers.get('content-length') || 0);
if (size > 6*1024*1024) throw new ApiError('文件过大,请上传小于 5MB 的图片',413);
body = request.headers.get('content-type')?.includes('multipart/form-data') ? await request.formData() : await request.json().catch(()=>({}));
}
const input = body && typeof body === 'object' && !Array.isArray(body) && !(body instanceof FormData) ? body as Record<string,unknown> : {};
if (establishesDevice) {
device = deviceId(request.cookies.get(DEVICE)?.value) || deviceId(input.devID) || device;
channel ||= text(input.channel).slice(0,100) || text(process.env.API_CHANNEL).slice(0,100);
}
let session: User | undefined;
let result: Envelope<unknown>;
let switching = false;
let previousUserId = 0;
let switchedDevice = '';
let switchAccount = '';
let switchPassword = '';
if (path === 'auth/guest') {
if (token) {
const info = await call('auth/getInfo',{token,device,userAgent});
if (info.code === 200) session = sessionUser(info.data,token,registered);
else if (!expired(info.code) || registered) throw new ApiError(info.tip || info.msg || '登录已过期,请重新登录',info.code);
}
session ||= await guest();
result = {code:200,data:session};
} else {
if (path === 'auth/login' || path === 'auth/register') {
const account = text(input[path === 'auth/login' ? 'account' : 'accountNo']);
const password = typeof input.password === 'string' ? input.password : '';
if (!account || !password) throw new ApiError('请填写账号和密码',400);
// Binding keeps the guest. Switching must not: the upstream login finds
// the current visitor by devID and would return that same account.
switching = path === 'auth/login' && input.switchAccount === true;
if (switching && token) {
const info = await call('auth/getInfo',{token,device,userAgent});
if (info.code === 200) previousUserId = userIdOf(info.data);
}
if (switching) switchedDevice = randomUUID();
switchAccount = account;
switchPassword = password;
body = path === 'auth/login'
? {account,password,devID:switchedDevice || device,channel}
: {accountNo:account,password,email:text(input.email) || account+'@jgdj.local',devID:device,channel,...(text(input.agentInviteCode) ? {agentInviteCode:text(input.agentInviteCode).slice(0,100)} : {})};
if (path === 'auth/login' || registered) token = '';
} else if (path === 'auth/resetPassword') {
const accountNo = text(input.accountNo);
const email = text(input.email);
const newPassword = typeof input.newPassword === 'string' ? input.newPassword : '';
if (!accountNo || !email || !newPassword) throw new ApiError('请填写账号、绑定邮箱和新密码',400);
body = {accountNo,email,newPassword};
token = '';
} else if (path === 'auth/profile') {
const next: Record<string, unknown> = {};
for (const key of ['nickName','avatar','introduction','oldPassword','newPassword'] as const) {
if (typeof input[key] === 'string' && input[key]) next[key] = input[key];
}
if (typeof input.privacyMode === 'boolean') next.privacyMode = input.privacyMode;
if (!Object.keys(next).length) throw new ApiError('请填写要修改的内容',400);
body = next;
} else if (path === 'feedback/add') {
const content = typeof input.content === 'string' ? input.content.trim().slice(0,500) : '';
let type = Number(input.type);
if (type === 5) type = 4;
if (!content) throw new ApiError('请填写问题描述',400);
if (![1,2,3,4].includes(type)) throw new ApiError('请选择问题类型',400);
const screenshots = Array.isArray(input.screenshots) ? input.screenshots.filter((item): item is string => typeof item === 'string' && !!item.trim()).map(item => item.trim()).slice(0,3) : [];
const contact = text(input.contact).slice(0,120);
body = {type,content,...(contact ? {contact} : {}),...(screenshots.length ? {screenshots} : {})};
}
result = await call(path+request.nextUrl.search,{method,body,token,device,userAgent});
if (switching && result.code === 200 && previousUserId && userIdOf(result.data) === previousUserId) {
body = {account:switchAccount,password:switchPassword,channel};
result = await call(path+request.nextUrl.search,{method,body,token:'',device:switchedDevice,userAgent});
}
if (switching && result.code === 200) {
if (previousUserId && userIdOf(result.data) === previousUserId) throw new ApiError('切换失败,请确认账号和密码',400);
device = switchedDevice;
}
// Retry only an explicitly rejected guest request, once; never downgrade registered actions.
if (expired(result.code) && !PUBLIC_PATHS.has(path) && !CREDENTIALS.has(path) && path !== 'auth/profile' && path !== 'feedback/add' && path !== 'upload/image' && !registered) {
session = await guest();
token = session.token!;
result = await call(path+request.nextUrl.search,{method,body,token,device,userAgent});
}
if (result.code === 200 && ['auth/login','auth/register','auth/getInfo'].includes(path)) {
session = sessionUser(result.data,path === 'auth/getInfo' ? token : '',registered || CREDENTIALS.has(path));
result = {...result,data:session};
}
}
const data = result.data && typeof result.data === 'object' && 'token' in result.data ? {...result.data,token:undefined} : result.data;
const isExpired = expired(result.code) && !CREDENTIALS.has(path);
const response = NextResponse.json({...result,data},{status:isExpired ? 401 : 200,headers});
if (isExpired) {
// A content or action failure must not erase a cookie that getInfo can still renew.
if (path === 'auth/getInfo' || path === 'auth/guest') clearSession(response);
if (registered) response.headers.set('X-Auth-State','login-required');
} else if (session?.token) {
if (session.token !== incomingToken) {
writeSession(response, session.token, !!(session.isRegister || session.type === 1 || registered));
} else if (!request.cookies.get(BEARER)?.value) {
// Same HttpOnly session: only backfill the client-visible mirror (jha-1048 cookie),
// so a stale refresh cannot overwrite a newer login cookie.
response.cookies.set(BEARER, session.token, {...cookieOptions, httpOnly:false});
}
}
setContextCookies(response);
return response;
} catch (error) {
const code = error instanceof ApiError ? error.code : 502;
const isExpired = expired(code) && !CREDENTIALS.has(path);
const response = NextResponse.json({code,msg:error instanceof Error ? error.message : '请求失败',data:null},{status:isExpired ? 401 : code >= 400 && code <= 599 ? code : 502,headers});
if (isExpired) {
if (path === 'auth/getInfo' || path === 'auth/guest') clearSession(response);
if (registered) response.headers.set('X-Auth-State','login-required');
}
setContextCookies(response);
return response;
}
}
export const GET = handler;
export const POST = handler;
+5
View File
@@ -0,0 +1,5 @@
import { proxyMedia } from '@/lib/api/media';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export function GET(request: Request) {return proxyMedia(request,'/enkey',true);}
+12
View File
@@ -0,0 +1,12 @@
'use client';
export default function PageError({retry}:{retry:()=>void}) {
return <main className="route-not-found">
<title>页面暂不可用 - 禁果短剧</title>
<meta name="robots" content="noindex,follow" />
<h1>页面暂不可用</h1>
<p>内容服务暂时异常,请稍后重试</p>
<button type="button" className="primary" onClick={retry}>重新加载</button>
<a href="/">返回首页</a>
</main>;
}
+2789
View File
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
import { proxyMedia } from '@/lib/api/media';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function GET(request: Request, context: {params: Promise<{path: string[]}>}) {
const {path} = await context.params;
if (path.some(part => !part || part === '..' || part === '.' || /[\\/?#\u0000-\u001f]/.test(part))) return new Response(null,{status:400});
return proxyMedia(request,'/h5/m3u8/'+path.map(encodeURIComponent).join('/'));
}
+6
View File
@@ -0,0 +1,6 @@
export const dynamic='force-dynamic';
export function GET() {
return Response.json({status:'ok',environment:process.env.APP_ENV||'production',application:'jha-1050-jgdj-web'},
{headers:{'Cache-Control':'no-store','X-Robots-Tag':'noindex, nofollow'}});
}
+32
View File
@@ -0,0 +1,32 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_42558_134113)">
<path d="M-0.0055542 19.984C-0.0055542 8.93831 8.94875 -0.0159912 19.9944 -0.0159912V-0.0159912C31.0401 -0.0159912 39.9944 8.93831 39.9944 19.984V19.984C39.9944 31.0297 31.0401 39.984 19.9944 39.984V39.984C8.94875 39.984 -0.0055542 31.0297 -0.0055542 19.984V19.984Z" fill="url(#paint0_linear_42558_134113)"/>
<path d="M34.9795 20.2336C34.9651 20.0651 34.9542 19.8931 34.9421 19.7241L34.9358 19.682C34.9231 19.3903 34.8054 18.875 34.7423 18.5923L34.7292 18.5587C34.4771 17.3099 33.6857 16.01 32.8351 15.082C32.3404 14.5425 31.7519 14.0335 31.1963 13.5545C28.6032 11.2953 25.6656 9.45862 22.4934 8.11303C20.9669 7.48079 19.8656 7.11202 18.2159 7.00627C17.9724 7.00179 17.7289 7 17.4849 7C16.8459 7.0242 16.1103 7.1053 15.4776 7.24913C13.6755 7.65913 11.9186 8.63281 10.6423 9.9645C10.0998 10.5309 9.58342 11.199 9.1816 11.8684C7.52878 14.621 6.8857 18.1034 7.01603 21.279C7.1662 24.9353 8.31483 28.9425 11.0969 31.4867C12.9806 33.1943 15.4668 34.0945 18.0148 33.9919C19.8372 33.9149 21.2992 33.3825 22.9624 32.6719C26.0962 31.3379 28.9825 29.4919 31.502 27.2102C32.5704 26.2366 33.5341 25.3189 34.1786 23.9961L34.3454 23.6377C34.7653 22.6107 34.8731 22.0161 34.9772 20.9107C34.9849 20.8892 34.9849 20.8829 34.9948 20.8417C34.9975 20.7014 35.0102 20.3568 34.98 20.2336H34.9795ZM27.4253 21.1329C27.1637 21.568 25.6385 22.8831 25.165 23.2062C24.9445 23.3984 24.6067 23.6466 24.3654 23.8169C22.9061 24.843 21.3331 25.6993 19.6775 26.3692C19.2631 26.539 18.6475 26.7823 18.2065 26.8697C17.8764 26.936 17.5368 26.9423 17.2044 26.8885C15.9268 26.6689 15.2612 25.7163 14.8133 24.6006C14.0616 22.7276 13.9349 20.4922 14.2853 18.5184C14.3926 17.7759 14.6889 16.9174 15.0118 16.2376C15.1935 15.9168 15.3505 15.6368 15.589 15.3513C16.2461 14.5681 17.2946 14.2688 18.2872 14.4543C18.7224 14.5354 19.1661 14.7294 19.5765 14.9006C20.5173 15.2881 21.4318 15.7353 22.3148 16.2403C22.4745 16.3304 22.6594 16.4357 22.8104 16.5401C24.1485 17.3404 25.3882 18.3024 26.5553 19.3307C26.8151 19.5597 27.1105 19.7819 27.3242 20.0548C27.4117 20.1664 27.4776 20.2869 27.5127 20.4245C27.5736 20.662 27.5533 20.9192 27.4257 21.1316L27.4253 21.1329Z" fill="white"/>
<g filter="url(#filter0_d_42558_134113)">
<path d="M20.5 28.7835C25.0621 28.7835 28.7604 25.0852 28.7604 20.5232C28.7604 15.9611 25.0621 12.2628 20.5 12.2628C15.938 12.2628 12.2397 15.9611 12.2397 20.5232C12.2397 25.0852 15.938 28.7835 20.5 28.7835Z" fill="#0F0F0F"/>
<path d="M16.6111 24.3571V17.1369L17.0821 17.7465L15.2763 18.6624L14.9065 17.212L17.3334 15.9523H18.2493V24.3542H16.6082L16.6111 24.3571ZM15.126 24.9783V23.4527H19.7777V24.9783H15.126Z" fill="white"/>
<path d="M23.5916 25.016C23.0224 25.016 22.5052 24.9004 22.0343 24.6664C21.5633 24.4323 21.1877 24.1203 20.9103 23.7245C20.6301 23.3315 20.4914 22.8924 20.4914 22.4099C20.4914 22.1209 20.5318 21.8551 20.6127 21.6095C20.6936 21.364 20.8121 21.1444 20.9681 20.9479C21.1241 20.7543 21.3148 20.5781 21.5402 20.4192C21.7656 20.2632 22.0198 20.1245 22.3087 20.006L22.2076 20.3614C21.988 20.2689 21.7858 20.1591 21.6038 20.032C21.4217 19.9049 21.2657 19.7575 21.1328 19.5929C20.9999 19.4282 20.9017 19.249 20.8352 19.0583C20.7688 18.8677 20.7341 18.6654 20.7341 18.4545C20.7341 17.998 20.8641 17.5848 21.1212 17.2208C21.3784 16.8567 21.7251 16.5649 22.1585 16.3511C22.5919 16.1344 23.0686 16.0275 23.5944 16.0275C24.1203 16.0275 24.6115 16.1344 25.0448 16.3511C25.4782 16.5678 25.8221 16.8567 26.0821 17.2208C26.3392 17.5848 26.4692 17.9951 26.4692 18.4545C26.4692 18.6654 26.4346 18.8677 26.3681 19.0583C26.3017 19.249 26.2063 19.4224 26.0821 19.5784C25.9578 19.7344 25.8047 19.8789 25.6169 20.0118C25.4291 20.1447 25.2153 20.2603 24.9697 20.3614L24.9062 20.0551C25.1778 20.1476 25.4233 20.2718 25.6429 20.4249C25.8625 20.5781 26.0532 20.7543 26.215 20.9595C26.3768 21.1617 26.501 21.3871 26.5906 21.6327C26.6802 21.8782 26.7235 22.1383 26.7235 22.407C26.7235 22.8981 26.5848 23.3402 26.3046 23.736C26.0243 24.129 25.6487 24.441 25.1806 24.6693C24.7126 24.8975 24.1839 25.0131 23.5973 25.0131L23.5916 25.016ZM23.5916 23.5945C23.8805 23.5945 24.1376 23.5396 24.3659 23.4298C24.5941 23.32 24.7733 23.1726 24.9062 22.9848C25.0362 22.7999 25.1026 22.5832 25.1026 22.3376C25.1026 22.0747 25.0362 21.8436 24.9062 21.6442C24.7733 21.4449 24.5941 21.2888 24.3659 21.1733C24.1376 21.0577 23.8776 21.0028 23.5916 21.0028C23.3055 21.0028 23.0599 21.0606 22.8346 21.1733C22.6092 21.2888 22.4301 21.4449 22.2943 21.6442C22.1585 21.8436 22.092 22.0747 22.092 22.3376C22.092 22.5746 22.1585 22.7884 22.2943 22.9733C22.4301 23.1611 22.6092 23.3113 22.8346 23.424C23.0599 23.5396 23.3113 23.5945 23.5916 23.5945ZM23.5916 19.6015C23.8458 19.6015 24.0712 19.5553 24.2705 19.4628C24.4699 19.3704 24.623 19.2433 24.7357 19.0815C24.8455 18.9197 24.9004 18.7347 24.9004 18.5209C24.9004 18.3071 24.8455 18.1078 24.7357 17.9431C24.6259 17.7784 24.4699 17.6455 24.2705 17.5502C24.0712 17.4519 23.8458 17.4028 23.5916 17.4028C23.3373 17.4028 23.1235 17.4519 22.9241 17.5502C22.7248 17.6484 22.5688 17.7784 22.459 17.9431C22.3492 18.1078 22.2943 18.3014 22.2943 18.5209C22.2943 18.7405 22.3492 18.9197 22.459 19.0815C22.5688 19.2433 22.7248 19.3704 22.9241 19.4628C23.1235 19.5553 23.346 19.6015 23.5916 19.6015Z" fill="white"/>
<path d="M20.5 11.5C15.5363 11.5 11.5 15.5363 11.5 20.5C11.5 25.4637 15.5363 29.5 20.5 29.5C25.4637 29.5 29.5 25.4637 29.5 20.5C29.5 15.5363 25.4637 11.5 20.5 11.5ZM28.0583 20.5C28.0583 22.0284 27.5989 23.4528 26.8159 24.6432L16.7266 13.9588C17.839 13.3144 19.1276 12.9417 20.5029 12.9417C24.6692 12.9417 28.0583 16.3308 28.0583 20.4971V20.5ZM20.5 28.0554C16.3337 28.0554 12.9446 24.6663 12.9446 20.5C12.9446 18.2319 13.9501 16.195 15.5392 14.8082L25.9 25.7787C24.5276 27.1828 22.6149 28.0554 20.5029 28.0554H20.5Z" fill="#DB0015"/>
</g>
</g>
<defs>
<filter id="filter0_d_42558_134113" x="10.4177" y="10.4177" width="20.1647" height="20.1647" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.541168"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_42558_134113"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_42558_134113" result="shape"/>
</filter>
<linearGradient id="paint0_linear_42558_134113" x1="30.5469" y1="9.57031" x2="5.54686" y2="33.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#89FFEB"/>
<stop offset="0.5" stop-color="#FFA53E"/>
<stop offset="1" stop-color="#FD6119"/>
</linearGradient>
<clipPath id="clip0_42558_134113">
<rect width="40" height="40" rx="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

+33
View File
@@ -0,0 +1,33 @@
import type { Metadata, Viewport } from "next";
import Script from "next/script";
import "./globals.css";
const GTM_ID = "GTM-TXLP67SX";
export const metadata: Metadata = {
title: "禁果短剧 · 精品原创 · 日更爽看",
applicationName: "禁果短剧",
icons: {icon:[{url:'/favicon.png',sizes:'48x48',type:'image/png'}],apple:[{url:'/apple-touch-icon.png',sizes:'180x180',type:'image/png'}]},
verification: {google: "QJFFfKYHJsNfa-lGTdGjsCJc4rytPw-4wsTty-BM6iU"},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
themeColor: "#131414",
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body>
<Script id="gtm" strategy="afterInteractive">{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${GTM_ID}');window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());`}</Script>
<noscript>
<iframe src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`} height={0} width={0} style={{display:"none",visibility:"hidden"}} title="Google Tag Manager" />
</noscript>
{children}
</body>
</html>
);
}
+13
View File
@@ -0,0 +1,13 @@
import type { Metadata } from 'next';
import { NavigationPage } from '@/components/site/NavigationPage';
import { getPageSeo, getSeoConfig, metadataFromHead } from '@/lib/seo-server';
import { downloadUrl } from '@/lib/app-download';
export const dynamic = 'force-dynamic';
export async function generateMetadata():Promise<Metadata>{return metadataFromHead((await getPageSeo('/navigation/')).head);}
export default async function Page() {
const {config,error}=await getSeoConfig();
if(error)throw new Error('入口配置暂不可用,请稍后重试');
return <NavigationPage config={config} download={downloadUrl(config)} error={error} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { proxyMedia } from '@/lib/api/media';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export function GET(request: Request) {return proxyMedia(request,'/newenkey',true);}
+13
View File
@@ -0,0 +1,13 @@
import type { Metadata } from 'next';
export const metadata:Metadata={title:'页面不存在 - 禁果短剧',robots:{index:false,follow:true},alternates:{canonical:null}};
export default function NotFound() {
return (
<main className="route-not-found">
<h1>404</h1>
<p>页面不存在或地址有误</p>
<a className="primary" href="/">返回首页</a>
</main>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { SiteFrame } from "@/components/site/SiteFrame";
import { getHome } from "@/lib/api/server";
import { getPageSeo, metadataFromHead, queryString } from '@/lib/seo-server';
import { assertPageContent } from '@/lib/page-response';
export const dynamic = "force-dynamic";
type PageProps={searchParams:Promise<Record<string,string|string[]|undefined>>};
export async function generateMetadata({searchParams}:PageProps){
const result=await getPageSeo('/',queryString(await searchParams));
assertPageContent('/',result.bootstrap.content);
return metadataFromHead(result.head);
}
export default async function Page({searchParams}:PageProps) {
const search=queryString(await searchParams);
const [{data:home,error},{bootstrap,session,shared}] = await Promise.all([getHome(),getPageSeo('/',search)]);
assertPageContent('/',bootstrap.content);
return <SiteFrame home={home} error={error} seo={bootstrap} session={session} shared={shared} />;
}
+16
View File
@@ -0,0 +1,16 @@
import type { MetadataRoute } from 'next';
import { getSeoOrigin } from '@/lib/seo-server';
export const dynamic = 'force-dynamic';
/** Keep HTML crawlable so crawlers can read its canonical and noindex directives. */
export default async function robots(): Promise<MetadataRoute.Robots> {
const environment=process.env.APP_ENV||(process.env.NODE_ENV==='development'?'development':'production');
const origin=environment==='production'?await getSeoOrigin():'';
return {
rules:environment==='production'
?{userAgent:'*',allow:'/',disallow:['/api/','/healthz']}
:{userAgent:'*',disallow:'/'},
...(origin?{sitemap:new URL('/sitemap.xml',origin).href}:{}),
};
}