写给每一个看到 Excel 就头疼、但心里又想做出酷东西的你。
13号周一,我收到一份文件——WAIC 2026 世界人工智能大会日程表。
打开一看,175 场论坛,密密麻麻排满了 4 天。13 条赛道从"综合论坛"到"女性与多元化发展",7 个主场馆外加 8 个分散场地,时间从早上 9 点排到晚上 9 点……
我盯着那个 Excel 看了三分钟,脑子里只有一个想法:
这谁看得过来啊?
你想找"大模型"相关的论坛?好,用 Excel 筛选功能。但筛完之后,它在哪个场馆?怎么走?跟其他活动时间冲不冲突?——对不起,Excel 不会告诉你。
于是我做了一个决定:把它变成一个真正好用的 Web 应用。
不是那种灰头土脸的表格页面,而是一个暗黑科技风、赛博朋克感拉满的交互式导航器。打开就是霓虹荧光、玻璃拟态卡片,还能在真实地图上看到所有场馆。

这篇文章,就是记录这个应用从 0 到 1 的全过程。
不仅仅是教程,也是真实故事。 我会把踩过的坑、拍过的脑袋、那些"原来如此"的瞬间,都讲给你听。
先说个很多人不知道的事——Excel 文件其实是个压缩包,里面装着 XML。但咱们不用管这些底层细节,Python 有个库叫 openpyxl,几行代码就能把数据读出来:
from openpyxl import load_workbookwb = load_workbook("WAIC 2026日程表.xlsx")ws = wb.activefor row in ws.iter_rows(min_row=2, values_only=True): name, track, date, time_start, time_end, venue = row[:6]# 处理每一行数据...这段代码做了件很简单的事:逐行读取 Excel,把每场论坛的名称、赛道、日期、时间、场馆提取出来。
但很快我们遇到了第一个坑:有些时间格式是"9:00",有些是"09:00",还有的是文本"上午"。Excel 你赢了。
最后写了个 parse_time() 函数统一处理,把所有时间都转成标准的 HH:MM 格式。这种"脏数据清洗"的工作,说起来不起眼,但占了整个项目差不多 20% 的时间。
💡 小启示:数据处理永远是第一步,也是最容易被低估的一步。别急着写界面,先把数据洗干净。
最终提取出的数据量:
数据准备好了,接下来——让它好看。
很多人一上来就问:"用什么框架?Vue 还是 React?要不要上 Next.js?"
我们的回答是:都不用。
这个项目的核心技术栈:
| HTML | ||
| Tailwind CSS | ||
| Leaflet.js | ||
| Vanilla JS |
没有 npm install,没有 webpack 配置,没有 node_modules 黑洞。
一个 index.html 文件,110KB,包含全部数据、样式和逻辑。部署?扔到 GitHub Pages 上就完事了。
💡 小启示:工具是手段不是目的。如果你的项目是一个"展示型应用",单文件 HTML 是最简单、最可靠的方案。没有构建步骤 = 没有构建错误。
但"简单"不等于"丑"。
好的设计不需要解释。你打开这个页面,第一感受应该是:这跟官方网站的日程表完全不是一个物种。
:root {--bg-deep: #060912; /* 深空背景 */--bg-base: #0a0e1a; /* 卡片底色 */--accent-cyan: #00f0ff; /* 霓虹青 */--accent-purple: #a855f7; /* 赛博紫 */--accent-pink: #ec4899; /* 荧光粉 */}背景不是纯黑 #000,而是 #060912——一种极其深邃的蓝。这个细节决定了整个页面是"廉价暗黑"还是"高级科技感"。
.glass {background: rgba(15, 22, 38, 0.7);backdrop-filter: blur(20px);border: 1px solid rgba(255, 255, 255, 0.06);border-radius: 16px;}backdrop-filter: blur(20px) 是灵魂——它让卡片有了一种毛玻璃的质感,背景的光晕透过来,像是从深空中浮现出来的全息面板。
.bg-glow-1 {position: fixed;top: -200px; left: -200px;width: 600px; height: 600px;background: radial-gradient(circle, rgba(0,240,255,0.08) 0%, transparent 70%);animation: float1 20s ease-in-out infinite;}两个巨大的径向渐变光斑,一青一紫,在页面角落缓慢漂浮。你不会刻意注意到它们,但它们让整个页面有了呼吸感。
这是设计的温度。
175 场活动直接铺开,不管多好看的页面都会变成灾难。
我们的方案是按天分组 + 时间标尺 + 赛道色条:
7月17日 星期五─────────────────────────────────09:00 │🟦 综合论坛:AI重塑未来 世博中心10:00 │🟪 大模型与AI基础:从Scale到 世博展览馆 │ Alignment14:00 │🟩 产业与工业智能化:智能工厂 西岸会展中心每张卡片左侧有一根 4px 的色条,颜色对应赛道。你扫一眼颜色分布,就知道今天是"大模型日"还是"产业日"。
关键代码其实很朴素:
functionrenderTimeline() {// 按日期分组const grouped = {}; events.forEach(e => {if (!grouped[e.date]) grouped[e.date] = []; grouped[e.date].push(e); });// 按天渲染Object.entries(grouped).forEach(([date, dayEvents]) => { dayEvents.sort((a, b) => timeToMinutes(a.time_start) - timeToMinutes(b.time_start) );// 生成 HTML... });}没有用到什么高深的算法,就是分组、排序、渲染。但我们就是把这些"简单的事"做到位。
地图是这个项目里最折腾的部分,没有之一。
Leaflet 加地图很简单,5 行代码:
const map = L.map('mapView').setView([31.19, 121.50], 12);L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {maxZoom: 18}).addTo(map);CARTO 的 dark_all 是暗色底图,跟赛博朋克风格完美搭配。桌面端一切正常。
然后我在手机上打开——地图上出现了大片深色方块,像是有人在地图上贴了几块黑色胶带。
我以为是偶发的瓦片加载失败,于是加了 OpenStreetMap 作为兜底:
// 底层放一个低透明度的 OSMconst fallback = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {opacity: 0.22}).addTo(map);// 上层放 CARTO 暗色const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png') .addTo(map);结果是:CARTO 加载失败的方块变成了灰蒙蒙的浅色方块——好了一点,但依然难看。
我又把 errorTileUrl 改成透明 GIF,让失败瓦片"消失":
errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'方块不黑了,但那块区域就是空的,什么地图都没有。
折腾了五轮之后,我终于意识到问题的本质:CARTO 的 CDN 在我们这就是不稳定。不是代码的问题,是网络的问题。
于是换了一个思路:用国内最稳的高德地图瓦片,再用 CSS 把它变成暗色。
// 高德道路图,国内加载飞快L.tileLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {maxZoom: 18,subdomains: '1234'}).addTo(map);高德瓦片是亮色的,跟暗黑风格不搭?没关系,CSS filter 一行搞定:
.leaflet-tile {filter: invert(1) hue-rotate(180deg) brightness(0.55) contrast(1.1) saturate(0.6);}这行 CSS 做了什么?
invert(1):颜色反转,亮变暗、暗变亮hue-rotate(180deg):色相旋转 180 度,让反转后的颜色不至于太诡异brightness(0.55):压暗到 55%,融入深空背景contrast(1.1):微提对比度,保持清晰saturate(0.6):降低饱和度,更高级效果:高德地图的亮色道路图,变成了一个暗色科技风底图,而且加载速度飞快,再也没有方块了。
💡 小启示:当第三方服务不稳定时,别死磕。换个稳定的服务,用 CSS/JS 做"二次加工",往往比硬等对方修复更高效。这个"高德 + 滤镜"的方案,后来发现很多暗色主题地图都在用。
这个应用的很多用户会在手机上打开(毕竟是在微信里分享的链接),所以移动端体验是重中之重。
最初地图高度用的 60vh——视口高度的 60%。但手机浏览器的地址栏会收起和展开,导致 vh 值反复变化,地图尺寸不停跳,瓦片加载到一半就被打断。
解决方案:移动端改用固定像素 500px,不用 vh。
@media (max-width:768px) {#mapView { height: 500px!important; }}视图切换时有个 fadeIn 动画,在动画开始的一瞬间,地图容器的 offsetHeight 还是 0,Leaflet 初始化就会出错。
解决方案:用双层 requestAnimationFrame 延迟初始化,并检查容器尺寸:
functionrenderMap() {if (!mapInitialized) { requestAnimationFrame(() => { requestAnimationFrame(() => { initMap(); }); }); }}functioninitMap() {const container = document.getElementById('mapView');if (container.offsetWidth === 0 || container.offsetHeight === 0) { setTimeout(() => initMap(), 100); // 容器还没准备好,100ms 后重试return; }// 正常初始化...}13 条赛道 + "全部"按钮 = 14 个标签。在手机上自动换行后,占了将近半屏,下面的内容被挤得只剩一条缝。
我试过横向滚动(用户嫌要滑来滑去看不全)、自动换行(用户嫌太占地方),最后找到了一个两全方案:可折叠面板。
┌─────────────────────────────────┐│ 赛道筛选 全部 175 场 ▼ │ ← 收起状态,只占一行└─────────────────────────────────┘点击展开后:┌─────────────────────────────────┐│ 赛道筛选 全部 175 场 ▲ ││ ● 全部 ● 综合论坛 ● 大模型 ││ ● 产业 ● 科学智能 ● 算力 ││ ● AI安全 ● 空间智能 ● 机器人 ││ ● 数据 ● 创投 ● 人才 ● 女性 │└─────────────────────────────────┘选择赛道后自动收起,直接看结果。桌面端保持原来的展开布局不变。
functiontoggleTrackFilter() {document.getElementById('trackToggle').classList.toggle('expanded');document.getElementById('trackFilterBody').classList.toggle('expanded');}functionfilterByTrack(trackName) {// ...筛选逻辑... collapseTrackFilter(); // 选完自动收起}💡 小启示:移动端设计的核心不是"塞进去",而是"藏起来"。用户需要的时候能找到,不需要的时候不挡路,就是最好的体验。
某次修改后,手机上打开页面——白屏。电脑上打开开发者工具,控制台赫然写着:
Uncaught SyntaxError: Unexpected token '}'一个多余的 },整页报废。
原因是我们在重构代码时(把 renderMap() 拆成 renderMap() + initMap()),残留了一个原来 if 块的结束大括号。
排查方法很原始但有效——把 <script> 里的代码单独提取出来,用 node --check 跑一遍:
node --check script.js# script.js:518# SyntaxError: Unexpected token '}'第 518 行,删掉那个多余的 },世界恢复了色彩。
💡 小启示:JavaScript 是解释型语言,一个语法错误就能让整个页面挂掉。遇到白屏,第一件事不是怀疑 CSS,而是打开控制台看有没有红色报错。
node --check是快速验证 JS 语法的好工具。
应用上线后,很自然的一个问题:有多少人在用?
对于一个纯静态网站(没有后端),加访客计数器听起来很麻烦,但其实一行脚本就够了:
<scriptdefersrc="https://events.vercount.one/js"></script>然后在页面底部放上两个 <span>:
<spanid="vercount_value_site_pv">···</span><!-- 总访问量 --><spanid="vercount_value_site_uv">···</span><!-- 访客数 -->Vercount 是"不蒜子"的现代替代品,Go + Redis 后端,Vercel CDN 加速,免费免注册。脚本加载后会自动请求接口,把数字填进对应的 span 里。
不过这里我踩了个坑:初始版本脚本 URL 写成了 vercount.one/js,少了 events. 子域名;元素 ID 用了连字符 vercount-value-site_pv,应该是下划线 vercount_value_site_pv。两个小错误,导致页面上一直显示 ···。
改完之后,数字终于跳出来了。第一次看到"1"的时候,还挺开心的。
💡 小启示:集成第三方服务时,文档上的每一个字符都要照抄。URL 少一个子域名、ID 少一个下划线,都不会报错——它只是静默失败。这时候去官方文档逐字比对,比到处搜 Stack Overflow 快得多。
代码写完了,怎么让全世界看到?
正常流程是 git push → GitHub Pages 自动构建 → 上线。但我的本地 git 因为代理问题连不上 github.com。
解决方案:用 GitHub API 直接推文件。
GitHub 提供了 REST API,可以通过 HTTP 请求直接操作仓库文件:
import base64, json, urllib.requestwith open("index.html", "rb") as f: content = base64.b64encode(f.read()).decode("utf-8")data = json.dumps({"message": "feat: 初始版本","content": content,"branch": "main"}).encode("utf-8")req = urllib.request.Request(f"https://api.github.com/repos/daka-agent/WAIC2026/contents/index.html", data=data, headers={"Authorization": "token YOUR_TOKEN","Accept": "application/vnd.github+json" }, method="PUT")urllib.request.urlopen(req)文件内容 Base64 编码后塞进 JSON,一个 PUT 请求搞定。后来又用同样的方式同步到了 AtomGit(国产代码托管平台)。
💡 小启示:当 A 路不通时,想想有没有 B 路。git 协议走不通,HTTP API 照样能推文件。"不能直连"从来不是终点,只是需要绕个弯。
从早上 7 点到晚上 12 点,一天之内,这个项目经历了:
中间踩了无数坑,但也学到了无数东西。
很多人觉得"编程"是程序员的专属技能,离自己很远。
但你看这个项目,它用到的技术其实很朴素:HTML、CSS、JavaScript,没有一个是"高级"的东西。可它们组合在一起,就能把一份让人头疼的 Excel 变成一个让人愿意打开第二次的应用。
技术最大的魅力,从来不是它有多复杂,而是它能解决我们真实的问题。
那个在地铁上用手机翻 WAIC 日程的人,不需要知道 CSS filter 是什么,不需要知道 requestAnimationFrame 怎么用。他只需要打开链接,看到深空蓝的背景、霓虹色的赛道标签、清晰的地图——然后快速找到自己想去的论坛。
这是技术的温度。

而且,在AI时代,我们完全可以和AI协作。AI帮我们写代码、查坐标、调样式、推部署。但真正决定这个项目"长什么样"的,是你的那个最初的念头:
"这个 Excel 太难看了,我想做个更好的。"
所有伟大的产品,都始于这样一个简单的念头。
而你要做的,就是别把它停留在念头上。
试试看:https://daka-agent.github.io/WAIC2026/
在手机上打开,体验赛博朋克风的 WAIC 议程导航。
项目开源地址:
GitHub: https://github.com/daka-agent/WAIC2026
AtomGit: https://atomgit.com/dakazhang/WAIC2026
如果你觉得有用,欢迎分享给要去 WAIC 的朋友 🤖
7月17日-20日,我们世界人工智能大会见!