
从 SIKAYD (swap AU) 开始教你搭建一个角色反应类 WIP 项目展示页如果你最近在同人创作圈里刷到类似SIKAYD (swap AU) react to their originals!! ||WIP|| ||NO MI IDEA/not my idea这样的标题可能会好奇两件事第一这个标题里的 AU、swap、react、WIP 分别是什么意思第二这种“角色反应类”创作项目除了发图文、做视频能不能用网页形式展示出来。这篇文章不打算代替原作者解释剧情设定而是换一个技术视角假设你也在做一个同人 AU 项目并且希望以“角色卡片 角色反应对话”的形式展示给读者或团队成员那么从零搭建一个响应式静态页面就是最稳妥、成本最低的方案。全文会从概念讲起逐步完成一个可离线打开、可继续迭代的展示页面包含完整的 HTML、CSS、JavaScript 和 JSON 数据文件并补充本地调试、版本管理和常见报错的解决方法。无论你是刚接触前端的同人创作者还是想找一个轻量级“内容展示页”练手项目的开发者都能从中得到可以直接复用的代码与思路。1. 背景与核心概念1.1 这个标题到底在说什么先把标题拆开看一遍SIKAYD正文里并没有给出具体设定来源。它可能是某个 AU 里的角色名或项目代号不同作者笔下可能有不同解释。这里只作为项目名称来使用不展开角色背景。(swap AU)swap AU 是同人创作里常见的“交换设定”。比如把角色 A 和角色 B 的身份、性格、阵营互换产生一种“同一个原作但世界线不同”的感觉。react to their originals这是“反应类”创作核心逻辑是让 AU 世界里的角色去观看或阅读“原版世界”的故事然后给出反应。WIPWork In Progress意思是“创作中尚未完结”。NO MI IDEA / not my idea表示这个设定并非发布者原创发布者只是参与二次创作所以特别标出“不是我的点子”。这样理解之后这类项目本质上就是一个内容展示系统需要展示哪些角色、角色之间有怎样的对应关系、每个角色对原版剧情的反应是什么。这些内容如果只用聊天记录或长图来呈现后期更新会越来越麻烦而如果整理成结构化数据再用网页渲染就能做到“改数据即改页面”。1.2 为什么用网页来承载 WIP 创作内容很多创作者会先把设定写进文档再配图发布。但文档的缺点是互动感弱无法按角色筛选更新一多就难以维护。网页方案有四个明显优势。第一内容与表现分离。角色信息、台词、对应关系都放在数据文件里页面只负责渲染。想新增角色不用改 HTML只改数据即可。第二方便分享。静态页面可以部署到托管平台也可以打包发送给朋友双击index.html就能看。第三利于版本管理。所有内容都是文本文件适合放进 Git 仓库每次修改都有历史记录。第四门槛低。不需要数据库不需要后端服务只要会一点 HTML、CSS、JavaScript 就能完成。1.3 本教程的边界需要特别强调本教程不包含任何 SIKAYD 原作设定。如果你正在创作或收集这个项目的内容请一定以原作者发布的设定为准。本文的示例数据只是用于演示功能你可以随时替换成自己的角色和台词。同人创作最忌讳的事情就是把别人的设想直接当成自己的原创所以“not my idea”这样的标注在最终作品里应该保留。2. 环境准备与版本说明2.1 运行环境这个项目是纯前端静态页面对运行环境要求很低。我推荐你准备以下环境软件作用版本建议Windows / macOS / Linux开发环境任意主流的 64 位系统VS Code代码编辑器任何稳定版本均可Node.js本地静态服务器16 版本即可Chrome / Edge浏览器调试建议更新到较新版本版本需要根据你的项目实际情况调整。如果你的电脑没有安装 Node.js也可以用 VS Code 里的Live Server插件来启动本地服务如果连编辑器都不方便装直接双击index.html也能看到大部分效果只是加载本地 JSON 数据时会受到浏览器 CORS跨域读文件限制。相关问题会在第六节详细说明。2.2 为什么需要本地服务器现代浏览器出于安全考虑默认不允许通过file://协议读取同目录下的 JSON 文件。换句话说直接双击 HTML 文件时fetch(data.json)很可能失败。所以本教程采用两种规避方案把数据直接写进一个data.js文件以全局变量暴露不触发 CORS。启动本地 HTTP 服务器访问页面让fetch正常工作。两种方案都会在实战中演示你可以根据使用场景二选一。3. 项目结构设计3.1 从数据出发设计页面角色反应类页面的核心不是样式而是数据关系。在动手写代码之前建议先把内容梳理成三类数据角色原始信息原名、在 AU 中的名字、形象描述。角色对应关系swap AU 中原版角色 A 对应 AU 角色 B。反应台词AU 角色看到原版剧情后的发言以及原版角色可能的回应。上面三类数据可以自然映射到 JSON 对象中。为了便于维护我建议把每一组反应做成一个item多个item组成数组。每个item包含角色标签、标题、一句导语以及若干条对话记录。3.2 推荐目录结构本项目共 6 个文件结构如下sikayd-reaction-page/ ├── index.html ├── css/ │ └── style.css ├── js/ │ ├── data.js │ └── main.js └── assets/ └── README.txtindex.html是页面入口css/style.css负责全部样式js/data.js保存角色和台词数据js/main.js负责渲染页面assets/README.txt可以放一段来源说明比如“本项目为技术演示角色设定版权归原作者”。这个目录结构足够简单也保留了一定的扩展空间。后续如果增加图片只需要在assets下再建images目录。4. 核心代码实现4.1 页面骨架index.html新建index.html内容如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleSIKAYD Swap AU 反应展示页/title link relstylesheet hrefcss/style.css /head body header classhero h1SIKAYD (Swap AU) React to Their Originals/h1 p classsubtitleWIP · 角色反应类展示页 · 技术演示/p p classnotice本文仅作技术演示角色设定版权归原作者所有。/p /header main idapp section classintro h2项目说明/h2 p该页面用于展示不同角色之间的反应对话点击卡片即可切换内容。/p /section section idcard-list classcard-list p正在加载数据.../p /section /main footer classpage-footer pPowered by Vanilla JS | Data from js/data.js/p /footer script srcjs/data.js/script script srcjs/main.js/script /body /html这段 HTML 有三处关键点meta charsetUTF-8保证中文不乱码。link引入样式表。页面底部先加载data.js再加载main.js顺序不能颠倒因为main.js需要读取data.js定义的全局变量。4.2 基础样式css/style.css新建css/style.css先实现一套简洁的卡片式布局* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: PingFang SC, Microsoft YaHei, sans-serif; background-color: #f5f7fb; color: #2d3436; line-height: 1.6; } .hero { background: linear-gradient(135deg, #6c5ce7, #a29bfe); color: #fff; text-align: center; padding: 48px 20px; } .hero h1 { font-size: 28px; margin-bottom: 8px; } .hero .subtitle { font-size: 14px; opacity: 0.9; } .hero .notice { font-size: 12px; margin-top: 12px; opacity: 0.75; } main { max-width: 960px; margin: 0 auto; padding: 24px 16px; } .intro { background: #ffffff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); margin-bottom: 24px; } .card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } .reaction-card { background: #fff; border-radius: 14px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; } .reaction-card:hover { transform: translateY(-4px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); } .reaction-card h3 { margin-bottom: 8px; color: #6c5ce7; } .reaction-card .role-pair { font-size: 13px; color: #636e72; margin-bottom: 12px; } .reaction-card .dialog { border-left: 3px solid #a29bfe; padding-left: 12px; font-size: 14px; } .page-footer { text-align: center; padding: 32px 20px; color: #b2bec3; font-size: 13px; }样式里的关键点是grid-template-columns配合auto-fill, minmax(280px, 1fr)让卡片在宽屏下多列排列、窄屏下自动单列不依赖任何 CSS 框架。4.3 数据层js/data.js新建js/data.js用全局变量保存演示数据const AU_DATA { projectName: SIKAYD Swap AU Reaction, status: WIP, sourceNote: 角色设定版权归原作者所有本项目仅供技术演示。, groups: [ { id: group-01, title: 第一组交换后的初遇, originalName: 原版角色 A, auName: Swap 角色 A, lines: [ { speaker: AU 角色, text: 所以你在这个世界里的经历和我看到的完全不同 }, { speaker: 原版角色, text: 框架相似但关键节点的选择不一样。 } ] }, { id: group-02, title: 第二组关于原作结局的讨论, originalName: 原版角色 B, auName: Swap 角色 B, lines: [ { speaker: AU 角色, text: 如果我在那个时间点没有回头结局是不是反而更好 }, { speaker: 原版角色, text: 没有人能确定但正因为走了不同的路才看到不同的风景。 } ] }, { id: group-03, title: 第三组留白与待补充, originalName: 待补充, auName: 待补充, lines: [ { speaker: 作者, text: 这里是 WIP 占位内容你可以继续追加角色分组。 } ] } ] };data.js 里没有任何具体设定目的是让代码可以照抄运行。你可以把groups数组替换成自己项目里的真实内容。注意每新增一组对话只需要在数组里追加一个对象不需要改动 HTML 和 CSS。4.4 逻辑层js/main.js新建js/main.js负责从数据生成 HTML(function () { const app document.getElementById(card-list); function init() { if (!AU_DATA || !Array.isArray(AU_DATA.groups)) { app.innerHTML p数据格式错误请检查 js/data.js。/p; return; } const cards AU_DATA.groups.map((group) renderCard(group)).join(); app.innerHTML cards; bindEvents(); } function renderCard(group) { const dialogHtml group.lines .map( (line) p classdialog-linestrong${line.speaker}/strong${line.text}/p ) .join(); return article classreaction-card>cd sikayd-reaction-page npx serve .npx serve会启动一个本地静态服务器默认端口一般是 3000。启动后终端会输出类似这样的地址┌───────────────────────────────────────────┐ │ │ │ Local: http://localhost:3000 │ │ │ └───────────────────────────────────────────┘接着在浏览器访问http://localhost:3000。如果你不想用 Node.js也可以安装 VS Code 的 Live Server 插件右键index.html选择Open with Live Server。5.2 预期效果页面应显示以下内容顶部是紫色渐变背景标题写着SIKAYD (Swap AU) React to Their Originals。中间是“项目说明”区域。下方是三张角色卡片每一张都包含标题、原版角色与 Swap 角色的对应关系以及几条对话。鼠标悬停时卡片会轻微上浮。点击卡片会弹出对话框提示当前分组信息。如果你打开浏览器控制台应该看不到任何报错。控制台没有红色错误说明data.js和main.js都正常加载了。5.3 修改数据验证现在尝试一个最简单的更新流程编辑js/data.js把第三组的title改为“第三组新的反应”保存后刷新页面。你会发现第三张卡片标题随之变化而其他文件不需要改动。这就是“数据驱动页面”带来的维护效率。6. 常见问题与排查思路在实际操作中最容易遇到的其实是环境相关问题。下面整理成一张排查表。问题现象常见原因解决思路页面中文显示成乱码HTML 未声明 UTF-8 编码确认meta charsetUTF-8存在且文件实际以 UTF-8 保存卡片没有渲染显示“正在加载数据...”main.js或data.js路径错误打开控制台查看 404 路径确认script标签顺序双击 index.html 后数据不显示浏览器 CORS 限制读取本地数据改用本地服务器启动或者改用data.js全局变量方式点击卡片没有反应main.js事件绑定失败确认页面里没有多个重复 id刷新后重试样式全丢了CSS 文件路径错误检查link标签的href是否指向css/style.css控制台报错AU_DATA is not defineddata.js未在main.js之前加载调整脚本加载顺序部署到托管平台后图片不显示图片使用绝对路径或大小写不一致使用相对路径并保持文件扩展名大小写一致6.1 关于 CORS 限制的详细说明如果你选择把数据从data.js改成data.json然后使用fetch(data.json)那么直接双击index.html时很可能会遇到下面的错误Access to fetch at file:///... from origin null has been blocked by CORS policy这是浏览器的安全策略并非代码写错了。解决方案有两个始终使用本地服务器访问页面。坚持静态打开则继续使用data.js全局变量方案不发起网络请求。如果以后内容量特别大可以再引入构建工具或后端接口但现阶段完全没必要。6.2 排查建议顺序如果你遇到的不是上面任何一条推荐按以下顺序排查按F12打开浏览器开发者工具。切到Console查看有没有红色报错。切到Network查看data.js、main.js是否加载成功。确认js/data.js的内容是否为合法 JavaScript比如花括号是否配对字符串是否缺引号。大部分问题都可以在 Console 面板里找到答案。7. 最佳实践与工程建议7.1 尊重原作与标注来源这虽然不是纯技术话题但对同人项目特别重要。标题里既然写了not my idea在页面里也应该有所体现。建议在index.html的引言区或者页脚保留一句话p classnotice本页面为技术演示用途角色设定和剧情创意版权归原作者所有。/p同时在assets/README.txt中记录原始灵感来源的链接或作者名称。这样既尊重了原作者也让读者明白内容归属。7.2 数据文件是项目的核心资产很多创作者习惯把内容分散在微信聊天记录、备忘录和多张截图里后续找资料很痛苦。现在把内容结构化到data.js中后整个项目就有了唯一的数据源。建议修改前先复制一份文件或者直接养成 Git 提交习惯。如果不会 Git也可以手工维护备份比如把文件放到网盘并且每次修改后另存一个带日期的副本。数据文件的稳定性和可恢复性比样式代码更重要。7.3 用 Git 管理版本如果你打算长期维护这个 WIP 项目建议在项目根目录执行一次 Git 初始化git init git add . git commit -m feat: 初始化反应展示页之后每更新一批角色台词就提交一次方便追踪改动。注意在.gitignore中添加无关目录避免把本地缓存或系统文件提交到仓库。7.4 功能扩展方向当前页面已经具备“展示角色反应”的核心能力。再往下一步可以考虑这些扩展点击卡片展开/收起对话而不是弹窗。增加图片字段让每个角色都有头像。增加筛选按钮按分组或按角色名称过滤。把data.js改成远程 JSON配合后台管理系统在线编辑内容。使用 Vue 或 React 改写便于更复杂的交互。不过在确认需求之前不建议一上来就引入框架。小项目用小工具原生 JavaScript 完全能撑住当前规模。8. 结语本篇文章从一个同人创作标题出发完成了一个角色反应类展示页的完整搭建。你不仅了解了 AU、Swap AU、React 类作品和 WIP 这些概念还掌握了通过index.html、css/style.css、js/data.js、js/main.js四个文件组织静态项目的方法。真正重要的不是代码量而是“数据和展示分离”的思想内容集中管理页面自动渲染后续更新只需要改一处。如果你正在维护自己的 SIKAYD 项目或者想给其他 AU 作品做一个类似的展示页可以从今天这套模板开始。建议先把角色对应关系整理成表格再填入data.js最后调整样式。遇到报错时不要盲目乱改代码优先看控制台按问题表逐条排查。希望这个小小的静态页面模板能让你的创作过程更轻松也期待你把它扩展成更有趣的互动形式。