# SAP Fiori 设计系统

## SAP Fiori 设计系统界面预览

编辑请购单、验证数量、更改密度，并生成本地摘要。

## SAP Fiori 设计系统设计参考

Morning Horizon（sap_horizon）源参数，以及当前 UI5 Web Components 文档。下方的 Less @sap 名称是源参数；浏览器样式使用对应的 --sap 自定义属性。受保护尺寸是观测值，并非建议覆盖的参数。

### Horizon 颜色角色

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| @sapBrandColor | #0070f2 | 主品牌角色。 |
| @sapBackgroundColor | #f5f6f7 | 页面背景。 |
| @sapBaseColor | #ffffff | 基础界面表面。 |
| @sapTextColor | #131e29 | 主文本。 |
| @sapNegativeColor | #aa0808 | 负向状态。 |
| @sapCriticalColor | #e76500 | 关键角色；文本使用单独派生的角色。 |
| @sapPositiveColor | #256f3a | 正向状态。 |
| @sapInformationBackground | #e1f4ff | 信息提示界面表面。 |
| @sapWarningBackground | #fff8d6 | 关键提示界面表面。 |
### 字体与几何尺寸

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| @sapFontFamily | "72", "72full", Arial, Helvetica, sans-serif | 已发布的字体栈。 |
| @sapFontSize | 0.875rem | 基础字号。 |
| @sapContent_LineHeight | 1.5 | 内容行高倍数。 |
| @sapElement_Height | 2.25rem | 受保护的常规尺寸。 |
| @sapElement_Compact_Height | 1.625rem | 受保护的紧凑尺寸。 |
| @sapElement_LineHeight | 2.75rem | 受保护的常规行框。 |
| @sapElement_Compact_LineHeight | 2rem | 受保护的紧凑行框。 |
| @sapButton_BorderCornerRadius | 0.5rem | 按钮圆角参数。 |
| @sapContent_FocusWidth | 0.125rem | 焦点宽度。 |
| @sapContent_FocusColor | #0032a5 | 焦点颜色。 |
### 主题与密度设置

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| sap_horizon | Morning Horizon | 组件内置默认主题。 |
| sap_horizon_dark | Evening Horizon | 其他主题需要额外资源。 |
| sap_horizon_hcb / sap_horizon_hcw | High contrast black / white | 分别支持多种高对比度主题。 |
| Assets.js | One import per component library used | 注册其他主题资源。 |
| setTheme(name) | @ui5/webcomponents-base/dist/config/Theme.js | 通过公开的 API 切换主题。 |
### Select 约定

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| ui5-select value | Matches option value or text | 不要与选项的 selected 标记混用。 |
| selectedOption | Read-only IOption | 读取选中的选项对象。 |
| change | event.detail.selectedOption | 已确认的选择变更。 |
| live-change | Navigation preview | 不要将其视为已提交的业务选择。 |
| accessible-name-ref | ID of visible labeling element | 将可见文本与控件关联。 |
### 输入与反馈

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| value | String | 显式转换数值输入。 |
| input | Each keystroke | 用于实时估算总额。 |
| change | Enter or focusout | 这不同于每次按键都触发输入。 |
| value-state | None / Negative / Critical / Information / Positive | 当前 API 中的状态名称。 |
| slot="valueStateMessage" | One root element | 针对特定字段的说明。 |
### 消息条

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| design | Information / Positive / Critical / Negative | 用于应用程序的语义化消息。 |
| hide-close-button | Boolean attribute | 始终显示必需的说明。 |
| default slot | Prefer text content | 避免将消息条变成复杂面板。 |
## SAP Fiori 设计系统完整指南

实用指南，介绍如何为真实界面选择、应用和评审此系统。

完整指导、实现起点和源代码链接。

## SAP Fiori 设计系统设计与 UI5 实现的适用范围不同

### 一个具体的切入点

> SAP Fiori 描述的是企业级用户体验。本参考选择 UI5 Web Components，提供一个可实现的采购申请示例。这并不意味着安装这些组件就会创建 SAPUI5 应用、Fiori elements 页面布局，或获准执行的 SAP 业务事务。

