Archived
Demo
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数学少年线 · 互动剧本</title>
|
||||
<style>
|
||||
:root{
|
||||
--ink:#182430;
|
||||
--ink-2:#263947;
|
||||
--paper:#F7F3EA;
|
||||
--paper-2:#FFFDF8;
|
||||
--teal:#2E7D72;
|
||||
--gold:#B8893A;
|
||||
--blue:#3F6C8F;
|
||||
--red:#A64A44;
|
||||
--muted:#7C8790;
|
||||
--line:#D8D2C4;
|
||||
--shadow:0 18px 50px rgba(24,36,48,.16);
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{
|
||||
min-height:100vh;
|
||||
background:
|
||||
radial-gradient(circle at 15% 10%, rgba(62,108,143,.16), transparent 32%),
|
||||
radial-gradient(circle at 85% 20%, rgba(46,125,114,.14), transparent 30%),
|
||||
linear-gradient(160deg,#E9E4D8 0%,#F4F0E7 45%,#DDE5E4 100%);
|
||||
color:var(--ink);
|
||||
font-family:"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:22px;
|
||||
}
|
||||
.shell{
|
||||
width:100%;
|
||||
max-width:860px;
|
||||
background:var(--paper-2);
|
||||
border:1px solid var(--line);
|
||||
border-radius:18px;
|
||||
box-shadow:var(--shadow);
|
||||
overflow:hidden;
|
||||
}
|
||||
.topbar{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:14px;
|
||||
padding:18px 22px;
|
||||
background:linear-gradient(120deg,var(--ink) 0%,var(--ink-2) 100%);
|
||||
color:#F7F3EA;
|
||||
}
|
||||
.mark{
|
||||
width:46px;
|
||||
height:46px;
|
||||
border-radius:50% 50% 50% 50% / 60% 60% 40% 40%;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-size:24px;
|
||||
font-weight:700;
|
||||
background:linear-gradient(140deg,var(--teal),var(--blue));
|
||||
color:#fff;
|
||||
box-shadow:0 8px 20px rgba(0,0,0,.22);
|
||||
flex:none;
|
||||
}
|
||||
.topbar h1{font-size:21px;font-weight:700;letter-spacing:.5px}
|
||||
.topbar p{font-size:12px;color:#C9D4DA;margin-top:4px;line-height:1.5}
|
||||
.top-actions{margin-left:auto;display:flex;gap:8px}
|
||||
.top-actions button{
|
||||
border:1px solid rgba(255,255,255,.24);
|
||||
background:rgba(255,255,255,.08);
|
||||
color:#F7F3EA;
|
||||
padding:8px 13px;
|
||||
border-radius:10px;
|
||||
font-size:12px;
|
||||
cursor:pointer;
|
||||
transition:.2s;
|
||||
}
|
||||
.top-actions button:hover{background:rgba(255,255,255,.18)}
|
||||
.stats{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
padding:14px 22px;
|
||||
border-bottom:1px solid var(--line);
|
||||
background:var(--paper);
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.stat{
|
||||
flex:1 1 130px;
|
||||
min-width:130px;
|
||||
border:1px solid var(--line);
|
||||
border-radius:12px;
|
||||
padding:10px 12px;
|
||||
background:#fff;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:9px;
|
||||
}
|
||||
.stat-ico{
|
||||
width:30px;
|
||||
height:30px;
|
||||
border-radius:9px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-size:14px;
|
||||
font-weight:700;
|
||||
color:#fff;
|
||||
flex:none;
|
||||
}
|
||||
.stat b{display:block;font-size:15px;line-height:1.2}
|
||||
.stat span{font-size:11px;color:var(--muted)}
|
||||
.stage{
|
||||
padding:22px 22px 0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
font-size:12px;
|
||||
color:var(--muted);
|
||||
}
|
||||
.stage-line{height:1px;flex:1;background:var(--line)}
|
||||
.stage-tag{
|
||||
color:var(--teal);
|
||||
font-weight:700;
|
||||
border:1px solid rgba(46,125,114,.35);
|
||||
background:rgba(46,125,114,.08);
|
||||
padding:4px 10px;
|
||||
border-radius:999px;
|
||||
letter-spacing:1px;
|
||||
}
|
||||
.reader{
|
||||
padding:18px 24px 26px;
|
||||
min-height:300px;
|
||||
}
|
||||
.speaker{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
margin-bottom:14px;
|
||||
color:var(--ink-2);
|
||||
font-size:13px;
|
||||
font-weight:700;
|
||||
}
|
||||
.speaker::before{
|
||||
content:"";
|
||||
width:8px;
|
||||
height:8px;
|
||||
border-radius:50%;
|
||||
background:var(--gold);
|
||||
}
|
||||
.scene-text{
|
||||
font-size:17px;
|
||||
line-height:1.95;
|
||||
white-space:pre-wrap;
|
||||
color:var(--ink);
|
||||
min-height:180px;
|
||||
}
|
||||
.choices{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
padding:0 24px 26px;
|
||||
}
|
||||
.choice{
|
||||
display:block;
|
||||
width:100%;
|
||||
text-align:left;
|
||||
border:1px solid var(--line);
|
||||
border-left:4px solid var(--teal);
|
||||
background:#fff;
|
||||
color:var(--ink);
|
||||
border-radius:12px;
|
||||
padding:14px 16px;
|
||||
font-size:15px;
|
||||
line-height:1.5;
|
||||
cursor:pointer;
|
||||
transition:.18s;
|
||||
}
|
||||
.choice:hover{
|
||||
transform:translateY(-2px);
|
||||
border-color:var(--teal);
|
||||
box-shadow:0 8px 20px rgba(46,125,114,.14);
|
||||
}
|
||||
.choice small{
|
||||
display:block;
|
||||
margin-top:5px;
|
||||
color:var(--muted);
|
||||
font-size:11px;
|
||||
}
|
||||
.end-note{
|
||||
text-align:center;
|
||||
padding:10px 0 4px;
|
||||
color:var(--muted);
|
||||
font-size:13px;
|
||||
}
|
||||
.footer{
|
||||
padding:12px 22px;
|
||||
border-top:1px solid var(--line);
|
||||
background:var(--paper);
|
||||
color:var(--muted);
|
||||
font-size:11px;
|
||||
text-align:center;
|
||||
}
|
||||
@media (max-width:640px){
|
||||
body{padding:12px}
|
||||
.shell{border-radius:14px}
|
||||
.topbar{padding:14px 16px}
|
||||
.stats{padding:12px 14px}
|
||||
.stat{flex-basis:calc(50% - 6px);min-width:0}
|
||||
.reader{padding:16px 16px 20px}
|
||||
.choices{padding:0 16px 20px}
|
||||
.scene-text{font-size:15px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="mark">葫</div>
|
||||
<div>
|
||||
<h1>数学少年线</h1>
|
||||
<p>八幕完整版 · 大学数学专业四年 · 从录取通知书到直博</p>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<button onclick="resetGame()">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats" id="statsBar"></div>
|
||||
<div class="stage" id="stageBar"></div>
|
||||
<div class="reader">
|
||||
<div class="speaker" id="speaker">旁白</div>
|
||||
<div class="scene-text" id="sceneText">正在加载剧本……</div>
|
||||
</div>
|
||||
<div class="choices" id="choicesArea"></div>
|
||||
<div class="footer">数学少年线 · 完整互动剧本 v2 · 85 节点 / 107 选项</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SAVE_KEY = 'math_teen_story_save';
|
||||
let storyData = null;
|
||||
let state = {
|
||||
currentNode: null,
|
||||
stats: { shuli: 10, xiayi: 10, xinzhi: 10 },
|
||||
fav: {},
|
||||
history: []
|
||||
};
|
||||
let typeTimer = null;
|
||||
|
||||
function esc(s){
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
function stageOf(id){
|
||||
const map = {
|
||||
c1: '第一幕 · 大一上:小镇少年走进数学系',
|
||||
c2: '第二幕 · 大一下:第一次完整证明',
|
||||
c3: '第三幕 · 大二上:讨论班与诺特',
|
||||
c4: '第四幕 · 大二下:数学与生活',
|
||||
c5: '第五幕 · 大三上:方向与高斯',
|
||||
c6: '第六幕 · 大三下:科研低谷与诺特',
|
||||
c7: '第七幕 · 大四上:直博申请',
|
||||
c8: '第八幕 · 毕业前夜与直博第一年'
|
||||
};
|
||||
const key = (id || '').slice(0,2);
|
||||
return map[key] || '数学少年线';
|
||||
}
|
||||
|
||||
function statColor(k){
|
||||
if(k==='shuli') return '#3F6C8F';
|
||||
if(k==='xinzhi') return '#2E7D72';
|
||||
return '#B8893A';
|
||||
}
|
||||
|
||||
function statLabel(k){
|
||||
if(k==='shuli') return '数理根基';
|
||||
if(k==='xinzhi') return '心志韧性';
|
||||
return '同行温度';
|
||||
}
|
||||
|
||||
function statIcon(k){
|
||||
if(k==='shuli') return '∑';
|
||||
if(k==='xinzhi') return 'θ';
|
||||
return 'π';
|
||||
}
|
||||
|
||||
function renderStats(){
|
||||
const keys = ['shuli','xiayi','xinzhi'];
|
||||
document.getElementById('statsBar').innerHTML = keys.map(k => {
|
||||
const v = state.stats[k] || 0;
|
||||
return '<div class="stat"><div class="stat-ico" style="background:'+statColor(k)+'">'+statIcon(k)+'</div><div><b>'+v+'</b><span>'+statLabel(k)+'</span></div></div>';
|
||||
}).join('') + '<div class="stat" style="flex-basis:120px"><div class="stat-ico" style="background:#A64A44">4</div><div><b>4 位数学家</b><span>高斯 · 祖冲之 · 欧拉 · 诺特</span></div></div>';
|
||||
}
|
||||
|
||||
function renderStage(){
|
||||
document.getElementById('stageBar').innerHTML = '<span class="stage-tag">'+esc(stageOf(state.currentNode))+'</span><span class="stage-line"></span><span>'+esc(storyData.title)+'</span>';
|
||||
}
|
||||
|
||||
function renderNode(){
|
||||
if(!storyData || !storyData.nodes) return;
|
||||
const node = storyData.nodes[state.currentNode];
|
||||
if(!node){
|
||||
document.getElementById('sceneText').textContent = '节点未找到,请重新开始。';
|
||||
return;
|
||||
}
|
||||
if(typeTimer) clearTimeout(typeTimer);
|
||||
renderStats();
|
||||
renderStage();
|
||||
document.getElementById('speaker').textContent = node.character || '旁白';
|
||||
const textEl = document.getElementById('sceneText');
|
||||
const full = node.scene || '';
|
||||
textEl.textContent = '';
|
||||
let i = 0;
|
||||
function type(){
|
||||
if(i < full.length){
|
||||
textEl.textContent += full.charAt(i);
|
||||
i++;
|
||||
typeTimer = setTimeout(type, 16);
|
||||
} else {
|
||||
renderChoices(node.choices || []);
|
||||
}
|
||||
}
|
||||
type();
|
||||
saveState();
|
||||
}
|
||||
|
||||
function renderChoices(choices){
|
||||
const area = document.getElementById('choicesArea');
|
||||
if(!choices.length){
|
||||
area.innerHTML = '<div class="end-note">故事结束 · 你的数学,刚刚开始</div><button class="choice" onclick="resetGame()">再走一次这段路</button>';
|
||||
return;
|
||||
}
|
||||
area.innerHTML = choices.map((c, idx) => {
|
||||
const effect = c.effects || {};
|
||||
const parts = [];
|
||||
if(effect.shuli) parts.push('数理根基 '+(effect.shuli>0?'+':'')+effect.shuli);
|
||||
if(effect.xinzhi) parts.push('心志韧性 '+(effect.xinzhi>0?'+':'')+effect.xinzhi);
|
||||
if(effect.xiayi) parts.push('同行温度 '+(effect.xiayi>0?'+':'')+effect.xiayi);
|
||||
const hint = parts.length ? '<small>'+parts.join(' · ')+'</small>' : '';
|
||||
return '<button class="choice" onclick="choose(\''+esc(c.next)+'\', '+idx+')">'+esc(c.text)+hint+'</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function choose(next, idx){
|
||||
if(typeTimer) clearTimeout(typeTimer);
|
||||
const node = storyData.nodes[state.currentNode];
|
||||
const choice = (node.choices || [])[idx];
|
||||
if(!choice) return;
|
||||
const effects = choice.effects || {};
|
||||
Object.keys(effects).forEach(k => {
|
||||
if(k === 'favorability'){
|
||||
Object.keys(effects[k] || {}).forEach(name => {
|
||||
state.fav[name] = (state.fav[name] || 0) + effects[k][name];
|
||||
});
|
||||
} else {
|
||||
state.stats[k] = (state.stats[k] || 0) + effects[k];
|
||||
}
|
||||
});
|
||||
state.history.push(state.currentNode);
|
||||
state.currentNode = next;
|
||||
renderNode();
|
||||
}
|
||||
|
||||
function saveState(){
|
||||
try { localStorage.setItem(SAVE_KEY, JSON.stringify(state)); } catch(e){}
|
||||
}
|
||||
|
||||
function loadState(){
|
||||
try {
|
||||
const raw = localStorage.getItem(SAVE_KEY);
|
||||
if(raw){
|
||||
const saved = JSON.parse(raw);
|
||||
if(saved && saved.currentNode && storyData && storyData.nodes[saved.currentNode]){
|
||||
state = Object.assign(state, saved);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch(e){}
|
||||
return false;
|
||||
}
|
||||
|
||||
function resetGame(){
|
||||
if(typeTimer) clearTimeout(typeTimer);
|
||||
state = {
|
||||
currentNode: storyData ? storyData.start_node : null,
|
||||
stats: { shuli: 10, xiayi: 10, xinzhi: 10 },
|
||||
fav: {},
|
||||
history: []
|
||||
};
|
||||
try { localStorage.removeItem(SAVE_KEY); } catch(e){}
|
||||
if(storyData) renderNode();
|
||||
}
|
||||
|
||||
async function boot(){
|
||||
try {
|
||||
const resp = await fetch('/api/story/math_teen');
|
||||
const json = await resp.json();
|
||||
if(json.code !== 0) throw new Error(json.msg || '加载失败');
|
||||
storyData = json.data;
|
||||
if(!loadState()){
|
||||
state.currentNode = storyData.start_node;
|
||||
}
|
||||
renderNode();
|
||||
} catch(e){
|
||||
document.getElementById('sceneText').textContent = '剧本加载失败,请先启动 Flask 服务后访问本页。';
|
||||
}
|
||||
}
|
||||
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user