UI 进阶指南 · 立绘、开场白、地图、交互与点击事件
进阶 UI 开发涉及更多前端技术与 RPH 平台交互机制。建议掌握 UI 基础后再阅读本文。
一、动态立绘
1.1 变量定义
在角色变量结构中增加 portraitTag 字段:
{ "girls": [ { "name": "小满", "portraitTag": "young adult woman, long black hair, loose ponytail, shy expression, fitted black dress, full body, standing, visual novel character sprite, simple background, no text" } ]}1.2 AI 维护规则
在变量说明或世界书中加入以下规则,指导 AI 每轮维护立绘提示词:
【AI动态立绘 UI · portraitTag 规则】
1. portraitTag 必须是英文绘图关键词,不要写中文2. 只描述角色立绘,不描述剧情插图3. 推荐包含:young adult woman, hair color, hair style, eye color, expression, outfit, full body, standing, visual novel character sprite, simple background, no text4. 同一角色必须保持固定外貌一致5. 普通对话不要频繁改变 portraitTag6. 只有首次登场、换装、受伤、觉醒、身份变化、明显状态变化时才更新7. 禁止使用 &、#、引号、尖括号,避免破坏图片地址1.3 HTML 模板结构
<div class="portrait-stage"> <div class="portrait-topline"> <span>AI GENERATED PORTRAIT</span> <span class="portrait-badge">{{name}}</span> </div> <div class="portrait-frame"> <img class="portrait-img js-auto-portrait" data-tag="{{portraitTag}}" alt="{{name}}立绘"> <div class="portrait-loading"> 正在调用 RP Hub 生图设置生成立绘<br> 如果一直不显示,请确认已在网站设置里填写自动生图密钥 </div> </div></div>1.4 CSS 样式
.portrait-stage { margin: 8px 0 12px; border: 1px solid rgba(244,114,182,.28); border-radius: 14px; overflow: hidden; background: radial-gradient(circle at 50% 12%, rgba(244,114,182,.18), rgba(2,6,23,.96) 68%);}.portrait-topline { display: flex; justify-content: space-between; padding: 7px 10px; font-size: 10px; color: rgba(226,232,240,.72); background: rgba(2,6,23,.72);}.portrait-badge { color: #f472b6; font-weight: 700; }.portrait-frame { position: relative; width: 100%; aspect-ratio: 2 / 3; min-height: 260px; display: flex; align-items: center; justify-content: center;}.portrait-img { display: none; width: 100%; height: auto; object-fit: contain; }.portrait-frame.loaded .portrait-img { display: block; }.portrait-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 12px; line-height: 1.7; color: rgba(226,232,240,.72); padding: 18px;}.portrait-frame.loaded .portrait-loading { display: none; }1.5 JavaScript 脚本
(function () { const NEG = 'bad anatomy,bad hands,blurry,low quality,text,watermark,worst quality'; const ARTIST = 'visual novel character sprite, very aesthetic, masterpiece, no text';
async function readSettings() { try { const db = await new Promise((ok, bad) => { const r = indexedDB.open('RPHubDB', 1); r.onsuccess = () => ok(r.result); r.onerror = () => bad(r.error); }); for (const storeName of Array.from(db.objectStoreNames || [])) { const data = await new Promise(resolve => { try { const req = db.transaction([storeName], 'readonly').objectStore(storeName).get('settings'); req.onsuccess = () => resolve(req.result); req.onerror = () => resolve(null); } catch (e) { resolve(null); } }); const settings = data && data.value ? data.value : data; if (settings && settings.imageGenKey) return settings; } } catch (e) {} return {}; }
function cleanTag(tag) { return String(tag || '').replace(/[<>"'&]/g, ' ').replace(/\s+/g, ' ').trim(); }
function buildUrl(tag, settings) { const token = String(settings.imageGenKey || '').trim(); if (!token) return ''; const base = token.toUpperCase().startsWith('STA1N') ? 'https://nai.sta1n.cn' : 'https://std.loliyc.com'; const fullTag = cleanTag(tag) + ', solo, full body, standing, visual novel character sprite, simple background, no text'; return base + '/generate?tag=' + encodeURIComponent(fullTag) + '&token=' + encodeURIComponent(token) + '&model=nai-diffusion-4-5-full' + '&artist=' + encodeURIComponent(ARTIST) + '&size=' + encodeURIComponent(settings.imageSize || '竖图(-1)') + '&steps=40&scale=6&cfg=0&sampler=k_dpmpp_2m_sde' + '&negative=' + encodeURIComponent(NEG) + '&nocache=0&noise_schedule=karras'; }
async function renderPortraits() { const settings = await readSettings(); document.querySelectorAll('.js-auto-portrait').forEach(img => { const frame = img.closest('.portrait-frame'); const url = buildUrl(img.getAttribute('data-tag'), settings); if (!url) return; img.onload = () => frame && frame.classList.add('loaded'); img.onerror = () => { const tip = frame && frame.querySelector('.portrait-loading'); if (tip) tip.innerHTML = '立绘生成失败<br>请检查密钥、额度或提示词'; }; img.src = url; }); }
renderPortraits();})();在向 AI 描述 UI 模板时,确保
initialVariableState中包含portraitTag,htmlTemplate中包含上述 HTML + CSS,script中包含上述读取密钥和生成图片的 JS。
二、全屏演出动画开场白
2.1 实现原理
使用短暗号触发正则替换,将暗号替换为完整的 HTML 开场白页面,以 iframe 形式嵌入正文。
2.2 基础模板
<div class="rp-open"><style>.rp-open{ position:relative; left:50%; transform:translateX(-50%); width:min(96vw,1360px); height:min(72vh,680px); min-height:480px; margin:8px 0; overflow:hidden; border-radius:18px; background:#080b12; color:#f4e7c2; font-family:"Microsoft YaHei","Noto Serif SC",serif; border:1px solid rgba(230,205,130,.28); box-shadow:0 18px 55px rgba(0,0,0,.42);}.rp-open *{box-sizing:border-box}.rp-open input{position:absolute; opacity:0; pointer-events:none}/* ... 更多样式请按需从源文件复制 ... */</style>
<input type="radio" name="rp-open-state" id="rp-s1" checked><input type="radio" name="rp-open-state" id="rp-s2"><!-- 继续添加场景所需的页面数量 -->
<div class="rp-bg"> <section class="rp-page p1"> <div class="rp-card"> <div class="rp-small">THE STORY BEGINS</div> <div class="rp-title">灰冠学院</div> <div class="rp-sub">旧王冠沉没后的第四百一十七年。</div> <label class="rp-btn" for="rp-s2">继续</label> </div> </section> <!-- 添加更多场景页面... --></div></div>2.3 核心结构说明
| 元素 | 作用 |
|---|---|
<input type="radio"> | 控制页面切换,每个场景一个 radio |
label[for="rp-sX"] | 点击跳转到对应场景页 |
.rp-page | 每个场景的内容页面 |
.rp-line.lX | 逐字打字机效果的文字行 |
.rp-btn-row | 多按钮选择行 |
.rp-bg | 背景层,含渐变和网格装饰 |
.rp-card | 内容卡片容器 |
2.4 动画效果
| 动画 | 说明 |
|---|---|
rpFade | 页面切换时的淡入上滑效果 |
rpCaret | 打字机光标闪烁效果 |
rpTypeX | 逐字显示动画,每个场景独立控制速度 |
2.5 正则替换配置
使用短暗号(如 <<<intro>>>)确保不会在正文中误触发,正则替换为完整的开场白 HTML。
如果需要 iframe 形式隔离,可使用以下 JS 结构:
(function(){ const html = `<!DOCTYPE html><html>...完整HTML...</html>`; const frame = document.currentScript.previousElementSibling; frame.srcdoc = html;})();三、实时地图 UI 模板
3.1 地图三要素
| 要素 | 说明 |
|---|---|
| 地点节点 | 地图上有哪些地方 |
| 节点位置 | 这些地方摆在地图哪里 |
| 状态变量 | 每个地方当前的状态(当前位置、可前往、锁定、危险、已完成) |
3.2 通用模板
<div class="rp-map"><style>.rp-map{ --bg:#101722; --panel:#182131; --gold:#d8bd76; --text:#eee3c8; --muted:#9faabd; --danger:#b85656; --open:#6f9f74; --locked:#596171; width:min(96vw,980px); margin:12px auto; padding:16px; border-radius:18px; background:linear-gradient(135deg,#101722,#20293b); border:1px solid rgba(216,189,118,.35); color:var(--text); font-family:"Microsoft YaHei","Noto Serif SC",serif; box-shadow:0 16px 42px rgba(0,0,0,.35);}/* 完整样式请参考实时地图UI简易模板教程.txt */</style>
<div class="map-head"> <div> <div class="map-title">{{currentRegion}}</div> <div class="map-sub">当前位置:{{currentLocation}} · 地图状态:{{mapStatus}}</div> </div> <div class="map-danger">危险等级:{{dangerLevel}}</div></div>
<div class="map-body"> <!-- 节点 --> <div class="map-node node-start {{nodeStartClass}}"> <div class="node-name">入口</div> <div class="node-state">{{nodeStartStatus}}</div> </div> <div class="map-node node-center {{nodeCenterClass}}"> <div class="node-name">中央区域</div> <div class="node-state">{{nodeCenterStatus}}</div> </div> <!-- 更多节点... --></div>
<div class="map-info"> <div class="info-card"> <div class="info-label">可前往路线</div> <div class="info-text">{{availableRoutes}}</div> </div> <div class="info-card"> <div class="info-label">最近事件</div> <div class="info-text">{{latestEvent}}</div> </div></div></div>3.3 变量示例
{ "currentRegion": "起始区域", "currentLocation": "入口", "mapStatus": "初始探索", "dangerLevel": "低",
"nodeStartClass": "active", "nodeStartStatus": "当前所在地",
"nodeCenterClass": "open", "nodeCenterStatus": "可前往",
"nodeEastClass": "locked", "nodeEastStatus": "尚未解锁",
"nodeWestClass": "locked", "nodeWestStatus": "尚未解锁",
"availableRoutes": "中央区域", "latestEvent": "玩家抵达入口,地图开始记录当前探索状态。"}3.4 节点状态
| 类名 | 视觉效果 | 含义 |
|---|---|---|
active | 高亮显示 | 当前所在地 |
open | 正常显示 | 已开放,可以前往 |
locked | 变灰/锁定 | 未解锁 |
danger | 警戒效果 | 危险区域 |
cleared | 已标记 | 已完成区域 |
3.5 变量说明(给 AI 看)
currentRegion:当前大区域名称currentLocation:玩家当前所在地点mapStatus:当前地图阶段(初始探索、调查中、战斗后、封锁中)dangerLevel:危险等级(低、中、高、极高)
nodeXXClass:控制地图节点样式,可填 active/open/locked/danger/clearednodeXXStatus:显示地点下面的小字状态
availableRoutes:当前可前往地点latestEvent:最近一次地图变化或剧情事件3.6 世界书更新规则
【实时地图更新规则】
- 每次剧情推进后,必须更新地图变量- currentLocation 必须等于玩家当前实际所在地点- 玩家进入某地点后,该地点 nodeClass 改为 active,其他地点根据情况改为 open/locked/danger/cleared- 如果玩家解锁新地点,将对应 nodeClass 从 locked 改为 open- 如果某地点发生危险事件,将对应 nodeClass 改为 danger- availableRoutes 只显示当前剧情下玩家合理可前往的地点- latestEvent 用一句话总结本次地图变化- 不得让 UI 地图显示与正文剧情冲突3.7 节点位置修改
在 CSS 中调整百分比值:
.node-start { left: 12%; top: 42%; }.node-center { left: 42%; top: 28%; }.node-east { right: 13%; top: 42%; }.node-west { left: 50%; bottom: 13%; }
/* 移动端适配 */@media (max-width: 720px) { .node-start { left: 8%; top: 42%; } .node-center { left: 36%; top: 18%; } .node-east { right: 6%; top: 45%; } .node-west { left: 34%; bottom: 12%; }}四、全息地图(同心圆环交互)
4.1 设计理念
以同心圆环形式展示多层区域地图,每一层可独立点击并显示详情。适合岛屿、城市圈、秘境等层级式地图。
4.2 核心结构
<div class="radar-ring-container"> <!-- 外层:边缘浅滩 --> <div class="ring-layer ring-shallows" data-region="边缘浅滩" data-desc="暗礁密布的珊瑚浅滩与红树林带..." data-risk="潮汐异动 · 灵汐族圣临之地"> </div> <!-- 第二层:翠叶密林 --> <div class="ring-layer ring-jungle" data-region="翠叶密林" data-desc="占岛屿面积60%的原始密林..." data-risk="猛兽出没 · 远古守护"> </div> <!-- 第三层:岩脊高地 --> <div class="ring-layer ring-highland" data-region="岩脊高地" data-desc="险峻的岩壁与铁皮竹林..." data-risk="落石险径 · 灵压紊乱"> </div> <!-- 核心:灵泉盆地 --> <div class="ring-layer ring-core" data-region="灵泉盆地 · Elyria之冠" data-desc="生命之泉与母树 Elyria 所在..." data-risk="能量衰微 · 命运枢纽"> </div></div>
<!-- 右侧详情展示区 --><div class="info-pavilion" id="infoPanel"> <div class="region-name"> <span>🌊</span> <span id="regionNameDisplay">边缘浅滩</span> </div> <div class="region-desc" id="regionDescDisplay">...</div> <div class="region-tag" id="regionRiskDisplay">⚠️ 潮汐异动</div></div>4.3 交互逻辑
- 点击任意环层 → 该层高亮 → 右侧面板更新为该区域的名称、描述和风险标记
- JS 使用
dataset读取每个环层的data-region、data-desc、data-risk - 包含波纹点击反馈效果
4.4 完整代码参考
完整实现请参见 全息地图.txt 源文件,内含全部 CSS 样式、JS 交互逻辑以及层级响应式适配。
五、适配边缘地图
5.1 说明
这是一个完整的 RPG 风格地图 HTML 页面,使用 CSS 网格 + SVG 路线绘制,包含已解锁/未锁定两种状态标记,适用于幻想风格角色卡 UI。
5.2 核心特征
| 特征 | 说明 |
|---|---|
| 地图容器 | .map-container 居中布局,仿古卷轴风格 |
| SVG 路线 | 使用 SVG path 绘制地点间连接线 |
| 地点标记 | .map-marker 包含 .unlocked 和 .locked 两种状态 |
| 提示框 | 点击 triggerSlash('/命令') 或弹出未解锁提示 |
| 图例 | 显示”已解锁 / 未解锁 / 当前进度” |
5.3 代码框架
<!DOCTYPE html><html lang="zh"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>幻想郷地図</title> <style> /* 完整样式请参考适配边缘地图.txt */ </style></head><body> <div class="map-container"> <button class="close-btn" onclick="handleClose()">✕</button> <div class="title-section"> <h2>幻想郷地図</h2> <span class="subtitle">RPG Map</span> </div> <div class="map-area"> <div class="map-grid"> <svg class="map-svg" viewBox="0 0 400 400"> <!-- SVG 路线 --> <path class="map-path" d="M200 100 Q250 140 280 180 T320 240"></path> </svg> <div class="map-locations"> <!-- 地点标记 --> <div class="map-marker unlocked" style="top:10%;left:48%"> <div class="marker-dot">1</div> <div class="marker-name">神社</div> </div> <div class="map-marker locked" style="top:55%;left:70%"> <div class="marker-dot">5</div> <div class="marker-name">香霖堂</div> </div> </div> </div> </div> <div class="legend"> <!-- 图例 --> </div> </div> <script> function handleClose() { if (window.triggerSlash) window.triggerSlash('/主页'); } function handleMarkerClick(number) { const commands = { '1': '/前往神社', '2': '/回家' }; if (window.triggerSlash) window.triggerSlash(commands[number]); } </script></body></html>完整实现请参考 适配边缘地图.txt 源文件。
六、暂存点击事件
6.1 说明
这是一个表单交互 UI,玩家可选择食材、放油量等选项,点击提交后通过 triggerSlash 将选择发送给 AI。
6.2 核心交互模式
- 使用
.toggle-btn切换按钮组实现选项选择 - 使用
+/-按钮控制油量数值 - 提交时将所有选择汇总为 JSON 字符串,通过
triggerSlash发送
6.3 关键代码
(function() { var state = { food: 'meat', useOil: 'yes', oilMl: 0 }; var submitBtn = document.getElementById('submitBtn');
function syncToButton() { submitBtn.dataset.food = state.food; submitBtn.dataset.oil = state.useOil; submitBtn.dataset.ml = state.oilMl; }
// 提交时触发 document.getElementById('submitBtn').onclick = function() { var p = { food: this.dataset.food === 'meat' ? '肉' : '菜', oil: this.dataset.oil === 'yes' ? '放油' : '不放油', ml: parseInt(this.dataset.ml) }; try { window.triggerSlash(JSON.stringify(p)); } catch(e) { alert(p.food + ' | ' + p.oil + ' | ' + p.ml + 'ml'); } };})();完整实现请参考 暂存点击事件.txt 源文件。
七、正文悬浮球正则卡
工具型卡片:通过正则追加完整 HTML loader,由 RP-Hub 包装成可执行 iframe,向父页面注入可拖动悬浮球菜单,在卡片卸载时自动清理。
7.1 正则配置
{ "name": "正文悬浮球菜单 父页面注入自动清理版", "regex": "^(?![\\s\\S]*rp-parent-float-owner-v2)([\\s\\S]*)$", "flags": "g", "replacement": "$1\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <style>html,body{margin:0;padding:0;width:1px;height:1px;background:transparent;overflow:hidden;font-size:0}</style>\n</head>\n<body>\n<script>\n(function(){\n // ...完整脚本见 正文悬浮球正则卡.json 源文件...\n})();\n<\/script>\n</body>\n</html>", "placement": [2], "markdownOnly": true, "promptOnly": false, "runOnEdit": true, "scope": "character", "disabled": false}7.2 功能说明
- 检测页面上是否已注入悬浮球(通过
rp-parent-float-owner-v2标记) - 未注入时,追加完整的 HTML 文档触发
createExecutableHtmlIframe - 向父页面注入可拖动的圆形悬浮球按钮
- 点击悬浮球展开快捷菜单
- 菜单包含多个功能按钮,点击触发
triggerSlash - 页面卸载时自动清理 DOM 元素
7.3 完整代码
完整实现请参考 正文悬浮球正则卡.json 源文件,内含:
- 悬浮球 CSS 样式(圆球 + 阴影 + 渐变)
- 菜单 CSS 样式(弹出动画 + 半透明背景)
- 拖动功能(
mousedown/mousemove/mouseup) - 存储位置到 localStorage
- 菜单按钮功能映射
pagehide/beforeunload自动清理
八、人物部位点击说明
8.1 说明
在角色立绘上放置透明热区按钮,玩家点击不同部位时在右侧面板显示对应描述。适用于角色介绍、服装展示、身体状态说明等场景。
8.2 HTML 结构
<div class="viewer"> <section class="image-card"> <div class="image-map-wrap"> <img class="character-image" src="人物图片URL" alt="人物示意图">
<!-- 透明热区按钮 --> <button class="hotspot" style="left: 47.77%; top: 19.65%; width: 19.07%; height: 11.79%;" data-name="头部" data-desc="这是头部的描述。"></button> <button class="hotspot" style="left: 48.39%; top: 31.43%; width: 17.84%; height: 15.23%;" data-name="躯干" data-desc="这是躯干的描述。"></button> <!-- 更多热区... --> </div> </section>
<aside class="info-card"> <h1 class="info-title" id="part-name">请选择部位</h1> <p class="info-desc" id="part-desc">点击图片中的区域查看说明。</p> </aside></div>8.3 CSS 要点
.hotspot完全透明(opacity: 0)- 热区位置和大小通过
left、top、width、height百分比定位 - 使用
aspect-ratio: 3/4保持图片比例
8.4 JavaScript
const hotspots = document.querySelectorAll('.hotspot');hotspots.forEach(function(hotspot) { hotspot.addEventListener('click', function() { document.getElementById('part-name').textContent = hotspot.dataset.name; document.getElementById('part-desc').textContent = hotspot.dataset.desc; });});8.5 如何适配自己的角色
- 替换
img的src为自己的角色图片 - 调整每个
.hotspot的left/top/width/height百分比使其匹配图片上的身体部位 - 修改
data-name和data-desc为对应部位的名称和描述 - 可在
info-card中加入部位列表芯片,展示全部可选部位
完整实现请参考 人物部位点击说明.html 源文件。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






