去年帮一个做适老化改造的团队收拾过一个半成品站点,用的就是一套黄色养老院网站模板。视觉上挺讨喜,暖黄配米白,看着有温度,结果上线三天就收到家属反馈:字太小看不清、按钮点不中、手机上打开首屏就是一整块黄。那次返工让我意识到,黄色调在养老类站点里不是"好看就行"的事,它同时牵着色觉识别、对比度、触控热区一整套硬指标,任何一项没算准,模板再漂亮也是白搭。
这篇内容想聊的就是这件事:一套以黄色为主色的养老机构网站模板,从配色决策、信息架构、适老化交互、首页模块拆解、移动端性能到上线排错,完整走一遍。适合两类人看——手里已经拿到一套模板、想把它改到能真正交付的前端;以及要给机构做官网、但不确定"适老化"到底该落到哪些具体参数上的开发者。文中所有数值、代码和排查思路都来自实际改版过程,能直接抄。
1. 黄色主色在养老场景里的真实作用
很多人拿到这套模板的第一反应是"换个色不就行了",其实黄色在这里承担的任务比想象中多。它不只是品牌色,还是导航锚点、状态提示和情绪暗示的载体。三个任务混在一起,就决定了你不能随便挑一个 #FFD700 就往 CSS 里塞。
1.1 黄色在这里要同时干三件事
第一件事是视觉锚定。养老院官网的访客里,中老年用户占比很高,他们对高饱和度暖色的敏感度普遍好于冷色系,黄色在页面里做按钮、做当前导航项、做"立即预约"这类行动点,识别的成本最低。第二件事是情绪传达,暖黄加米白传递的是"阳光、有人气、不冷清",这跟机构想塑造的形象是对得上的,换成蓝灰就变成医院感了。第三件事是区域分隔,模板里大量用浅黄底块来切分不同的信息区块,比单纯画灰色分割线更柔和。
问题就在这三件事对黄色的要求是矛盾的。做锚点要饱和、要跳;做情绪底色要淡、要大面积;做分隔要低对比、不能抢内容。所以一套成熟的模板里,黄色从来不是"一个颜色",而是一条完整的色阶,从近白的浅黄到偏褐的深黄,每一档对应一种用途。
我在改版时把色阶压到了五档,够用且不容易乱。超过五档,维护的人自己都记不住哪个该用在哪。
1.2 从色卡到 CSS 变量:一份能直接用的色阶表
具体数值是这样的,命名按 50 到 700 递进,跟常见设计系统的习惯保持一致,方便交接:
:root{ /* 品牌黄:从浅到深 */ --brand-50: #FFF9E6; /* 页面大面积底色、区块背景 */ --brand-100: #FFF1C2; /* 卡片悬停、表格斑马纹 */ --brand-300: #FFD75E; /* 次级按钮、标签、图标底 */ --brand-500: #F5B301; /* 主按钮、当前导航、强调数字 */ --brand-700: #8A5E00; /* 黄色底上的深色文字、边框 */ /* 中性色:刻意偏暖,避免和黄色打架 */ --ink-900: #2B2A28; --ink-600: #5A5751; --ink-300: #B8B4AC; --paper: #FFFDF7; --radius: 12px; --shadow-soft: 0 2px 10px rgba(138,94,0,.08); }这里有两个容易被忽略的细节。一是--paper没有用纯白#FFFFFF,而是用了略带暖调的#FFFDF7。纯白和浅黄并排时,浅黄会显得"脏",暖白能把两者黏合起来,整页看起来是一体的。二是中性色全部偏暖(--ink-900是#2B2A28而不是#333333),因为纯灰文字压在黄底上会有明显的"发青"错觉,暖灰读起来更舒服。
提醒:色阶一旦定下来,就写进项目根目录的一份
tokens.css,所有组件只准引用变量,不准写死十六进制。这是后面能把这套模板复用给第二家机构的前提。
1.3 对比度是黄色最容易翻车的地方
黄色最大的陷阱是明度高。人的眼睛对高明度色的细节分辨力会下降,白字压在黄底上几乎是灾难。我实测过几组常见组合,按 WCAG 的相对亮度公式算对比度,结果如下:
| 前景色 | 背景色 | 对比度 | 判定 |
|---|---|---|---|
#FFFFFF白字 | #F5B301主黄 | 约 1.8:1 | 不合格,几乎不可读 |
#2B2A28深墨 | #F5B301主黄 | 约 9.6:1 | 优秀,正文可用 |
#5A5751暖灰 | #FFF9E6浅黄 | 约 6.4:1 | 合格,辅助文字可用 |
#8A5E00深褐 | #FFF1C2次浅黄 | 约 5.1:1 | 合格,标签与小字可用 |
#F5B301主黄 | #FFFDF7暖白 | 约 1.9:1 | 不合格,不能做文字色 |
结论很直接:黄色永远当背景,深色永远当文字。主按钮的写法应该是黄底 + 深墨字,而不是常见的"彩色底 + 白字"。这条规则看起来反直觉,但只要你在手机屏幕上把白字黄底放到户外光线下看一眼,就不会再犹豫。
对比度的计算其实不难,相对亮度是对每个通道做 gamma 反校正后加权求和,权重分别是 0.2126、0.7152、0.0722,最后用(L1+0.05)/(L2+0.05)得出比值。写个十几行的小脚本跑一遍,比肉眼猜靠谱得多。我当时是把手上的候选色全丢进脚本批量算,一次筛掉了一半。
还有一条经验:黄色区块不要挨着另一个黄色区块。两个不同明度的黄贴在一起,边界会"融化",中老年用户分不清这是两块内容还是一块。中间必须夹一条暖白间隙或者一条--brand-700的细边。
2. 从访客诉求倒推页面清单
模板到手时通常已经带了十几个页面,但里头有一半是凑数的。我的做法是先不管模板给了什么,坐下来把访客分成几类,看他们第一眼想找什么,再决定哪些页面留、哪些砍。
2.1 三类访客,三种完全不同的路径
家属是主要决策者,通常三四十岁,在手机上搜"附近养老院",最关心的是:离得远不远、多少钱、护理等级怎么分、能不能先去看看。他们的路径短,一般两三次点击内就要么打电话要么填表。老人本人偶尔会自己看,关注点更具体:吃得好不好、有没有活动、房间朝南不朝南,他们看得慢,需要大字和图片。合作方(社区、康复机构、渠道)关心的是资质、床位规模和对接方式,人数少但价值高,需要一个能下载资料的入口。
把这三条路径画出来之后,页面清单基本就定了。首页要同时承载家属的行动入口和老人的信任建立;"房型与收费"必须独立成页,因为这是家属的核心决策依据;"院内环境"用相册承担视觉说服;"预约参观"做成一个轻量表单,不要做成注册流程。
2.2 站点地图与栅格约定
最终留下的页面是七个:首页、机构介绍、房型与收费、护理服务、院内环境、常见问题、预约参观。多出来的"新闻中心""人才招聘"我建议先砍掉——模板里这两个页面通常只有三条占位新闻,上线后半年不更新,反而显得机构没人管。真要放,等有内容了再加。
栅格用 12 列,容器最大宽度 1200px,两侧留白在 1024px 以下降到 24px,768px 以下降到 16px。这个约定写进 CSS:
.container{ width: min(100% - 48px, 1200px); margin-inline: auto; } @media (max-width: 768px){ .container{ width: calc(100% - 32px); } }margin-inline和width: min()这套写法现在已经很稳了,比老的margin: 0 auto加媒体查询少一半代码,也不会出现横向滚动条。
2.3 目录结构:为"二次交付"提前留位
这套模板我后来复用给了另外两家机构,所以目录结构从一开始就是按"可替换"设计的:
/site /assets /css tokens.css # 色阶、字号、间距,只改这里就能换主题 base.css # 重置、排版、通用工具类 components.css # 按钮、卡片、表格、表单 pages.css # 各页面专属样式 /js main.js # 导航、滚动、灯箱 form.js # 表单校验与提交 /img /raw # 原图,不进构建 /web # 压缩后的 webp + jpg 兜底 /index.html /rooms.html /services.html /gallery.html /faq.html /booking.html /about.html关键点是tokens.css单独抽出来,并且pages.css里绝不允许出现十六进制颜色。这条规矩听着死板,但它决定了后面换色是十分钟还是两天。
3. 适老化交互的硬指标怎么定
"适老化"这三个字最容易被做成口号——加个字号切换按钮就算交差。真正落到代码上,它是几个可以量出来的数字:基准字号、行高倍数、点击热区尺寸、动效时长上限。
3.1 基准字号和行高是算出来的,不是拍的
模板原始的根字号是 16px,正文 14px,行高 1.5。这个配置在普通站点没问题,但在养老场景里偏小。我最终的取值是:根字号 18px,正文继承根字号,行高 1.75,段落间距 1em。
为什么是 18px?参考的是移动端正文可读性的一般结论:中老年用户在 375px 宽的手机上,正文低于 17px 时阅读速度明显下降,而超过 20px 会导致每行字数过少、频繁换行反而疲劳。18px 落在这个区间的中间偏上,同时能保证 375px 宽下每行大约 18 到 22 个汉字,是一个比较舒服的密度。
行高 1.75 的理由不太一样。行高偏小时,读者从一行末尾回到下一行行首容易串行,这个现象在视力下降后会更明显。1.75 倍的间距给了眼睛足够的"回程余量",同时又不至于让段落松散到失去整体感。
标题层级也跟着放大,但比例要收敛。原始模板是 48/36/24/18 的跨度,我压成了 40/32/24/18,也就是标题之间的差距变小了。因为大字号在窄屏上会挤成两三行,跨度太大的话,页面节奏会变得很碎。
html{ font-size: 18px; } body{ font-size: 1rem; line-height: 1.75; color: var(--ink-900); background: var(--paper); } h1{ font-size: clamp(1.75rem, 1.2rem + 2vw, 2.25rem); line-height: 1.3; } h2{ font-size: clamp(1.4rem, 1.1rem + 1.2vw, 1.75rem); line-height: 1.35; } h3{ font-size: 1.15rem; line-height: 1.5; } p{ margin-block: 0 1em; }clamp()在这里很省事,标题能随视口平滑缩放,不用写一堆断点。注意clamp的中间项用了rem + vw的混合写法,这是为了避免纯vw在超大屏上把标题撑得过大。
3.2 触控热区:44 像素是底线,不是目标
按钮尺寸这块,行业里常提的最小触控目标是 44×44 CSS 像素。实际做下来,44 只能算底线,养老场景建议做到 48 以上,主行动按钮(拨号、预约)做到 52 以上更稳妥。
除了尺寸,间距同样重要。两个按钮紧挨着,即使各自都有 48px 高,手指落点偏一点还是会点错。我定的规则是:相邻可点击元素之间的间隙不小于 12px,主次按钮并排时不小于 16px。
导航栏是这个规则最容易失守的地方。原始的顶部导航在 768px 以下会缩成汉堡菜单,展开后每一项高度只有 40px 左右,密排在一起。我改成了每项最小高度 56px,项与项之间加一条 1px 的浅黄分隔线,视觉上更清楚,误触也少了。
.nav-list a{ display: flex; align-items: center; min-height: 56px; padding-inline: 20px; font-size: 1.05rem; color: var(--ink-900); } .nav-list a[aria-current="page"]{ background: var(--brand-100); box-shadow: inset 4px 0 0 var(--brand-500); }用inset阴影做左边框,是为了不改变盒模型宽度,避免布局跳动。当前页的标识同时用了底色和左边条两种手段,因为只靠颜色深浅区分,对色觉不敏感的用户不够友好。
3.3 动效收敛和无障碍属性
模板自带的滚动入场动画是每个区块淡入 + 上移 40px,还带了一点缩放。这套效果在营销站上没问题,但在这个场景里有两个毛病:一是滚动到一半内容还是半透明的,老人会以为页面没加载完;二是动画时长 800ms 偏长,连续滚动时会有明显的"追不上手"的感觉。
我把时长压到 240ms,位移减到 12px,去掉缩放,并且加上系统级的动效偏好判断:
@media (prefers-reduced-motion: reduce){ *, *::before, *::after{ animation-duration: .01ms !important; transition-duration: .01ms !important; } }无障碍属性这块,实话说原始的黄色模板做得比较糙——一堆div当按钮用,图片全没有alt,表单的label跟输入框没有关联。改造成本不高,但收益明显,尤其是对用读屏软件的用户。具体做了几件事:所有可点击的非<a>/<button>元素要么换成语义标签,要么补role和tabindex;所有内容图片补alt,纯装饰图alt="";表单的label用for绑定id;折叠菜单加aria-expanded。
4. 首页各模块的拆解与实现
首页是这套模板里改动最大的地方,原因很实际:家属在手机上进来,首屏看不到关键信息就会走。下面按从上到下的顺序,把几个核心模块的处理方式说清楚。
4.1 顶部导航与吸底拨号条
桌面端是常规的左侧 Logo + 右侧导航,Logo 区用--brand-50打底,跟页面主体做一点区分。真正重要的是移动端:模板原本只做了汉堡菜单,我额外加了一条吸底拨号条,固定在屏幕底部,左边是"电话咨询",右边是"预约参观"。
这条吸底条是整个改版里转化提升最明显的部件。原因很简单——中老年用户不习惯在网页里填表,他们更愿意直接打电话;而拨号入口如果藏在菜单里,多一步就会流失一批人。实现上用了position: fixed,加上padding-bottom: env(safe-area-inset-bottom)来适配全面屏底部的手势区:
.call-bar{ position: fixed; inset: auto 0 0 0; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); background: rgba(255,253,247,.94); backdrop-filter: blur(8px); border-top: 1px solid var(--brand-100); z-index: 50; } @media (min-width: 1024px){ .call-bar{ display: none; } }同时 body 要补一个padding-bottom,值等于吸底条的高度(含安全区),否则页面最底部的内容会被永久遮住。这个细节我第一次就漏了,结果"常见问题"页最后一条永远看不全。
还有一个坑:吸底条上的拨号链接用href="tel:...",在某些内嵌浏览器(比如从社交软件里点开的页面)里不会直接唤起拨号,而是弹一个提示。这种情况下需要在按钮旁边保留一个"复制电话"的次要操作,长按复制对老人来说反而更难,所以又加了一句"点击号码复制"的小字提示。
4.2 首屏横幅:黄色叠加图片的可读性处理
原始首屏是一张大图 + 白色标题压在图上,图片本身偏亮,白字基本看不清。我改成了双层结构:底层图片,上层一层从--brand-700到透明的渐变遮罩,标题用暖白字,副标题用--brand-100。
遮罩的方向和浓度需要调。纯黑遮罩会把图片压死,失去"明亮"的感觉,用深褐(--brand-700的暗化版本)更协调:
.hero{ position: relative; isolation: isolate; } .hero img{ width: 100%; height: clamp(320px, 52vh, 560px); object-fit: cover; } .hero::after{ content: ""; position: absolute; inset: 0; background: linear-gradient( 100deg, rgba(90,62,0,.82) 0%, rgba(90,62,0,.55) 42%, rgba(90,62,0,.10) 75% ); } .hero-content{ position: relative; z-index: 1; color: #FFFDF7; }isolation: isolate这行是为了让伪元素的层级只在本区域内生效,避免跟页面其他z-index打架,这个坑我在另一个项目里踩过——不加的话,遮罩偶尔会盖到吸底条上面去。
首屏文案只放三样东西:一句话说清机构定位、一行补充信息(床位数、位置、成立年限)、一个主按钮。模板原本放了五条卖点小图标,在手机上全挤在首屏下方,反而把主按钮挤出了可视区。这类"卖点罗列"移到第二屏更合适。
4.3 房型表格:让家属能横向对比
"房型与收费"是决策页,模板给的是一个三列卡片布局,每张卡片写房型、面积、价格。问题在于卡片无法横向对比,家属要来回扫视。我改成了表格,并且做了一件事:把最关键的两列(护理等级、月费区间)固定在左侧,其余列在窄屏上横向滚动。
| 房型 | 护理等级 | 月费区间 | 面积 | 床位数 | 朝向 |
|---|---|---|---|---|---|
| 双人间 | 自理 | 面议 | 约 22 ㎡ | 2 | 南 / 东 |
| 双人间 | 半自理 | 面议 | 约 22 ㎡ | 2 | 南 / 东 |
| 单人间 | 自理 | 面议 | 约 16 ㎡ | 1 | 南 |
| 护理间 | 全护理 | 面议 | 约 26 ㎡ | 3 | 东南 |
价格一栏我统一写"面议",这是个务实的选择。养老机构的收费跟护理等级、餐费、押金强相关,网上写一个具体数字,家属到店后发现不一样,反而增加沟通成本。表格旁边配一句"具体费用以到院评估为准",把预期先框住。
表格的移动端处理用了容器横向滚动 + 首列吸附:
.table-wrap{ overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-wrap th:first-child, .table-wrap td:first-child{ position: sticky; left: 0; background: var(--brand-50); z-index: 1; }-webkit-overflow-scrolling现在基本是历史遗留了,加上也不影响,主要是为了照顾一些老版本的内嵌浏览器。
4.4 环境相册:灯箱要能单手操作
相册模块用的是 CSS Grid 瀑布式布局,桌面上三列,平板两列,手机上两列但不做高度错落(错落在窄屏上会让滚动很跳)。点击放大用的是一个轻量灯箱,自己写的,没引第三方库,因为需要的东西很少:全屏遮罩、左右切换、关闭按钮、键盘 Esc 支持。
单手操作这点值得单独说。灯箱的关闭按钮如果放在右上角,右手单手持机时够起来很别扭。我把它放在了右下角,同时保留点击遮罩关闭,切换按钮放在底部居中偏两侧,拇指刚好覆盖。
const lb = document.querySelector('.lightbox'); const imgs = [...document.querySelectorAll('.gallery img')]; let idx = 0; function open(i){ idx = i; lb.querySelector('img').src = imgs[idx].dataset.full; lb.hidden = false; document.body.style.overflow = 'hidden'; } function close(){ lb.hidden = true; document.body.style.overflow = ''; } function step(d){ idx = (idx + d + imgs.length) % imgs.length; lb.querySelector('img').src = imgs[idx].dataset.full; } lb.addEventListener('click', e => { if (e.target.matches('[data-close]')) close(); if (e.target.matches('[data-prev]')) step(-1); if (e.target.matches('[data-next]')) step(1); }); document.addEventListener('keydown', e => { if (lb.hidden) return; if (e.key === 'Escape') close(); if (e.key === 'ArrowLeft') step(-1); if (e.key === 'ArrowRight') step(1); });图片本身要备两套:列表用压缩过的缩略图(宽 640,webp),灯箱用大图(宽 1600)。一开始我偷懒只放了大图,手机上相册页要加载十几兆,首屏白屏了两秒多,非常影响体验。
4.5 预约表单:字段砍到四个,垃圾提交单独处理
模板给的预约表单有九个字段,包括身份证号、紧急联系人、老人年龄、既往病史等等。这个设计在线上是行不通的——填的人少,而且收集敏感信息的风险也高。
我砍到了四个:称呼、联系电话、想了解的房型(下拉)、方便联系的时段(下拉)。备注留一个可选的多行输入。四个字段足够让机构回电,剩下的信息电话里问更合适。
垃圾提交是绕不过去的问题。这套表单上线两周后开始收到机器提交,一开始一天几条,后来几十条。我试过三种方案,最后用的是"蜜罐字段 + 时间戳校验"的组合:
<form id="booking" novalidate> <input type="text" name="company" tabindex="-1" autocomplete="off" aria-hidden="true" class="hp-field"> <input type="hidden" name="ts" id="ts"> <!-- 其余字段 --> </form>document.getElementById('ts').value = Date.now(); document.getElementById('booking').addEventListener('submit', e => { e.preventDefault(); const f = e.target; if (f.company.value) return; // 蜜罐被填,直接丢弃 if (Date.now() - Number(f.ts.value) < 3000) return; // 3 秒内提交,判定为机器 // 正常校验与提交 });蜜罐字段用 CSS 移出可视区而不是display: none,因为部分自动化脚本会跳过隐藏元素,移出视口反而更容易命中:
.hp-field{ position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }加上这两条之后,机器提交基本清零,偶尔漏进来的一两条人工也能识别。这里没必要上验证码,老人的识别成本太高,得不偿失。
5. 移动端优先的性能处理
这套站点的真实流量分布很集中:八成以上来自手机,其中相当一部分是中低端机型,还有不少是从社交软件里点开的内嵌浏览器。这意味着性能优化不是加分项,是及格线。
5.1 断点只留三个
模板原本有六个断点,从 480 到 1440,每档都写了独立样式,维护起来极其痛苦。我压到了三个:768、1024、1280。小于 768 走移动布局,768 到 1023 走平板双列,1024 以上走桌面。剩下那些细微差别全部用clamp()和min()消化掉。
这个做法有个前提:布局本身要足够弹性。具体来说,卡片区用grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)),让列数自动决定,而不是在每个断点里手写repeat(3, 1fr)。这样即使设备宽度落在一个不常用的尺寸上,布局也不会塌。
.card-grid{ display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }minmax(280px, 1fr)里的 280 是个需要斟酌的数字。太小会导致手机上字被挤成竖条,太大则在平板上只排一列很浪费。这个宽度大致对应"一行能放 12 到 14 个汉字",配合 18px 的字号刚好。
5.2 图片和字体的取舍
图片是这类站点最大的体积来源。相册、环境图、房型图加起来几十张,原图单张动辄几兆。处理流程是这样:原图统一进/img/raw不参与构建,用脚本批量输出两档尺寸的 webp,同时保留 jpg 兜底。HTML 里用<picture>+srcset:
<picture> <source type="image/webp" srcset="/img/web/room-640.webp 640w, /img/web/room-1280.webp 1280w" sizes="(max-width: 768px) 100vw, 50vw"> <img src="/img/web/room-1280.jpg" alt="双人间室内实景,南向采光" width="1280" height="853" loading="lazy" decoding="async"> </picture>width和height一定要写,这能提前占位,避免图片加载时页面跳动。这个跳动在长页面上尤其致命——用户正读到一半,内容突然被推下去,很容易误触。
首屏那张横幅图不走懒加载,反过来要加fetchpriority="high"并预加载:
<link rel="preload" as="image" href="/img/web/hero-1280.webp" type="image/webp">字体这块我做了减法。模板引了两款中文字体,一款正文一款标题,加起来快两兆。中文字体做子集化在内容会变的站点上很麻烦,所以我的方案是:全站不引中文字体,只用在系统字体栈上,靠字重和字号建立层次。
body{ font-family: -apple-system, "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", sans-serif; }系统字体在各类设备上都做过显示优化,在小字号下反而比网页字体更清晰。代价是不同平台的字形略有差异,但对这类机构站来说完全可以接受。
5.3 首屏实测与优化前后的对比
改完之后做了一轮实测,用同一台中端手机、同一个网络环境,取三次中位数。数据如下:
| 指标 | 改版前 | 改版后 |
|---|---|---|
| 首屏内容出现 | 3.4 s | 1.1 s |
| 首页总请求数 | 62 | 21 |
| 首页传输体积 | 4.8 MB | 620 KB |
| 最大内容绘制 | 5.2 s | 1.9 s |
| 布局偏移量 | 0.21 | 0.03 |
体积降幅最大的一步是图片换成 webp 并按需加载,占了总减少量的八成以上。请求数减少主要靠两件事:把零散的图标字体换成内联 SVG,把几个小的 JS 文件合并。布局偏移从 0.21 降到 0.03 全靠给图片写死宽高比和给广告位预留高度。
经验:这类站点不要用"我本地打开挺快的"来判断。开发机一般性能过剩,还得在真机上、用限速网络跑一遍,问题立刻现形。
6. 上线前后踩过的坑与排查链路
前面讲的都是"应该怎么做",但真正花时间的往往是"为什么现在不对"。这一节把几个印象深的坑完整记下来,包括排查过程,因为结论不值钱,路径才值钱。
6.1 预约表单收不到提交:一条完整的排查链路
现象是后台一条记录都没有,但前端点击提交后提示"提交成功"。这种"看起来成功实际没到"的问题最耗时间,因为前端没有报错,你会本能地怀疑后端。
我的排查顺序是这样的,从最便宜的一步开始,逐层收敛:
第一步,看网络面板。打开开发者工具,点提交,看有没有请求发出。结果是有请求,状态码 200,响应体是一段 HTML 而不是 JSON。这一下就把范围缩小了——请求发出去了,但地址指错了。
第二步,对比请求地址。请求打到了/booking.html,而不是后端接口。原因是模板里的<form>没有写action,同时提交逻辑里有一段老代码用form.submit()直接触发原生提交。而我的preventDefault写在了另一个监听器里,执行顺序在它后面,没拦住。
第三步,确认执行顺序。把两个监听器的注册顺序调过来就好了吗?不,这治标。正确的做法是把原生提交彻底断掉,只走fetch:
form.addEventListener('submit', async (e) => { e.preventDefault(); e.stopImmediatePropagation(); // 切断同一元素上后续监听器 // ...校验 const res = await fetch('/api/booking', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); });stopImmediatePropagation()这一行是关键,它能阻止同一元素上其他监听器继续执行。如果只是preventDefault(),后面那段form.submit()的逻辑照样会跑。
第四步,验证成功判定。修完地址问题后,我又发现代码把"HTTP 200"直接当成成功,而后端返回的是{code: 1, msg: "..."}这种信封结构。这意味着接口出错时前端也会提示成功。后来统一改成判断res.ok && data.code === 0。
第五步,模拟真实场景。最后一轮用限速网络测,发现手机端偶发失败。查下来是提交按钮没有做防重复点击,弱网下用户会连点,第二、三次请求被后端拒掉并返回错误,覆盖了第一次的成功提示。加了按钮禁用和 3 秒节流解决。
整个链路走完花了将近一天,但每一步的判定依据都很明确,没有再走回头路。
6.2 手机上出现横向滚动条:三个常见源头
这个问题在改版过程中反复出现,每次的原因都不一样:
| 现象 | 常见原因 | 定位方法 |
|---|---|---|
| 整页可左右拖动 | 某个元素宽度超出容器 | 给*加outline: 1px solid red逐层扫 |
| 只有某个区块能拖 | 表格或代码块没套滚动容器 | 找overflow-x缺失的父级 |
| 拖动后右侧有白边 | 负边距或100vw含滚动条宽度 | 换成100%或calc(100vw - var(--sbw)) |
第三个最常见也最隐蔽。100vw在某些浏览器里包含竖向滚动条的宽度,比可视区多出十几像素,于是必然出现横向滚动。我的处理是给根元素设overflow-x: hidden治标,同时把用到100vw的地方全部换成100%或min(100%, 100vw)。前者是应急,后者才是根治。
另外提醒一句:加overflow-x: hidden到body上,在部分移动浏览器里会连带影响吸底条的定位,position: fixed会退化成相对 body 定位。所以这个属性最好加在html上,或者干脆只做根治不做遮掩。
6.3 上线前的那张清单
改版收尾时我整理了一份检查项,每次交付前过一遍,能挡掉大部分低级问题:
- 所有
href="tel:"链接在真机上点一遍,确认能唤起拨号 - 表单提交走一遍完整链路,包括失败分支和重复提交
- 首页、房型页、相册页在 375px 宽下无横向滚动
- 所有图片有
alt,装饰图alt="" - 页面标题、描述、
h1每页唯一且不重复 - 404 页面存在,且能回到首页
- HTTPS 证书有效期确认,站点内不混用 http 资源
- 用读屏软件过一遍首页,确认朗读顺序合理
最后一条很多人会跳过,但成本很低,收获很大。我就是在那一步才发现吸底条在朗读顺序里排在了页面最前面,导致每次打开都在念"电话咨询、预约参观",把正文全盖过去了。修法很简单,给吸底条加aria-hidden之外,还需要在 DOM 里把它放到文档末尾,靠 CSS 定位到视觉上的底部。
7. 把黄色模板复用到第二家机构的做法
这套模板后来改给了另外两家机构——一家主色换成了墨绿,一家换成了砖红。之所以能在一周内完成,靠的不是手快,而是前面那些"写死变量"的规矩。
7.1 换色到底要改哪里
换主题的实际操作只有一步:改tokens.css里的品牌色阶。但前提是组件层没有硬编码颜色。我在交付第一版之前做了一次全项目搜索,把pages.css和components.css里所有十六进制色值全部换成了变量引用,一共清出七十多处。
有一类颜色比较麻烦:遮罩和阴影。这些地方用的是rgba(),直接换变量不生效。我的处理是把它们也抽成变量:
:root{ --scrim: rgba(90,62,0,.82); --shadow-soft: 0 2px 10px rgba(90,62,0,.08); }换主题时连这两个一起换,整站色调才统一。如果只换了主色,遮罩还是褐黄的,在墨绿主题下会显得脏。
另一类麻烦是内联 SVG 里的颜色。图标如果用了fill="#F5B301",换主题就不会跟着变。修法是把fill改成currentColor,由父元素的color决定:
<svg viewBox="0 0 24 24" aria-hidden="true"> <path d="..." fill="currentColor"/> </svg>这个改动看着琐碎,但它让图标能跟着按钮的文字色一起走,反而少维护一套逻辑。
7.2 交付给非技术人员的编辑说明
机构那边通常没有专职前端,内容更新靠行政人员。所以我每次交付都会附一份一页纸的说明,只讲三件事:
第一,改文字在哪改。指到具体文件和具体行,用注释标出来,比如<!-- 修改这里:机构简介第一段 -->。第二,换图片的规矩。要求宽度不超过 1600,格式用 jpg,文件名不带中文和空格。第三,不能碰的地方。明确告诉他们哪些文件不要动,尤其是tokens.css和main.js。
这份说明的价值在半年后体现。第一套站上线八个月后,机构要改首页一句话,行政人员自己五分钟就改完了,没找我。而在此之前,他们每次改字都要发消息过来,来回沟通的成本远超改字本身。
还有一个小技巧:把首页里最容易被改动的几处(机构简介、床位数字、联系方式)集中放在 HTML 的同一个区块里,并用醒目的注释框起来。这样即使对方不懂代码,也能靠注释定位,不至于在几百行里翻找。
7.3 这套结构还能怎么延展
从黄色模板这套结构出发,其实不止能用在养老机构。它的核心是"高可读性 + 强行动入口 + 图片说服",这三件事在康复中心、社区日间照料、家政护理这类场景里是通用的,改一改文案和图片就能复用。
真要延展,我会按这个顺序动:先把色阶换掉(十分钟),再调字号(如果是面向更年轻的用户,可以把根字号从 18 降回 16),最后换首页模块的排序(比如把"服务项目"提到首屏之后的位置,把"环境相册"往后挪)。三步下来基本就是另一套站点了,而代码量几乎不变。
我自己的体会是,做这类模板项目,前期多花半天把颜色和字号抽成变量,后面会省下好几天的返工。反过来,图快直接改样式表,等到第二家、第三家找上门,那点省下的时间会连本带利还回去。