主要组件包提供 Input、Select 和 Button。更高级的 Fiori 组件位于单独的包中。只导入本示例用到的模块，可以让入口更明确，也避免让人误以为存在不受支持的全组件 CDN。

阅读配套规范

## SAP Fiori 设计系统：使用具名 Horizon 参数，而非通用蓝色配色

### 界面表面与状态角色

Morning Horizon 将页面呈现为浅灰色界面表面，并使用白色作为基础表面。品牌蓝和文本颜色由不同的参数定义。Negative、Critical、Positive 和 Informative 表示语义角色，并非供卡片轮流使用的一组装饰色。

预览使用信息条展示准备好的摘要，因为本地准备并不等于采购审批。示例中的商品价格、成本中心和审批说明均为原创样例数据，不应被视为 SAP 提供的业务规则。

阅读配套规范

## SAP Fiori 设计系统：紧凑模式是组件设置，不是缩小版截图

### 将密度应用于有意义的区域

UI5 支持在根元素、body、区域或组件上添加紧凑模式标记。属性 data-ui5-compact-size 和类 ui5-content-density-compact 是公开机制。动态更改标记不需要单独的密度 API。

仅在交互仍然合适时，才在密集的桌面任务中使用紧凑控件。嵌入式视图会缩小控件间距和行间距，但保留相同的信息。窄屏布局会将区域纵向堆叠，而不是把每个标签和按钮都缩小。

```
<section data-ui5-compact-size>
  <ui5-input accessible-name="Request title"></ui5-input>
</section>
```

代码参考

阅读配套规范

## SAP Fiori 设计系统：72 字体系列及其加载行为

### 字体可用性是设置的一部分

Horizon 主题会在回退字体之前指定 72 和 72full。UI5 文档介绍了如何停用默认字体加载，以及如何在本地托管字体。便捷入口 FontFace.css 标记为实验性功能；如果选择此方式，请固定其路径并在之后重新检查。

独立预览明确使用 Arial 作为离线回退字体。原生 UI5 示例使用已发布的 font-family 变量。这样的区分可以避免把外观相似的回退字体截图误当作 SAP 字体资源已成功加载的证明。

阅读配套规范

## SAP Fiori 设计系统：Select 有已确认和临时变更

### 选择一种受控的选择策略

Select 的 value 必须与某个 Option 的 value 或文本相匹配。不建议同时设置 Select 的 value 和 Option 的 selected 标记；如果 value 不存在，就不会选中任何选项。本示例先初始化一个有效的成本中心选项，再通过 Select 的 value 读取结果。

change 事件会携带选中的选项。浏览选项以及按 Escape 键还原选择时，也会触发 live-change。业务数据应使用已确认的选择；临时变更则留给预览使用。

- 渲染这些标签前，先导入 Select 和 Option 注册模块。

- 通过 accessible-name-ref 将可见的成本中心标签与控件关联。

- 本示例中只使用 value 这一种选择策略。

阅读配套规范

## SAP Fiori 设计系统：数值输入仍需应用层验证

### 值状态与消息应配套使用

即使输入类型为 Number，Input 的 value 仍是字符串。执行乘以示例价格的计算前，应先进行转换，并检查数值是否为有限整数且在范围内。空值不能被当作有效的零。当前语义化错误状态使用 Negative，而不是自行编造 Error 枚举值。

valueStateMessage 插槽会提供一个包含具体说明的根元素。实时总额会监听 input 事件；只有用户触发操作后，才会更新准备好的摘要。

1. 从对应组件读取标题和数值字符串。

2. 在本示例中，标题不能为空，数量必须是 1 到 20 之间的整数。

3. 将受影响控件的状态标记为 Negative，并保留用户输入的值。

4. 只有两项条件都满足后，才准备摘要。

阅读配套规范

## SAP Fiori 设计系统：让结果简洁明了，并保持本地处理

### 用消息条呈现简要结果

MessageStrip 支持语义化设计和关闭控件。对于必需的说明，可以隐藏关闭按钮，确保说明始终可见。其默认插槽最好只放文本，不要扩展成表单或仪表板。

