把全国大学画进山河:拆解高考志愿填报模拟器

发布于 2026年07月14日 00:46 #前端#Maps 原文链接

把全国大学画进山河:拆解高考志愿填报模拟器 封面图
  • 使用 MapLibre GL 和分层设色地形构建可交互中国地图,支持球面与平面投影切换
  • 国风视觉系统以宣纸色、朱砂红、楷体为核心,旗子系统按 985/211 等分层着色
  • 冲稳保推荐基于概率区间,聚焦模式降低非推荐校透明度以减轻信息过载
  • 志愿表支持拖拽排序、多校对比、风险提示,并集成 RIASEC 兴趣测评辅助选专业
  • 工程细节包括内联 SVG 宣纸噪点、prefers-reduced-motion 兜底、移动端底部抽屉适配

在线体验:高考志愿填报模拟器

大家好,我是若风。

最近刷到一个小而美的工具——高考志愿填报模拟器。它做的事并不新:填个分数或位次,告诉你能上哪些大学。但打开它的那一瞬间,我有点意外:它没有做成一个表格、也没有做成一个排行榜,而是把全国 1500+ 所大学落在了一幅可以漫游的山河画卷上,背景是水墨宣纸,选中的大学会立起朱砂红的旗子。

填报志愿本来是一件非常焦虑的事,但这个网站硬是把它做得很安静、很顺滑。

我花了点时间把它从 HTML 到 JS 全翻了一遍,想搞清楚一个问题:它到底是怎么把地图、数据和志愿填报这三件事揉到一起的? 结论比想象的清晰——它的核心是 MapLibre 地图 + 分层设色地形 + 一套状态驱动的 UI,外加几个非常克制的小创新。

它到底做了什么

先交代背景。这个模拟器核心场景是:

  1. 在一幅可缩放、可平移的中国地形图上,标注全国 1500+ 所本科院校的位置。
  2. 用户填入省份、科类(文/理或新高考选科)、分数或位次。
  3. 系统按「冲、稳、保」三档点亮地图上的学校,并给出录取概率。
  4. 点开任一学校,看历年各专业的录取位次曲线。
  5. 把心仪的学校收进「志愿表」,可拖拽排序、多校对比。

围绕这条主线,它还加了几个增强体验的东西:RIASEC 兴趣测评帮你找专业方向、一分一段曲线可视化、分区天气动画、地球/平面两种投影切换,以及一个每天一抽的「今日签」彩蛋。

下面拆它的技术实现。

一、地图引擎:MapLibre GL + 分层设色

整个网站的底座是一张地图。但它没有用百度/高德这些国内商业地图,而是用了开源的 MapLibre GL JS——这是 Mapbox GL 的开源 fork,矢量瓦片渲染、支持 3D 地形和地球投影。

<script src="vendor/maplibre-gl.js"></script>
<script src="gk-engine.js?v=10"></script>

引擎脚本 maplibre-gl.js 约 1MB,被刻意放到了 <body> 末尾,而不是 <head>。注释写得很直白:

引擎(~1MB)挪到 body 末尾:原本在 <head> 阻塞渲染,
整页(含加载页)要等它下完才出;
现在加载页先秒出山水底,引擎在其后台下载

这是一个很实在的首屏优化:先让用户看到一张静态的精致地形底图(一张 156K 的 relief-cn2.webp,在 <head>preload),引擎在后台悄悄下,下完再无缝替换成可交互地图。用户感知到的不是「白屏等加载」,而是「画卷正在铺开」。

分层设色:让地形看起来像水墨画

地图的视觉风格是这个网站最抓人的地方。它默认是「山形符号」画风,可以切换到「水彩浮雕」。核心实现是 MapLibre 的颜色分层(ramp),按海拔用一组色标做线性插值:

const STYLES = {
  sym: {
    ramp: ["interpolate", ["linear"], ["elevation"],
      -11000, "#efe7d2", -1, "#efe7d2", 0, "#9fb87e",
      250, "#b3bc7e", 700, "#d2bd72", 1200, "#e3b95c",
      2000, "#dfa64e", 3000, "#bc8a52", 4000, "#a87f54",
      5000, "#cbb78f", 5600, "#f6f0e1"],
    hsShadow: "#6e4f2c", hsHighlight: "#fff6dd",
    hsAccent: "#8a6a3f", hsEx: 0.18,
    terrainEx: 3.2, symbols: true, label: "画风:山形符号"
  },
  wc: {
    ramp: ["interpolate", ["linear"], ["elevation"],
      -11000, "#eee7d3", 0, "#a9bf8e", 250, "#bcc88e",
      700, "#d6c98f", 1300, "#ddbe7e", 2100, "#d3a368",
      3000, "#b98e63", 4700, "#b3a7a8", 5800, "#fbf9f4"],
    label: "画风:水彩浮雕"
    // ...
  }
};

