Vue.js + TailwindCSS 适老化改造实践:从字号缩放到焦点管理
1. 适老化改造前我在养老项目现场踩过的坑1.1 一次实机测试逼我推翻的“假适老”方案我接手这个社区养老服务平台Web端重构时最初的理解也非常朴素适老化就是把字放大、把按钮加宽、把间距拉开。于是第一版原型我用了很多现成组件库的“大号”样式全局字体调到了18px按钮宽度全部超过100px自认为已经很到位了。结果项目进入到真机测试阶段我拿着平板去社区活动中心找了几位60到75岁的长辈现场试用才真正意识到问题有多严重。一位阿姨拿到平板后的第一个动作是打开系统设置把手机全局字号调到最大然后回到我们的页面。页面纹丝不动——因为原型里字号全用的是px写死的WebView里的CSS压根没有跟随系统字体缩放。另一位大爷指着健康档案里的“血糖 5.6mmol/L”问我“这个数字是不是不正常”我凑过去一看数值颜色是浅灰色背景是更浅的灰白两个颜色叠在一起别说老人我都要仔细辨认。还有一次更典型一位奶奶在预约体检的流程里填错了手机号表单项下方弹出一行深红色小字提示。她完全没看到连续点了三次“下一步”页面没有任何反应她就把平板递给我说“坏了不动了。”我盯着那行字号只有12px、对比度只有3:1左右的错误提示终于明白——问题根本不在“是不是有提示”而在于“这个提示在目标用户眼里根本不存在”。那天回去我重新整理了测试记录把所有问题归类之后发现适老化改造根本不是一件“风格美化”的工作它覆盖了字号体系、色彩对比度、焦点管理、交互反馈、容错设计、性能表现等一整条链路。每一个环节都必须落在具体的技术实现里靠CSS调几个像素是完全不够的。也正是在这个背景下我确定了用Vue.js和TailwindCSS这套组合做整体重构而不是继续在旧代码上打补丁。如果你现在也在做面向中老年用户的Web应用我建议你先别急着写代码找几位真正的目标用户做一次实机观察。你会发现“适老化”三个字背后藏着的问题比自己想象的多得多。1.2 从实测记录中拆出的四类核心痛点把现场观察、访谈和操作录像整理后我把所有问题归纳成了四类视力层面的、操作层面的、理解层面的、反馈层面的。视力层面最直接字号不能跟着系统设置放大这已经劝退了一批视力下降的用户文字和背景对比度不足信息直接“隐形”图标尺寸过小而且表意太抽象。举例说很多后台系统通用的“齿轮”设置图标年轻用户一看就懂但不少长辈会把它理解成“机器零件”完全不知道那是设置入口。操作层面也很典型点击目标太小按钮之间的间距不够指尖稍微偏一点就点空。横向轮播图和长列表滑动时容易误触部分交互还依赖双击、长按这类高阶手势长辈很难掌握。有位老人反复说“我明明点到了”后来我看录像才发现他确实点到了但点在了按钮边缘的padding范围之外。理解层面的问题往往要细看文案和流程才能发现。“同步”“授权”“缓存”这类术语对长辈来说就是天书多步骤流程里点了“下一步”之后会发生什么、需要准备什么材料界面完全不给预期提示。长辈只能硬着头皮往下走走错了就退出。反馈层面最容易被研发忽略操作成功了页面只是轻微变化一下没有明确提示失败时错误信息不显眼弹窗一闪而过遇到网络加载只有转圈没有文字说明。长辈对着页面发愣不知道到底发生了什么。这四个维度的痛点必须在实现阶段分别用不同手段去解决痛点维度具体表现技术对策视力字号不可调、对比度不足rem字号体系、高对比度主题令牌操作点击目标小、易误触44px以上命中区域、间距矩阵理解术语多、流程不可预期文案降级、步骤条、前置说明反馈提示不显眼、状态不明确多模态反馈、ARIA live区域、动态提示Vue.js和TailwindCSS在后面的改造里帮了大忙但你得先把这些问题看清选型才有意义。2. Vue.js与TailwindCSS的选型账我是这样算的2.1 为什么没有直接用现成组件库很多Vue项目做后台管理系统第一反应是装Element Plus、Ant Design Vue这类组件库我也这么干过。但是真到了适老化改造场景这些组件库反而成了阻力。问题主要有三个。第一组件库的样式体系通常基于一套固定的设计令牌虽然提供了CSS变量去覆盖主色、圆角、字体但深入到每个组件的内部结构时你会发现大量间距、字号、颜色是写死在组件样式里的想全面调整就得用!important去覆盖改得越多项目里积累的hack越多。第二组件库的无障碍基础参差不齐。有的组件能正确处理aria-label和焦点管理有的则完全不管键盘Tab进去之后焦点直接“消失”。我总不能为了一个下拉框去改组件源码吧。第三组件库为了兼容多种场景打包体积一直偏大而养老平台这边有不少用户还在用几年前的安卓手机打开页面的速度和流畅度非常敏感。Bootstrap同样不适合。它的栅格和工具类好用但默认视觉风格太重想做成适合长辈阅读的柔和高对比风格需要覆盖的样式非常多。而且Bootstrap的JavaScript组件依赖jQuery对一个Vue项目来说属于额外负担。纯手写CSS也不是不行但项目一旦膨胀到几十个页面颜色值、字号值分散在各处后期想整体切换对比度主题几乎等于重写一遍。我需要的是“设计令牌集中管理、页面样式足够轻、组件语义自己可控”的方案。TailwindCSS的原子化思路正好匹配这个需求。2.2 原子化类名在适老化场景里的杠杆作用TailwindCSS的class是直接在HTML里组合的这带来一个看上去不起眼、实际上非常重要的能力样式不再耦合在组件文件里而是变成了数据。举个实际的例子。健康数据卡片要区分正常、偏高、偏低三种状态。用传统写法我得写三个CSS类is-normal、is-high、is-low分别定义边框、背景、文字颜色。用Tailwind我只需要在类名里组合border-l-4、bg-emerald-50、text-emerald-900。状态数据从接口返回时直接映射到类名数组逻辑非常直观。适老化还需要做高对比度主题。Tailwind的class可以任意组合所以我可以把语义化的颜色类名比如bg-surface、text-primary定义成一组CSS变量而变量值在不同主题下由html根节点上的>// tailwind.config.js module.exports { theme: { fontSize: { xs: [0.75rem, { lineHeight: 1.25rem }], sm: [0.875rem, { lineHeight: 1.5rem }], base: [1rem, { lineHeight: 1.75rem }], lg: [1.125rem, { lineHeight: 1.75rem }], xl: [1.25rem, { lineHeight: 2rem }], 2xl: [1.5rem, { lineHeight: 2rem }], 3xl: [1.875rem, { lineHeight: 2.25rem }], 4xl: [2.25rem, { lineHeight: 2.5rem }], }, }, }所有字号全部以rem为单位意味着根节点font-size一变全站字号等比缩放。我在实际项目里把健康档案数字、预约按钮文字、表单label这三类信息单独提了出来让它们在“超大模式”下再额外放大一档确保最关键的信息永远最醒目。3.2 字号档位切换组件的实现字号切换不能只停留在理论上得有一个明确的UI入口。我把它放在了页面的顶部栏一个显示“默认 / 大 / 超大”的切换控件用三个大按钮横向排列按钮命中区域做到48px高整个组件在视觉上非常直白。组件逻辑用Vue组合式函数抽出来方便多个页面复用script setup import { ref, watch } from vue import { useLocalStorage } from vueuse/core const fontLevel useLocalStorage(care-home-font-level, 1) const fontSizeMap { 1: 16px, 2: 18px, 3: 20px, } watch(fontLevel, (level) { document.documentElement.style.fontSize fontSizeMap[level] }, { immediate: true }) function setLevel(level) { fontLevel.value level } /script这段代码有两点值得说明。第一我把用户选择持久化到了localStorage长辈设置过一次大字模式下次打开还是大字模式不需要每次重新设置。第二watch的immediate: true会让页面加载时立即读取本地存储并把根字号应用上去但这个动作发生在Vue实例挂载后还是会造成一次肉眼可见的闪烁。更好的做法是在index.html的head里放一段内联脚本提前从localStorage读值并设置html的字体大小这样用户打开页面第一帧就是正确的字号。!DOCTYPE html html langzh-CN head script // 先于Vue加载执行避免字号闪烁 (function () { try { var level localStorage.getItem(care-home-font-level) var map { 1: 16px, 2: 18px, 3: 20px } if (level map[level]) { document.documentElement.style.fontSize map[level] } } catch (e) {} })() /script /head3.3 高对比度主题的令牌化设计字号解决的是“看得见”颜色解决的是“看得清”。在养老场景里颜色对比度的优先级高于审美。我参考WCAG的对比度要求做了硬性约束正文内容至少4.5:1大字和粗体至少3:1非文本图形元素至少3:1。主题设计上我把颜色抽成了语义令牌而不是直接用色名:root { --color-bg: #ffffff; --color-surface: #f8fafc; --color-text: #1e293b; --color-text-secondary: #475569; --color-text-error: #b91c1c; --color-border: #94a3b8; --color-primary: #0369a1; --color-primary-hover: #075985; } [data-themehigh-contrast] { --color-bg: #ffffff; --color-surface: #f1f5f9; --color-text: #111827; --color-text-secondary: #334155; --color-text-error: #991b1b; --color-border: #64748b; --color-primary: #1e40af; --color-primary-hover: #1e3a8a; }Tailwind配置里把颜色映射为CSS变量// tailwind.config.js colors: { bg: var(--color-bg), surface: var(--color-surface), content: var(--color-text), muted: var(--color-text-secondary), danger: var(--color-text-error), border: var(--color-border), primary: { DEFAULT: var(--color-primary), hover: var(--color-primary-hover), }, }这样组件里写bg-bg、text-content、border-border语义非常清晰一旦需要切换高对比度主题只需要在html标签上设置>template div classflex items-center gap-2 rounded-lg border border-border bg-surface px-4 py-3 svg v-ifstatus high classh-6 w-6 text-danger aria-hiddentrue path d... fillcurrentColor / /svg svg v-else-ifstatus normal classh-6 w-6 text-success aria-hiddentrue path d... fillcurrentColor / /svg div p classfont-bold text-content血压/p p classtext-content138/86 span classtext-muted· 偏高建议复查/span/p /div /div /template同时整个“健康指标统计”模块里我确保状态标签的文字直接写在界面上比如“偏高”“正常”“偏低”而不是只用一个小圆点或者一个颜色块表达。图标只是辅助视觉区分真正传递信息的是文字。4.3 键盘焦点让不使用触摸屏的人也能顺畅操作养老平台的操作者不只是长辈本人还有社工、护理员。很多社工习惯用电脑键盘操作后台键盘的可访问性就成了刚需。最基础但最容易被忽略的一点是焦点可见性。浏览器默认有一个outline但很多前端为了美观会把它消掉*:focus { outline: none; }这行代码堪称可访问性的第一大敌。键盘用户按Tab键在表单和按钮之间移动时如果焦点没有任何视觉显示他们根本不知道当前焦点在哪操作直接就断了。我的方案是分层处理。鼠标点击时不需要粗重的焦点环但键盘操作时需要。所以我用Tailwind的focus-visible修饰符只有键盘聚焦时才显示焦点环鼠标点击不触发button classrounded-lg bg-primary px-6 py-3 text-lg font-medium text-white hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-blue-200 确认预约 /buttonring的宽度和颜色我做了统一约定所有可聚焦元素的focus-visible焦点环统一为ring-4颜色用浅蓝色对比度和页面背景足够拉开肉眼很明显。除了按钮表单输入框的焦点处理也需要一致。我在全局样式里补了一条兜底规则layer base { :focus-visible { apply outline-none ring-4 ring-blue-200; } }这样即使有组件漏写了焦点环类名浏览器也会自动加上不会出现按Tab之后找不到焦点的尴尬情况。5. 养老业务模块中的适老化组件落地记录5.1 健康数据卡片核心指标要用视觉重量“压”出来健康档案是养老平台里使用频率最高的模块。长辈最关心的是今天血压、血糖、心率这些指标是否在正常范围。旧版界面把所有指标一视同仁地排列在表格里数字很小没有主次。重构时我做了一个“主指标卡次指标列表”的布局。主指标卡显示当前最需要关注的指标字号用text-3xl加粗数值颜色以是否异常决定右侧或下方放一个简短的文字说明。其他指标收进次级列表字号相对小一档但仍然保持在text-lg以上。最核心的设计约束是页面最重要的数字应该是最先被眼睛捕捉到的。我给主指标数字增加了色彩和粗体双重强调并配上了状态标签。比如“血压 138/86 偏高”数字用深红色加粗显示旁边再放一个“建议复查”的标签双保险。5.2 点击目标尺寸与防误触布局业界公认的移动端最小点击目标尺寸是44x44px这是苹果HIG和Material Design都采用的标准。我在养老场景里直接把主操作按钮的高度定位48px以上按钮之间的间距至少12px避免误触。另外很多界面元素看上去是按钮其实只是个text-on-hover的hover区域在触屏上根本没有反馈。我在重构时给所有可点击元素统一加了active:scale-[0.98]和active:bg-primary-hover这类效果让用户按下时能明显感觉到“我按中了”。轮播图和横向滚动区域是防误触的重灾区。旧版首页有一个横向滑动的“活动公告栏”长辈手指稍微偏一点就会滑走甚至误触跳到活动详情页。我的做法是把横向滑动区域上下都加上明显的边界阴影滑动提示文字固定显示并且把滑动区域和相邻按钮之间用16px以上的间距隔开。更重要的一步是给轮播图加了“点击区域必须超过宽度50%才视为点击”的逻辑避免轻微滑动也被判定成点击。5.3 表单校验与操作反馈让长辈不再对着提示发愣预约体检、填写健康档案是表单交互最密集的场景。旧版的校验集中在表单提交时一次性把所有错误罗列在页面顶部用户往下翻着找体验非常差。我改成了“即时校验字段级提示”的模式。用户离开某个输入框时立即校验错误信息直接显示在该输入框下方同时用红色边框和浅红色背景双重标出。表单提交时如果还有未填或填错的项目页面自动滚动到第一个错误字段并将焦点移到该输入框上同时用rolealert的容器播报一次错误摘要读屏器也能收到。template div classmb-4 label forphone classmb-1 block text-lg font-medium text-content 手机号码 /label input idphone v-model.trimphone typetel classw-full rounded-lg border px-4 py-3 text-lg focus-visible:ring-4 focus-visible:ring-blue-200 :classphoneError ? border-danger bg-red-50 : border-border :aria-invalid!!phoneError :aria-describedbyphoneError ? phone-error : undefined / p v-ifphoneError idphone-error rolealert classmt-1 text-base font-medium text-danger {{ phoneError }} /p /div /template操作反馈上我统一推翻了“提交后静默等待”的做法。所有表单提交按钮点击后进入加载态时按钮文字会变成“提交中请稍候…”同时按钮本身禁用并显示转圈图标让用户明确知道系统正在处理。提交成功后有全屏绿色确认页提交失败则在按钮旁弹出红色提示条而不是只在角落弹一个小toast。6. 真机验证与排错开发环境里不会告诉你的问题6.1 亮度差异让你精心设计的对比度在长辈手机上“失灵”代码写完之后我经历了一段非常折磨的测试期。一个反复出现的问题是颜色类名没有写错、对比度计算也达标了但在长辈的手机上看起来还是灰蒙蒙的。后来我找到了两个原因。第一个是屏幕亮度。很多长辈习惯把手机亮度调得很低而颜色对比度值是基于标准亮度环境计算的低亮度下所有颜色的观感都会被压暗。我不可能让每个用户都提高亮度只能在设计侧做补偿把正文色调得更深把背景灰度的底色调得更浅让推荐对比度从4.5:1提高到6:1以上才放心。第二个原因是屏幕硬件差异。我开发时用的显示器色域广、色准好但许多中低端安卓手机的屏幕偏黄、偏淡。同一个色值在不同屏幕上显示效果差别很大尤其浅灰底和白色底的区分度会明显下降。我后来在项目里直接规定正文区域的背景和文字之间色差要足够大不允许用“深灰配浅灰”这类高审美的邻近色配搭。6.2 安卓WebView不跟随系统字体缩放的补救方案回到第一节提到的那个问题很多长辈会在系统设置里把全局字体调到最大但页面纹丝不动。原因在于安卓WebView对系统字体缩放的响应并不一致部分版本的WebView根本不把系统字体大小传给域名CSS。我给出的补救是双轨并行。一方面在应用内提供显式的字号切换按钮也就是前面实现的“默认/大/超大”三级档位不依赖系统设置。另一方面尝试监听系统字体变化并调整根字号。监听系统字体缩放安卓WebView可以通过onPageFinished后读取document.getElementsByTagName(html)[0].style.fontSize来感知但兼容性很不理想。我实测下来部分机型只有在WebView启用了setTextZoom时才有反馈。因此在工程层面我最终把“应用内按钮切换”作为主方案把系统字体缩放降级为“尽力兼容”级别并在帮助文档里写明请优先使用界面上的字号切换功能。6.3 低端安卓机上的性能调优与“白屏”风险养老平台的用户设备差异极大后台数据显示有近三成用户还在用五六年前的安卓中低端机。这类设备内存小、CPU弱Vue应用如果打包体积过大首屏加载可能直接卡死或白屏。我做的第一步优化是路由级代码分割。按页面拆包首页静态资源控制在200KB以内其他页面按需加载。第二步是给组件库“翻箱底”把所有只用到一次的组件改为defineAsyncComponent异步加载。第三步是处理图片所有图片统一走压缩和WebP格式并且添加loadinglazy。还有一个容易忽略但影响很大的点CSS解析和重排。Tailwind默认产出的CSS体积在全站场景下不小如果构建配置不当首屏会加载大量无用样式。我通过设置content路径扫描只保留实际使用到的class最终样式文件从原先的340KB压缩到了90KB左右。最后给一条很实在的建议不要只在Chrome DevTools的设备模拟器里测试。用真实的低端安卓机连上chrome://inspect打开Performance面板把CPU降速4倍跑一遍核心流程。你会发现很多在MacBook上完全流畅的交互在真机上会有明显的掉帧和延迟这些都是上线前必须处理的问题。老年用户的耐心通常比年轻用户更有限页面卡顿会直接导致他们放弃操作。适老化不只是在视觉上照顾他们在性能上也要尽量做到“点了就有反应”。这是整个项目做下来我自己最有感触的一点。