摘要包含标题、数量、示例总额和成本中心。最后一句说明尚未提交任何内容。实际服务会将此本地步骤替换为获准的请求，并返回包含真实后端状态的响应。

阅读配套规范

## SAP Fiori 设计系统：主题资源与模块注册

### 主题选择需要对应的资源

Morning Horizon 是文档所述的默认主题，并内置于组件中。其他主题需要为所用的每个库提供 Assets.js 模块。然后，setTheme 才能选择 Evening Horizon 或某个高对比度变体。选择一个深色主题名称，却不确认相应资源已加载，并不算完整集成。

所提供的入口文件会先注册每个自定义元素，再创建标记。Vite 会处理动态 ES 模块导入和 JSON 资源。请单独编译原生项目，并验证字体、主题加载和键盘行为，不要将其与离线 HTML 示例混为一谈。

阅读配套规范

## SAP Fiori 设计系统组件示例

### 完整的独立交互示例

将完整文档保存为 index.html，并在浏览器中打开。这是对 Morning Horizon 参数和 UI5 表单组合方式的离线 HTML 演绎。原生程序包示例另行提供。

```
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Fiori purchase request</title><style>
*{box-sizing:border-box}body{margin:0;color:#131e29;background:#f5f6f7;font:14px/1.5 Arial,sans-serif}header{background:white;padding:12px 24px;display:flex;gap:16px;align-items:center;border-bottom:1px solid #d5dadd}.symbol{width:32px;height:32px;display:grid;place-items:center;background:#0070f2;color:white;border-radius:8px;font-weight:bold}header strong{font-size:16px}header span:last-child{margin-left:auto;color:#556b82;font-size:12px}main{max-width:1100px;margin:auto;padding:24px}.objecthead{display:flex;justify-content:space-between;gap:20px;align-items:start;margin-bottom:24px}h1{font-size:28px;line-height:36px;margin:0 0 6px}h2{font-size:18px;margin:0 0 16px}.subtitle{color:#556b82;margin:0}.draft{background:#e1f4ff;color:#0032a5;padding:6px 12px;border-radius:6px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 280px;gap:24px}.panel{background:white;border-radius:12px;padding:24px;border:1px solid #d5dadd}label{display:block;margin-bottom:6px;color:#556b82}input,select{width:100%;height:44px;border:1px solid #758ca4;border-radius:8px;padding:8px 12px;background:#fff;font:inherit;color:inherit}input:focus,select:focus,button:focus-visible{outline:2px solid #0032a5;outline-offset:3px}.fields{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px}.fields>div:first-child{grid-column:1/-1}.line{border-top:1px solid #d5dadd;padding-top:20px;display:grid;grid-template-columns:64px minmax(0,1fr) auto;gap:16px;align-items:center}.monitor{background:#e1f4ff;border:2px solid #556b82;height:42px;border-radius:4px;position:relative;margin-bottom:12px}.monitor:after{content:'';position:absolute;bottom:-12px;left:25%;width:50%;height:10px;border-bottom:3px solid #556b82;border-left:8px solid transparent;border-right:8px solid transparent}.line strong{display:block}.line small{color:#556b82}.money{font-size:24px;font-weight:600;white-space:nowrap}.unit{font-size:12px;color:#556b82}.total{display:flex;justify-content:space-between;padding-top:24px;font-weight:600}.controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:24px}button{border:1px solid #0070f2;background:white;color:#0057b8;padding:10px 16px;border-radius:8px;font:600 14px/20px Arial;cursor:pointer}button.primary{background:#0070f2;color:white}button:hover{filter:brightness(.95)}.steps{list-style:none;padding:0;margin:0;counter-reset:step}.steps li{position:relative;padding:0 0 24px 36px;color:#556b82}.steps li:before{counter-increment:step;content:counter(step);position:absolute;left:0;top:0;border:1px solid #758ca4;border-radius:50%;width:24px;height:24px;text-align:center;background:white}.steps strong{display:block;color:#131e29}.steps li:first-child:before{background:#e1f4ff;color:#0057b8;border-color:#0070f2}.strip{margin-top:24px;background:#e1f4ff;border:1px solid #0070f2;padding:12px 16px;border-radius:8px}.strip.error{background:#ffeaf4;border-color:#aa0808;color:#aa0808}.foot{font-size:12px;color:#556b82;margin-top:24px}.compact input,.compact select{height:32px;padding:4px 8px}.compact .fields{gap:12px}.compact .panel{padding:16px}.compact .line{padding-top:12px}.compact button{padding:6px 12px}.hint{font-size:12px;color:#556b82;margin:6px 0 0}@media(max-width:700px){main{padding:20px 16px}.layout{grid-template-columns:1fr}.fields{grid-template-columns:1fr}.objecthead{display:block}.draft{display:inline-block;margin-top:12px}.line{grid-template-columns:48px minmax(0,1fr)}.line>.money{grid-column:2}.panel{padding:20px}header{padding:12px 16px}}
</style><header><span class="symbol">P</span><strong>Procurement workspace</strong><span>Sample environment</span></header><main><div class="objecthead"><div><h1>Purchase request</h1><p class="subtitle">PR-DRAFT · Workplace equipment</p></div><span class="draft">Draft · not submitted</span></div><div class="layout"><section class="panel"><h2>Request details</h2><form id="form" novalidate><div class="fields"><div><label for="title">Request title</label><input id="title" placeholder="e.g. Studio display upgrade" aria-describedby="notice"></div><div><label for="team">Cost center</label><select id="team"><option value="Design · CC-240">Design · CC-240</option><option value="Research · CC-360">Research · CC-360</option></select></div><div><label for="qty">Monitor quantity</label><input id="qty" type="number" min="1" max="20" step="1" value="2" aria-describedby="qty-help notice"><p id="qty-help" class="hint">Whole number from 1 to 20.</p></div></div><div class="line"><div class="monitor" aria-hidden="true"></div><div><strong>27-inch studio monitor</strong><small>Equipment · Item 100240</small></div><div class="money">€320 <span class="unit">/ unit</span></div></div><div class="total"><span>Estimated line total</span><output id="total">€640</output></div><div class="controls"><button class="primary">Prepare summary</button><button type="button" id="density" aria-pressed="false">Compact controls</button></div></form></section><aside class="panel"><h2>Request path</h2><ol class="steps"><li><strong>Prepare draft</strong>Check quantities and cost center.</li><li><strong>Review summary</strong>Confirm the local request details.</li><li><strong>Submit to service</strong>Not connected in this preview.</li></ol><p class="hint">The review path and price are illustrative application data.</p></aside></div><div class="strip" id="notice" role="status">Complete the draft before preparing the summary.</div><p class="foot">HTML interpretation of SAP Morning Horizon · UI5 native integration provided in guide · No SAP transaction created</p></main><script>
const $=id=>document.getElementById(id);function amount(){const n=Number($('qty').value);return Number.isInteger(n)&&n>=1&&n<=20?n:null}$('qty').oninput=()=>{$('total').textContent=amount()===null?'Invalid quantity':'€'+amount()*320};$('density').onclick=()=>{const active=document.body.classList.toggle('compact');$('density').setAttribute('aria-pressed',String(active));$('density').textContent=active?'Cozy controls':'Compact controls'};$('form').onsubmit=e=>{e.preventDefault();const title=$('title').value.trim(),n=amount();$('title').setAttribute('aria-invalid',String(!title));$('qty').setAttribute('aria-invalid',String(n===null));$('notice').classList.toggle('error',!title||n===null);if(!title||n===null){$('notice').textContent=!title?'Enter a title for this request.':'Use a whole quantity from 1 to 20.';(!title?$('title'):$('qty')).focus();return}$('notice').textContent='Local summary: '+title+' · '+n+' monitors · €'+n*320+' · '+$('team').value+'. Nothing submitted.'};
</script></html>
```