这套色标不是随便选的,它对应的是经典的分层设色法(hypsometric tint):平原绿、丘陵黄、山地棕、雪线白。这是地理学教材里用了几十年的地形表达法,所以一眼看上去「很地理」,但整体色温被压暖、饱和度被压低,于是又像一幅旧式山水画。

除了颜色分层,它还叠加了晕渲(hillshade)——hsShadow(阴影色)、hsHighlight(高光色)、hsAccent(强调色)三个参数控制光照。两种画风的区别主要在光照强度:山形符号画风 hsEx: 0.18,对比弱、像木刻;水彩浮雕 hsEx: 0.55,对比强、像立体浮雕。

投影切换:地球与画卷

它支持两种投影模式:球面地球(globe)和平面 Mercator 画卷。切换时不只是换投影,连天空、雾色、背景色都一起换:

function applyProjection(on) {
  globeOn = on;
  map.setProjection({ type: on ? "globe" : "mercator" });
  map.setPaintProperty("bg", "background-color",
    on ? "#c3d3cf" : "#ece2c8");  // 球面淡青海,平面宣纸色
  try { map.setSky(on ? SKY_GLOBE : SKY_FLAT); } catch(e) {}
  flyHome(700);
}

一个细节值得注意:默认是球面,但放大到一定级别(z≈5)会自动摊平成画卷——既有「俯瞰九州」的仪式感,又保证放大后查校时是正读的平面图。

二、国风视觉系统:朱砂、宣纸与楷体

除了地图本身,整个 UI 的视觉语言是一套高度统一的中式设计。CSS 变量定义在 :root

:root {
  --paper-a: rgba(251,244,228,.95);
  --paper-b: rgba(243,234,213,.95);
  --ink: #3f3220;
  --ink-soft: #6b5630;
  --zhu: #9c2f1b;        /* 朱砂红,主操作色 */
  --gold: #bd9a55;
  --gold-soft: #d8c08a;
  --elev: 0 14px 34px -8px rgba(74,52,20,.30),
          0 0 0 3px rgba(189,154,85,.12),
          0 1px 0 rgba(255,250,238,.6) inset;
}

这套配色的逻辑很清楚:

  • 宣纸色--paper-a/b)做面板背景,半透明 + 毛玻璃(backdrop-filter: blur(10px)),像一张半透明的宣纸浮在地图上。
  • 朱砂红--zhu)只用在「主操作」和「选中态」——推荐按钮、选中校的旗子描边、概率徽章。它出现得越克制,出现时越有份量。
  • 金色--gold)做边框和点缀,是宣纸和朱砂之间的过渡色。
  • 字体全场用楷体栈 "Kaiti SC","STKaiti","KaiTi",serif,连 <select><input> 都不放过。

更巧妙的是旗子系统。地图上每所大学是一面旗子,按层次用不同颜色:

