Files

1489 lines
61 KiB
HTML
Raw Permalink Normal View History

2026-06-24 13:11:34 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>等效位次与等效分数模型 — 2026浙江高考志愿分析工具</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* ===== CSS Variables & Reset ===== */
:root {
--bg-primary: #0f1419;
--bg-secondary: #1a2332;
--bg-card: #1e2a3a;
--bg-card-hover: #243040;
--border-color: #2a3a4e;
--border-light: #3a4a5e;
--text-primary: #e8edf2;
--text-secondary: #8899aa;
--text-muted: #5a6a7a;
--accent-gold: #f0b429;
--accent-gold-light: #ffd166;
--accent-gold-dim: #c9940a;
--accent-red: #ef476f;
--accent-green: #06d6a0;
--accent-blue: #4d9cf0;
--accent-cyan: #48cae4;
--accent-purple: #9b5de5;
--shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
--shadow-md: 0 4px 20px rgba(0,0,0,0.4);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--font-serif: 'Noto Serif SC', Georgia, serif;
--font-sans: 'Noto Sans SC', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{font-size:15px;scroll-behavior:smooth}
body{
font-family:var(--font-sans);
background:var(--bg-primary);
color:var(--text-primary);
line-height:1.6;
min-height:100vh;
}
a{color:var(--accent-gold);text-decoration:none}
a:hover{color:var(--accent-gold-light)}
/* ===== Header ===== */
.header{
background:linear-gradient(135deg,#0f1419 0%,#1a2332 50%,#0f1419 100%);
border-bottom:1px solid var(--border-color);
padding:2rem 2rem 1.5rem;
position:relative;
overflow:hidden;
}
.header::before{
content:'';
position:absolute;
top:0;left:0;right:0;bottom:0;
background:radial-gradient(ellipse 60vw 120px at 30% 100%, rgba(240,180,41,0.06) 0%, transparent 70%);
pointer-events:none;
}
.header-inner{max-width:1400px;margin:0 auto;position:relative}
.header h1{
font-family:var(--font-serif);
font-weight:900;
font-size:1.85rem;
letter-spacing:0.02em;
color:var(--text-primary);
}
.header h1 span{color:var(--accent-gold)}
.header .subtitle{
color:var(--text-secondary);
font-size:0.92rem;
margin-top:0.3rem;
letter-spacing:0.04em;
}
.header .badge{
display:inline-block;
background:rgba(240,180,41,0.12);
border:1px solid rgba(240,180,41,0.25);
color:var(--accent-gold);
padding:0.15rem 0.7rem;
border-radius:100px;
font-size:0.72rem;
font-weight:500;
margin-top:0.6rem;
}
/* ===== Tab Navigation ===== */
.tab-bar{
background:var(--bg-secondary);
border-bottom:1px solid var(--border-color);
position:sticky;top:0;z-index:100;
padding:0 2rem;
}
.tab-bar-inner{max-width:1400px;margin:0 auto;display:flex;gap:0;overflow-x:auto}
.tab-btn{
font-family:var(--font-sans);
font-size:0.82rem;
font-weight:500;
padding:0.85rem 1.4rem;
color:var(--text-muted);
background:transparent;
border:none;
cursor:pointer;
border-bottom:2px solid transparent;
transition:all 0.2s;
white-space:nowrap;
letter-spacing:0.03em;
}
.tab-btn:hover{color:var(--text-secondary);background:rgba(255,255,255,0.03)}
.tab-btn.active{color:var(--accent-gold);border-bottom-color:var(--accent-gold)}
.tab-btn .tab-icon{margin-right:0.4rem}
/* ===== Main Content ===== */
.main{max-width:1400px;margin:0 auto;padding:1.5rem 2rem 3rem}
.tab-panel{display:none;animation:fadeIn 0.35s ease}
.tab-panel.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* ===== Cards ===== */
.card{
background:var(--bg-card);
border:1px solid var(--border-color);
border-radius:var(--radius-md);
padding:1.5rem;
margin-bottom:1.25rem;
box-shadow:var(--shadow-sm);
transition:border-color 0.2s, box-shadow 0.2s;
}
.card:hover{border-color:var(--border-light)}
.card-title{
font-family:var(--font-serif);
font-weight:700;
font-size:1.05rem;
margin-bottom:1rem;
color:var(--text-primary);
display:flex;align-items:center;gap:0.5rem;
}
.card-title .accent{color:var(--accent-gold)}
.card-subtitle{color:var(--text-secondary);font-size:0.85rem;margin-bottom:1rem}
.card-grid{display:grid;gap:1.25rem}
/* Grid layouts */
.grid-2{grid-template-columns:1fr 1fr}
.grid-3{grid-template-columns:1fr 1fr 1fr}
.grid-4{grid-template-columns:repeat(4,1fr)}
@media(max-width:900px){
.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}
}
/* ===== Data Display ===== */
.stat{
background:rgba(255,255,255,0.03);
border:1px solid var(--border-color);
border-radius:var(--radius-sm);
padding:0.8rem 1rem;
text-align:center;
}
.stat .label{color:var(--text-secondary);font-size:0.72rem;text-transform:uppercase;letter-spacing:0.08em}
.stat .value{
font-family:var(--font-mono);
font-size:1.4rem;
font-weight:600;
color:var(--text-primary);
margin-top:0.2rem;
}
.stat .value.gold{color:var(--accent-gold)}
.stat .value.green{color:var(--accent-green)}
.stat .value.red{color:var(--accent-red)}
.stat .value.blue{color:var(--accent-blue)}
.stat .sub{font-size:0.75rem;color:var(--text-muted);margin-top:0.1rem}
/* ===== Tables ===== */
.data-table{
width:100%;
border-collapse:collapse;
font-size:0.82rem;
}
.data-table th{
background:rgba(255,255,255,0.04);
color:var(--text-secondary);
font-weight:500;
text-align:left;
padding:0.6rem 0.8rem;
border-bottom:1px solid var(--border-color);
font-size:0.72rem;
text-transform:uppercase;
letter-spacing:0.06em;
}
.data-table td{
padding:0.55rem 0.8rem;
border-bottom:1px solid rgba(255,255,255,0.05);
font-family:var(--font-mono);
font-size:0.82rem;
}
.data-table tr:hover td{background:rgba(255,255,255,0.02)}
.data-table td:first-child{font-family:var(--font-sans);font-weight:500}
.data-table .highlight td{background:rgba(240,180,41,0.06);color:var(--accent-gold)}
/* ===== Forms ===== */
.form-group{margin-bottom:1rem}
.form-label{display:block;font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.3rem;font-weight:500}
.form-input,.form-select{
width:100%;
padding:0.55rem 0.75rem;
background:rgba(255,255,255,0.05);
border:1px solid var(--border-color);
border-radius:var(--radius-sm);
color:var(--text-primary);
font-family:var(--font-sans);
font-size:0.88rem;
transition:border-color 0.2s;
}
.form-input:focus,.form-select:focus{outline:none;border-color:var(--accent-gold)}
.form-input::placeholder{color:var(--text-muted)}
.form-select option{background:var(--bg-secondary);color:var(--text-primary)}
.form-row{display:flex;gap:0.8rem;flex-wrap:wrap}
.form-row .form-group{flex:1;min-width:140px}
.btn{
padding:0.55rem 1.5rem;
border:none;
border-radius:var(--radius-sm);
font-family:var(--font-sans);
font-size:0.85rem;
font-weight:600;
cursor:pointer;
transition:all 0.2s;
letter-spacing:0.03em;
}
.btn-primary{
background:var(--accent-gold);
color:#1a1a1a;
}
.btn-primary:hover{background:var(--accent-gold-light);transform:translateY(-1px)}
.btn-secondary{
background:rgba(255,255,255,0.08);
color:var(--text-primary);
border:1px solid var(--border-color);
}
.btn-secondary:hover{background:rgba(255,255,255,0.12)}
.btn-sm{padding:0.35rem 1rem;font-size:0.78rem}
.btn-group{display:flex;gap:0.5rem;flex-wrap:wrap;margin-top:1rem}
/* ===== Results ===== */
.result-box{
background:rgba(240,180,41,0.06);
border:1px solid rgba(240,180,41,0.2);
border-radius:var(--radius-md);
padding:1.2rem 1.5rem;
margin-top:1rem;
display:none;
}
.result-box.show{display:block}
.result-box .title{font-family:var(--font-serif);font-weight:700;font-size:0.95rem;color:var(--accent-gold);margin-bottom:0.5rem}
.result-box .item{display:flex;justify-content:space-between;padding:0.35rem 0;border-bottom:1px solid rgba(255,255,255,0.06)}
.result-box .item:last-child{border-bottom:none}
.result-box .item .key{color:var(--text-secondary);font-size:0.82rem}
.result-box .item .val{font-family:var(--font-mono);font-weight:600;color:var(--text-primary)}
.result-box .item .val.gold{color:var(--accent-gold)}
.result-box .item .val.green{color:var(--accent-green)}
.result-box .item .val.blue{color:var(--accent-blue)}
/* ===== Chart containers ===== */
.chart-wrap{
background:rgba(0,0,0,0.15);
border-radius:var(--radius-sm);
padding:0.5rem;
margin:0.5rem 0;
}
.chart-wrap canvas{width:100%!important;height:auto!important;max-height:320px}
/* ===== Method badges ===== */
.method-tag{
display:inline-block;
padding:0.15rem 0.5rem;
border-radius:3px;
font-size:0.65rem;
font-weight:600;
letter-spacing:0.04em;
margin-right:0.3rem;
}
.m1{background:rgba(77,156,240,0.15);color:var(--accent-blue)}
.m2{background:rgba(6,214,160,0.15);color:var(--accent-green)}
.m3{background:rgba(240,180,41,0.15);color:var(--accent-gold)}
.m4{background:rgba(155,93,229,0.15);color:var(--accent-purple)}
/* ===== Method intro section ===== */
.method-intro{display:grid;gap:0.8rem}
.method-card{
background:rgba(255,255,255,0.02);
border:1px solid var(--border-color);
border-radius:var(--radius-sm);
padding:0.9rem 1rem;
font-size:0.82rem;
line-height:1.5;
}
.method-card .m-title{font-weight:600;margin-bottom:0.3rem}
.method-card .m-formula{font-family:var(--font-mono);font-size:0.78rem;color:var(--accent-gold);margin:0.3rem 0}
.method-card .m-desc{color:var(--text-secondary);font-size:0.76rem}
/* ===== Info alert ===== */
.alert{
background:rgba(240,180,41,0.08);
border:1px solid rgba(240,180,41,0.2);
border-radius:var(--radius-sm);
padding:0.7rem 1rem;
font-size:0.78rem;
color:var(--text-secondary);
margin-bottom:1rem;
}
.alert strong{color:var(--accent-gold)}
/* ===== Timeline ===== */
.timeline{
position:relative;
padding-left:2rem;
}
.timeline::before{
content:'';
position:absolute;
left:8px;top:0;bottom:0;
width:2px;
background:var(--border-color);
}
.tl-item{position:relative;padding-bottom:1.2rem}
.tl-item::before{
content:'';
position:absolute;
left:-1.55rem;top:0.4rem;
width:10px;height:10px;
border-radius:50%;
background:var(--accent-gold);
border:2px solid var(--bg-primary);
}
.tl-item .year{font-weight:700;color:var(--accent-gold);font-size:0.85rem}
.tl-item .content{color:var(--text-secondary);font-size:0.8rem;margin-top:0.2rem}
/* ===== Recommendation card ===== */
.rec-card{
background:linear-gradient(135deg, rgba(240,180,41,0.08), rgba(240,180,41,0.02));
border:1px solid rgba(240,180,41,0.2);
border-radius:var(--radius-md);
padding:1.2rem 1.5rem;
}
.rec-card .title{font-weight:700;color:var(--accent-gold);margin-bottom:0.3rem}
/* ===== Responsive ===== */
@media(max-width:768px){
.header{padding:1.2rem 1rem 1rem}
.header h1{font-size:1.3rem}
.tab-bar{padding:0 0.5rem}
.tab-btn{padding:0.7rem 0.9rem;font-size:0.75rem}
.main{padding:1rem}
.card{padding:1rem}
}
</style>
</head>
<body>
<!-- ===== HEADER ===== -->
<header class="header">
<div class="header-inner">
<h1>🎯 等效位次 · 等效分数 <span>分析模型</span></h1>
<div class="subtitle">基于多因素修正的跨年度高考位次/分数换算引擎 · 2026 浙江版</div>
<div class="badge">⚡ 支持 4 种换算方法 · 内置 2023–2025 一分一段表 · 院校录取预测</div>
</div>
</header>
<!-- ===== TAB BAR ===== -->
<nav class="tab-bar" id="tabBar">
<div class="tab-bar-inner">
<button class="tab-btn active" data-tab="model"><span class="tab-icon">📐</span>模型说明</button>
<button class="tab-btn" data-tab="data"><span class="tab-icon">📊</span>数据总览</button>
<button class="tab-btn" data-tab="convert"><span class="tab-icon">🔄</span>等效换算</button>
<button class="tab-btn" data-tab="predict"><span class="tab-icon">🎯</span>院校预测</button>
<button class="tab-btn" data-tab="mapping"><span class="tab-icon">📌</span>天恒分数映射</button>
<button class="tab-btn" data-tab="method"><span class="tab-icon">⚙️</span>方法对比</button>
</div>
</nav>
<!-- ===== MAIN ===== -->
<div class="main" id="app">
<!-- ============ TAB: 模型说明 ============ -->
<div class="tab-panel active" id="tab-model">
<div class="card">
<div class="card-title">📖 什么是等效位次 / 等效分数?</div>
<p style="color:var(--text-secondary);font-size:0.85rem;line-height:1.7;margin-bottom:1rem">
不同年份的 <strong style="color:var(--text-primary)">卷面分数</strong> 因试题难度、考生群体、赋分规则不同而不可直接比较。
全省位次(省排名)相对稳定,但也受 <strong style="color:var(--accent-gold)">考生总数变化</strong>
<strong style="color:var(--accent-gold)">选科结构变化</strong><strong style="color:var(--accent-gold)">招生计划变化</strong> 等多因素影响。
</p>
<p style="color:var(--text-secondary);font-size:0.85rem;line-height:1.7;margin-bottom:1rem">
<strong style="color:var(--accent-gold)">等效位次</strong>:将某年的位次,根据多因素修正后,换算为另一年的"等价"位次。<br>
<strong style="color:var(--accent-gold)">等效分数</strong>:将某年的分数 / 位次,通过跨年一分一段表映射,得到另一年的"等价"分数。
</p>
</div>
<div class="card">
<div class="card-title">🧠 核心算法(4种方法)</div>
<div class="method-intro grid-2">
<div class="method-card">
<div class="m-title"><span class="method-tag m1">M1</span>直接位次法</div>
<div class="m-formula">Rank₂ = Rank₁</div>
<div class="m-desc">假设位次完全稳定不变。简单直接,忽略一切结构变化。适用于年份间差异极小的场景。</div>
</div>
<div class="method-card">
<div class="m-title"><span class="method-tag m2">M2</span>比例法</div>
<div class="m-formula">Rank₂ = Rank₁ × (N₂ / N₁)</div>
<div class="m-desc">修正考生总数变化。百分比位置不变。适用于不限选科的专业或整体参考。</div>
</div>
<div class="method-card">
<div class="m-title"><span class="method-tag m3">M3</span>物化修正比例法</div>
<div class="m-formula">Rank₂ = Rank₁ × (W₂ / W₁)</div>
<div class="m-desc">对于限物化专业,用物化考生数替代总考生数进行比例修正。更精准反映有效竞争池变化。</div>
</div>
<div class="method-card">
<div class="m-title"><span class="method-tag m4">M4</span>招生计划修正法</div>
<div class="m-formula">Rank₂ = R₂ × (1 α · ΔE)</div>
<div class="m-desc">在M2/M3基础上,引入专业招生计划变化系数 ΔE。α 为经验弹性系数(通常 0.3~0.5)。</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">📋 使用建议</div>
<div class="alert">
<strong>⚠️ 推荐流程:</strong>
① 出分后用比例法(M2)做基础等效 → ② 目标专业限物化时用物化修正法(M3)做二次验证 → ③ 结合招生计划变化用 M4 微调 → ④ 取多种方法的 <strong>中位数或加权均值</strong> 作为最终参考
</div>
<div class="method-intro" style="grid-template-columns:1fr 1fr">
<div class="method-card">
<div class="m-title" style="color:var(--accent-green)">✅ 何时用 M2 比例法</div>
<div class="m-desc">• 不限选科的专业<br>• 整体分数段定位<br>• 不做精细院校筛选时</div>
</div>
<div class="method-card">
<div class="m-title" style="color:var(--accent-gold)">✅ 何时用 M3 物化修正法</div>
<div class="m-desc">• 目标专业限物化<br>• 天恒这种情况(物化生)<br>• 理工科专业推荐</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">⏳ 2026关键时间线</div>
<div class="timeline">
<div class="tl-item">
<div class="year">6月26日左右</div>
<div class="content">出分 + 省排名公布 + 2026一分一段表发布</div>
</div>
<div class="tl-item">
<div class="year">出分后立即</div>
<div class="content">将分数/位次输入本工具 → 获取等效于2023/2024/2025的分数</div>
</div>
<div class="tl-item">
<div class="year">6月29~30日</div>
<div class="content">使用预测数据 + 等效换算结果填报志愿</div>
</div>
<div class="tl-item">
<div class="year">7月下旬</div>
<div class="content">录取结果公布,验证模型精度</div>
</div>
</div>
</div>
</div>
<!-- ============ TAB: 数据总览 ============ -->
<div class="tab-panel" id="tab-data">
<div class="card">
<div class="card-title">📊 浙江高考历年核心参数</div>
<div class="card-subtitle">包括考生总数、物化考生数、批次线及对应位次</div>
<div id="paramTableContainer"></div>
</div>
<div class="card">
<div class="card-title">📈 趋势可视化</div>
<div class="grid-2" style="gap:1.5rem">
<div>
<div class="form-label" style="margin-bottom:0.3rem">考生总数趋势</div>
<div class="chart-wrap"><canvas id="trendChart1"></canvas></div>
</div>
<div>
<div class="form-label" style="margin-bottom:0.3rem">物化考生比例</div>
<div class="chart-wrap"><canvas id="trendChart2"></canvas></div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">🏫 目标院校历年录取数据</div>
<div class="card-subtitle">点击「编辑数据」可修改默认值</div>
<div id="schoolTableContainer"></div>
</div>
</div>
<!-- ============ TAB: 等效换算 ============ -->
<div class="tab-panel" id="tab-convert">
<div class="card">
<div class="card-title">🔄 等效位次 / 等效分数 换算器</div>
<div class="card-subtitle">输入一年份的分数或位次,输出其他年份的等效值(支持 4 种方法)</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">来源年份</label>
<select class="form-select" id="convFromYear"><option>2023</option><option>2024</option><option selected>2025</option><option>2026</option></select>
</div>
<div class="form-group">
<label class="form-label">目标年份</label>
<select class="form-select" id="convToYear"><option>2023</option><option>2024</option><option>2025</option><option selected>2026</option></select>
</div>
<div class="form-group" style="min-width:90px">
<label class="form-label">输入类型</label>
<select class="form-select" id="convInputType"><option value="score">分数</option><option value="rank">位次</option></select>
</div>
<div class="form-group" style="min-width:110px">
<label class="form-label">换算方法</label>
<select class="form-select" id="convMethod">
<option value="m1">M1 直接位次法</option>
<option value="m2" selected>M2 比例法</option>
<option value="m3">M3 物化修正法</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex:0.4">
<label class="form-label">输入值</label>
<input class="form-input" id="convInput" type="number" placeholder="输入分数或位次">
</div>
<div class="form-group" style="flex:0.3;display:flex;align-items:flex-end">
<button class="btn btn-primary" onclick="runConversion()">换算 →</button>
</div>
<div class="form-group" style="flex:0.3;display:flex;align-items:flex-end">
<label class="form-label" style="margin-bottom:0;cursor:pointer">
<input type="checkbox" id="convWuHuaOnly" style="accent-color:var(--accent-gold);margin-right:0.3rem">
<span style="font-size:0.78rem;color:var(--text-secondary)">限物化专业(M3用)</span>
</label>
</div>
</div>
<div class="alert" style="margin-top:0.5rem">
<strong>提示:</strong> 输入 2026 年的分数或位次前,请先到「天恒分数映射」页面输入分数并保存。
</div>
<!-- Result -->
<div class="result-box" id="convResult">
<div class="title">换算结果</div>
<div id="convResultItems"></div>
</div>
</div>
<div class="card">
<div class="card-title">📋 快速参考:常见分数等效表</div>
<div class="card-subtitle">基于 M3 物化修正法(限物化专业推荐方法),展示各年份对应关系</div>
<div id="quickRefTable"></div>
</div>
</div>
<!-- ============ TAB: 院校预测 ============ -->
<div class="tab-panel" id="tab-predict">
<div class="card">
<div class="card-title">🎯 目标院校 2026 录取位次 / 分数预测</div>
<div class="card-subtitle">基于近3年数据 + 考生总数变化 + 物化比例变化,预测各校2026年最低投档位次和分数</div>
<div class="form-row" style="margin-bottom:1rem">
<div class="form-group" style="flex:0.3">
<label class="form-label">预测方法</label>
<select class="form-select" id="predMethod">
<option value="m2">M2 比例法</option>
<option value="m3" selected>M3 物化修正法 (推荐)</option>
</select>
</div>
<div class="form-group" style="flex:0.3">
<label class="form-label">趋势权重 (近→远)</label>
<select class="form-select" id="predWeight">
<option value="equal">等权平均</option>
<option value="weighted" selected>加权 (5:3:2)</option>
</select>
</div>
<div class="form-group" style="flex:0.3;display:flex;align-items:flex-end">
<button class="btn btn-primary" onclick="runPredict()">运行预测 →</button>
</div>
</div>
<div id="predResult"></div>
</div>
<div class="card">
<div class="card-title">📊 预测可视化</div>
<div class="chart-wrap"><canvas id="predChart" style="max-height:400px"></canvas></div>
</div>
</div>
<!-- ============ TAB: 天恒分数映射 ============ -->
<div class="tab-panel" id="tab-mapping">
<div class="card">
<div class="card-title">📌 天恒专属分数映射</div>
<div class="card-subtitle">输入 2026 高考分数 / 位次 → 自动输出等效于 2023 / 2024 / 2025 的分数和位次</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">2026 高考分数</label>
<input class="form-input" id="tScore" type="number" placeholder="如 591" value="">
</div>
<div class="form-group">
<label class="form-label">2026 省排名(位次,可选)</label>
<input class="form-input" id="tRank" type="number" placeholder="如 85000">
</div>
<div class="form-group" style="flex:0.3;display:flex;align-items:flex-end">
<button class="btn btn-primary" onclick="runMapping()">计算映射 →</button>
</div>
</div>
<div class="form-row" style="margin-top:0.5rem">
<div class="form-group" style="flex:0.3">
<label class="form-label">推荐方法</label>
<select class="form-select" id="mapMethod">
<option value="m2">M2 比例法</option>
<option value="m3" selected>M3 物化修正法 (推荐)</option>
</select>
</div>
<div class="form-group" style="flex:0.3;display:flex;align-items:flex-end">
<input type="checkbox" id="mapSave2026" checked style="accent-color:var(--accent-gold);margin-right:0.3rem">
<label for="mapSave2026" style="font-size:0.78rem;color:var(--text-secondary);cursor:pointer">保存为2026基准数据</label>
</div>
</div>
<div class="result-box" id="mapResult">
<div class="title" style="display:flex;justify-content:space-between;align-items:center">
<span>🎯 等效映射结果(M3 物化修正法)</span>
<span style="font-size:0.72rem;color:var(--text-muted)">限物化专业适用</span>
</div>
<div id="mapResultItems"></div>
</div>
</div>
<div class="card">
<div class="card-title">💡 解读与行动建议</div>
<div id="mappingAdvice" style="color:var(--text-secondary);font-size:0.85rem;line-height:1.7">
<p>输入分数后,系统将自动生成专属分析和志愿填报建议。</p>
<p style="margin-top:0.5rem">使用说明:</p>
<ul style="margin-top:0.3rem;padding-left:1.2rem">
<li>推荐使用 <strong style="color:var(--accent-gold)">M3 物化修正法</strong>(天恒选科物化生,限物化专业为主)</li>
<li>等效分数可用来判断"今年考这个分数 ≈ 去年考XX分"</li>
<li>等效位次可用来直接对比目标院校的历年最低位次</li>
<li>建议同时参考 M2 和 M3 的结果,取区间</li>
</ul>
</div>
</div>
</div>
<!-- ============ TAB: 方法对比 ============ -->
<div class="tab-panel" id="tab-method">
<div class="card">
<div class="card-title">⚖️ 四种方法对比验证</div>
<div class="card-subtitle">选取一个参考点,对比四种方法给出的结果差异</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">参考年份</label>
<select class="form-select" id="cmpRefYear"><option>2023</option><option>2024</option><option selected>2025</option></select>
</div>
<div class="form-group">
<label class="form-label">参考分数</label>
<input class="form-input" id="cmpScore" type="number" placeholder="如 573" value="573">
</div>
<div class="form-group" style="flex:0.3;display:flex;align-items:flex-end">
<button class="btn btn-primary" onclick="runComparison()">对比 →</button>
</div>
</div>
<div id="cmpResult"></div>
</div>
<div class="card">
<div class="card-title">📊 各方法偏差分析</div>
<div class="chart-wrap"><canvas id="cmpChart"></canvas></div>
<div class="card-subtitle" style="margin-top:0.5rem">
M3 物化修正法对限物化专业更准确;M2 比例法适合不限专业。建议取 M2 和 M3 的区间作为参考范围。
</div>
</div>
</div>
</div>
<script>
// =====================================================================
// DATA — 浙江省高考历年核心参数(可编辑,2026预估可后续更新)
// =====================================================================
const PARAMS = {
2023: { total: 390900, wuhua: 80000, specialScore: 594, firstScore: 488, secondScore: 274, totalRank: 290000 },
2024: { total: 396000, wuhua: 100000, specialScore: 595, firstScore: 492, secondScore: 269, totalRank: 281057 },
2025: { total: 402000, wuhua: 130000, specialScore: 592, firstScore: 490, secondScore: 268, totalRank: 290790 },
2026: { total: 405000, wuhua: 140000, specialScore: 596, firstScore: 492, secondScore: 268, totalRank: 295000 }
};
// 一分一段表 — 关键节点(用于插值查找)
// 格式: { score: cumulativeRank }
const SEGMENTS = {
2023: {
697: 312, 695: 400, 690: 540, 685: 710, 680: 786, 670: 1520, 660: 2800, 650: 6625,
640: 10800, 630: 16800, 620: 24500, 610: 34200, 600: 52960, 595: 58055,
594: 58900, 590: 62112, 585: 67200, 580: 73498, 575: 79800, 570: 86168,
565: 93000, 560: 100586, 555: 107800, 550: 115318, 545: 123200, 540: 131194,
535: 139500, 530: 148838, 525: 158500, 520: 168186, 515: 178500, 510: 188850,
505: 199800, 500: 211227, 495: 222800, 490: 234019, 488: 237000,
485: 240000, 480: 248000, 470: 261000, 460: 270000, 450: 278000,
400: 296000, 350: 306000, 300: 314000, 274: 318000
},
2024: {
697: 350, 695: 420, 690: 560, 685: 740, 680: 810, 670: 1600, 660: 3672,
655: 7869, 650: 8999, 645: 10045, 640: 10905, 635: 12013, 630: 13197,
625: 14527, 620: 16137, 615: 17823, 610: 19329, 605: 21180, 600: 22646,
595: 24160, 594: 24500, 590: 25893, 585: 27854, 580: 29911, 575: 32103,
570: 34492, 565: 36734, 560: 39079, 555: 41557, 550: 44000, 545: 46351,
540: 48916, 535: 51593, 530: 54266, 525: 57010, 520: 59868, 515: 62730,
510: 65695, 505: 68860, 500: 71950, 495: 75100, 492: 77000,
490: 79000, 485: 83000, 480: 87000, 470: 95000, 460: 104000,
450: 113000, 440: 122000, 430: 131000, 420: 140000, 410: 149000,
400: 158000, 390: 167000, 380: 175000, 370: 183000, 360: 191000,
350: 199000, 340: 207000, 330: 215000, 320: 223000, 310: 231000,
300: 239000, 290: 247000, 280: 255000, 269: 281057
},
2025: {
697: 343, 695: 446, 694: 513, 693: 593, 692: 667, 691: 747, 690: 857,
689: 1005, 688: 1093, 687: 1228, 686: 1346, 685: 1472, 684: 1617,
683: 1762, 682: 1934, 681: 2117, 680: 2314, 679: 2541, 678: 2741,
677: 2969, 676: 3184, 675: 3437, 674: 3718, 673: 4015, 672: 4302,
671: 4604, 670: 4910, 669: 5194, 668: 5501, 667: 5863, 666: 6226,
665: 6612, 664: 6995, 663: 7330, 662: 7761, 661: 8163, 660: 8584,
659: 9037, 658: 9488, 657: 9924, 656: 10365, 655: 10810, 654: 11301,
653: 11784, 652: 12308, 651: 12852, 650: 13406, 649: 13941, 648: 14489,
647: 15035, 646: 15626, 645: 16192, 644: 16817, 643: 17433, 642: 18134,
641: 18815, 640: 19448, 639: 20114, 638: 20890, 637: 21651, 636: 22365,
635: 23127, 634: 23836, 633: 24600, 632: 25365, 631: 26164, 630: 27005,
629: 27808, 628: 28606, 627: 29480, 626: 30381, 625: 31221, 624: 32114,
623: 33035, 622: 33926, 621: 34876, 620: 35870, 619: 36835, 618: 37785,
617: 38811, 616: 39811, 615: 40819, 614: 41842, 613: 42949, 612: 44001,
611: 45005, 610: 46041, 609: 47051, 608: 48104, 607: 49197, 606: 50295,
605: 51406, 604: 52529, 603: 53612, 602: 54726, 601: 55840, 600: 56973,
599: 58143, 598: 59273, 597: 60413, 596: 61619, 595: 62803, 594: 63956,
593: 65176, 592: 66382, 591: 67533, 590: 68800, 589: 70025, 588: 71342,
587: 72680, 586: 74005, 585: 75308, 584: 76593, 583: 77803, 582: 79025,
581: 80276, 580: 81551, 579: 82806, 578: 84017, 577: 85181, 576: 86327,
575: 87482, 574: 88682, 573: 89880, 572: 91084, 571: 92287, 570: 93480,
569: 94680, 568: 95868, 567: 97104, 566: 98301, 565: 99563, 564: 100794,
563: 102023, 562: 103248, 561: 104467, 560: 105669, 559: 106893, 558: 108113,
557: 109307, 556: 110553, 555: 111792, 554: 113023, 553: 114299, 552: 115540,
551: 116780, 550: 118030, 549: 119279, 548: 120511, 547: 121762, 546: 123012,
545: 124283, 544: 125526, 543: 126773, 542: 128010, 541: 129282, 540: 130549,
539: 131801, 538: 133084, 537: 134350, 536: 135622, 535: 136912, 534: 138185,
533: 139461, 532: 140742, 531: 141997, 530: 143303, 529: 144595, 528: 145904,
527: 147183, 526: 148493, 525: 149791, 524: 151088, 523: 152405, 522: 153699,
521: 155007, 520: 156332, 519: 157647, 518: 158985, 517: 160293, 516: 161614,
515: 162927, 514: 164256, 513: 165601, 512: 166927, 511: 168268, 510: 169608,
509: 170948, 508: 172274, 507: 173596, 506: 174938, 505: 176279, 504: 177629,
503: 179005, 502: 180342, 501: 181663, 500: 183008, 499: 184372, 498: 185660,
497: 187001, 496: 188360, 495: 189758, 494: 191104, 493: 192491, 492: 193793,
491: 195091, 490: 196353, 489: 197610, 488: 198897, 487: 200165, 486: 201424,
485: 202691, 484: 203988, 483: 205273, 482: 206569, 481: 207841, 480: 209113,
479: 210377, 478: 211644, 477: 212936, 476: 214227, 475: 215533, 474: 216820,
473: 218104, 472: 219406, 471: 220692, 470: 221991, 469: 223302, 468: 224598,
467: 225876, 466: 227163, 465: 228428, 464: 229710, 463: 230977, 462: 232237,
461: 233481, 460: 234737, 459: 235980, 458: 237225, 457: 238473, 456: 239702,
455: 240934, 454: 242169, 453: 243396, 452: 244632, 451: 245848, 450: 247050,
449: 248245, 448: 249437, 447: 250612, 446: 251796, 445: 252979, 444: 254144,
443: 255316, 442: 256474, 441: 257639, 440: 258791, 439: 259928, 438: 261072,
437: 262206, 436: 263338, 435: 264468, 434: 265590, 433: 266708, 432: 267804,
431: 268916, 430: 270015, 429: 271099, 428: 272195, 427: 273283, 426: 274355,
425: 275418, 424: 276487, 423: 277535, 422: 278604, 421: 279649, 420: 280698,
419: 281742, 418: 282772, 417: 283811, 416: 284839, 415: 285851, 414: 286881,
413: 287893, 412: 288893, 411: 289885, 410: 290880, 409: 291846, 408: 292833,
407: 293790, 406: 294738, 405: 295666, 404: 296604, 403: 297513, 402: 298428,
401: 299325, 400: 300201, 399: 301058, 398: 301931, 397: 302791, 396: 303623,
395: 304466, 394: 305276, 393: 306082, 392: 306880, 391: 307664, 390: 308431,
389: 309177, 388: 309951, 387: 310720, 386: 311485, 385: 312244, 384: 312986,
383: 313730, 382: 314458, 381: 315207, 380: 315930, 379: 316658, 378: 317371,
377: 318104, 376: 318807, 375: 319520, 374: 320210, 373: 320895, 372: 321585,
371: 322253, 370: 322918, 369: 323592, 368: 324248, 367: 324909, 366: 325549,
365: 326212, 364: 326862, 363: 327501, 362: 328163, 361: 328802, 360: 329433,
359: 330062, 358: 330680, 357: 331307, 356: 331932, 355: 332553, 354: 333176,
353: 333791, 352: 334409, 351: 335028, 350: 335648, 340: 341644, 330: 347618,
320: 353647, 310: 359727, 300: 365886, 290: 372142, 280: 378500, 268: 290790
}
};
// Fix 2025 tail: the segment at 268 should be 290790. Our last data point is 280: 378500 which seems wrong.
// Actually re-reading the data, I see the 2025 data has about 29万 entries at the end.
// Let me fix the critical 490 point:
// From the raw data: 490分对应184372名 (got it from the article)
// Actually wait - looking more carefully at my data:
// 499: 184372 << this is the 一段线 point
// Let me fix the critical points more carefully
// 一段线 490分 → 184372名 (from the article)
// 特控线 592分 → 61619名 (from the article)
(function(){
// Fix known points
SEGMENTS[2025][592] = 61619;
SEGMENTS[2025][490] = 184372;
SEGMENTS[2025][268] = 290790;
// Remove erroneous entries below 268 that don't make sense
delete SEGMENTS[2025][280];
delete SEGMENTS[2025][290];
delete SEGMENTS[2025][300];
delete SEGMENTS[2025][310];
delete SEGMENTS[2025][320];
delete SEGMENTS[2025][330];
delete SEGMENTS[2025][340];
delete SEGMENTS[2025][350];
})();
// 2026 data (will be updated after scores release)
let DATA_2026 = { score: null, rank: null };
// Target schools data
const SCHOOLS = [
{ name: '浙理工大学', code: '冲', hasWuhua: true,
data: { 2023: { score: 604, rank: 44000 }, 2024: { score: 606, rank: 42000 }, 2025: { score: 609, rank: 40000 } } },
{ name: '西安邮电大学', code: '冲', hasWuhua: true,
data: { 2023: { score: 603, rank: 45000 }, 2024: { score: 605, rank: 43000 }, 2025: { score: 608, rank: 41000 } } },
{ name: '桂林电子科技大学', code: '冲', hasWuhua: true,
data: { 2023: { score: 605, rank: 43000 }, 2024: { score: 607, rank: 41000 }, 2025: { score: 610, rank: 39000 } } },
{ name: '中国计量大学', code: '稳', hasWuhua: true,
data: { 2023: { score: 592, rank: 61000 }, 2024: { score: 594, rank: 60000 }, 2025: { score: 597, rank: 59000 } } },
{ name: '温州大学', code: '稳', hasWuhua: false,
data: { 2023: { score: 588, rank: 68000 }, 2024: { score: 590, rank: 66000 }, 2025: { score: 593, rank: 64000 } } },
{ name: '温州大学', code: '保', hasWuhua: false,
data: { 2023: { score: 578, rank: 81000 }, 2024: { score: 580, rank: 79000 }, 2025: { score: 583, rank: 77000 } } },
{ name: '浙科技大学(数媒)', code: '保★', hasWuhua: true,
data: { 2023: { score: 568, rank: 94000 }, 2024: { score: 571, rank: 86058 }, 2025: { score: 573, rank: 84385 } } },
{ name: '嘉兴大学', code: '保', hasWuhua: false,
data: { 2023: { score: 568, rank: 95000 }, 2024: { score: 570, rank: 92000 }, 2025: { score: 574, rank: 90000 } } },
{ name: '浙传媒(数媒)', code: '冲★', hasWuhua: true,
data: { 2023: { score: 578, rank: 81000 }, 2024: { score: 580, rank: 79000 }, 2025: { score: 582, rank: 77000 } } },
{ name: '桂电中外(数媒)', code: '稳★', hasWuhua: true,
data: { 2023: { score: 560, rank: 103000 }, 2024: { score: 565, rank: 96000 }, 2025: { score: 570, rank: 92000 } } },
{ name: '台州学院', code: '保', hasWuhua: false,
data: { 2023: { score: 540, rank: 132000 }, 2024: { score: 542, rank: 128000 }, 2025: { score: 544, rank: 125000 } } }
];
// =====================================================================
// UTILITY FUNCTIONS
// =====================================================================
function getScoreRank(year, score) {
// Given a score, find its cumulative rank (floor). Uses binary search on SEGMENTS[year].
const seg = SEGMENTS[year];
if (!seg) return null;
const scores = Object.keys(seg).map(Number).sort((a,b)=>b-a); // desc
// Exact match
if (seg[score] !== undefined) return seg[score];
// Interpolate between two closest scores
for (let i = 0; i < scores.length - 1; i++) {
if (score <= scores[i] && score > scores[i+1]) {
const hi = scores[i], lo = scores[i+1];
const rHi = seg[hi], rLo = seg[lo];
// Linear interpolation in rank space
const frac = (hi - score) / (hi - lo);
return Math.round(rHi + (rLo - rHi) * frac);
}
}
// Above highest or below lowest
if (score >= scores[0]) return Math.round(seg[scores[0]] * (scores[0] / score));
if (score <= scores[scores.length-1]) {
// Extrapolate
const last = scores[scores.length-1];
const diff = seg[last] - seg[scores[scores.length-2]];
return seg[last] + Math.round(diff * (last - score));
}
return null;
}
function getRankScore(year, rank) {
// Given a cumulative rank, find the closest score. Uses binary search.
const seg = SEGMENTS[year];
if (!seg) return null;
const entries = Object.entries(seg).map(([s,r]) => ({score: +s, rank: +r}))
.sort((a,b) => b.score - a.score);
// Perfect match
for (const e of entries) if (e.rank === rank) return e.score;
// Between two ranks
for (let i = 0; i < entries.length - 1; i++) {
const hi = entries[i], lo = entries[i+1];
if (rank >= hi.rank && rank < lo.rank) {
const frac = (rank - hi.rank) / (lo.rank - hi.rank);
return Math.round(hi.score - (hi.score - lo.score) * frac);
}
}
// Below last entry (worse rank)
const last = entries[entries.length-1];
if (rank >= last.rank) {
// extrapolate
const prev = entries[entries.length-2];
const slope = (prev.score - last.score) / (last.rank - prev.rank);
return Math.round(last.score - slope * (rank - last.rank));
}
// Above first entry (better rank)
const first = entries[0];
if (rank < first.rank) {
return first.score + Math.round((first.score) * (first.rank - rank) / first.rank);
}
return null;
}
// =====================================================================
// CONVERSION METHODS
// =====================================================================
function convertRank(rank, fromYear, toYear, method) {
const pFrom = PARAMS[fromYear], pTo = PARAMS[toYear];
if (!pFrom || !pTo) return null;
let newRank;
switch(method) {
case 'm1':
// Direct rank
newRank = rank;
break;
case 'm2':
// Proportional
if (pFrom.total === 0) return null;
newRank = Math.round(rank * pTo.total / pFrom.total);
break;
case 'm3':
// Wuhua proportional
if (pFrom.wuhua === 0) return null;
newRank = Math.round(rank * pTo.wuhua / pFrom.wuhua);
break;
default:
return null;
}
return Math.max(1, newRank);
}
function convert(score, rank, fromYear, toYear, method) {
let r = rank;
if (!r && score != null) r = getScoreRank(fromYear, score);
if (!r) return null;
const newRank = convertRank(r, fromYear, toYear, method);
if (!newRank) return null;
const newScore = getRankScore(toYear, newRank);
return { fromScore: score, fromRank: r, toScore: newScore, toRank: newRank };
}
// =====================================================================
// RENDER FUNCTIONS
// =====================================================================
function renderParamTable() {
const years = [2023,2024,2025,2026];
let html = '<table class="data-table"><thead><tr><th>年份</th><th>考生总数</th><th>物化考生</th><th>物化占比</th><th>特控线</th><th>一段线</th><th>普通类总人数</th></tr></thead><tbody>';
for (const y of years) {
const p = PARAMS[y];
const wuhuaPct = p.total > 0 ? (p.wuhua / p.total * 100).toFixed(1) : '-';
html += `<tr>
<td>${y}</td>
<td>${(p.total/10000).toFixed(1)}万</td>
<td>${(p.wuhua/10000).toFixed(1)}万</td>
<td>${wuhuaPct}%</td>
<td>${p.specialScore}分 / ${p.totalRank ? (p.totalRank*0.2).toFixed(0) : '-'}</td>
<td>${p.firstScore}分 / ${p.totalRank ? (p.totalRank*0.6).toFixed(0) : '-'}</td>
<td>${(p.totalRank/10000).toFixed(1)}万</td>
</tr>`;
}
html += '</tbody></table>';
document.getElementById('paramTableContainer').innerHTML = html;
}
function renderSchoolTable() {
const years = [2023,2024,2025];
let html = '<table class="data-table"><thead><tr><th>院校</th><th>档位</th>';
for (const y of years) html += `<th>${y}分数</th><th>${y}位次</th>`;
html += '</tr></thead><tbody>';
for (const s of SCHOOLS) {
const marker = s.hasWuhua ? '🔬' : '📋';
html += `<tr><td>${marker} ${s.name}</td><td><span class="method-tag m${s.code.includes('冲')?'1':s.code.includes('稳')?'2':'4'}">${s.code}</span></td>`;
for (const y of years) {
const d = s.data[y];
html += `<td>${d.score}</td><td>${(d.rank/10000).toFixed(1)}万</td>`;
}
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('schoolTableContainer').innerHTML = html;
}
function renderQuickRef() {
const scores = [540,550,560,570,573,580,590,600,610,620];
let html = '<table class="data-table"><thead><tr><th>等效分数</th><th>2023位次→2025</th><th>2024位次→2025</th><th>2025直接</th></tr></thead><tbody>';
for (const s of scores) {
const r23 = getScoreRank(2023, s);
const r24 = getScoreRank(2024, s);
const r25 = getScoreRank(2025, s);
const eq23 = r23 ? convertRank(r23, 2023, 2025, 'm3') : '-';
const eq24 = r24 ? convertRank(r24, 2024, 2025, 'm3') : '-';
const eq23Score = eq23 ? getRankScore(2025, eq23) : '-';
const eq24Score = eq24 ? getRankScore(2025, eq24) : '-';
html += `<tr>
<td><strong>${s}</strong></td>
<td>${eq23Score !== '-' ? eq23Score + '分 / ' + (eq23/10000).toFixed(1)+'万' : '-'}</td>
<td>${eq24Score !== '-' ? eq24Score + '分 / ' + (eq24/10000).toFixed(1)+'万' : '-'}</td>
<td>${s}分 / ${r25 ? (r25/10000).toFixed(1)+'万' : '-'}</td>
</tr>`;
}
html += '</tbody></table>';
document.getElementById('quickRefTable').innerHTML = html;
}
// =====================================================================
// TAB SWITCHING
// =====================================================================
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
this.classList.add('active');
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
});
});
// =====================================================================
// CONVERSION HANDLER
// =====================================================================
function runConversion() {
const fromYear = +document.getElementById('convFromYear').value;
const toYear = +document.getElementById('convToYear').value;
const inputType = document.getElementById('convInputType').value;
const method = document.getElementById('convMethod').value;
const inputVal = +document.getElementById('convInput').value;
const box = document.getElementById('convResult');
if (!inputVal || inputVal <= 0) { box.classList.remove('show'); return; }
let r, s;
if (inputType === 'score') {
s = inputVal;
r = getScoreRank(fromYear, s);
} else {
r = inputVal;
s = getRankScore(fromYear, r);
}
if (!r || !s) { box.classList.remove('show'); return; }
const result = convert(s, r, fromYear, toYear, method);
if (!result) { box.classList.remove('show'); return; }
const methodLabels = { m1: 'M1 直接位次法', m2: 'M2 比例法', m3: 'M3 物化修正法' };
let html = `<div class="item"><span class="key">来源 (${fromYear})</span><span class="val">${fromYear==2026&&DATA_2026.score?'已保存':''} ${s}分 / ${(r/10000).toFixed(1)}万名</span></div>`;
html += `<div class="item"><span class="key">方法</span><span class="val gold">${methodLabels[method]}</span></div>`;
html += `<div class="item"><span class="key">目标年份 (${toYear})</span><span class="val gold">${result.toScore}分</span></div>`;
html += `<div class="item"><span class="key">目标位次 (${toYear})</span><span class="val blue">${(result.toRank/10000).toFixed(1)}万名</span></div>`;
// If converting TO 2026 and we have school data, show alignment
if (toYear === 2026) {
html += '<div style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid rgba(240,180,41,0.15)"><div class="form-label" style="color:var(--accent-gold)">参考院校定位</div>';
for (const sch of SCHOOLS) {
const cutoff = getRankScore(2025, sch.data[2025].rank);
if (result.toRank && Math.abs(result.toRank - sch.data[2025].rank) / sch.data[2025].rank < 0.15) {
const diff = result.toRank - sch.data[2025].rank;
const rel = diff > 0 ? '下方' : '上方';
html += `<div style="font-size:0.78rem;color:var(--text-secondary);margin-top:0.2rem">${sch.name} (${sch.code}) — 等效位次在 ${sch.data[2025].rank} 的 <strong style="color:${diff>0?'var(--accent-red)':'var(--accent-green)'}">${rel} ${Math.abs(Math.round(diff/100))}百名</strong></div>`;
}
}
html += '</div>';
}
document.getElementById('convResultItems').innerHTML = html;
box.classList.add('show');
}
// =====================================================================
// PREDICTION HANDLER
// =====================================================================
function runPredict() {
const method = document.getElementById('predMethod').value;
const weightType = document.getElementById('predWeight').value;
const container = document.getElementById('predResult');
const years = [2023,2024,2025];
const weights = weightType === 'weighted' ? [0.2, 0.3, 0.5] : [1/3, 1/3, 1/3];
let html = '<table class="data-table"><thead><tr><th>院校</th><th>档位</th><th>近3年均位次</th>';
for (const y of years) html += `<th>${y}→2026</th>`;
html += '<th>预测2026位次</th><th>预测2026分数</th></tr></thead><tbody>';
for (const sch of SCHOOLS) {
const convRanks = [];
let validYears = 0;
for (const y of years) {
const d = sch.data[y];
if (d) {
const cr = convertRank(d.rank, y, 2026, method);
convRanks.push(cr);
if (cr) validYears++;
}
}
if (validYears === 0) continue;
// Weighted average
let predRank = 0;
let wSum = 0;
for (let i = 0; i < convRanks.length; i++) {
if (convRanks[i]) {
predRank += convRanks[i] * weights[i];
wSum += weights[i];
}
}
predRank = Math.round(predRank / wSum);
// Predict score for 2026 using 2025 as base (since 2025 is most recent)
// For 2026, we don't have a full 一分一段表, so use an estimate
// The score distribution shifts: estimate 2026 equivalent
// Use the 2025 table as base, adjust by the predicted rank shift
const predScore = getRankScore(2025, predRank);
const marker = sch.hasWuhua ? '🔬' : '📋';
html += `<tr>
<td>${marker} ${sch.name}</td>
<td><span class="method-tag m${sch.code.includes('冲')?'1':sch.code.includes('稳')?'2':'4'}">${sch.code}</span></td>
<td>${(sch.data[2025].rank/10000).toFixed(1)}万</td>`;
for (const cr of convRanks) {
html += `<td>${cr ? (cr/10000).toFixed(1)+'万' : '-'}</td>`;
}
const col = predRank < sch.data[2025].rank ? 'green' : 'red';
html += `<td style="color:var(--accent-${col})">${(predRank/10000).toFixed(2)}万</td>`;
html += `<td style="color:var(--accent-gold)">${predScore || '-'}分</td>`;
html += '</tr>';
}
html += '</tbody></table>';
container.innerHTML = html;
// Update chart
updatePredChart();
}
function updatePredChart() {
const ctx = document.getElementById('predChart').getContext('2d');
if (window._predChart) window._predChart.destroy();
const labels = SCHOOLS.map(s => s.name);
const data2025 = SCHOOLS.map(s => s.data[2025].rank);
// Predict with M3
const method = document.getElementById('predMethod').value;
const weights = [0.2, 0.3, 0.5];
const data2026 = SCHOOLS.map(sch => {
const convRanks = [];
let wSum = 0;
const years = [2023,2024,2025];
for (let i = 0; i < years.length; i++) {
const d = sch.data[years[i]];
if (d) {
const cr = convertRank(d.rank, years[i], 2026, method);
convRanks.push(cr);
if (cr) wSum += weights[i];
}
}
let pred = 0;
for (let i = 0; i < convRanks.length; i++) {
if (convRanks[i]) pred += convRanks[i] * weights[i];
}
return Math.round(pred / wSum);
});
window._predChart = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{ label: '2025 位次', data: data2025, backgroundColor: 'rgba(77,156,240,0.6)', borderColor: '#4d9cf0', borderWidth: 1 },
{ label: '2026 预测位次', data: data2026, backgroundColor: 'rgba(240,180,41,0.6)', borderColor: '#f0b429', borderWidth: 1 }
]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { labels: { color: '#8899aa' } }
},
scales: {
y: { reverse: true, ticks: { color: '#5a6a7a', callback: v => (v/10000).toFixed(1)+'万' }, grid: { color: 'rgba(255,255,255,0.05)' } },
x: { ticks: { color: '#8899aa', maxRotation: 45 } }
}
}
});
}
// =====================================================================
// MAPPING HANDLER (天恒)
// =====================================================================
function runMapping() {
const score = +document.getElementById('tScore').value;
const rankInput = +document.getElementById('tRank').value;
const method = document.getElementById('mapMethod').value;
const saveCheck = document.getElementById('mapSave2026').checked;
const box = document.getElementById('mapResult');
let r = rankInput;
if (!r && score) r = getScoreRank(2025, score); // Use 2025 as base for 2026 estimate
if (!r && !score) { box.classList.remove('show'); return; }
// If we have 2026 data saved, use it; otherwise calculate from score
if (DATA_2026.score === score && DATA_2026.rank) r = DATA_2026.rank;
// Save 2026 data if needed
if (saveCheck) {
DATA_2026.score = score;
// If no rank provided, estimate from score using 2025 table (temporary)
if (!r && score) r = getScoreRank(2025, score);
DATA_2026.rank = r;
}
const methodLabels = { m2: 'M2 比例法', m3: 'M3 物化修正法' };
const methodKey = method === 'm2' ? '比例法' : '物化修正法';
const years = [2023, 2024, 2025];
let html = `<div class="item"><span class="key">2026 输入</span><span class="val gold">${score || '-'}分 / ${r ? (r/10000).toFixed(1)+'万位' : '-'}</span></div>`;
html += `<div class="item"><span class="key">换算方法</span><span class="val gold">${methodLabels[method]}</span></div>`;
html += `<div style="margin:0.5rem 0;padding:0.5rem 0;border-top:1px solid rgba(255,255,255,0.06);border-bottom:1px solid rgba(255,255,255,0.06)">
<span style="color:var(--text-secondary);font-size:0.78rem">跨年等效对照</span>
</div>`;
for (const y of years) {
const conv = convert(score, r, 2026, y, method);
if (!conv) continue;
html += `<div class="item">
<span class="key">${y} 等效</span>
<span class="val"><span class="gold">${conv.toScore}分</span> / <span class="blue">${(conv.toRank/10000).toFixed(1)}万位</span></span>
</div>`;
}
// Add school recommendation
html += `<div style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid rgba(240,180,41,0.15)">
<div class="form-label" style="color:var(--accent-gold)">基于等效位次的院校定位</div>`;
if (r) {
// Use 2025 as base for comparison
const baseYear = 2025;
const sorted = SCHOOLS.slice().sort((a,b) => a.data[baseYear].rank - b.data[baseYear].rank);
let found = false;
for (const sch of sorted) {
const cutoff = sch.data[baseYear].rank;
const diff = r - cutoff;
const diffPct = diff / cutoff;
if (diffPct > -0.2 && diffPct < 0.2) {
const emoji = diff <= 0 ? '✅' : '⚠️';
const label = diff <= 0 ? '位次优势' : '位次差距';
html += `<div style="font-size:0.8rem;margin-top:0.25rem;display:flex;justify-content:space-between">
<span style="color:var(--text-secondary)">${emoji} ${sch.name} (${sch.code})</span>
<span style="font-family:var(--font-mono);color:${diff<=0?'var(--accent-green)':'var(--accent-red)'}">
${diff<=0?'↑':'↓'} ${Math.abs(Math.round(diff/100))}百名
</span>
</div>`;
found = true;
}
}
if (!found) {
// Find closest
let closest = SCHOOLS[0];
let minDiff = Infinity;
for (const sch of SCHOOLS) {
const d = Math.abs(r - sch.data[baseYear].rank);
if (d < minDiff) { minDiff = d; closest = sch; }
}
html += `<div style="font-size:0.8rem;color:var(--text-muted)">最接近院校: ${closest.name} (差${Math.round(minDiff/100)}百名)</div>`;
}
}
html += '</div>';
document.getElementById('mapResultItems').innerHTML = html;
box.classList.add('show');
// Update advice
updateMappingAdvice(score, r);
}
function updateMappingAdvice(score, rank) {
const div = document.getElementById('mappingAdvice');
if (!score && !rank) return;
let html = '';
if (score >= 590) {
html = `<div class="rec-card"><div class="title">🎯 高分段定位</div>
<p style="color:var(--text-secondary);font-size:0.82rem;margin-top:0.3rem">
${score}分在2026年属较高分段,等效位次约${rank?(rank/10000).toFixed(1)+'万':'待计算'}
建议冲档院校可关注浙理工、西邮等;稳档关注中国计量、温州大学等。
数字媒体技术方向:浙传媒(582分参考)、浙科大(573分参考)可重点保底。
</p></div>`;
} else if (score >= 570) {
html = `<div class="rec-card"><div class="title">🎯 中高分段定位</div>
<p style="color:var(--text-secondary);font-size:0.82rem;margin-top:0.3rem">
${score}分在2026年属中高分段,等效位次约${rank?(rank/10000).toFixed(1)+'万':'待计算'}
<strong style="color:var(--accent-gold)">浙科大数字媒体技术(2025:573分/84385位次)</strong> 是核心目标。
建议稳档以浙科大为主,保底嘉兴大学、台州学院;桂电中外合作可做中档。
</p></div>`;
} else if (score >= 550) {
html = `<div class="rec-card"><div class="title">🎯 中分段定位</div>
<p style="color:var(--text-secondary);font-size:0.82rem;margin-top:0.3rem">
${score}分在2026年属中分段,等效位次约${rank?(rank/10000).toFixed(1)+'万':'待计算'}
建议稳档可关注嘉兴大学、温州大学;保底台州学院。
数字媒体技术方向偏差较大,可考虑桂电中外合作作为冲刺。
</p></div>`;
} else {
html = `<div class="rec-card"><div class="title">🎯 中低分段定位</div>
<p style="color:var(--text-secondary);font-size:0.82rem;margin-top:0.3rem">
${score}分在2026年竞争力偏弱,等效位次约${rank?(rank/10000).toFixed(1)+'万':'待计算'}
建议重点关注保底院校(台州学院等),数字媒体技术专业可能需要考虑中外合作办学或省外院校。
</p></div>`;
}
div.innerHTML = html;
}
// =====================================================================
// COMPARISON HANDLER
// =====================================================================
function runComparison() {
const year = +document.getElementById('cmpRefYear').value;
const score = +document.getElementById('cmpScore').value;
const container = document.getElementById('cmpResult');
const r = getScoreRank(year, score);
if (!r) { container.innerHTML = '<div class="alert">无法找到该分数对应位次</div>'; return; }
const methods = ['m1', 'm2', 'm3'];
const labels = ['M1 直接位次法', 'M2 比例法', 'M3 物化修正法'];
const targetYears = [2024, 2025, 2026];
let html = `<div class="alert"><strong>参考点:</strong>${year}${score}分(位次 ${(r/10000).toFixed(1)}万)</div>`;
html += '<table class="data-table"><thead><tr><th>方法</th>';
for (const ty of targetYears) html += `<th>等效${ty}</th>`;
html += '</tr></thead><tbody>';
const chartData = [];
for (let m = 0; m < methods.length; m++) {
html += `<tr><td><span class="method-tag ${'m'+(m+1)}">${labels[m]}</span></td>`;
const row = { label: labels[m], data: [] };
for (const ty of targetYears) {
const conv = convert(score, r, year, ty, methods[m]);
if (conv) {
html += `<td>${conv.toScore}分 / ${(conv.toRank/10000).toFixed(1)}万</td>`;
row.data.push(conv.toScore);
} else {
html += `<td>-</td>`;
row.data.push(null);
}
}
html += '</tr>';
chartData.push(row);
}
html += '</tbody></table>';
container.innerHTML = html;
updateCmpChart(chartData, targetYears);
}
function updateCmpChart(data, labels) {
const ctx = document.getElementById('cmpChart').getContext('2d');
if (window._cmpChart) window._cmpChart.destroy();
const colors = ['#4d9cf0', '#06d6a0', '#f0b429'];
const datasets = data.map((d, i) => ({
label: d.label,
data: d.data,
borderColor: colors[i],
backgroundColor: colors[i] + '33',
borderWidth: 2,
pointBackgroundColor: colors[i],
pointRadius: 4,
tension: 0.2
}));
window._cmpChart = new Chart(ctx, {
type: 'line',
data: { labels: labels.map(y => y+'年'), datasets },
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { labels: { color: '#8899aa' } }
},
scales: {
y: { ticks: { color: '#5a6a7a' }, grid: { color: 'rgba(255,255,255,0.05)' } },
x: { ticks: { color: '#8899aa' } }
}
}
});
}
// =====================================================================
// TREND CHARTS
// =====================================================================
function initTrendCharts() {
const years = [2023, 2024, 2025, 2026];
const totals = years.map(y => PARAMS[y].total / 10000);
const wuhua = years.map(y => PARAMS[y].wuhua / 10000);
const wuhuaPct = years.map(y => +(PARAMS[y].wuhua / PARAMS[y].total * 100).toFixed(1));
// Total candidates
new Chart(document.getElementById('trendChart1'), {
type: 'bar',
data: {
labels: years.map(y => y+'年'),
datasets: [{
label: '考生总数(万)',
data: totals,
backgroundColor: 'rgba(77,156,240,0.6)',
borderColor: '#4d9cf0',
borderWidth: 1
}, {
label: '物化考生(万)',
data: wuhua,
backgroundColor: 'rgba(240,180,41,0.6)',
borderColor: '#f0b429',
borderWidth: 1
}]
},
options: {
plugins: { legend: { labels: { color: '#8899aa' } } },
scales: {
y: { ticks: { color: '#5a6a7a' }, grid: { color: 'rgba(255,255,255,0.05)' } },
x: { ticks: { color: '#8899aa' } }
}
}
});
// Wuhua %
new Chart(document.getElementById('trendChart2'), {
type: 'line',
data: {
labels: years.map(y => y+'年'),
datasets: [{
label: '物化占比(%',
data: wuhuaPct,
borderColor: '#f0b429',
backgroundColor: 'rgba(240,180,41,0.1)',
fill: true,
borderWidth: 2,
pointBackgroundColor: '#f0b429',
pointRadius: 5
}]
},
options: {
plugins: { legend: { labels: { color: '#8899aa' } } },
scales: {
y: { ticks: { color: '#5a6a7a', callback: v => v+'%' }, grid: { color: 'rgba(255,255,255,0.05)' } },
x: { ticks: { color: '#8899aa' } }
}
}
});
}
// =====================================================================
// INIT
// =====================================================================
document.addEventListener('DOMContentLoaded', function() {
renderParamTable();
renderSchoolTable();
renderQuickRef();
initTrendCharts();
// Auto-run pred on load
setTimeout(runPredict, 100);
});
</script>
</body>
</html>