使用 SAP UI5 Web Components 与 Vue 3 构建Fiori风格的 Web 应用

132 阅读4分钟

引言

SAP UI5 Web Components 是一套基于原生 Web Components 标准构建的企业级 UI 组件库,它不依赖于特定框架,可以直接在 Vue、React、Angular 甚至原生 HTML 中使用。

结合 Vue 3 的响应式系统和组合式 API(Composition API),我们可以快速构建出语义清晰、风格统一的企业级应用。本文将带你使用 纯 JavaScript 从零开始,用 SAP UI5 Web Components 和 Vue 3 搭建一个简单的“员工查询”应用。

一、技术栈

  • Vue 3(Composition API)
  • SAP UI5 Web Components
  • Vite(构建工具)
  • JavaScript

二、初始化项目

  1. 使用 Vite 创建 Vue 项目(选择 JavaScript + Vue 模板):
npm create vite@latest es-ui5-app -- --template vue
cd es-ui5-app
npm install
  1. 安装 SAP UI5 Web Components:
npm install @ui5/webcomponents @ui5/webcomponents-fiori @ui5/webcomponents-icons

三、 构建员工查询页面

创建 src/components/EmployeeSearch.vue在这里插入图片描述

<template>
  <div class="page-container">
    <!-- 顶部导航 -->
    <ui5-shellbar>
      <ui5-shellbar-branding slot="branding">
        员工信息管理系统
        <img
          slot="logo"
          src="https://sap.github.io/ui5-webcomponents/images/sap-logo-svg.svg"
        />
      </ui5-shellbar-branding>

      <ui5-button slot="startButton" icon="menu"></ui5-button>
    </ui5-shellbar>

    <!-- 查询表单 -->
    <div class="search-form">
      <ui5-label for="nameInput">姓名</ui5-label>
      <ui5-input
        id="nameInput"
        placeholder="请输入姓名"
        v-model="filters.name"
      ></ui5-input>

      <ui5-label for="deptSelect">部门</ui5-label>
      <ui5-select id="deptSelect" v-model="filters.department">
        <ui5-option value="">全部部门</ui5-option>
        <ui5-option value="技术部">技术部</ui5-option>
        <ui5-option value="销售部">销售部</ui5-option>
        <ui5-option value="人事部">人事部</ui5-option>
        <ui5-option value="财务部">财务部</ui5-option>
      </ui5-select>

      <ui5-label for="datePicker">入职时间</ui5-label>
      <ui5-date-picker
        id="datePicker"
        v-model="filters.joinDate"
        formatPattern="YYYY-MM-dd"
        @change="onChangeDate"
      ></ui5-date-picker>

      <ui5-button icon="search" @click="search" design="Emphasized"
        >查询</ui5-button
      >
      <ui5-button icon="reset" @click="reset">重置</ui5-button>
    </div>

    <!-- 数据表格 -->
    <div
      class="table-container"
      style="height: calc(100vh - 200px); overflow: auto"
    >
      <ui5-table aria-label="员工列表" no-data-text="暂无数据" mode="None">
        <ui5-table-growing
          mode="Button"
          slot="features"
          @load-more="onLoadMore"
          v-if="hasMore"
        ></ui5-table-growing>
        <!-- 表头 -->
        <ui5-table-header-row slot="headerRow">
          <ui5-table-header-cell><span>工号</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>姓名</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>部门</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>职位</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>入职时间</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>操作</span></ui5-table-header-cell>
        </ui5-table-header-row>

        <!-- 表体 -->
        <ui5-table-row v-for="emp in visibleData" :key="emp.id">
          <ui5-table-cell
            ><span>{{ emp.id }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.name }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.department }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.position }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.joinDate }}</span></ui5-table-cell
          >
          <ui5-table-cell>
            <ui5-button
              icon="action"
              size="small"
              title="更多操作"
            ></ui5-button>
          </ui5-table-cell>
        </ui5-table-row>
      </ui5-table>
    </div>
  </div>
</template>

<script>
import { ref, computed, watch } from "vue";

import "@ui5/webcomponents-icons/dist/AllIcons.js";

import "@ui5/webcomponents/dist/Button.js";
import "@ui5/webcomponents/dist/Input.js";
import "@ui5/webcomponents/dist/Label.js";
import "@ui5/webcomponents/dist/Select.js";
import "@ui5/webcomponents/dist/Option.js";

import "@ui5/webcomponents/dist/DatePicker.js";

import "@ui5/webcomponents/dist/Table.js";
import "@ui5/webcomponents/dist/TableRow.js";
import "@ui5/webcomponents/dist/TableCell.js";
import "@ui5/webcomponents/dist/TableHeaderRow.js";
import "@ui5/webcomponents/dist/TableHeaderCell.js";
import "@ui5/webcomponents/dist/TableGrowing.js";