代码参考

- 本 HTML 文档中的密度切换是演绎示例。UI5 入口展示了真实的紧凑模式标记、已注册的自定义元素和 Horizon 资源。

### Horizon 采购申请，数量已验证

使用 Input、Select/Option、Button 和 MessageStrip 组合本地请购摘要。每次按键后更新的总额与准备好的结果相互独立。

```
// main.js in a Vite vanilla project. index.html contains <div id="app"></div>.
import '@ui5/webcomponents/dist/Button.js';
import '@ui5/webcomponents/dist/Input.js';
import '@ui5/webcomponents/dist/Select.js';
import '@ui5/webcomponents/dist/Option.js';
import '@ui5/webcomponents/dist/MessageStrip.js';
import '@ui5/webcomponents/dist/Assets.js';
import {setTheme} from '@ui5/webcomponents-base/dist/config/Theme.js';
setTheme('sap_horizon');
document.querySelector('#app').innerHTML = `
  <main style="max-width:800px;margin:auto;padding:24px;font-family:var(--sapFontFamily)">
    <h1>Purchase request</h1>
    <p>Prepare a local requisition summary. No SAP transaction is created.</p>
    <p id="request-label">Request title</p>
    <ui5-input id="title" accessible-name-ref="request-label" required>
      <span slot="valueStateMessage">Enter a title for this request.</span>
    </ui5-input>
    <p id="team-label">Cost center</p>
    <ui5-select id="team" accessible-name-ref="team-label" value="design">
      <ui5-option value="design">Design · CC-240</ui5-option>
      <ui5-option value="research">Research · CC-360</ui5-option>
    </ui5-select>
    <p id="qty-label">Monitor quantity</p>
    <ui5-input id="qty" type="Number" accessible-name-ref="qty-label" value="2">
      <span slot="valueStateMessage">Use a whole number from 1 to 20.</span>
    </ui5-input>
    <p id="total">Estimated line total: €640</p>
    <ui5-button id="prepare" design="Emphasized">Prepare summary</ui5-button>
    <ui5-button id="density">Toggle compact controls</ui5-button>
    <ui5-message-strip id="notice" design="Information" hide-close-button style="display:block;margin-top:24px">
      Complete the draft before preparing the summary.
    </ui5-message-strip>
  </main>`;
const title=document.querySelector('#title'),qty=document.querySelector('#qty');
qty.addEventListener('input',()=>{
  const n=Number(qty.value);
  document.querySelector('#total').textContent=Number.isInteger(n)&&n>=1&&n<=20
    ? `Estimated line total: €${n*320}` : 'Enter a valid quantity to calculate the total.';
});
document.querySelector('#prepare').addEventListener('click',()=>{
  const n=Number(qty.value), validTitle=!!title.value.trim(), validQty=Number.isInteger(n)&&n>=1&&n<=20;
  title.valueState=validTitle?'None':'Negative';qty.valueState=validQty?'None':'Negative';
  const notice=document.querySelector('#notice');
  if(!validTitle||!validQty){notice.design='Negative';notice.textContent='Correct the request title or quantity before continuing.';return;}
  notice.design='Information';
  notice.textContent=`Local summary: ${title.value.trim()}, ${n} monitors, €${n*320}, ${document.querySelector('#team').value}. Nothing submitted.`;
});
document.querySelector('#density').addEventListener('click',()=>document.body.toggleAttribute('data-ui5-compact-size'));
```

