上个月帮一家制造企业做前端治理,打开他们的系统,左边是 SAPUI5 的采购审批,右边是 React 做的看板,下面还挂着一个 Vue 2 的老门户。三个前端三种按钮风格,光是一个“保存”按钮,在同一个页面里就有三种圆角和三种 hover 状态。这种情况在 SAP 生态里太常见了——后端 ERP 是 SAP,前端团队却早就用 Vue、React 写新功能了。两个世界一旦并存,视觉和交互就会迅速四分五裂。解决这个问题的关键,不是把 Vue 项目干掉,也不是逼所有人回到 SAPUI5,而是找到一层能跨框架复用的设计基础。SAP Fiori Fundamentals 就是干这件事的。
这篇文章我会从实际项目角度拆解:它到底解决什么问题、核心由哪几部分组成、怎么接入 Vue 项目、从 SAPUI5 迁移时最容易踩哪些坑,以及最后怎么让“一致体验”不只是一次性工程,而成为团队能长期维护的机制。
1. 为什么偏偏是 Fiori Fundamentals:SAPUI5 的“看不见的枷锁”
1.1 SAPUI5 的稳定,是建立在封闭之上的
SAPUI5 本身是一个很成熟的企业级前端框架,MVC 分层、模型绑定、国际化、主题切换、控件库都很完善。做 SAP 系统它确实是主力军,但这套东西有一个很现实的问题:它只服务于 SAP 生态。
你可以把 SAPUI5 想象成一家高档餐厅的固定套餐。厨师把所有搭配都配好了,端上来就是一道完整的菜,稳定、好吃,但你没法把这道菜的“盘子”单独拿出去装你自己做的菜。SAPUI5 的控件和主题是深度耦合的,控件封装了 HTML 结构、CSS 样式、交互行为甚至 ARIA 属性。想在外部用一行new sap.m.Button()渲染出 Fiori 风格按钮,就必须在 SAPUI5 运行时里跑,其他框架根本使唤不动它。
很多企业就是在这里被卡住的。SAP 侧的业务页面都有 Fiori 设计语言,Vue 侧的新报表、看板、审批流却要用自己习惯的组件库。两边各做各的,企业主品牌就被稀释成“三种圆角并存”的混搭风。不是视觉团队不努力,而是缺少一个能让 SAPUI5 和 Vue 共用的设计底座。
1.2 Fiori Fundamentals 的定位:它是规范,不是框架
SAP Fiori Fundamentals 的定位可以简单理解为:把 Fiori 这套设计语言从 SAPUI5 中“提炼”出来的开源底层。
早期它叫 Fiori Fundamentals,后来在社区里演化出 Fundamental Library Styles、Fundamental Vue、Fundamental NGX 等一堆衍生包。名字容易让人犯迷糊,但核心思想一直没变:提供一套不绑死框架的 CSS 样式、图标字体和主题变量,让任何技术栈都能写出长得像 Fiori 的界面。
它不是 Web Components,也不是要替代 SAPUI5。它的工作方式更像一份“设计系统翻译器”:SAPUI5 里一套 Fiori 外观,被翻译成 HTML + CSS 类名 + CSS 自定义属性。Vue 项目拿到这份“翻译结果”,就能在不引 SAPUI5 的前提下,做出同款按钮、输入框、表格、对话框。
这里的价值不是“抄一下风格”,而是统一设计令牌。SAPUI5 的主题引擎背后有一套 SAP 主题参数,比如品牌色、背景色、链接色、按钮各状态的边框色和文字色。Fiori Fundamentals 把这些参数用 CSS 变量暴露出来,Vue 项目只要在同一套变量体系下写样式,两边就天然趋同。
我见过不少团队试图用“复制一段 SCSS 变量”或者“全局覆盖 Element Plus 主题”来对齐视觉,最后都会在某个界面细节上翻车。原因很简单:你只是改了几个颜色变量,交互状态、密度、圆角、图标风格并没有真正对齐。Fiori Fundamentals 的价值在于,它把整套规范都带过来,而不是给你几个零散颜色。
2. Fiori Fundamentals 的组成:样式、图标、主题令牌
2.1 样式层:以fd-前缀为线索的组件类
Fiori Fundamentals 的样式层命名很有辨识度,几乎所有组件类都以fd-开头。比如按钮是fd-button,输入框是fd-input,表格是fd-table,对话框是fd-dialog。
它跟 Bootstrap 的思路类似,但没有强行绑定 JavaScript 插件。比如你想做一个强调按钮,只需要这样写:
<button class="fd-button fd-button--emphasized"> 保存 </button>它内部会处理 Fiori 的圆角、边框、阴影、hover 态和 active 态。你不需要关心:hover时文字颜色怎么变,因为样式库里已经定义好了。这种“类名组合”的设计,让 Vue 组件封装变得非常轻量。
我第一次接入时其实有点不适应,因为之前用 Element Plus 或 Ant Design Vue 时,按钮是<el-button>、<a-button>,习惯了组件库的 API。但 Fiori Fundamentals 的样式类不承担行为逻辑,它只管“长什么样”。行为、事件、数据绑定都交给前端框架解决。这反而让它在多前端场景下通用性极强,因为 React、Vue、原生 HTML 都能使用同一套类名。
2.2 图标层:一套字体,两处使用
SAP 的图标体系是企业级后台里比较完整的一套,购物车、审批、单据、过账、搜索这类场景都有对应图标。Fiori Fundamentals 把 SAP 图标字体也引入了开源生态,在 Vue 项目里可以直接用sap-icon--前缀。
大致使用方式是这样:
<i class="sap-icon--cart" aria-hidden="true"></i>如果页面里没有辅助文案,最好加上aria-label或者用<span>包裹一个视觉隐藏文字,保证读屏器能识别。这个细节我在后面“体验对齐”部分会再展开。
图标字体听起来老派,但在企业后台里非常实用。它是纯字体文件,没有 SVG 组件树的维护成本,切换品牌色也很方便,直接继承color。当然你也可以在构建时把图标拆成 SVG,但如果是追求“SAPUI5 一致体验”,直接用同款字体是最省事的。
2.3 主题令牌:让颜色、圆角、间距不再各写各的
现代视觉一致性最关键的不是组件长得像,而是“改一个品牌色,全端跟着变”。Fiori Fundamentals 的核心机制是 CSS 变量,也就是设计令牌。
SAPUI5 的主题引擎里有一组经典变量,比如--sapBrandColor代表品牌主色,--sapBackgroundColor代表全局背景色,--sapButton_Background、--sapButton_BorderColor这些则控制按钮在不同状态下的视觉表现。Fiori Fundamentals 的样式内部大量引用了这类变量,所以我们只需要在 Vue 项目的根节点覆盖它们,就能让全站视觉跟着变。
举个例子:
:root { --sapBrandColor: #0070f2; --sapLinkColor: #0070f2; --sapBackgroundColor: #f5f6f7; --sapButton_Background: #ffffff; --sapButton_BorderColor: #0070f2; --sapButton_TextColor: #0070f2; }这段代码表面上是几个变量,实际作用是:所有用到fd-button、fd-input、fd-table的地方,只要内部消费了这些变量,就会统一换肤。
我见过很多团队在 Vue 项目里维护了一套variables.scss,又在 SAPUI5 项目里维护另一套主题参数,两边颜色经常对不齐。用 Fiori Fundamentals 后,两套体系共用同一组 CSS 变量名,虽然最终映射路径不完全相同,但至少可以在根节点校验谁该是什么色值。这个“对账”成本远比维护两套完全独立的样式系统低。
3. 在 Vue3 + Vite 项目里把 Fiori Fundamentals 跑起来
3.1 安装与入口加载
如果你的 Vue 项目是 Vite 构建,接入成本其实很低。以基础样式库fundamental-styles为例,安装命令只需要:
npm install fundamental-styles然后在main.js或main.ts里一次性引入样式:
import 'fundamental-styles/dist/fundamental-styles.css'如果还需要 SAP 主题和图标资源,可以再安装配合的主题包,但最核心的样式层其实就这一个入口。我建议不要一次性把一堆 SAP 相关依赖全塞进来,先跑通样式,再按需补图标和主题变量。
这里有一个容易被忽略的点:Vite 对 CSS 的依赖预构建通常没问题,但如果你用 Sass 写业务样式,并且希望在业务 SCSS 里引用 Fiori 的变量文件,就需要配置css.preprocessorOptions.additionalData,把变量文件注入到每个模块。这样做能省去在几百个业务组件里手动@import的麻烦。
3.2 先搭一个 Fiori 风格的页面壳子
接入新样式库时,不要一上来就重构整个页面。先做一个最小可用的页面壳子,验证整体布局是否正常。Fiori 经典结构是顶部的 Shellbar、中部的页面内容、底部或侧边的导航。
用 Fiori Fundamentals 写一个简化版页面壳子,大概长这样:
<template> <div class="fd-page"> <header class="fd-shellbar"> <div class="fd-shellbar__group fd-shellbar__group--start"> <span class="fd-shellbar__title">采购协同平台</span> </div> <div class="fd-shellbar__group fd-shellbar__group--end"> <button class="fd-button fd-button--transparent">退出</button> </div> </header> <section class="fd-section"> <div class="fd-row"> <div class="fd-col fd-col--8"> <div class="fd-panel"> <div class="fd-panel__header"> <h2 class="fd-panel__title">待办审批</h2> </div> <div class="fd-panel__content"> 内容区域 </div> </div> </div> <div class="fd-col fd-col--4"> <div class="fd-panel"> <div class="fd-panel__header"> <h2 class="fd-panel__title">关键指标</h2> </div> <div class="fd-panel__content"> 指标卡片 </div> </div> </div> </div> </section> </div> </template>不要在意fd-col--8和fd-col--4是否完全适配小屏,先验证它在桌面端的栅格表现。Fiori Fundamentals 默认是 12 列栅格,熟悉 Bootstrap 的人很快就能上手。先把页面结构铺出来,再逐步往里填具体控件。
3.3 把fd-类封装成 Vue 组件
直接使用类名写业务代码虽然可行,但一旦业务复杂,模板里全是class="fd-button fd-button--emphasized fd-button--compact"这样的长字符串,维护起来非常痛苦。我习惯在项目里做一层轻量封装,把常用组件包成 Vue 组件,业务代码只关心variant、disabled、type这类语义化属性。
比如按钮可以这样封装:
<script setup> defineProps({ variant: { type: String, default: 'standard', }, type: { type: String, default: 'button', }, disabled: Boolean, }) </script> <template> <button :type="type" :disabled="disabled" :class="['fd-button', `fd-button--${variant}`]" > <slot /> </button> </template>这样业务里写<AppButton variant="emphasized">保存</AppButton>就可以了。需要全局统一修改圆角、补图标、加 loading 态时,只需要改这一个组件。
输入框的封装比按钮复杂一点,因为要兼容v-model。用 Vue 3 组合式 API 可以这样写:
<script setup> defineProps({ modelValue: String, }) defineEmits(['update:modelValue']) </script> <template> <input class="fd-input" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template>记住这一条:凡是封装带表单语义的组件,必须处理好modelValue和update:modelValue的透传,否则接入表单库时会出现“输入框能显示,但表单值始终为空”的诡异 bug。这个问题我在项目里亲眼见过不止一次。
3.4 主题切换:从光亮到深色
企业后台不一定都要求深色模式,但一旦要做,Fiori Fundamentals 的主题变量优势就体现出来了。只要在根节点切换主题属性,CSS 变量就会换一套值。
常见的做法是给<html>或<body>加上主题标记,然后加载对应的主题样式。示例:
function switchTheme(themeName) { document.documentElement.setAttribute('data-sap-theme', themeName) }对应的 CSS 里可以这样处理:
:root[data-sap-theme='dark'] { --sapBackgroundColor: #1a1d21; --sapBrandColor: #91c8f6; --sapButton_Background: #2b2f36; --sapButton_BorderColor: #91c8f6; --sapButton_TextColor: #91c8f6; }在我的实际项目里,深色模式验收时最容易出问题的不是颜色,而是边框和阴影对比度。业务组件如果硬编码了浅色阴影,到深色模式下就会显得很突兀。所以团队规范里我始终强调:所有颜色、阴影、圆角尽量走设计令牌,业务代码里不要出现box-shadow: 0 2px 4px #ccc这种硬编码。
4. 从 SAPUI5 迁到 Vue 时,最难的不是功能而是体验对齐
4.1 先从按钮和输入框开始做映射
从 SAPUI5 迁移到 Vue,很多人第一反应是“把页面做出来”,但真正的难点在于让老用户感觉“还是同一个系统”。SAPUI5 的老用户已经被 Fiori 的交互习惯训练过了:按钮的视觉层级、输入框的边框颜色、表格的行高、弹窗的关闭方式,这些肌肉记忆不能丢。
最直接的方法是建立一张控制映射表,把 SAPUI5 控件和 Fiori Fundamentals 的样式类对应起来。我整理过一张常用对照,如下:
| 业务场景 | SAPUI5 控件 | Fiori Fundamentals 对应样式 |
|---|---|---|
| 普通按钮 | sap.m.Button | fd-button |
| 强调按钮 | sap.m.Buttontype=Emphasized | fd-button--emphasized |
| 输入框 | sap.m.Input | fd-input |
| 下拉选择 | sap.m.Select | fd-select |
| 表格 | sap.ui.table.Table | fd-table |
| 对话框 | sap.m.Dialog | fd-dialog |
| 对象状态 | sap.m.ObjectStatus | fd-object-status |
| 消息提示 | sap.m.MessageToast | fd-message-strip |
不要试图一天之内把所有控件都映射完。先从高频、低风险的按钮和输入框开始。因为这两个组件几乎出现在每个页面里,改完它们,用户能明显感受到视觉一致性提升了。
4.2 布局:Fiori 栅格不是 Bootstrap 栅格
SAPUI5 的布局强依赖sap.m.Page、sap.layout.Grid这些容器。起源虽然是响应式设计,但它对不同屏幕断点的处理逻辑和 Bootstrap 并不完全一致。迁移到 Vue 后,如果你直接用 Element Plus 的栅格去排 Fiori 页面,视觉上会对不齐。
Fiori Fundamentals 自带的布局栅格采用类似 12 列体系,但类名和行为都更贴近 Fiori 自身规范。我的建议是:页面级布局沿用 Fiori Fundamentals 的栅格类,业务卡片内部再用 Vue 的 flex 布局微调。这样能保证页面骨架和 SAPUI5 页面的观感一致,同时又给 Vue 团队留了灵活性。
还有一个容易被忽略的地方是fd-section和fd-panel之间的间距。Fiori 对内容区块的 padding 和 margin 有严格控制,很多人迁移时把它当成通用 div 处理,结果页面显得非常拥挤。最稳妥的做法是先看官方示例里“Page with sections”的模板,再依葫芦画瓢。
4.3 状态与校验:ValueState 的迁移处理
SAPUI5 里几乎所有表单控件都有ValueState这个概念,用来表达 None、Success、Warning、Error 四种状态。视觉上,错误状态通常表现为红色边框和红底消息提示。迁到 Vue 项目后,不能只在输入框上简单加一个红色边框,因为 Fiori 的校验反馈还包括图标、消息条、读屏器提示。
用 Fiori Fundamentals 做错误状态时,思路大概是这样的:
<template> <div> <input class="fd-input is-error" aria-invalid="true" aria-describedby="error-message" /> <div id="error-message" class="fd-form-message fd-form-message--error" > 采购数量不能小于 0 </div> </div> </template>这里最关键的细节是aria-invalid和aria-describedby。视觉上,红框已经能提示用户出错,但读屏器用户需要额外信息,知道这个输入框为什么出错、错在哪。很多从 SAPUI5 迁过来的页面,视觉做到了,无障碍属性却没跟上,这会让企业级系统的合规验收出问题。
4.4 键盘交互与焦点管理
Fiori Fundamentals 的样式层只负责视觉,不负责键盘行为。这一点必须提醒所有做迁移的团队成员:你以为用了fd-button就有了 Fiori 的完整交互,其实焦点管理、Tab 顺序、弹窗关闭 ESC 键、下拉列表的方向键操作,全部需要自己写。
SAPUI5 控件内部已经把这些行为封装好了,用户早就习惯了。迁移到 Vue 后,用原生 input 和 button 问题不大,浏览器天然支持 Tab 和 Enter。但遇到组合型组件,比如下拉选择、日期选择、对话框,就需要手工补焦点管理。
通常的做法是写一个可复用的useFocusTrap组合函数来处理对话框焦点锁定:按下 Tab 时,焦点在弹窗内部循环;按 ESC 时关闭弹窗并把焦点还原到打开弹窗的按钮上。这个细节不做,用户操作一次就会感觉“这个系统不是原来的系统”。
5. 让“一致体验”活下去:团队协作与演进节奏
5.1 先定样式基线,再谈组件复用
很多团队接完 Fiori Fundamentals 后,第一件事就是急着封装一堆公司级组件。我的建议正好相反:先用两周时间把样式基线定下来,再开始做组件库。
样式基线包括四件事:主题变量、字体栈、间距体系、圆角规则。把这四样写进项目文档,并给出几个必看示例页面。组件库只是基线的“具象化”,如果基线没定清楚,组件迟早会被各种业务需求带偏。
实际项目中,我会让一个资浅前端专门维护一个“样式对账表”,里面记录 SAPUI5 页面里出现的按钮状态、输入框状态、表格行高、弹窗宽度,然后一行一行和 Vue 侧的 CSS 变量对照。这个过程很机械,但很有效,能逼着团队发现很多“我以为已经对齐了”的差异。
5.2 用设计令牌驱动,少用硬编码颜色
一个最容易被忽视的坑是:Fiori Fundamentals 提供了完整的设计令牌,但团队在写业务组件时,还是会因为“这个按钮颜色比较特殊”而直接写#ff5500。一旦这种硬编码多了,设计令牌就形同虚设。
在代码评审里,我通常会挂一条规则:业务样式里不允许直接出现颜色值,必须引用 CSS 变量。如果确实是新品牌色,先同步设计系统,再在全局变量里增加,而不是在业务组件里临时写死。
这一步会让人觉得繁琐,但长期收益很大。等到品牌换色或者要做深色模式时,不用再到处找色值,所有改动只发生在全局变量文件里。这也是“多前端时代”里最容易达成一致体验的底层机制。
5.3 视觉回归测试要放进发布流水线
跨团队协作时,最怕的是“A 团队改了主题变量,B 团队没察觉”。视觉回归测试是最好的保险。
不需要一开始就上复杂的 VRT 平台。先用 Playwright 或 Cypress 对几个核心页面做截图,定期对比像素差异。页面数量控制在 10 个以内,重点覆盖包含多种按钮状态、表单校验、表格、弹窗的高复用页面。只要主题变量或基础样式库版本变化,就跑一遍回归脚本,能把大部分视觉回归提前拦下。
我踩过的一个真实坑是:某次升级fundamental-styles后,表格行高从 44px 变成了 48px,单看表格并不觉得有问题,但打印报表时页面多了几页,用户立刻发现输出内容不一样了。后来我们才意识到,几乎每次基础库升级都会有一些细小的视觉漂移,没有自动化截图的团队只能靠用户反馈后知后觉。
5.4 渐进式替换旧页面的顺序
从 SAPUI5 迁到 Vue,如果采用“一口吃成胖子”的节奏,十有八九会失败。企业系统页面动辄几十上百个,每个页面背后还有复杂的 OData 请求和权限逻辑。
推荐顺序是:先替换高频且独立的功能页,比如“我的待办”、“通知中心”、“通用查询”。这类页面逻辑简单,迁移风险低,用户接触频率高,能快速建立信心。再替换涉及主数据维护的页面,这类页面表单比较复杂,需要处理校验和状态映射。最后才动跨模块流程页面,因为它们往往依赖 SAPUI5 的路由、启动器和全局上下文。
替换时还有一个技巧:尽可能保留原来的 URL 和路由结构。SAPUI5 的 Hash 路由和 Vue Router 在语义上有差异,但用户不关心底层技术,他们只关心收藏的链接是否还能打开。前期我建议用一层兼容处理把旧 hash 映射到新路由,等用户适应后再逐步清理。
我在实际迭代里的最后一个习惯是:每次收到视觉走查意见,先改设计令牌,再改组件;组件一次只改一个状态,比如先处理 hover 再处理 disabled。这样三个月后,SAPUI5 和 Vue 两边的主色、圆角、字体基本能做到像素级一致。这不是靠一次重构,而是靠每次小步快跑攒下来的。多前端时代从来不是选边站,而是找到一条让所有技术栈都能对同一套设计语言负责的路径,Fiori Fundamentals 至少把这条路修通了。