.t985 .banner, .t985 .pole { background:#8c2f1b; color:#f7e8d6; }  /* 985:朱砂红 */
.t211 .banner, .t211 .pole { background:#96660f; color:#f7e8d6; }  /* 211:赭金 */
.tdfc .banner, .tdfc .pole { background:#564a85; color:#ece9f7; }  /* 双一流:靛紫 */
.tben .banner, .tben .pole { background:#6b6357; color:#f2ece0; }  /* 其他:素灰 */

重叠时按层次叠放(985 最高,z-index:14),悬停时按光标距离动态排序,选中的校会描朱砂边 + 脉冲光圈。这不是一个简单的「打点标注」,而是一套有视觉层级的空间信息系统。

三、冲稳保推荐:从分数到点亮

推荐是这个工具的核心。它的输入很简单——省份、科类、分数或位次——输出是地图上的「冲稳保」三档高亮。

从源码能看到推荐的判定逻辑是概率区间。每个推荐结果带一个录取概率,映射到三档:

const REC_MAP = {
  "复旦大学": ["冲", "35%"],
  "南京大学": ["冲", "42%"],
  "中国科学技术大学": ["冲", "30%"],
  "厦门大学": ["稳", "68%"],
  "武汉大学": ["稳", "72%"],
  "华中科技大学": ["稳", "70%"],
  "中南大学": ["保", "91%"],
  "兰州大学": ["保", "93%"]
};

映射到 UI 上,三档有各自的强调色:

.umk.rec-冲 { --rc: #d85a30; }   /* 冲:橙红,有风险 */
.umk.rec-稳 { --rc: #e0a32e; }   /* 稳:金黄,较稳妥 */
.umk.rec-保 { --rc: #1d9e75; }   /* 保:墨绿,稳进 */

推荐结果在地图上有两种呈现模式:

  • 正常模式:所有学校旗子照常显示,推荐校加描边和脉冲。
  • 聚焦模式body.rec-mode):非推荐校的旗子降到 12% 透明度且不可点击,只让推荐的学校亮着——这是很聪明的信息减负,1500+ 所学校里只突出十几所。
body.rec-mode .umk .dot { opacity: .12; }
body.rec-mode .umk.rec .dot { opacity: 1; }
body.rec-mode .umk.rec .banner { outline: 2px solid var(--rc); animation: pulse 1.6s infinite; }

除了分数,推荐面板还支持高级筛选:限定地区、限定专业类(支持多选 + 搜索)、选科匹配。表单控件全部重绘成国风——连原生 <select> 的下拉箭头都被替换成了一个手绘的小折角 SVG:

.recform select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg ...><path d='M1 1l4 4 4-4' .../></svg>"), linear-gradient(...);
}

这是细节控才会做的事:原生 select 在不同操作系统长得不一样、且无法主题化,所以干脆自己画一个箭头。

四、院校详情与一分一段

点开任一所学校,会展开一张详情卡片。在宽屏(>1200px)下,卡片右侧会并排出现一个「招录数据看板」,展示近三年各专业的录取位次,甚至有位次升降趋势:

.dtrend.up { color: #d85a30; }   /* 位次上升=变难 */
.dtrend.dn { color: #1d9e75; }   /* 位次下降=变易 */

这里有一个特别用心的设计:历年多专业组用「最难组」展示,悬停看完整区间:

.dtab .rg { border-bottom: 1px dotted #c4b79a; cursor: help; }

这些数据呈现是「让人做决定」的关键,而不是单纯堆数据。它区分了「变难/变易」,让趋势一眼可见。

更进阶的是独立的「数说高考」可视化面板,有三个 tab:

  • 位次曲线:一分一段的历年变化。
  • 学科实力榜:各校学科评估。
  • 各省考生:考生分布。

这是一个独立的模态层(.vizpanel),居中弹出,用 SVG 自己画图表(#viz-chart svg),不依赖 Chart.js 这类库。

五、志愿表:可拖拽、可对比、可导出

志愿表是这个工具的「出口」——你在这里把选中的学校组织成一份正式的志愿表。

它的交互做得相当完整:

  • 收藏:从地图、推荐面板、详情卡片多处都能加入收藏,收藏数实时显示在右下角徽章。
  • 省份格式表头:志愿表顶部按省份的志愿表格式显示(fav-prov),还显示已填数和上限(超额时变红)。
  • 院校专业组聚合:同一所学校的不同专业组嵌套显示,支持组级拖拽排序。
.fav-g-h { cursor: grab; }
.fav-row[draggable] { cursor: grab; }
.fav-row.dragging { opacity: .4; }
  • 风险提示:志愿表下方有风险卡片,绿色(ok)表示志愿结构合理,红色(warn)表示冲稳保失衡。
.fav-risk.ok   { background: rgba(29,158,117,.12); color: #15795a; }
.fav-risk.warn { background: rgba(216,90,48,.14);  color: #9c2f1b; }
  • 多校对比:勾选 2-4 所学校,进入对比模态。对比表支持表头吸顶、按方向(地区/科类)分组、展开专业详情。
  • 导出:一键复制清单,还能同步到某个外部服务(fav-sync 区域)。

这是一个完整的、可用的志愿草表工具,而不是一个「收藏夹」。

六、RIASEC 兴趣测评:用心理学工具找方向

这是我觉得最有「产品思考」的一个功能。志愿填报最难的不是算分,是不知道学什么。它内置了一个基于霍兰德职业兴趣理论(RIASEC)的测评——24 道题,测出 6 个维度的兴趣画像,再匹配适合的专业方向。

从引导文案看它的设计意图:

花 1 分钟,测出适合你的专业方向
RIASEC 兴趣测评 · 十几道题即测即得 · 不存隐私

它甚至把测评放在了首访引导(onboarding)的第一选项——先测方向,再填分数。这个漏斗顺序很关键:先帮用户解决「学什么」,再解决「去哪里」,比一上来就让人填分更人性化。

测评结果还会反向影响推荐,形成「兴趣 → 专业 → 学校 → 录取概率」的完整闭环。

七、几个值得抄的工程细节

翻源码时,有几个细节让我觉得「这人真的在乎」。

1. 宣纸噪点用 inline SVG,不依赖图片

纸质感不是一张贴图,而是一段内联的 SVG 滤镜:

.paper {
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg ...%3E
    %3Cfilter id='n'%3E
      %3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E
      %3CfeColorMatrix type='saturate' values='0'/%3E
      %3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.07'/%3E%3C/feComponentTransfer%3E
    %3C/filter%3E
    %3Crect width='300' height='300' filter='url(%23n)'/%3E
  %3C/svg%3E");
}

零额外请求,且 mix-blend-mode: multiply 让它融进底图而不是盖在上面。

2. 落花动画带 prefers-reduced-motion 兜底

地图上有飘落的花瓣动画,但尊重无障碍设置:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .petals { display: none !important; }
}

整个站对 prefers-reduced-motion 都有完整降级——骆驼、帆船、风筝、大雁、灯塔光这些点缀动画全部停止。这是很多炫酷网站会漏掉的一环。

3. 移动端面板转底部抽屉

桌面端的面板在移动端不是简单缩小,而是全部转为「底部抽屉」:

@media (max-width: 640px) {
  .card, .recpanel, .quizpanel, .favpanel {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    border-radius: 16px 16px 0 0;
    max-height: 80dvh;
    animation: sheetUp .35s cubic-bezier(.22,1,.36,1);
  }
}

注意它用 80dvh(动态视口高度)而非 80vh,规避了移动端浏览器地址栏伸缩的坑。抽屉顶部还加了抓手视觉提示(一条小横条)。这是真在手机上测过的设计。

4. 即时浮标替代 tooltip

工具栏按钮的说明没有用原生 title 的延迟提示,而是自绘了一个 .tb-flyout,鼠标移上去立刻显示,且根据按钮位置自动选择从右侧(dir="r")或上方(dir="u")弹出:

const FLY = {
  stylebtn: ['画风 · 山水 / 水彩', 'r'],
  'act-rec': ['填分数 / 位次 → 冲稳保院校', 'u'],
  // ...
};

原生 tooltip 在不同浏览器、不同设备表现差异巨大,自绘是更可控的选择。

5. 全站无障碍语义

从搜索框到下拉菜单,都带了完整的 ARIA 属性:

<input id="search-input" type="text" role="combobox"
  aria-expanded="false" aria-controls="search-results"
  aria-autocomplete="list" aria-label="搜索学校、城市或省份">

省份下拉是自绘的 listbox,带 aria-haspopuparia-expandedaria-selected。键盘焦点有可见的 :focus-visible 样式。这是一个认真对待可达性的项目。

八、如果我要做一个类似的产品

拆完它的实现,我总结一个可复用的技术清单:

模块技术选型关键点
地图底座MapLibre GL JS开源、矢量瓦片、支持 3D 地形和 globe
地形可视化分层设色 + hillshade按海拔 ramp 插值 + 光照参数
视觉风格CSS 变量 + 楷体字体栈2-3 个主色,朱砂只用于主操作
数据呈现自绘 SVG 图表不引重型图表库
推荐概率区间映射冲稳保聚焦模式降低信息密度
表单重绘原生控件自绘 select 箭头、国风下拉
动效CSS animation + prefers-reduced-motion所有动画都有降级
移动端面板转底部抽屉 + dvh真机测试过的适配
无障碍完整 ARIA + focus-visible自绘组件也带语义

它的核心启示是:地图不只是一个底图,它可以是一个信息组织方式。 1500 所大学放进一个表格是灾难,放进一个排行榜是焦虑,但放进一幅山河里,它变成了「你可以去哪里看看」。

写在最后

这个模拟器让我印象最深的,不是它用了什么高深的技术——MapLibre、CSS 动画、SVG 都是常规武器。而是它把一件焦虑的事,做成了一件安静的事

朱砂、宣纸、楷体、山河、落花——这些元素不是装饰,它在服务于一个体验目标:让你在做一个重要决定时,感觉是在「展开一幅画卷」,而不是「填一份表格」。

冲稳保的橙红、金黄、墨绿,不是随便选的颜色,它们对应着你内心对风险的直觉感受。志愿表的风险提示,不是冷冰冰的「建议」,是「你这盘棋稳不稳」的判断。RIASEC 测评放在第一位,是因为它知道——比「能上哪」更难的,是「想学啥」。

技术选型是手段。一个产品最终好不好用,取决于你有没有把用户的真实处境想透。这个模拟器想透了。


参考资料:

评论互动

© 2026 王若风的技术博客 · Powered by Astro