代码参考

- 数量规则、€320 的商品价格和成本中心均为虚构的应用数据，不代表 SAP 政策。

- UI5 Web Components 是实现 Fiori 模式的一种方式；这不是完整的 SAPUI5 应用，也不是 Fiori elements 页面模板。

- 不会创建 SAP 事务。通过离线预览验证也不意味着已完成原生依赖编译。

## SAP Fiori 设计系统 Agent 设置

### 实现起点

将 Vite 的 main.js 替换为完整示例。UI5 文档要求使用 ES 模块打包器；官方并未提供涵盖所有组件的 CDN。

```
npm create vite@latest fiori-request -- --template vanilla
cd fiori-request
npm install
npm install @ui5/webcomponents @ui5/webcomponents-base
npm run dev
```

### 适用于你项目的提示词

```
使用 UI5 Web Components 的 Input、Select、Button 和 MessageStrip，搭配 Horizon 主题，组合 Fiori 采购申请。保留数值验证、正确的 Negative 值状态、明确的紧凑密度，以及符合实际情况的本地摘要行为。
```

## SAP Fiori 设计系统来源与范围

独立的 OpenDesign 参考示例。此处展示的是 UI5 Web Components，不是已部署的 SAP 事务或完整的 Fiori elements 应用。

- 快速入门 官方程序包与打包器设置。

