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
+48
View File
@@ -0,0 +1,48 @@
import type { Ref } from "react";
interface AppDownloadDialogProps {
download?: string;
qr?: string;
status?: string;
dialogRef?: Ref<HTMLDialogElement>;
onClose?: () => void;
onCopy?: () => void;
}
export function AppDownloadDialog({download, qr, status, dialogRef, onClose, onCopy}: AppDownloadDialogProps) {
return (
<dialog ref={dialogRef} id={"app-download-dialog"} className={"app-download-dialog"} aria-labelledby={"app-download-heading"}>
<button type={"button"} className={"close"} id={"app-download-close"} onClick={onClose} aria-label={"关闭下载弹窗"}>
<svg className={"dialog-close-icon"} viewBox={"0 0 24 24"} aria-hidden={"true"} focusable={"false"}>
<path d={"m6 6 12 12M18 6 6 18"}>
</path>
</svg>
</button>
<h2 id={"app-download-heading"}>
{"下载禁果短剧 App"}
</h2>
<p className={"app-download-intro"}>
{"扫码下载,随时继续追剧"}
</p>
<div className={"app-download-code"}>
<img src={qr || undefined} id={"app-platform-qr"} width={"192"} height={"192"} alt={"禁果短剧 App 下载二维码"} />
</div>
<p id={"app-platform-caption"}>
{download === undefined ? "" : download ? "扫码下载安装 禁果短剧 App" : "暂未配置 App 下载地址"}
</p>
<div className={"app-download-link"}>
<input id={"app-platform-link"} value={download || ""} readOnly aria-label={"下载链接"} />
<button type={"button"} id={"app-copy-link"} onClick={onCopy}>
{"复制链接"}
</button>
</div>
<p className={"app-copy-status"} id={"app-copy-status"} role={"status"} aria-live={"polite"}>
{status}
</p>
<p className={"app-download-demo"} hidden={download !== undefined}>
{"下载地址加载中"}
</p>
</dialog>
);
}
+76
View File
@@ -0,0 +1,76 @@
import type { InfoPageDefinition } from '@/lib/info-pages';
import type { PageView } from '@/lib/page-data';
import { parseSitePath } from '@/lib/routing';
export function ChannelPages({info,view}: {info?:InfoPageDefinition;view?:PageView}={}) {
const channel=view?.panel==='channel'?view:undefined;
const route=channel?.bootstrap?parseSitePath(channel.bootstrap.path.split('?')[0]):null;
const topicDetail=route?.group==='page'&&(route.key==='theme'||route.key==='topics'&&!!route.value);
info ||= view?.panel==='info'?{key:view.bootstrap?.info?.key||'',title:view.title,html:view.html}:undefined;
const ChannelTitle=channel?'h1':'div';
const InfoTitle=info?'h1':'div';
return (
<>
<main id={"channel-page"} className={"shell channel-shell"} hidden={!channel} data-channel={channel?.channel} aria-labelledby={"channel-title"}>
<nav className={"page-breadcrumb"} id={"channel-breadcrumb"} aria-label={"面包屑导航"}>
{channel?<><a href="/">首页</a><span>/</span>{topicDetail?<><a href="/topics/">专题</a><span>/</span></>:null}<span aria-current="page">{channel.title}</span></>:null}
</nav>
<header className={"info-heading"}>
<small id={"channel-kicker"}>
</small>
<ChannelTitle id={"channel-title"} tabIndex={-1}>
{channel?.title}
</ChannelTitle>
<p id={"channel-subtitle"}>
{channel?.subtitle}
</p>
</header>
<div id={"channel-content"} dangerouslySetInnerHTML={channel?{__html:channel.html}:undefined} />
</main>
<main id={"info-page"} className={"shell info-shell"} hidden={!info} data-info={info?.key} aria-labelledby={"info-title"}>
<nav className={"page-breadcrumb"} id={"info-breadcrumb"} aria-label={"面包屑导航"}>
{info?<><a href="/">首页</a><span>/</span>{info.key==='settings'?<><a href="/member/center/">个人中心</a><span>/</span></>:null}<span aria-current="page">{info.title}</span></>:null}
</nav>
<header className={"info-heading"}>
<small>
{"INFORMATION & SUPPORT"}
</small>
<InfoTitle id={"info-title"} tabIndex={-1}>
{info?.title}
</InfoTitle>
<p id={"info-subtitle"}>
</p>
</header>
<div className={"info-layout"}>
<nav className={"info-nav"} aria-label={"信息页面"} hidden={!!info}>
<a href={"/info/about-us/"} data-info={"about-us"}>
{"关于我们"}
</a>
<a href={"/info/user-agreement/"} data-info={"user-agreement"}>
{"用户协议"}
</a>
<a href={"/info/privacy-policy/"} data-info={"privacy-policy"}>
{"隐私政策"}
</a>
<a href={"/info/help-center/"} data-info={"help-center"}>
{"常见问题"}
</a>
<a href={"/info/2257/"} data-info={"2257"}>
{"2257"}
</a>
<a href={"/info/dmca/"} data-info={"dmca"}>
{"DMCA"}
</a>
<a href={"/info/feedback/"} data-info={"feedback"}>
{"意见反馈"}
</a>
</nav>
<article className={"info-card"} id={"info-content"} dangerouslySetInnerHTML={info?{__html:info.html}:undefined} />
</div>
</main>
<main id={"search-results-page"} className={"shell"} hidden={view?.panel!=='search'} aria-labelledby={"search-results-title"} dangerouslySetInnerHTML={view?.panel==='search'?{__html:view.html}:undefined} />
</>
);
}
+49
View File
@@ -0,0 +1,49 @@
import type { navigationPageData } from '@/lib/navigation-page';
import { footerLinkCurrent, type FooterPolicyLink } from '@/lib/footer-links';
export function Footer({title,content,contacts,links=[],address='/'}:{title?:string;content?:string;contacts?:Pick<ReturnType<typeof navigationPageData>,'telegram'|'tgGroup'|'twitter'>;links?:FooterPolicyLink[];address?:string}={}) {
const brand=title?.trim()||'';
const slogan=content?.trim()||'';
const year=new Date().getFullYear();
return (
<>
<footer id="shared-site-footer" aria-label={"网站底部"}>
<div className="site-footer-inner">
<strong className="site-footer-title">
{brand}{brand?<i aria-hidden="true"/>:null}
</strong>
<p className="site-footer-slogan hg-footer-original-slogan">
{slogan}
</p>
{links.length?<nav className="site-footer-links" aria-label={"平台规则与帮助"}>
{links.map((link,index)=><a key={`${link.id}-${index}`} href={link.href} aria-current={footerLinkCurrent(address,link.href)?'page':undefined} {...(link.external?{target:link.href.startsWith('mailto:')?undefined:'_blank',rel:'noopener noreferrer'}:{})}>
{link.name}
</a>)}
</nav>:null}
<small className="site-footer-copyright">
{`© ${year}${brand?` ${brand} `:''}All rights reserved.`}
</small>
<div className="site-footer-social" aria-label={"联系我们"}>
{contacts?.tgGroup&&<a href={contacts.tgGroup} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} aria-label={"官方社群"} title={"官方社群"}>
<img src={"/assets/c9e57990dff7b688.png"} alt={"官方社群图标"} />
</a>}
{contacts?.telegram&&<a href={contacts.telegram} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} aria-label={"Telegram"} title={"Telegram"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<path d={"M21.7 3.3 18.4 20c-.2 1.2-.9 1.5-1.9.9l-5-3.7-2.4 2.3c-.3.3-.5.5-1 .5l.4-5.1 9.3-8.4c.4-.4-.1-.6-.6-.3L5.7 13.4.8 11.9c-1-.3-1-1 .2-1.5L20.2 3c.9-.3 1.7.2 1.5.3Z"}>
</path>
</svg>
</a>}
{contacts?.twitter&&<a href={contacts.twitter} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} aria-label={"X"} title={"X"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<path d={"M18.9 2H22l-6.8 7.8L23 22h-6.3l-4.9-7.4L5.3 22H2l8.3-9.5L2 2h6.5l4.4 6.8L18.9 2ZM17.8 20h1.8L7.5 4H5.6l12.2 16Z"}>
</path>
</svg>
</a>}
</div>
</div>
</footer>
</>
);
}
+140
View File
@@ -0,0 +1,140 @@
import type { HomeData, User } from "@/lib/api/types";
import { renderHome } from "@/lib/client/render";
import type { NavigationEntry } from '@/lib/site-navigation';
import { SiteNavigation } from './SiteNavigation';
export function HomePage({home,error,hideContent=false,navigation=[],address='/',navigationError=false,user=null}: {user?:User|null;home: HomeData; error?: string;hideContent?:boolean;navigation?:NavigationEntry[];address?:string;navigationError?:boolean}) {
return <>
<div id="global-site-header" className="shell">
<header className={"top"}>
<a className="hg-header-brand" href="/" aria-label="禁果短剧首页"><img className="hg-complete-logo" src="/assets/40803f26f3c0acbb.svg" width={135} height={40} alt="禁果短剧" /></a>
<SiteNavigation entries={navigation} address={address} surface="top" error={navigationError} />
<form className={"search"} id={"search"} role={"search"}>
<input id={"query"} aria-controls={"search-discovery"} aria-expanded={"false"} type={"search"} placeholder={"找一部,想看的好剧"} aria-label={"搜索短剧"} autoComplete={"off"} />
<button type={"submit"} aria-label={"执行搜索"}>
<svg className={"search-icon-solid"} aria-hidden={"true"} width={"20"} height={"20"} viewBox={"0 0 20 20"} fill={"none"} xmlns={"http://www.w3.org/2000/svg"}>
<path d={"M14.6843 14.7115C15.0064 14.392 15.5225 14.3919 15.8445 14.7115L17.9734 16.4303H18.0105C18.4411 16.8656 18.441 17.5711 18.0105 18.0065C17.5798 18.4418 16.8816 18.4418 16.4509 18.0065L14.6843 15.9821C14.5174 15.8139 14.4237 15.5855 14.4236 15.3473C14.4236 15.109 14.5173 14.8798 14.6843 14.7115ZM8.81519 1.66663C10.7108 1.66673 12.5285 2.42807 13.8689 3.78284C15.2093 5.1377 15.9626 6.97518 15.9626 8.89124C15.9626 12.8813 12.7627 16.1166 8.81519 16.1168C4.86744 16.1168 1.66675 12.8815 1.66675 8.89124C1.66692 4.90116 4.86754 1.66663 8.81519 1.66663Z"} fill={"white"}>
</path>
</svg>
</button>
<section id={"search-discovery"} className={"search-discovery"} aria-label={"搜索推荐"} hidden>
<div className={"search-history-section"}>
<div className={"search-discovery-heading"}>
<h2>
{"搜索记录"}
</h2>
<div className={"search-history-actions"}>
<button type={"button"} id={"search-clear"}>
{"清除"}
</button>
<button type={"button"} id={"search-panel-close"} aria-label={"关闭搜索推荐"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<circle cx={"12"} cy={"12"} r={"9"}>
</circle>
<path d={"m9 9 6 6m0-6-6 6"}>
</path>
</svg>
</button>
</div>
</div>
<div className={"search-history-tags"} id={"search-history-tags"}>
</div>
</div>
<div className={"search-ranking-section"}>
<div className={"search-discovery-heading"}>
<h2 id={"search-suggestion-title"}>
{"热搜榜"}
</h2>
</div>
<div className={"search-suggestion-list"} id={"search-suggestion-list"} aria-live={"polite"}>
</div>
</div>
<div className={"search-picks-section"}>
<div className={"search-discovery-heading"}>
<h2>
{"猜你喜欢"}
</h2>
<button type={"button"} id={"search-picks-refresh"}>
{"换一换"}
</button>
</div>
<div className={"search-picks-grid"} id={"search-picks-grid"}>
</div>
</div>
</section>
</form>
<div className={"header-actions"} aria-label={"用户服务"}>
<button className={"header-action coin-entry"} data-account-action={"coins"} aria-label={'金币余额 '+(user?.recharge||0)}>
<span className={"coin-symbol"} aria-hidden={"true"}>
<img className={"coin-soft-star"} src={"/assets/897547dcbff56113.svg"} alt={"金币图标"} aria-hidden={"true"} />
</span>
<span className={"coin-caption"}>
{"金币 "}
<span className={"coin-value"}>
{user?.recharge||0}
</span>
</span>
</button>
<div className="jg-history-wrap">
<a className="header-action jg-history-entry" href="/member/history/" id="header-history-button" aria-label="观看历史" aria-controls="header-history-panel" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span>历史</span>
</a>
<section className="jg-history-panel" id="header-history-panel" aria-labelledby="header-history-title" hidden>
<h2 id="header-history-title">观看历史</h2><div className="jg-history-list" id="header-history-list" aria-live="polite"/>
<a className="jg-history-all" href="/member/history/">查看全部 <span aria-hidden="true">›</span></a>
</section>
</div>
<div className={"header-download-wrap"} id={"header-download-wrap"} data-app-download-entry="" hidden>
<a className={"header-action download-entry"} id={"header-download-button"} data-app-download="" target="_blank" rel="sponsored nofollow noopener noreferrer" aria-label={"下载 App"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<rect x={"6"} y={"2"} width={"12"} height={"20"} rx={"3"}>
</rect>
<path d={"M10 5h4m-3 14h2"}>
</path>
</svg>
<span>
{"下载 App"}
</span>
</a>
</div>
<button className={"header-action account-entry "} id={"account-entry"} aria-controls={"profile-popover"} aria-expanded={"false"}>
<span className={"account-avatar"} aria-hidden={"true"}>
<img className={"member-avatar-image"} src={"/assets/8837c3e58159bd48.png"} alt={"默认用户头像"} width={"50"} height={"50"} />
</span>
<span className={"account-copy"}>
<strong>
{user?.nickName||user?.accountNo||'我的'}
</strong>
<small id={"account-status"}>
{user?(user.isRegister||user.type===1?'已登录':'游客账号'):'未登录'}
</small>
</span>
</button>
</div>
<button className={"saved header-saved-proxy"} id={"saved"} hidden>
{"♡ 我的片单 "}
<span id={"count"}>
0
</span>
</button>
</header>
<div className="jg-masthead">
<a className="jg-masthead-brand" href="/" aria-label="禁果短剧首页"><img src="/assets/40803f26f3c0acbb.svg" alt="禁果短剧" width={182} height={54}/><div className="jg-masthead-copy"><span>精品原创 · 日更爽看</span></div></a>
<span className="jg-masthead-note">每一种心情,都有一个好故事</span>
</div>
</div>
<main className="shell" hidden={hideContent} aria-labelledby={hideContent?undefined:'home-title'}>
{!hideContent&&<h1 id="home-title" className="home-page-title">禁果短剧 · 精品原创 · 日更爽看</h1>}
<div className="home-content" hidden={hideContent} dangerouslySetInnerHTML={{__html:hideContent?'':renderHome(home,error)}} />
</main>
</>;
}
+169
View File
@@ -0,0 +1,169 @@
"use client";
import { useEffect, useRef, useState, type MouseEvent } from "react";
import { startTracker } from "@/lib/client/analytics";
import type { Config } from "@/lib/api/types";
import { navigationPageData } from "@/lib/navigation-page";
import { AppDownloadDialog } from "./AppDownloadDialog";
import { createDownloadQr, shouldUseDownloadDialog } from "@/lib/client/download";
export function NavigationPage({config, download, error}: {config: Config; download: string; error?: string}) {
useEffect(()=>{startTracker(config);},[config]);
const dialog = useRef<HTMLDialogElement>(null);
const [status, setStatus] = useState("");
const [qr, setQr] = useState<string>();
const {siteName,description,routes,permanent,telegram,tgGroup,twitter}=navigationPageData(config);
const channelCount=2+Number(!!tgGroup)+Number(!!twitter)+Number(!!download);
const openDownload = async (event: MouseEvent<HTMLAnchorElement>) => {
if (!shouldUseDownloadDialog(event)) return;
event.preventDefault();setStatus("");dialog.current?.showModal();
try {setQr(await createDownloadQr(download));} catch {setStatus("二维码生成失败,请复制下载链接");}
};
const copyDownload = async () => {
if (!download) {setStatus("暂未配置 App 下载地址"); return;}
try {await navigator.clipboard.writeText(download); setStatus("链接已复制");}
catch {setStatus("复制失败,请手动复制下载链接");}
};
return (
<>
<div id={"entry-navigation"} className={"navigation-page"}>
<img className={"entry-background"} src={"/assets/d99080bbba89c515.png"} alt={"禁果短剧官方入口背景"} aria-hidden={"true"} />
<div className={"entry-stage"}>
<main className={"entry-content"} aria-labelledby={"entry-page-title"}>
<header className={"entry-brand"}>
<h1 id="entry-page-title" className="entry-page-title">{siteName+"官方入口"}</h1>
<img className={"entry-brand-logo hg-complete-logo"} src={"/assets/40803f26f3c0acbb.svg"} alt={siteName} width={"500"} height={"176"} />
<p>
{description}
</p>
<strong>
{"精品原创 · 日更爽看"}
</strong>
</header>
<nav className={"entry-routes"} aria-label={"访问线路"}>
{routes.map((route,index)=><a key={route.line} className={"entry-route"} href={route.href} rel="sponsored nofollow noopener noreferrer">
<span className={"entry-route-copy"}>
<b>{siteName+" · "+route.label}</b>
<small>{index===0?"推荐入口":"备用入口"}</small>
</span>
<span className={"entry-route-cta"}>
{"立即进入"}
<img src={"/assets/2d24f60e409ee818.svg"} width={"5"} height={"3.5"} alt={"进入"+route.label} aria-hidden={"true"} />
</span>
<img className={"entry-route-glow"} src={"/assets/974b889940739032.svg"} width={"292"} height={"263"} alt={"访问线路背景光效"} aria-hidden={"true"} />
</a>)}
{!routes.length&&<p className="entry-route-empty" role="status">暂未配置访问线路</p>}
{permanent&&<a className={"entry-permanent"} href={permanent} rel="sponsored nofollow noopener noreferrer">
<span>
{"永久入口"}
</span>
<span>
{"收藏回家的路,获取最新地址"}
</span>
</a>}
</nav>
{download && <a className={"entry-download"} href={download} target="_blank" rel="sponsored nofollow noopener noreferrer" data-app-download="" onClick={event => {void openDownload(event);}}>
<img src={"/assets/cea8731b980a14a0.png"} alt={siteName+" App 下载横幅"} />
<span>
{siteName+" · APP 下载"}
</span>
</a>}
{telegram&&<a className={"entry-contact"} href={telegram} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"}>
{"官方联系:" + telegram}
</a>}
<nav className={"entry-channels"} aria-label={"官方渠道"} style={{gridTemplateColumns: `repeat(${channelCount}, minmax(0, 1fr))`}}>
{tgGroup&&<a className={"entry-channel"} href={tgGroup} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"}>
<span className={"entry-channel-icon"}>
<svg className={"entry-channel-svg"} aria-hidden={"true"} focusable={"false"} preserveAspectRatio={"none"} overflow={"visible"} style={{"display": "block"}} width={"16"} height={"16"} viewBox={"0 0 16 16"} fill={"none"} xmlns={"http://www.w3.org/2000/svg"}>
<g>
<path d={"M14.2291 1.84364C13.9819 1.64734 13.6874 1.51976 13.3752 1.47379C13.0629 1.42781 12.7441 1.46508 12.4509 1.58182L2.09451 5.68364C1.71253 5.83484 1.38148 6.09166 1.14009 6.42409C0.898696 6.75651 0.756934 7.15079 0.731378 7.56082C0.705823 7.97084 0.797532 8.37967 0.99578 8.7395C1.19403 9.09932 1.49062 9.39527 1.85088 9.59273L3.38179 10.44L3.41088 12.9855C3.41562 13.2326 3.48748 13.4739 3.61877 13.6834C3.75005 13.8929 3.93582 14.0627 4.1562 14.1748C4.37658 14.2869 4.62328 14.3369 4.8699 14.3196C5.11653 14.3022 5.3538 14.2182 5.55633 14.0764L7.53451 12.7273L10.3527 14.28C10.6588 14.4488 11.0013 14.5409 11.3508 14.5484C11.7003 14.5559 12.0465 14.4786 12.3596 14.3231C12.6727 14.1676 12.9434 13.9386 13.1487 13.6556C13.354 13.3726 13.4876 13.0441 13.5382 12.6982L14.9091 3.51273C14.9547 3.19916 14.9157 2.8791 14.7962 2.58565C14.6766 2.2922 14.4808 2.03603 14.2291 1.84364Z"} fill={"currentColor"}>
</path>
</g>
</svg>
</span>
<span>
{"官方TG群"}
</span>
</a>}
{twitter&&<a className={"entry-channel"} href={twitter} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"}>
<span className={"entry-channel-icon"}>
<svg className={"entry-channel-svg"} aria-hidden={"true"} focusable={"false"} preserveAspectRatio={"none"} overflow={"visible"} style={{"display": "block"}} width={"16"} height={"16"} viewBox={"0 0 16 16"} fill={"none"} xmlns={"http://www.w3.org/2000/svg"}>
<g>
<path d={"M11.9025 1.90609H13.97L9.45311 7.06859L14.7669 14.0936H10.6062L7.34748 9.83296L3.61872 14.0936H1.54998L6.38123 8.57171L1.28373 1.90609H5.54999L8.49561 5.80046L11.9025 1.90609ZM11.1769 12.8561H12.3225L4.92747 3.07859H3.69809L11.1769 12.8561Z"} fill={"currentColor"}>
</path>
</g>
</svg>
</span>
<span>
{"官方推特"}
</span>
</a>}
<a className={"entry-channel"} href={"/home-road/"}>
<span className={"entry-channel-icon"}>
<svg className={"entry-channel-svg"} aria-hidden={"true"} focusable={"false"} preserveAspectRatio={"none"} overflow={"visible"} style={{"display": "block"}} width={"16"} height={"16"} viewBox={"0 0 16 16"} fill={"none"} xmlns={"http://www.w3.org/2000/svg"}>
<g>
<path d={"M13.5164 4.22909L9.11636 1.74909C8.77667 1.5559 8.3926 1.45433 8.00182 1.45433C7.61104 1.45433 7.22696 1.5559 6.88727 1.74909L2.48364 4.22909C2.17124 4.40646 1.91143 4.66345 1.73066 4.97389C1.54989 5.28433 1.45462 5.63713 1.45455 5.99636L2.01455 12.5745C2.0267 13.0959 2.24022 13.5924 2.61037 13.9598C2.98051 14.3273 3.47852 14.5371 4 14.5455H12C12.5241 14.54 13.0256 14.3316 13.3993 13.9641C13.7729 13.5965 13.9896 13.0985 14.0036 12.5745L14.5455 6.07636V5.99636C14.5461 5.63698 14.4512 5.28388 14.2703 4.97331C14.0895 4.66274 13.8293 4.40588 13.5164 4.22909ZM9.15273 11.7636H6.86909C6.72443 11.7636 6.58569 11.7062 6.4834 11.6039C6.3811 11.5016 6.32364 11.3628 6.32364 11.2182C6.32364 11.0735 6.3811 10.9348 6.4834 10.8325C6.58569 10.7302 6.72443 10.6727 6.86909 10.6727H9.14545C9.29012 10.6727 9.42886 10.7302 9.53115 10.8325C9.63344 10.9348 9.69091 11.0735 9.69091 11.2182C9.69091 11.3628 9.63344 11.5016 9.53115 11.6039C9.42886 11.7062 9.29012 11.7636 9.14545 11.7636H9.15273Z"} fill={"currentColor"}>
</path>
</g>
</svg>
</span>
<span>
{"回家的路"}
</span>
</a>
{download && <a className={"entry-channel"} href={download} target="_blank" rel="sponsored nofollow noopener noreferrer" data-app-download="" onClick={event => {void openDownload(event);}}>
<span className={"entry-channel-icon"}>
<svg className={"entry-channel-svg"} aria-hidden={"true"} focusable={"false"} preserveAspectRatio={"none"} overflow={"visible"} style={{"display": "block"}} width={"18"} height={"18"} viewBox={"0 0 18 18"} fill={"none"} xmlns={"http://www.w3.org/2000/svg"}>
<g>
<path d={"M13.2955 1.22727H4.70455C4.10814 1.22835 3.53647 1.46575 3.11475 1.88748C2.69303 2.3092 2.45563 2.88087 2.45455 3.47727V14.5227C2.45563 15.1191 2.69303 15.6908 3.11475 16.1125C3.53647 16.5342 4.10814 16.7716 4.70455 16.7727H13.2955C13.8922 16.7727 14.4645 16.5357 14.8864 16.1137C15.3084 15.6918 15.5455 15.1195 15.5455 14.5227V3.47727C15.5455 2.88053 15.3084 2.30824 14.8864 1.88628C14.4645 1.46432 13.8922 1.22727 13.2955 1.22727ZM10.8409 13.5H7.15909C6.99634 13.5 6.84026 13.4353 6.72518 13.3203C6.61011 13.2052 6.54546 13.0491 6.54546 12.8864C6.54546 12.7236 6.61011 12.5675 6.72518 12.4525C6.84026 12.3374 6.99634 12.2727 7.15909 12.2727H10.8409C11.0037 12.2727 11.1597 12.3374 11.2748 12.4525C11.3899 12.5675 11.4545 12.7236 11.4545 12.8864C11.4545 13.0491 11.3899 13.2052 11.2748 13.3203C11.1597 13.4353 11.0037 13.5 10.8409 13.5Z"} fill={"currentColor"}>
</path>
</g>
</svg>
</span>
<span>
{"APP"}
</span>
</a>}
<a className={"entry-channel"} href={"/apps/"}>
<span className={"entry-channel-icon"}>
<svg className={"entry-channel-svg"} aria-hidden={"true"} focusable={"false"} preserveAspectRatio={"none"} overflow={"visible"} style={{"display": "block"}} width={"16"} height={"16"} viewBox={"0 0 16 16"} fill={"none"} xmlns={"http://www.w3.org/2000/svg"}>
<g>
<path d={"M4.43637 10.3782C4.35322 10.4255 4.25929 10.4505 4.16364 10.4509C4.04345 10.451 3.92659 10.4115 3.8312 10.3384C3.73581 10.2653 3.66725 10.1627 3.63615 10.0466C3.60505 9.93049 3.61317 9.80738 3.65924 9.69638C3.70531 9.58537 3.78675 9.49269 3.89091 9.43273L5.01818 8.78182L4.32364 8.38182C3.91526 8.16333 3.60971 7.79237 3.4735 7.3497C3.33729 6.90702 3.38145 6.42846 3.59636 6.01818C3.71269 5.81966 3.87027 5.64842 4.05848 5.51605C4.24668 5.38368 4.46111 5.29326 4.68727 5.25091C5.00863 5.18723 5.34112 5.20982 5.65091 5.31637V1.81818H2.74182C2.47813 1.81578 2.21657 1.86565 1.97226 1.9649C1.72795 2.06415 1.50572 2.21082 1.31841 2.39644C1.1311 2.58206 0.98242 2.80295 0.880959 3.04635C0.779497 3.28975 0.727263 3.55084 0.727274 3.81455V12.1782C0.726317 12.441 0.777248 12.7013 0.877146 12.9444C0.977044 13.1874 1.12394 13.4084 1.30942 13.5945C1.49489 13.7807 1.71529 13.9284 1.95798 14.0292C2.20066 14.1299 2.46086 14.1818 2.72364 14.1818H5.63273V9.68364L4.43637 10.3782Z"} fill={"currentColor"}>
</path>
<path d={"M4.89091 6.32C4.81646 6.33243 4.74552 6.36059 4.68281 6.4026C4.62011 6.44461 4.56708 6.49951 4.52727 6.56364C4.45835 6.72444 4.45516 6.90584 4.51837 7.06897C4.58159 7.23211 4.70618 7.36399 4.86545 7.43637L5.63273 7.88V6.48364C5.56864 6.4752 5.50685 6.4542 5.45091 6.42182C5.28354 6.31906 5.08374 6.28274 4.89091 6.32ZM13.28 1.81818H6.73455V5.27637C7.00405 5.20499 7.28614 5.19505 7.56 5.24727C7.78608 5.28985 8.00042 5.38036 8.18859 5.51271C8.37676 5.64506 8.5344 5.81618 8.65091 6.01455C8.76587 6.21316 8.83645 6.43432 8.85779 6.66282C8.87914 6.89131 8.85074 7.12171 8.77455 7.33818C8.61873 7.77517 8.30827 8.13996 7.90182 8.36364L7.2 8.78182L8.33091 9.45455C8.43507 9.51451 8.51651 9.60719 8.56258 9.71819C8.60865 9.8292 8.61677 9.95231 8.58567 10.0684C8.55457 10.1845 8.48601 10.2871 8.39062 10.3602C8.29523 10.4333 8.17837 10.4729 8.05818 10.4727C7.96266 10.4714 7.86896 10.4464 7.78545 10.4L6.72727 9.77091V14.1818H13.2727C13.5352 14.1823 13.7952 14.131 14.0378 14.0309C14.2804 13.9308 14.501 13.7838 14.6867 13.5984C14.8725 13.413 15.0199 13.1927 15.1204 12.9503C15.221 12.7078 15.2727 12.4479 15.2727 12.1855V3.82182C15.2737 3.55935 15.2229 3.29926 15.1232 3.05645C15.0235 2.81364 14.877 2.59286 14.6919 2.40676C14.5068 2.22066 14.2868 2.07288 14.0445 1.97189C13.8023 1.87089 13.5425 1.81866 13.28 1.81818Z"} fill={"currentColor"}>
</path>
<path d={"M7.72364 6.99636C7.7495 6.92548 7.75996 6.8499 7.75432 6.77465C7.74868 6.69941 7.72706 6.62623 7.69091 6.56C7.65165 6.4954 7.59878 6.44013 7.53597 6.39805C7.47317 6.35598 7.40195 6.3281 7.32727 6.31636C7.13445 6.27837 6.93438 6.31474 6.76727 6.41818L6.72364 6.43636V7.79273L7.35273 7.42909C7.52486 7.33474 7.65672 7.1809 7.72364 6.99636Z"} fill={"currentColor"}>
</path>
</g>
</svg>
</span>
<span>
{"福利应用"}
</span>
</a>
</nav>
<footer className={"entry-footer"}>
{error && <p role="status">{error}</p>}
<p>
{"本站仅提供视频内容与产品服务"}
<br />
{"请遵守当地法律法规,仅限成年人访问"}
</p>
<p>
{"© 2026 "+siteName+" · 官方发布页"}
</p>
</footer>
</main>
</div>
</div>
{download && <AppDownloadDialog download={download} qr={qr} status={status} dialogRef={dialog} onClose={() => dialog.current?.close()} onCopy={() => {void copyDownload();}} />}
</>
);
}
+30
View File
@@ -0,0 +1,30 @@
export function PageTools() {
return (
<>
<aside className={"page-tools"} id={"page-tools"} aria-label={"页面快捷工具"} aria-hidden={"true"} inert>
<div className={"page-tool-download"} id={"page-tool-download"} data-app-download-entry="" hidden>
<a className={"page-tool"} id={"floating-download"} data-app-download="" target="_blank" rel="sponsored nofollow noopener noreferrer" aria-label={"下载 App"}>
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 5h4m-3 14h2"/></svg>
</a>
</div>
<button className={"page-tool"} id={"floating-history"} type={"button"} aria-label={"历史记录"} title={"历史记录"} aria-haspopup={"dialog"} aria-controls={"history-dialog"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<circle cx={"12"} cy={"12"} r={"9"} fill={"none"} stroke={"currentColor"} strokeWidth={"1.8"}>
</circle>
<path d={"M12 7v5l3 2"} fill={"none"} stroke={"currentColor"} strokeWidth={"1.8"} strokeLinecap={"round"} strokeLinejoin={"round"}>
</path>
</svg>
</button>
<button className={"page-tool"} id={"back-to-top"} type={"button"} aria-label={"回到顶部"} title={"回到顶部"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<path d={"M5 4h14M12 20V8m-6 6 6-6 6 6"}>
</path>
</svg>
</button>
</aside>
</>
);
}
+9
View File
@@ -0,0 +1,9 @@
import type { PageView } from '@/lib/page-data';
export function PlayerPage({view}:{view?:PageView}) {
return (
<>
<main id={"drama-play-page"} className={`shell${view?.overview?' is-overview':''}`} hidden={view?.panel!=='player'} aria-labelledby={"play-detail-title"} dangerouslySetInnerHTML={view?.panel==='player'?{__html:view.html}:undefined} />
</>
);
}
+99
View File
@@ -0,0 +1,99 @@
import type { NavigationEntry } from '@/lib/site-navigation';
import type { navigationPageData } from '@/lib/navigation-page';
import { SiteNavigation } from './SiteNavigation';
export function Sidebar({navigation,address,error,contacts}:{navigation:NavigationEntry[];address:string;error?:boolean;contacts?:Pick<ReturnType<typeof navigationPageData>,'telegram'|'tgGroup'|'twitter'>}) {
return (
<>
<aside className={"side"} id={"site-sidebar"} aria-label={"网站侧边栏"}>
<div className={"sidebar-header"}>
<a className={"mark sidebar-logo-toggle"} href={"/"} aria-label={"禁果短剧首页"}>
<span className={"sidebar-brand-window"}>
<img className={"sidebar-brand"} src={"/assets/40803f26f3c0acbb.svg"} alt={"禁果短剧"} width={"500"} height={"176"} />
</span>
</a>
<button className={"sidebar-toggle"} id={"sidebar-toggle"} type={"button"} aria-controls={"site-sidebar"} aria-expanded={"true"} aria-label={"收起侧边栏"} title={"收起侧边栏"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<rect x={"3"} y={"4"} width={"18"} height={"16"} rx={"3"}>
</rect>
<path d={"M9 4v16"}>
</path>
</svg>
</button>
</div>
<SiteNavigation entries={navigation} address={address} error={error} />
<div className={"side-info"}>
<strong>
<img className={"side-info-wordmark"} src={"/assets/87eeb5bf339a03f0.svg"} alt={"禁果短剧"} width={308} height={73} />
<i className={"side-info-brand-dot"} aria-hidden={"true"}>
</i>
</strong>
<p className={"side-info-slogan"}>
{"精品原创 · 日更爽看"}
</p>
<div className={"policy-links"} aria-label={"平台规则与帮助"}>
<a href={"/info/about-us/"} data-info={"about-us"}>
{"关于我们"}
</a>
<a href={"/info/user-agreement/"} data-info={"user-agreement"}>
{"用户协议"}
</a>
<a href={"/info/privacy-policy/"} data-info={"privacy-policy"}>
{"隐私政策"}
</a>
<a href={"/info/help-center/"} data-info={"help-center"}>
{"常见问题"}
</a>
<a href={"/info/2257/"} data-info={"2257"}>
{"2257"}
</a>
<a href={"/info/dmca/"} data-info={"dmca"}>
{"DMCA"}
</a>
<a href={"/info/feedback/"} data-info={"feedback"}>
{"意见反馈"}
</a>
</div>
<small>
{"© 2026 禁果短剧"}
<br />
{"All rights reserved."}
</small>
</div>
<a className={"collapsed-help"} href={"/info/help-center/"} title={"规则与帮助"} aria-label={"规则与帮助"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<circle cx={"12"} cy={"12"} r={"9"}>
</circle>
<path d={"M9.5 9a2.5 2.5 0 0 1 5 0c0 2-2.5 2-2.5 4m0 3v.1"}>
</path>
</svg>
</a>
<div className={"sidebar-social"} aria-label={"联系我们"}>
{contacts?.tgGroup&&<a href={contacts.tgGroup} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} aria-label={"官方社群"} title={"官方社群"}>
<img src={"/assets/c9e57990dff7b688.png"} alt={"官方社群图标"} />
</a>}
{contacts?.telegram&&<a href={contacts.telegram} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} aria-label={"Telegram"} title={"Telegram"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<path d={"M21.7 3.3 18.4 20c-.2 1.2-.9 1.5-1.9.9l-5-3.7-2.4 2.3c-.3.3-.5.5-1 .5l.4-5.1 9.3-8.4c.4-.4-.1-.6-.6-.3L5.7 13.4.8 11.9c-1-.3-1-1 .2-1.5L20.2 3c.9-.3 1.7.2 1.5.3Z"}>
</path>
</svg>
</a>}
{contacts?.twitter&&<a href={contacts.twitter} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} aria-label={"X"} title={"X"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<path d={"M18.9 2H22l-6.8 7.8L23 22h-6.3l-4.9-7.4L5.3 22H2l8.3-9.5L2 2h6.5l4.4 6.8L18.9 2ZM17.8 20h1.8L7.5 4H5.6l12.2 16Z"}>
</path>
</svg>
</a>}
</div>
</aside>
<SiteNavigation entries={navigation} address={address} surface="mobile" />
</>
);
}
+172
View File
@@ -0,0 +1,172 @@
import { AppDownloadDialog } from "./AppDownloadDialog";
export function SiteDialogs() {
return (
<>
<dialog id={"detail"} aria-label={"短剧预览"}>
</dialog>
<div className={"toast hidden"} id={"toast"} role={"status"}>
</div>
<dialog id={"account-dialog"} aria-labelledby={"account-dialog-title"}>
</dialog>
<dialog id={"history-dialog"} className={"history-dialog"} aria-labelledby={"history-dialog-title"}>
<button className={"close"} id={"history-close"} type={"button"} aria-label={"关闭历史记录"}>
<svg className={"dialog-close-icon"} viewBox={"0 0 24 24"} aria-hidden={"true"} focusable={"false"}>
<path d={"m6 6 12 12M18 6 6 18"}>
</path>
</svg>
</button>
<h2 id={"history-dialog-title"}>
{"历史记录"}
</h2>
<p className={"history-note"}>
{"最近浏览的剧集"}
</p>
<div className={"account-history-list"} id={"history-list"}>
</div>
<button type={"button"} className={"history-clear"} id={"history-clear"}>
{"清空记录"}
</button>
</dialog>
<AppDownloadDialog />
<dialog id={"member-delete-dialog"} aria-labelledby={"member-delete-title"} aria-describedby={"member-delete-description"}>
<form method={"dialog"}>
<h2 id={"member-delete-title"}>
{"确认删除"}
</h2>
<p id={"member-delete-description"}>
</p>
<div className={"member-delete-actions"}>
<button value={"cancel"} autoFocus>
{"取消"}
</button>
<button value={"confirm"}>
{"确认删除"}
</button>
</div>
</form>
</dialog>
<dialog id={"member-auth-dialog"} aria-labelledby={"member-auth-title"}>
</dialog>
<dialog id={"wallet-payment-dialog"} aria-labelledby={"wallet-payment-title"}>
</dialog>
<dialog id={"play-episode-lock"} aria-labelledby={"episode-lock-title"} aria-describedby={"episode-lock-description episode-lock-note"}>
<button type={"button"} className={"unlock-close"} aria-label={"关闭金币解锁弹窗"}>
<svg viewBox={"0 0 24 24"} aria-hidden={"true"}>
<path d={"m6 6 12 12M18 6 6 18"}>
</path>
</svg>
</button>
<div className={"unlock-coin"} aria-hidden={"true"}>
<span className={"coin-symbol"} aria-hidden={"true"}>
<img className={"coin-soft-star"} src={"/assets/897547dcbff56113.svg"} alt={"金币图标"} aria-hidden={"true"} />
</span>
</div>
<h2 id={"episode-lock-title"}>
{"使用金币解锁本集"}
</h2>
<p id={"episode-lock-description"} className={"episode-lock-copy"}>
{"解锁后可永久观看本集内容。"}
</p>
<div className={"unlock-cost-summary"}>
<div>
<span>
{"当前余额"}
</span>
<strong id={"episode-lock-balance"}>
{"— 金币"}
</strong>
</div>
<div>
<span>
{"本次消耗"}
</span>
<strong className={"unlock-cost"}>
{"— 金币"}
</strong>
</div>
</div>
<p id={"episode-lock-note"} className={"unlock-confirm-note"}>
{"后续付费剧集仍需逐集确认"}
</p>
<p id={"episode-lock-error"} className={"unlock-error"} role={"alert"} hidden></p>
<button type={"button"} className={"unlock-pay"}>
{"支付 — 金币并播放"}
</button>
<a className={"unlock-recharge"} href={"/info/deposit/"} data-close-dialog="">
{"金币不足?去充值"}
</a>
</dialog>
<dialog id={"startup-age"} className={"startup-popup"} aria-labelledby={"startup-age-title"} aria-describedby={"startup-age-description"}>
<div className={"startup-age-layout"}>
<div className={"startup-age-mark"}>
<img className={"startup-age-desktop-icon"} src={"/assets/ceabbe508bcc39ab.svg"} width={"96"} height={"96"} alt={"18岁以上"} />
<img className={"startup-age-mobile-icon"} src={"/assets/f948f2f92ec05f6f.svg"} width={"78"} height={"78"} alt={"18岁以上"} />
</div>
<div className={"startup-age-copy"}>
<div className={"startup-age-desktop-copy"}>
<h2 id={"startup-age-title"}>
{"本网站仅限成年人访问"}
</h2>
<div id={"startup-age-description"}>
<p>
{"本网站包含仅限18岁及以上成年人浏览的内容。若您未满18岁,或所在地法律不允许您浏览此类内容,请立即离开。"}
</p>
<p>
{"继续进入即表示您确认已年满18岁,并达到所在地的法定成年年龄。"}
</p>
</div>
</div>
<div className={"startup-age-mobile-copy"}>
<h2 id={"startup-age-mobile-title"}>
{"这是个成人网站"}
</h2>
<p id={"startup-age-mobile-description"}>
{"本网站包含有年龄限制的内容,包括裸体和露骨色情素材的内容。 点击继续即表示您确认您已年满 18 岁,或在您访问本网站时所在的司法管辖区已是成年人"}
</p>
</div>
<div className={"startup-age-actions"}>
<button className={"startup-age-confirm"} type={"button"} autoFocus>
{"我已年满18岁"}
</button>
<button className={"startup-age-leave"} type={"button"}>
{"离开网站"}
</button>
</div>
</div>
</div>
<button className={"startup-close"} type={"button"} aria-label={"离开网站"}>
<img src={"/assets/189d7323288bd44d.svg"} width={"36"} height={"36"} alt={"离开网站"} aria-hidden={"true"} />
</button>
</dialog>
<dialog id={"startup-ad"} className={"startup-popup"} aria-label={"广告推荐"}>
<a className={"startup-ad-media"} target={"_blank"} rel={"sponsored nofollow noopener noreferrer"} data-track-ad="external-dialog" data-ad-type="interstitial" hidden>
<img className={"startup-ad-image"} width={"400"} height={"602"} alt={"广告"} />
</a>
<p className={"startup-ad-loading"} role={"status"} hidden>{"正在加载下一条广告…"}</p>
<button className={"startup-close"} type={"button"} aria-label={"关闭广告"}>
<picture>
<source media={"(max-width:760px)"} srcSet={"/assets/189d7323288bd44d.svg"} />
<img src={"/assets/189d7323288bd44d.svg"} width={"36"} height={"36"} alt={"关闭广告"} aria-hidden={"true"} />
</picture>
</button>
</dialog>
<dialog id="startup-grid" className="startup-popup" aria-label="应用推广">
<div className="startup-promotions" id="startup-promotions"></div>
<button className="startup-close" type="button" aria-label="关闭应用推广">
<img src="/assets/189d7323288bd44d.svg" width="36" height="36" alt="关闭应用推广" aria-hidden="true" />
</button>
</dialog>
</>
);
}
+35
View File
@@ -0,0 +1,35 @@
import type { HomeData } from '@/lib/api/types';
import type { InfoPageDefinition } from '@/lib/info-pages';
import { getHeaderNavigation } from '@/lib/api/server';
import { navigationEntries } from '@/lib/site-navigation';
import { navigationPageData } from '@/lib/navigation-page';
import { Sidebar } from './Sidebar';
import { HomePage } from './HomePage';
import { ChannelPages } from './ChannelPages';
import { PlayerPage } from './PlayerPage';
import { PageTools } from './PageTools';
import { SiteDialogs } from './SiteDialogs';
import { Footer } from './Footer';
import { footerPolicyLinks } from '@/lib/footer-links';
import { SiteRuntime } from './SiteRuntime';
import type { SeoBootstrap } from '@/lib/seo';
import { StructuredData } from './StructuredData';
import type { PageBootstrap, PageView, SiteSharedData } from '@/lib/page-data';
export async function SiteFrame({home,error,info,seo,view,homeLoaded=true,session,shared}: {home:HomeData;error?:string;info?:InfoPageDefinition;seo?:SeoBootstrap;view?:PageView;homeLoaded?:boolean;session?:PageBootstrap['session'];shared?:SiteSharedData}) {
const header=await getHeaderNavigation();
const navigation=navigationEntries(header.data,seo?.environment.origin);
const address=seo?.path||'/';
const contacts=navigationPageData(shared?.config||{});
return <>
<StructuredData seo={seo} />
<Sidebar navigation={navigation} address={address} error={header.error} contacts={contacts} />
<HomePage user={session?.user||view?.bootstrap?.session?.user} navigation={navigation} address={address} navigationError={header.error} home={home} error={error} hideContent={!!info||!!view} />
<ChannelPages info={info} view={view} />
<PlayerPage view={view} />
<PageTools />
<SiteDialogs />
<Footer title={shared?.footer.footerInfo?.brand?.title} content={shared?.footer.footerInfo?.brand?.content} contacts={contacts} links={footerPolicyLinks(shared?.footer)} address={address} />
<SiteRuntime home={home} homeLoaded={homeLoaded&&!info} seo={seo} page={view?.bootstrap} navigation={navigation} session={session} shared={shared} />
</>;
}
+34
View File
@@ -0,0 +1,34 @@
import type { NavigationEntry, NavigationSurface } from '@/lib/site-navigation';
import { mobileNavigationEntries, navigationIsActive } from '@/lib/site-navigation';
const icons:Record<string,string>={
"home": "M778.85 226.8H639.62v58.49l65.87 54.05-17.61 5.61-183.52-107.69-318.02 186.62V815H404.6V601.86h199.53V815H822.4V423.88l-43.55-25.55V226.8z",
"shorts": "M765 558l-22.93-14.02L725 366h1.44l-1.17-12.3c-24.41-22.2-39.3-35.74-63.71-57.95H265.44c-24.41 22.2-39.3 35.74-63.71 57.95l-37.72 397c25.39 27.92 38.32 42.14 63.71 70.05h471.55c25.39-27.92 38.32-42.14 63.71-70.05l-9.03-95.04 132.04 55.6v-306l-116.13 48.9-4.87 103.85z",
"topics": "M473.6 307.44L373.88 234H190.45c-13.24 14.55-19.99 21.97-33.24 36.52l44.5 470.8 52.39 47.67h536.5l52.31-46.92 28.26-259.47-523.33-12.55 20.98-25.18 508.58-19.49 8.82-80.95c-13.27-14.79-19.92-22.21-33.19-37H473.58z",
"rank": "M710.83 236.25c-109.55-109.55-287.18-109.55-396.73 0-109.55 109.55-109.55 287.18 0 396.73 41.51 41.51 92.8 67.28 146.46 77.33l-14.22 28.06c-42.51 2.32-87.82-4.48-131.06-38.02l-18.72 181.57 215.04-83.69 215.04 83.69-24.78-240.38c3.03-2.78 6.03-5.62 8.97-8.56 109.55-109.55 109.55-287.18 0-396.73z",
"home-road": "M534.27 168v0.36h-46.45l-13.39 461.39h-0.19l-0.99 40.53-1.78 61.34h0.28l-0.17 6.93-24.29-15.19-6.63-91.65c-146.16 11.32-255.42 56.61-255.42 110.77 0 62.68 146.35 113.5 326.88 113.5S839 805.16 839 742.48c0-58.26-126.42-106.25-289.19-112.74l-4.84-166.76h346.86l-80.71-147.5 80.71-147.5H534.27z",
"apps": "M178.74 474.5h46.77v0.28l33.03-0.28h305.41l-25.85 16.77-312.59 22.53V812h570.47V474.5h46.76v-102h-664v102zM662 243l-119 61-204.78-75.63L287 340l195.97 23 261.77-25L662 243z"
};
function NavigationIcon({item}:{item:NavigationEntry}) {
const path=Object.hasOwn(icons,item.key)?icons[item.key]:'';
return item.icon?<img className="site-navigation-icon" src="/placeholder.png" data-src={item.icon} alt={item.name+'导航图标'} width={24} height={24} loading="lazy"/>:<svg className="nav-collection-icon" viewBox={path?'0 0 1024 1024':'0 0 24 24'} aria-hidden="true" focusable="false"><path d={path||'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z'}/></svg>;
}
function NavigationLink({item,address,surface}:{item:NavigationEntry;address:string;surface:NavigationSurface}) {
const active=navigationIsActive(item.href,address,surface);
return <a href={item.href} data-site-nav={surface} data-nav-id={item.id} data-page={surface==='top'?undefined:item.key} data-hg-page={surface==='top'?item.key:undefined} className={active?'active':undefined} aria-current={active?'page':undefined} aria-label={item.name} title={item.name} target={item.external?'_blank':undefined} rel={item.external?'sponsored nofollow noopener noreferrer':undefined}>
{surface==='sidebar'||surface==='top'?<NavigationIcon item={item}/>:null}<span className={surface==='sidebar'?'side-nav-label':undefined}>{item.name}</span>
</a>;
}
export function SiteNavigation({entries,address='/',surface='sidebar',error=false}:{entries:NavigationEntry[];address?:string;surface?:NavigationSurface;error?:boolean}) {
const items=surface==='mobile'?mobileNavigationEntries(entries):entries;
return <nav className={surface==='top'?'jg-primary-nav':surface==='mobile'?'mobile-pages':undefined} aria-label={surface==='top'?'顶部主导航':surface==='mobile'?'移动端主导航':'主导航'} data-site-navigation={surface}>
{items.map(item=>item.children.length?<details className="site-nav-group" key={item.id} open={surface==='sidebar'&&item.children.some(child=>navigationIsActive(child.href,address,surface))||undefined}>
<summary>{surface==='sidebar'?<NavigationIcon item={item}/>:null}<span className={surface==='sidebar'?'side-nav-label':undefined}>{item.name}</span><span className="site-nav-caret" aria-hidden="true">⌄</span></summary>
<div className="site-nav-children">{item.href?<NavigationLink item={{...item,children:[]}} address={address} surface={surface}/>:null}{item.children.map(child=><NavigationLink key={child.id} item={child} address={address} surface={surface}/>)}</div>
</details>:<NavigationLink key={item.id} item={item} address={address} surface={surface}/>)}
{error?<span className="site-navigation-status" role="status">导航暂不可用</span>:null}
</nav>;
}
+16
View File
@@ -0,0 +1,16 @@
"use client";
import Script from "next/script";
import type { HomeData } from "@/lib/api/types";
import { startApplication } from "@/lib/client/application";
import { startTracker } from "@/lib/client/analytics";
import type { SeoBootstrap } from '@/lib/seo';
import type { PageBootstrap, SiteSharedData } from '@/lib/page-data';
import type { NavigationEntry } from '@/lib/site-navigation';
/** Initialize the reference interactions once, after React finishes hydration. */
export function SiteRuntime({home,homeLoaded=true,seo,page,session,shared}: {home: HomeData;homeLoaded?:boolean;seo?:SeoBootstrap;page?:PageBootstrap;navigation?:NavigationEntry[];session?:PageBootstrap['session'];shared?:SiteSharedData}) {
return <Script src="/scripts/site-runtime.js" strategy="afterInteractive" onReady={()=>{
startTracker();startApplication(home,homeLoaded,seo,page,session,shared);
}} />;
}
+12
View File
@@ -0,0 +1,12 @@
import { buildPageHead, type SeoBootstrap } from '@/lib/seo';
import { serializeStructuredData } from '@/lib/structured-data';
/** A native script is rendered in the initial HTML, before client routing starts. */
export function StructuredData({seo}:{seo?:SeoBootstrap}) {
if(!seo)return null;
const query=seo.path.indexOf('?');
const pathname=query<0?seo.path:seo.path.slice(0,query),search=query<0?'':seo.path.slice(query);
const data=buildPageHead(pathname,search,seo.environment,seo.content).jsonLd;
if(!data)return null;
return <script id="site-json-ld" type="application/ld+json" data-site-json-ld="" dangerouslySetInnerHTML={{__html:serializeStructuredData(data)}} />;
}