import "@ui5/webcomponents-fiori/dist/ShellBar.js";
import "@ui5/webcomponents-fiori/dist/ShellBarBranding.js";
export default {
  setup() {
    const logo =
      "https://images.sj33.cn/uploads/allimg/201401/7-140131225442O6.png";

    // 模拟员工数据
    const employees = ref([
      {
        id: "E001",
        name: "张伟",
        department: "技术部",
        position: "前端工程师",
        joinDate: "2022-03-15",
      },
      {
        id: "E002",
        name: "李娜",
        department: "销售部",
        position: "客户经理",
        joinDate: "2021-07-22",
      },
      {
        id: "E003",
        name: "王强",
        department: "技术部",
        position: "后端工程师",
        joinDate: "2023-01-10",
      },
      {
        id: "E004",
        name: "陈芳",
        department: "人事部",
        position: "HR专员",
        joinDate: "2020-11-05",
      },
      {
        id: "E005",
        name: "刘洋",
        department: "财务部",
        position: "会计",
        joinDate: "2022-08-18",
      },
      {
        id: "E006",
        name: "赵敏",
        department: "技术部",
        position: "UI设计师",
        joinDate: "2023-05-12",
      },
      {
        id: "E007",
        name: "孙磊",
        department: "销售部",
        position: "销售代表",
        joinDate: "2021-09-30",
      },
      {
        id: "E008",
        name: "周婷",
        department: "财务部",
        position: "出纳",
        joinDate: "2022-12-03",
      },
    ]);

    // 筛选条件
    const filters = ref({
      name: "",
      department: "",
      joinDate: "",
    });

    // 表格增长(代替分页)
    const growingStep = 5;
    const visibleCount = ref(growingStep);

    // 计算筛选后的数据
    const filteredData = computed(() => {
      return employees.value.filter((emp) => {
        const matchName =
          !filters.value.name || emp.name.includes(filters.value.name);
        const matchDept =
          !filters.value.department ||
          emp.department === filters.value.department;
        const matchDate =
          !filters.value.joinDate || emp.joinDate === filters.value.joinDate;
        return matchName && matchDept && matchDate;
      });
    });

    const onChangeDate = (e) => {
      filters.value.joinDate = e.target.value;
    };

    // 可见数据(用于表格growing展示)
    const visibleData = computed(() => {
      return filteredData.value.slice(0, visibleCount.value);
    });

    // 是否还有更多数据
    const hasMore = computed(
      () => visibleCount.value < filteredData.value.length
    );

    // 处理加载更多
    const onLoadMore = () => {
      const next = Math.min(
        visibleCount.value + growingStep,
        filteredData.value.length
      );
      visibleCount.value = next;
    };

    // 查询
    const search = () => {
      visibleCount.value = growingStep; // 重置可见数量
    };

    // 重置
    const reset = () => {
      filters.value = {
        name: "",
        department: "",
        joinDate: "",
      };
      visibleCount.value = growingStep;
    };

    // 监听筛选条件变化,自动重置可见数量

    // 监听筛选条件变化,自动重置到第一页
    watch(
      filters,
      () => {
        visibleCount.value = growingStep;
      },
      { deep: true }
    );

    return {
      logo,
      filters,
      filteredData,
      visibleData,
      hasMore,
      search,
      reset,
      onLoadMore,
      onChangeDate,
    };
  },
};
</script>

<style scoped>
.page-container {
  font-family: "Segoe UI", system-ui, sans-serif;
  max-width: 1200px;
  margin: 0 auto;
  background: #f9f9f9;
}

/* 表单样式 */
.search-form {
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto 1fr auto auto;
  gap: 12px;
  padding: 16px;
  background: white;
  border-bottom: 1px solid #e5e5e5;
  align-items: center;
}

.search-form ui5-label {
  text-align: right;
  font-weight: 500;
}

/* 表格容器 */
.table-container {
  padding: 16px;
  background: white;
  margin: 0 16px;
  border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

/* 分页 */
.pagination {
  display: flex;
  justify-content: flex-end;
  padding: 16px;
}
</style>

四、在 App.vue 中引入

在这里插入图片描述

<template>
  <EmployeeSearch />
</template>

<script>
import EmployeeSearch from "./components/EmployeeSearch.vue";

export default {
  components: {
    EmployeeSearch,
  },
};
</script>

五、检查vite.config.js

在这里插入图片描述

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // treat all tags with a ui5- as custom elements
          isCustomElement: (tag) => tag.includes("ui5-"),
        },
      },
    }),
  ],
});

六、运行项目

npm run dev

打开 http://localhost:5173,你将看到:

在这里插入图片描述