Files
2026gaokaozhiyuan/deploy/nginx/html/reports/等效位次与等效分数分析工具.html
T
simon b6df028839 v1.0.0 高考志愿门户完整修复
变更摘要:
- [fix] 清理根目录 index.html 遗留代码(questionnaire-error + retryLoad 移除)
- [fix] 副标题同步更新为自估591分 + 候选志愿546条数据校准
- [fix] 页脚注入版本号 v1.0.0
- [fix] 部署版 index.html: 取消家长标签,统一 filler-tianheng
- [fix] Dashboard 文案修正(4份→2份线上问卷)
- [fix] 清理冗余部署脚本(v1-v4归档至 archived_scripts/)
- [fix] 等效位次工具纳入 deploy 目录
- [chore] .gitignore 初始化
- [init] Git 仓库初始化
2026-06-24 13:11:34 +08:00

1489 lines
61 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>