- Horizon 来源 主题参数快照。

- 配置 主题名称与资源加载。

- 密度 紧凑模式标记与 RTL。

- 字体 72 加载与本地字体指南。

- 输入 值状态与输入事件。

- Select 选择策略与事件。

- MessageStrip 语义消息结构。

- Button 来源 原生按钮实现。

## SAP Fiori 设计系统相关资源

### SEEK Braid

通过原创的求职界面设计了解 Braid：按主题作用域划分的变量、四个真实断点、结合上下文的文本、不同色调的按钮，以及会说明验证原因而非禁用操作的受控字段。

### Segment Evergreen

一份 Evergreen 参考示例，包含实际主题取值、受控标签输入、实时规则匹配、空状态恢复路径和原生审核对话框。

### Semi Design

创建顺序式工作区设置，采用 Semi 推荐的基础 Steps、受控 Input 和 Switch、真实主题令牌，并明确选择使用 React 18 还是 React 19 程序包。


- [实现参考](https://raw.githubusercontent.com/SAP/theming-base-content/master/content/Base/baseLib/sap_horizon/base.less)
- [来源](https://raw.githubusercontent.com/SAP/theming-base-content/master/content/Base/baseLib/sap_horizon/base.less)
- [来源](https://ui5.github.io/webcomponents/docs/advanced/configuration/)
- [来源](https://ui5.github.io/webcomponents/components/Select/)
- [来源](https://ui5.github.io/webcomponents/components/Input/)
- [来源](https://ui5.github.io/webcomponents/components/MessageStrip/)
- [阅读配套规范](https://ui5.github.io/webcomponents/docs/getting-started/first-steps/)
- [阅读配套规范](https://raw.githubusercontent.com/SAP/theming-base-content/master/content/Base/baseLib/sap_horizon/base.less)
- [代码参考](https://ui5.github.io/webcomponents/docs/advanced/RTL-and-compact-mode/)
- [阅读配套规范](https://ui5.github.io/webcomponents/docs/advanced/RTL-and-compact-mode/)
- [阅读配套规范](https://ui5.github.io/webcomponents/docs/advanced/fonts/)
- [阅读配套规范](https://ui5.github.io/webcomponents/components/Select/)
- [阅读配套规范](https://ui5.github.io/webcomponents/components/Input/)
- [阅读配套规范](https://ui5.github.io/webcomponents/components/MessageStrip/)
- [阅读配套规范](https://ui5.github.io/webcomponents/docs/advanced/configuration/)
- [代码参考](https://raw.githubusercontent.com/SAP/theming-base-content/master/content/Base/baseLib/sap_horizon/base.less)
- [代码参考](https://ui5.github.io/webcomponents/docs/getting-started/first-steps/)
- [快速入门 官方程序包与打包器设置。](https://ui5.github.io/webcomponents/docs/getting-started/first-steps/)
- [Horizon 来源 主题参数快照。](https://raw.githubusercontent.com/SAP/theming-base-content/master/content/Base/baseLib/sap_horizon/base.less)
- [配置 主题名称与资源加载。](https://ui5.github.io/webcomponents/docs/advanced/configuration/)
- [密度 紧凑模式标记与 RTL。](https://ui5.github.io/webcomponents/docs/advanced/RTL-and-compact-mode/)
- [字体 72 加载与本地字体指南。](https://ui5.github.io/webcomponents/docs/advanced/fonts/)
- [输入 值状态与输入事件。](https://ui5.github.io/webcomponents/components/Input/)
- [Select 选择策略与事件。](https://ui5.github.io/webcomponents/components/Select/)
- [MessageStrip 语义消息结构。](https://ui5.github.io/webcomponents/components/MessageStrip/)
- [Button 来源 原生按钮实现。](https://raw.githubusercontent.com/SAP/ui5-webcomponents/main/packages/main/src/Button.ts)
