"use client"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; /* ============================================================================ InfiPlot · 首页(编辑式视觉风格 · 居中构图,呼应低保真原型) - 顶部 Header:左上角衬线 wordmark logo - Hero 控制区(居中):标题 / prompt 输入框 + 开始 / 5 个类别选择器 - 统一瀑布流(居中定宽):7 张主推 + 16 张画廊,按性向整体 crossfade 切换 - 项目介绍(题跋式排版) ========================================================================== */ type Gender = "男性向" | "女性向"; const EXAMPLE_PHRASES: Record = { 男性向: [ "从小一起长大的青梅竹马,突然红着脸向我告白", "一觉醒来,班上的女生好像都偷偷喜欢上了我", "三年之期已到,原来我是富家公子,报仇时机已到", "我带着无限 Token 穿越回了互联网诞生前夕……", ], 女性向: [ "穿越成将军府的废物嫡女,冷面摄政王却独宠我一人", "重生回到分手前夜,这一次换我先放手", "一觉醒来成了乙游里的恶役千金,要躲开所有死亡结局", ], }; type Opt = { label: string; items: string[]; defaultIndex?: number; modal?: boolean; }; const OPTS: Opt[] = [ { label: "性向", items: ["男性向", "女性向"] }, { label: "绘画风格", modal: true, items: [ "自动", "二次元", "吉卜力", "真实系", "超写实", "水彩", "像素风", "日系动画", "3D 渲染", "蒸汽朋克", "玄幻", "国风水墨", "赛博朋克", ], }, { label: "剧情风格", items: ["平铺直叙", "多线转折", "悬疑烧脑", "治愈日常"], defaultIndex: 1 }, { label: "语音配音", items: ["关闭", "开启"], defaultIndex: 1 }, { label: "内容节奏", items: ["慢热细腻", "紧凑爽快"], defaultIndex: 1 }, ]; type StoryContent = { title: string; outline: string; style: string }; const STYLE_MAP: Record = { 二次元: "唯美二次元动漫插画,日系 galgame 精致质感,柔和温暖的自然光照。", 吉卜力: "吉卜力工作室风格,手绘动画质感,柔和水彩底色,温暖治愈的氛围。", 真实系: "真实电影感,柔和自然光照,胶片颗粒。", 超写实: "超写实人像与场景,电影级布光,皮肤与材质细节精致。", 水彩: "水彩插画,湿润晕染笔触,纸纹底色。", 像素风: "像素风格,复古游戏 16-bit 调色,方块化几何造型。", 日系动画: "现代日系动画 cel-shading,硬光阴影分层,赛璐璐风。", "3D 渲染": "3D 渲染卡通风格,柔和次表面散射,干净的电影级布光。", 蒸汽朋克: "蒸汽朋克美学,铜色齿轮与蒸汽,工业革命氛围。", 玄幻: "国风玄幻插画,仙气缭绕,群山烟雨与神兽萦绕。", 国风水墨: "国潮唯美古风插画,水墨微晕渲染,仙侠浪漫色彩,极具东方神韵。", 赛博朋克: "赛博朋克都市,霓虹反射湿润街道,电子义体高光。", }; /* 每个性向 32 篇预设剧情(红果短视频式开场钩子)。与封面 /home/{m|f}{i}.webp 按索引 一一对应;style 字段决定点卡片进入 /play 时使用的画风(对应 styleMap 的 12 种风格)。 男/女同索引共享画面尺寸,切性向 crossfade 时卡片高度不跳变。 */ const STORIES: Record = { 男性向: [ { title: "战神归来", outline: "五年前我战死边境,灵柩送回家时她抱着儿子改嫁了。今天我站在他们的婚礼门口,新郎刚要骂人,跪在他面前的二十个保镖喊了我一声「上将」。", style: "真实系" }, { title: "神医归乡", outline: "在城里被嘲笑成「江湖野医生」的我,回了一趟老家。村口的老人见到我直接哭了:「您终于回来了,您当年的师父…病了。」其实他们不知道,我现在是国手第一。", style: "吉卜力" }, { title: "赘婿亮剑", outline: "岳父大寿,我端着茶被全场嫌弃,一句「废物」让我滚出去。门外停着九辆悬挂军牌的劳斯莱斯,下来的人朝我深深一鞠躬:「少爷,集团等您回去签字。」", style: "真实系" }, { title: "送外卖的少主", outline: "你以为我是给你送了三个月外卖的那个小哥?昨晚有人对我说:「少主,您隐姓埋名的三年,到了。」——而你昨天还笑我连一杯咖啡都买不起。", style: "二次元" }, { title: "兵王食言", outline: "退役那天我答应过队长:「这辈子不再开枪。」但你今天在我面前打了她一巴掌,那我食言一次。", style: "真实系" }, { title: "重生分手前夜", outline: "凌晨四点,我醒在我们分手的那个夜晚——她正打开门要走。这一次,我先把戒指递了出去:「分手,但戒指你拿好,下个月你会用到它。」", style: "日系动画" }, { title: "重生回到高考前", outline: "我重生回到高考前一周。这一次,我提前知道了每一道压轴题,也知道了——三天后,她会在天台上跳下去。", style: "吉卜力" }, { title: "墓前签到", outline: "我每天去亡妻的墓地签到,第七天,系统弹出一行字:「奖励到账:未亡人 × 1。」墓碑后走出一个长得和她一模一样的姑娘:「你是…谁?」", style: "二次元" }, { title: "凌晨四点抽卡", outline: "凌晨三点,我十连抽 SSR 出货,光柱从屏幕里溢出来。客厅响起脚步声,一个穿着我 T 恤的女人揉着眼睛走出来:「老公,你也太晚了。」", style: "3D 渲染" }, { title: "系统选妃", outline: "系统给了我七个未婚妻候选,每错一个,地图上就有一座城被抹掉。倒计时 30 秒,她们七个同时朝我看过来。", style: "二次元" }, { title: "穿成废柴皇子", outline: "睁眼是冷宫废柴皇子,太监正在念赐死圣旨。我笑了——上辈子读的那本《这就是大唐》,是我自己写的。", style: "国风水墨" }, { title: "穿成乙游男配", outline: "我穿成了乙游里第一章就被处刑的反派男配。倒计时三个月。可女主她…昨天竟然主动来找我了。", style: "二次元" }, { title: "毒酒之后", outline: "睁眼是 1928 年,我刚被亲弟弟下毒,倒在少帅府的红毯上。门外军靴声逼近——他来确认我是不是真死了。", style: "真实系" }, { title: "九重雷劫", outline: "修了三百年,今夜九重雷劫降下。第八道劫雷劈开时,我看见劫云之上,那个一直在偷偷护我的人,竟是她。", style: "玄幻" }, { title: "山门扫地僧", outline: "我在山门扫地三十年,谁都看不起我。今日魔尊踏破山门,宗主跪地求饶。我抬头:「让一让,我去扫他。」", style: "国风水墨" }, { title: "末世第一夜", outline: "同寝的兄弟开始啃我的脖子。我抬手将他甩开——指尖滴下的血珠悬在半空,凝结成了一柄银白小剑。", style: "真实系" }, { title: "雷霆觉醒", outline: "雷劈不死的第七天,我握紧了拳头。掌心炸开一道闪电,把面前的丧尸群一齐劈成了灰。", style: "赛博朋克" }, { title: "家宴镇压", outline: "家宴上岳父冷笑:「你也敢上桌?」我手机震了一下,是父亲发来的:「儿,神州七大家主,已到楼下。」", style: "真实系" }, { title: "买葱归来", outline: "二十年前那场天工大会上消失的人——今天回菜市场买葱,被小贩多收了两毛。他笑了:「这二十年的利息,连本带利,今晚一起还。」", style: "国风水墨" }, { title: "红盖头之下", outline: "敌对家族送来一个新娘,遮着红盖头。我掀开那一刻,下面是和我死去的妹妹一模一样的脸。她抬眼:「哥…你别杀我。」", style: "超写实" }, { title: "上海双面谍", outline: "1936 年。军统让我潜入日方,日方让我潜入军统。今晚——他们要见面,而我必须同时出现在两间房里。", style: "真实系" }, { title: "比武场的茶博士", outline: "比武大会上,我端着茶水路过,宗主们的剑突然全都举不起来了。我抬眼:「老衲只是看不下去你们吵架。」", style: "国风水墨" }, { title: "高考前夜", outline: "全市模考垫底的我,高考前夜被四个西装男按在桌前:「这次,你必须考第一。」原来——我爸是教育部的人。", style: "日系动画" }, { title: "失踪一年", outline: "我被宣告死亡 12 个月后,背着血迹斑斑的包,站在了她婚礼现场的门口。新郎认出我,杯子摔到了地上。", style: "真实系" }, { title: "天台堵她", outline: "学校最不好惹的那位转学生,第一天就堵了我的天台。我把她书包一扯——里面掉出来一沓我从小写的情书。", style: "日系动画" }, { title: "转学第一天", outline: "转学第一天,年级第一坐我后桌。下课她把试卷拍在我面前:「这道题,你为什么写得和我答案一字不差?」", style: "二次元" }, { title: "无职觉醒", outline: "成年礼上全班觉醒职业,只有我天命「无职」。所有人嘲笑我的时候,光柱从我身上炸开——觉醒结果:「神」。", style: "玄幻" }, { title: "草稿纸里的我", outline: "睁眼发现自己是一张草稿纸上的火柴小人,住在 16-bit 的网格世界里。橡皮擦从天而降,正在抹掉这一行字——也包括我。", style: "像素风" }, { title: "云上的国家", outline: "齿轮轰鸣的飞艇甲板上,独眼船长把望远镜递到我手里:「云的那一头有个国家,专门关像你这样的人。」", style: "蒸汽朋克" }, { title: "舰桥上的少年", outline: "殖民母舰只剩 30 秒,主炮指挥官的椅子是空的。舰长抬眼看着 17 岁的我:「上去。整个人类,就交给你了。」", style: "赛博朋克" }, { title: "末节队长服", outline: "全联盟都骂我废柴,机甲赛决赛末节,教练把队长徽章按在我手里:「上去,把这局赢回来——这一台,是人类最后的机甲。」", style: "赛博朋克" }, { title: "学长的真面目", outline: "三年青梅当众接过富二代的玫瑰,转身扑进他怀里。我笑了笑——明天,是我接手父亲那个上市公司的日子。", style: "真实系" }, ], 女性向: [ { title: "废柴嫡女", outline: "穿成将军府众人嫌弃的废柴嫡女,第一天就被打了一巴掌。门外冷面摄政王翻身下马,「我夫人的脸,谁敢动?」", style: "国风水墨" }, { title: "乙游恶役", outline: "睁眼是乙游里五分钟必死的恶役千金,所有男主都恨我。我合上剧本笑了——上一世我是这游戏的主笔。", style: "二次元" }, { title: "白月光归来", outline: "穿成男主念念不忘的白月光,但全书她只有死亡这一种结局。我捏着男主送的玉佩走进祠堂——这一次,我不躲了。", style: "玄幻" }, { title: "凤袍之下", outline: "穿越来就是当朝皇后,三千佳丽看我笑话。皇上掀开龙袍跪在我面前:「皇后,朕想她想了三十年了。」", style: "国风水墨" }, { title: "嫁错重生", outline: "嫁错了人毁了一辈子,重生回到婚礼前夜。这一次新娘休书我先写。新郎的弟弟突然走进来:「嫂子,要换人,换我。」", style: "二次元" }, { title: "那杯咖啡", outline: "重生回到他亲手把我送进车祸的前夜。我笑着接过他递来的咖啡——这是一杯我前世死前最想泼他脸上的咖啡。", style: "真实系" }, { title: "雨中撑伞", outline: "重生回到我亲手要了她命的前一天。她正抱着公文包路过我的车——这一次,我下车撑伞。", style: "真实系" }, { title: "三十亿合同", outline: "重生回到我被父亲扫地出门的那个清晨。这一次,扫地出门前我把家族 30 亿的合同提前签了。", style: "真实系" }, { title: "替嫁霸总", outline: "替姐姐嫁给那个传说眼瞎心冷的总裁。新婚夜他俯身在我耳边:「你姐没告诉你?我等了你三年了。」", style: "二次元" }, { title: "错嫁那一夜", outline: "醉酒夜我闯进了错的酒店房间,醒来戒指已在手上。他穿好西装回头:「夫人,签字仪式三小时后。」", style: "真实系" }, { title: "撕了离婚书", outline: "为了避税,我和那个最讨厌我的总裁假结婚一年。半年后他突然把离婚协议撕了——「续约。」", style: "真实系" }, { title: "死对头跪了", outline: "天天和我互掐的死对头,今天跪在我面前。他递上戒指:「再吵下去要影响我们的孩子。」——什么孩子?!", style: "二次元" }, { title: "抽到的霸总", outline: "凌晨四点抽到 UR 卡——画面里是城里那个传说没人见过脸的盛家总裁。第二天他敲我家门:「我来报到。」", style: "3D 渲染" }, { title: "攻略任务", outline: "系统说:「攻略他,否则你死。」可他是这本书里唯一恨我入骨的人。今天他亲手把我堵在了墙角。", style: "二次元" }, { title: "商城上架", outline: "系统商城上架了「市值 800 亿盛总 × 1」。我咬牙刷光积蓄。下一秒,他出现在我家门口:「夫人,我已购入。」", style: "二次元" }, { title: "老公赞助", outline: "直播间打赏榜第一名连续 30 天,备注写着「老公赞助」。我点开他的资料——城里那位传说从不出门的盛少。", style: "日系动画" }, { title: "门外的他", outline: "末世第一夜,门外是丧尸群的撕咬声。隔壁刚搬来的男人撞开我家门:「我能进来吗?我有一把枪。」", style: "真实系" }, { title: "末世空间", outline: "末世爆发的第一天,我意外觉醒了储物空间。屯了三车物资回家,发现那个总欺负我的高冷邻居跪在我门口。", style: "真实系" }, { title: "异能撒娇", outline: "末世里所有男人都怕的那位 S 级异能者,今天蹲在我家门口:「姐姐,能让我进去吗?外面…丧尸太可怕了。」", style: "二次元" }, { title: "末世重生", outline: "重生回到末世爆发前一周。这一次,那个抛弃我的男人——我先把他赶出门,把上一世救我的人接回家。", style: "真实系" }, { title: "课桌里的纸条", outline: "隔壁班那个高冷年级第一,今天把一本日记塞进我课桌。第一页写着:「她笑起来的时候,三角函数都没那么复杂。」", style: "二次元" }, { title: "校草八年", outline: "暗恋了八年的校草,今天突然走到我面前:「跟我走,我已经查清楚了——把你妹妹接走的那个人在哪。」", style: "吉卜力" }, { title: "班长的秘密", outline: "天天和我同桌的班长,今天被四个保镖按在校门口接走。临走前他回头喊:「老婆,我先回总部一趟。」", style: "二次元" }, { title: "走廊的手腕", outline: "走廊上人最多的时候,全校最不好惹的学长抓住了我的手腕:「我等了你三年,今天给我一个回应。」", style: "日系动画" }, { title: "上海公馆", outline: "1936,我是父亲遗产的唯一继承人,全上海都在等看我嫁谁。今晚我推开门——那个传说不要女人的留洋先生,在喝我父亲的茶。", style: "超写实" }, { title: "书店里的他", outline: "我是租界一家书店的老板娘。今晚穿西装的他第三次坐在窗边,第一次开口:「小姐,可以借您的店…藏一个东西吗?」", style: "真实系" }, { title: "炼丹意外", outline: "我是仙门最废柴的炼丹弟子,三年没炼出一颗丹。今天偶然撞翻师尊的丹炉——一道光柱直冲云霄,惊动了三大长老。", style: "玄幻" }, { title: "江湖归人", outline: "我一个人闯江湖三年,今天回到那座小镇。门口的少年抬头:「师姐,你说过五年就回,我等了三年又两个月。」", style: "国风水墨" }, { title: "顶流的西瓜", outline: "顶流男星上节目被问感情,他笑了笑:「我老婆?她现在大概在家里啃我刚买的西瓜。」全网爆炸——我正趴在沙发上看直播。", style: "真实系" }, { title: "同居一年", outline: "和合租室友同居一年了,今晚他突然把我堵在门口:「你说,我们…要不要别再装陌生人了?」", style: "日系动画" }, { title: "机甲撞门", outline: "丧尸潮第七夜,全城断电。地下室的门被撞开,一架满是弹痕的机甲低下头,舱门弹开——里面坐着我那个失联三年的他。", style: "赛博朋克" }, { title: "三分绝杀", outline: "决赛最后一秒,他在场边看了我一眼,转身投出那一记三分。哨声响时,他把奖杯举过头顶,朝我跑来。", style: "日系动画" }, ], }; /* ---------- typewriter ---------- */ function Typewriter({ phrases }: { phrases: string[] }) { const [txt, setTxt] = useState(""); useEffect(() => { let p = 0; let i = 0; let del = false; let timer: ReturnType; setTxt(""); const tick = () => { const full = phrases[p] ?? ""; if (!del) { i++; setTxt(full.slice(0, i)); if (i >= full.length) { del = true; timer = setTimeout(tick, 1700); return; } timer = setTimeout(tick, 70); } else { i--; setTxt(full.slice(0, i)); if (i <= 0) { del = false; p = (p + 1) % phrases.length; timer = setTimeout(tick, 450); return; } timer = setTimeout(tick, 28); } }; timer = setTimeout(tick, 500); return () => clearTimeout(timer); }, [phrases]); return ( <> {txt} ); } /* ---------- masonry story card ---------- */ function StoryCard({ title, outline, image, onClick, }: { title: string; outline: string; image: string; onClick: () => void; }) { // 全卡片统一 4:5 portrait 比例。原来按图片真实 naturalWidth/Height 动态设 aspectRatio // 会跟懒加载顺序耦合:视口下方还没加载的卡停在 placeholder 比例,上方已加载的卡变成 // 图片真实比例(可能是 1.6 横图或 0.75 竖图),视觉差异巨大;刷新后图从缓存读, // onLoad 几乎同步触发,看起来又恢复正常 —— 用户感知到的「偶尔尺寸不一样」就是这个。 // 改为固定比例后所有卡片视觉一致,object-cover 让不同长宽比的图自动裁切适配。 return ( ); } /* ---------- collapsible category selector ---------- */ function CategorySelect({ label, items, value, open, onToggle, onPick, }: { label: string; items: string[]; value: number; open: boolean; onToggle: () => void; onPick: (i: number) => void; }) { return (
{open && (
{items.map((it, i) => ( ))}
)}
); } /* ---------- style picker modal ---------- */ function StyleModal({ items, value, onPick, onClose, }: { items: string[]; value: number; onPick: (i: number) => void; onClose: () => void; }) { const [q, setQ] = useState(""); const [shown, setShown] = useState(false); useEffect(() => { const id = requestAnimationFrame(() => setShown(true)); return () => cancelAnimationFrame(id); }, []); const close = () => { setShown(false); setTimeout(onClose, 280); }; const list = items.map((name, i) => ({ name, i })).filter((x) => x.name.includes(q.trim())); return (
e.stopPropagation()} className={ "flex w-[1000px] max-w-[94vw] max-h-[86vh] flex-col overflow-hidden rounded-sm border border-clay-900/15 bg-cream-50 shadow-2xl shadow-clay-900/25 transition-all duration-300 " + (shown ? "opacity-100 scale-100" : "opacity-0 scale-95") } >
选择绘画风格 默认「自动」· 由模型根据 prompt 判断风格
setQ(e.target.value)} placeholder="搜索风格…" autoFocus className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-10 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" />
{list.map(({ name, i }) => ( ))} {list.length === 0 && (
没有匹配的风格
)}
); } /* ---------- page ---------- */ export default function HomePage() { const router = useRouter(); const [sel, setSel] = useState(OPTS.map((o) => o.defaultIndex ?? 0)); const [open, setOpen] = useState(-1); const [styleOpen, setStyleOpen] = useState(false); const [prompt, setPrompt] = useState(""); const inputRef = useRef(null); // 顶部使用提示:默认展示,用户可点 × 永久关闭(localStorage:infiplot:hintClosed)。 const [hintClosed, setHintClosed] = useState(false); const styleRow = OPTS.findIndex((o) => o.modal); const genderIndex = sel[0] ?? 0; const gender = (OPTS[0]!.items[genderIndex] as Gender) ?? "男性向"; const phrases = EXAMPLE_PHRASES[gender]; // 性向切换时,整片瀑布流做淡出→换图→淡入的过渡(而非瞬切)。 const [galleryGender, setGalleryGender] = useState(gender); const [fading, setFading] = useState(false); useEffect(() => { if (gender === galleryGender) return; setFading(true); const t = setTimeout(() => { setGalleryGender(gender); setFading(false); }, 280); return () => clearTimeout(t); }, [gender, galleryGender]); /* close any open dropdown on outside click */ useEffect(() => { const h = (e: MouseEvent) => { const target = e.target as HTMLElement | null; if (!target?.closest?.("[data-cat]")) setOpen(-1); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); useEffect(() => { try { if (localStorage.getItem("infiplot:hintClosed") === "1") setHintClosed(true); } catch { /* ignore */ } }, []); // 输入框随内容自动增高:长文本整段可见(打字与点卡片填入都覆盖)。 useEffect(() => { const el = inputRef.current; if (!el) return; el.style.height = "auto"; el.style.height = `${el.scrollHeight}px`; }, [prompt]); const closeHint = () => { setHintClosed(true); try { localStorage.setItem("infiplot:hintClosed", "1"); } catch { /* ignore */ } }; const start = () => { const userPrompt = prompt.trim(); const artStyle = OPTS[1]!.items[sel[1] ?? 0]!; const plotStyle = OPTS[2]!.items[sel[2] ?? 1]!; const voice = OPTS[3]!.items[sel[3] ?? 1]!; const pace = OPTS[4]!.items[sel[4] ?? 1]!; const worldSetting = [ `这是一款面向【${gender}】观众的 AI 交互剧情游戏。`, `剧情风格:${plotStyle}。内容节奏:${pace}。`, userPrompt ? `玩家给出的故事种子:「${userPrompt}」。` : "", `请依据上述设定,以极致的戏剧张力与细腻的情感起伏,为玩家编织精彩的故事分支与对话。`, ] .filter(Boolean) .join("\n"); // 「自动」→ fall back to 二次元 (project default). Plain prompts like // "由模型自动判断画风" are not understood by FLUX — it just paints them // literally, so we'd rather lock in a sensible default. // TODO(自动路由): 后续实现真正的「自动」——由模型依据世界观 / 玩家 prompt // 选出最合适的画风,再映射到对应风格提示词,而非固定回退到二次元。届时 // 同步更新风格弹窗副标题(「由模型根据 prompt 判断风格」)使文案与行为一致。 const effectiveStyle = artStyle === "自动" ? "二次元" : artStyle; const styleGuide = STYLE_MAP[effectiveStyle] ?? STYLE_MAP["二次元"]!; const audioEnabled = voice === "开启"; sessionStorage.setItem( "infiplot:custom", JSON.stringify({ worldSetting, styleGuide, audioEnabled }), ); router.push("/play?custom=1"); }; const stories = STORIES[galleryGender]; const imgPrefix = galleryGender === "女性向" ? "f" : "m"; // 点卡片 = 直接开始这张卡的故事,零等待:跳 /play?card=m0/f0... 由 /play // 页面从 /home/firstact/{name}.json 静态文件加载预烘焙好的首幕(含 scene / // 角色 / 图片 URL / storyState),整张图都已在 FLUX 上画好且 URL 缓存命中。 // 「语音配音」选择仍然生效:把 audioEnabled 留在 sessionStorage 里,/play 的 // useState 初始化器会读它来设 muted 初值。其余选项(剧情风格 / 内容节奏) // 在预烘焙时已锁成「多线转折 / 紧凑爽快」的红果默认基调,对精选卡不再生效。 const onCardClick = (idx: number, card: StoryContent) => { const voice = OPTS[3]!.items[sel[3] ?? 1]!; const audioEnabled = voice === "开启"; // 复用 infiplot:custom 这个 key 只为传递 audioEnabled —— ws/sg 在 ?card= 路径 // 上不会被读取(/play 里 cardName 优先级高于 sessionStorage)。这样实现量最小, // 不必另起一个 audio-only 的 storage key。 sessionStorage.setItem( "infiplot:custom", JSON.stringify({ worldSetting: "", styleGuide: "", audioEnabled }), ); router.push(`/play?card=${imgPrefix}${idx}`); }; return (
{/* ================== HEADER ================== */}
InfiPlot
{/* ================== HERO 控制区(居中,呼应原型布局) ================== */}

今天想体验什么故事?

{/* prompt 输入(居中) */}
{ e.preventDefault(); start(); }} className="mx-auto mt-9 md:mt-12 max-w-[760px]" >