领域模型 模板引擎 dashboard应用列表及配置接口实现

领域模型 模板引擎 dashboard应用列表及配置接口实现

一、概述

本文档旨在详细介绍本项目中模板引擎 dashboard 应用列表及配置接口的实现,包括接口设计、实现逻辑、代码结构以及相关的配置说明。

二、模板引擎API接口实现

2.1 dashboard模板引擎应用列表及配置接口

app/router-schema/project.js 接口参数设计规范

js 复制代码
module.exports = {
  ...,
  "/api/project/list": {
    get: {
      query: {
        type: "object",
        properties: {
          proj_key: { type: "string" },
        },
      },
    },
  },
  "/api/project": {
    get: {
      query: {
        type: "object",
        properties: {
          proj_key: { type: "string" },
        },
        required: ["proj_key"],
      },
    },
  },
  ...
};

app/router/project.js 接口定义

js 复制代码
module.exports = (app, router) => {
  const { project: projectController } = app.controller;
  
  router.get(
    "/api/project/list",
    projectController.getList.bind(projectController)
  );
  
  router.get("/api/project", projectController.get.bind(projectController));
  ...
};

app/controller/project.js 控制器定义getListget方法

js 复制代码
module.exports = (app) => {
  const BaseController = require("./base")(app);
  return class ProjectController extends BaseController {
  
    /**
     * 获取当前 projectKey 对应模型下的项目列表 [如果无projectKey 全量获取]
     * @param {object} ctx  上下文
     */
    getList(ctx) {
      const { proj_key: projKey } = ctx.request.query;
      const { project: projectService } = app.service;
      const projectList = projectService.getList(projKey);

      // 构造项目关键数据
      const dtoProjectList = projectList.reduce((preList, item) => {
        const { modelKey, key, name, desc, homePage } = item;
        preList.push({ modelKey, key, name, desc, homePage });
        return preList;
      }, []);
      this.success(ctx, dtoProjectList);
    }
    
     /**
     * 根据 proj_key 获取项目配置
     * @param {object} ctx  上下文
     */
    get(ctx) {
      const { proj_key: projKey } = ctx.request.query;
      const { project: projectService } = app.service;
      const projConfig = projectService.get(projKey);

      if (!projConfig) {
        this.fail(ctx, "项目获取异常", 50000);
        return;
      }

      this.success(ctx, projConfig);
    }
    ...
  };
};

app/service/project.js 服务层定义getList方法 通过model文件获取模型相关数据

js 复制代码
module.exports = (app) => {
  const BaseService = require("./base")(app);
  const modelList = require("../../model/index.js")(app);
  return class ProjectService extends BaseService {
  
    /**
     * 获取统一模型下的项目列表,[如果无 projKey 则全量获取]
     * @param {string} projKey 项目 key
     * */
    getList(projKey) {
      return modelList.reduce((projList, modelItem) => {
        const { project } = modelItem;
        if (projKey && !project[projKey]) return projList;

        for (const pKey in project) {
          projList.push(project[pKey]);
        }
        return projList;
      }, []);
    }
    
     /**
     * 根据 projKey 获取项目配置
     * @param {string} projKey 项目 key
     * */
    get(projKey) {
      let projConfig;
      modelList.forEach((modelItem) => {
        if (modelItem.project[projKey]) {
          projConfig = modelItem.project[projKey];
        }
      });
      return projConfig;
    }
  };
};

2.2 Mocha 添加接口测试逻辑

/api/project/list 接口添加测试用例 /test/controller/project.test.js
Mocha 是一个功能强大的 JavaScript 测试框架,主要用于 Node.js 和浏览器环境中的单元测试和集成测试,对项目相关的接口进行测试,确保接口的功能正确性和稳定性。

js 复制代码
// test/controller/project.test.js
const assert = require("assert");
const supertest = require("supertest");
const md5 = require("md5");
const eplisCore = require("../../elpis-core");

const signKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCg";
const st = Date.now();

describe("测试 project 相关接口", function () {
  this.timeout(60000);

  let modelList;
  let projectList = [];

  let request;

  it("启动测试服务", async () => {
    const app = await eplisCore.start();
    modelList = require("../../model/index.js")(app);
    modelList.forEach((modelItem) => {
      const { project } = modelItem;
      Object.keys(project).forEach((projectItem) => {
        projectList.push(project[projectItem]);
      });
    });
    request = supertest(app.listen());
  });


  it("GET /api/project/list without proj_key", async () => {
    let tmpRes = request.get("/api/project/list");
    tmpRes = tmpRes.set("s_t", st);
    tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
    const res = await tmpRes;
    assert(res.body.success === true);

    const resData = res.body.data;

    assert(resData.length === projectList.length);
    resData.forEach((item) => {
      assert(item.modelKey);
      assert(item.key);
      assert(item.desc !== undefined);
      assert(item.homePage !== undefined);
    });
  });


  it("GET /api/project/list with proj_key", async () => {
    const { key: projKey } =
      projectList[Math.floor(Math.random() * projectList.length)];

    const { modelKey } = projectList.find((item) => item.key === projKey);

    let tmpRes = request.get("/api/project/list");
    tmpRes = tmpRes.set("s_t", st);
    tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
    tmpRes = tmpRes.query({ proj_key: projKey });
    const res = await tmpRes;
    assert(res.body.success === true);

    const resData = res.body.data;
    assert(
      projectList.filter((item) => item.modelKey === modelKey).length ===
        resData.length
    );
    resData.forEach((item) => {
      assert(item.modelKey);
      assert(item.key);
      assert(item.desc !== undefined);
      assert(item.homePage !== undefined);
    });
  });


  it("GET /api/project without proj_key", async () => {
    let tmpRes = request.get("/api/project");
    tmpRes = tmpRes.set("s_t", st);
    tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
    const res = await tmpRes;
    const resData = res.body;
    assert(resData.success === false);
    assert(resData.code === 442);
    assert(resData.message.indexOf("request validate fail") > -1);
  });


  it("GET /api/project without fail", async () => {
    let tmpRes = request.get("/api/project");
    tmpRes = tmpRes.set("s_t", st);
    tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
    tmpRes = tmpRes.query({ proj_key: "xxxxxxxxxxxx" });
    const res = await tmpRes;
    const resData = res.body;
    assert(resData.success === false);
    assert(resData.code === 50000);
    assert(resData.message.indexOf("项目获取异常") > -1);
  });


  it("GET /api/project with proj_key", () => {
    projectList.forEach(async (projItem) => {
      const { key: projKey } = projItem;
      let tmpRes = request.get("/api/project");
      tmpRes = tmpRes.set("s_t", st);
      tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
      tmpRes = tmpRes.query({ proj_key: projKey });
      const res = await tmpRes;
      assert(res.body.success === true);
      const resData = res.body.data;
      assert(resData.key === projKey);
      assert(resData.modelKey);
      assert(resData.name);
      assert(resData.desc !== undefined);
      assert(resData.homePage !== undefined);

      const { menu } = resData;
      menu.forEach((menuItem) => {
        console.log(`-- ${projKey} --`, menuItem.key);
        checkMenuItem(menuItem);
      });
    });

    // 递归校验 menu 菜单
    const checkMenuItem = (menuItem) => {
      assert(menuItem.key);
      assert(menuItem.name);
      assert(menuItem.menuType);

      if (menuItem.menuType === "group") {
        assert(menuItem.subMenu != undefined);
        menuItem.subMenu.forEach((subMenuItem) => {
          assert(subMenuItem.key);
          assert(subMenuItem.name);
          assert(menuItem.menuType === "module");
          checkMenuItem(subMenuItem);
        });
      }
      if (menuItem.menuType === "module") {
        checkModule(menuItem);
      }
    };

    //检查 module 菜单配置
    const checkModule = (moduleItem) => {
      const { moduleType } = moduleItem;
      assert(moduleType);
      if (moduleType === "sider") {
        const { siderConfig } = moduleItem;
        assert(siderConfig);
        assert(siderConfig.menu);
        siderConfig.menu.forEach((siderMenuItem) => {
          checkMenuItem(siderMenuItem);
        });
      }
      if (moduleType === "iframe") {
        const { iframeConfig } = moduleItem;
        assert(iframeConfig);
        assert(iframeConfig.path != undefined);
      }
      if (moduleType === "custom") {
        const { customConfig } = moduleItem;
        assert(customConfig);
        assert(customConfig.path != undefined);
      }
      if (moduleType === "schema") {
        const { schemaConfig } = moduleItem;
        assert(schemaConfig);
        assert(schemaConfig.api != undefined);
        assert(schemaConfig.schema);
      }
    };
  });
});
代码解释

checkMenuItem(menuItem): 这个函数用于递归验证菜单项的结构和内容。

  • 基本验证
    • assert(menuItem.key):验证菜单项的 key 字段存在。
    • assert(menuItem.name):验证菜单项的 name 字段存在。
    • assert(menuItem.menuType):验证菜单项的 menuType 字段存在。
  • 分组菜单验证
    • if (menuItem.menuType === "group") { ... }:如果菜单项的类型是 group,则验证其子菜单。
  • 模块菜单验证
    • if (menuItem.menuType === "module") { ... }:如果菜单项的类型是 module,则调用 checkModule 函数进行验证。

checkModule(moduleItem):这个函数用于验证模块菜单的配置。

  • 基本验证
    • assert(moduleType):验证模块菜单的 moduleType 字段存在。
  • 侧边栏菜单验证
    • if (moduleType === "sider") { ... }:如果模块菜单的类型是 sider,则验证其侧边栏配置。
  • iframe 菜单验证
    • if (moduleType === "iframe") { ... }:如果模块菜单的类型是 iframe,则验证其 iframe 配置。
  • 自定义菜单验证
    • if (moduleType === "custom") { ... }:如果模块菜单的类型是 custom,则验证其自定义配置。
  • 模式菜单验证
    • if (moduleType === "schema") { ... }:如果模块菜单的类型是 schema,则验证其模式配置。

三、模板 dashboard 页面实现

3.1 模板页面功能

  1. entry.dashboard.js 入口文件
js 复制代码
// entry.dashboard.js
import boot from "$pages/boot.js";
import dashboard from "./dashboard.vue";

const routes = [];

// 头部菜单路由
routes.push({
  path: "/iframe",
  component: () => import("./complex-view/iframe-view/iframe-view.vue"),
});
routes.push({
  path: "/schema",
  component: () => import("./complex-view/schema-view/schema-view.vue"),
});
// custom 自定义路由
routes.push({
  path: "/todo",
  component: () => import("./todo/todo-view.vue"),
});

// 侧边栏菜单路由
routes.push({
  path: "/sider",
  component: () => import("./complex-view/sider-view/sider-view.vue"),
  children: [
    {
      path: "/iframe",
      component: () => import("./complex-view/iframe-view/iframe-view.vue"),
    },
    {
      path: "/schema",
      component: () => import("./complex-view/schema-view/schema-view.vue"),
    },
    {
      // custom 自定义路由
      path: "/todo",
      component: () => import("./todo/todo-view.vue"),
    },
  ],
});

// 侧边栏 兜底 策略
routes.push({
  path: "/sider/:chapters+",
  component: () => import("./complex-view/sider-view/sider-view.vue"),
});

boot(dashboard, { routes });
  1. dashboard.vue dashboard 页面组件
js 复制代码
// dashboard.vue
<template>
  <el-config-provider :locale="zhCn">
    <header-view :proj-name="projName" @menu-select="onMenuSelect">
      <template #main-container>
        <router-view />
      </template>
    </header-view>
  </el-config-provider>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import headerView from "./complex-view/header-view/header-view.vue";
import $curl from "$common/curl.js";
import { useProjectStore } from "$store/project";
import { useMenuStore } from "$store/menu";

const router = useRouter();
const route = useRoute();
const projectStore = useProjectStore();
const menuStore = useMenuStore();

const projName = ref("");

onMounted(() => {
  getProjectList();
  getProjectConfig();
});

// 请求 /api/project/list 接口, 存至 project-store
const getProjectList = async () => {
  const res = await $curl({
    url: "/api/project/list",
    method: "GET",
    query: { proj_key: route.query?.proj_key },
  });

  if (!res || !res.success || !res.data) return;

  projectStore.setProjectList(res.data);
};

// 请求 /api/project 接口,存至 menu-store
const getProjectConfig = async () => {
  const res = await $curl({
    url: "/api/project",
    method: "GET",
    query: { proj_key: route.query?.proj_key },
  });

  if (!res || !res.success || !res.data) return;

  const { name, menu } = res.data;

  projName.value = name;

  menuStore.setMenuList(menu);
};

// 点击左侧菜单,触发路由跳转
const onMenuSelect = (menuItem) => {
  console.log("menuItem", menuItem);
  debugger;
  const { moduleType, key, customConfig } = menuItem;
  if (key === route.query.key) {
    return;
  }
  const pathMap = {
    sider: "/sider",
    iframe: "/iframe",
    schema: "/schema",
    custom: customConfig?.path,
  };
  router.push({
    path: pathMap[moduleType],
    query: { key, proj_key: route.query.proj_key },
  });
};
</script>
<style leng="less" scoped></style>
  1. header-view.vue 模板顶部菜单栏目组件
js 复制代码
// header-view.vue
<template>
  <header-contaier :title="projName">
    <template #menu-container>
      <!-- 根据 menuStore.menu 渲染菜单 -->
      <el-menu
        mode="horizontal"
        :default-active="activeKey"
        :ellipsis="false"
        @select="onMenuSelect"
      >
        <template v-for="item in menuStore.menuList">
          <sub-menu
            v-if="item.subMenu && item.subMenu.length > 0"
            :key="item.key"
            :menu-item="item"
          />
          <el-menu-item v-else :index="item.key">
            {{ item.name }}
          </el-menu-item>
        </template>
      </el-menu>
    </template>
    <template #setting-container>
      <!-- 根据 projectStore.projectList 渲染设置-->
      <el-dropdown @command="hanleProjectCommand">
        <span class="project-list">
          {{ projName }}
          <el-icon
            v-if="projectStore.projectList.length > 1"
            class="el-icon--right"
          >
            <arrow-down />
          </el-icon>
        </span>
        <template v-if="projectStore.projectList.length > 1" #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item
              v-for="item in projectStore.projectList"
              :key="item.key"
              :command="item.key"
              :disabled="item.key === route.query.proj_key"
            >
              {{ item.name }}
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </template>
    <template #main-container>
      <slot name="main-container" />
    </template>
  </header-contaier>
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import { useRoute } from "vue-router";
import { ArrowDown } from "@element-plus/icons-vue";
import subMenu from "./complex-view/sub-menu/sub-menu.vue";
import headerContaier from "$widgets/header-container/header-container.vue";
import { useProjectStore } from "$store/project";
import { useMenuStore } from "$store/menu";

const route = useRoute();
const projectStore = useProjectStore();
const menuStore = useMenuStore();

defineProps({
  projName: String,
});

const emit = defineEmits(["menu-select"]);

const activeKey = ref("");

watch(
  () => route.query.key,
  () => {
    setActive();
  }
);

watch(
  () => menuStore.menuList,
  () => {
    setActive();
  }
);

onMounted(() => {
  setActive();
});

const setActive = () => {
  const menuItem = menuStore.findMenuItem({
    key: "key",
    value: route.query.key,
  });
  activeKey.value = menuItem?.key;
};

const onMenuSelect = (menuKey) => {
  debugger;
  if (!menuKey) return;
  const menuItem = menuStore.findMenuItem({
    key: "key",
    value: menuKey,
  });
  emit("menu-select", menuItem);
};

const hanleProjectCommand = (projKey) => {
  console.log(projKey);
  const projectItem = projectStore.projectList.find(
    (item) => item.key === projKey
  );
  if (!projectItem || !projectItem.homePage) return;
  const { origin, pathname } = window.location;
  window.location.replace(`${origin}${pathname}#${projectItem.homePage}`);
  window.location.reload();
};
</script>
<style lang="less" scoped>
.project-list {
  margin-right: 20px;
  cursor: pointer;
  color: var(--el-color-primary);
  display: flex;
  align-items: center;
  outline: none;
}
</style>

五、总结

1. 接口设计与实现

  • 接口参数规范 :在 app/router-schema/project.js 中定义了 /api/project/list/api/project 接口的参数规范,明确了请求参数的类型和要求。
  • 接口定义app/router/project.js 完成了接口的定义,将请求映射到相应的控制器方法。
  • 控制器实现app/controller/project.js 实现了 getListget 方法,分别用于获取项目列表和项目配置。根据请求参数调用服务层方法,并处理响应结果。
  • 服务层实现app/service/project.js 负责从模型文件中获取相关数据,实现了 getListget 方法,根据 projKey 获取项目列表或配置。

2. 接口测试

使用 Mocha 测试框架在 /test/controller/project.test.js 中为项目相关接口添加了测试用例,确保接口的功能正确性和稳定性。测试用例覆盖了不同参数情况下的请求,包括无 proj_key、无效 proj_key 和有效 proj_key 的情况,并对响应结果进行了详细验证。

3. 页面实现

  • 入口文件entry.dashboard.js 定义了路由配置,包括头部菜单路由、侧边栏菜单路由和兜底策略,为页面导航提供支持。
  • dashboard 页面组件dashboard.vue 是核心页面组件,在页面加载时请求 /api/project/list/api/project 接口,将数据存储到相应的状态管理中,并处理菜单点击事件,实现路由跳转。
  • 顶部菜单栏目组件header-view.vue 根据状态管理中的数据渲染顶部菜单和设置选项,处理菜单选择和项目切换事件,实现页面的交互功能。
相关推荐
x-cmd6 分钟前
[250512] Node.js 24 发布:ClangCL 构建,升级 V8 引擎、集成 npm 11
前端·javascript·windows·npm·node.js
夏之小星星19 分钟前
el-tree结合checkbox实现数据回显
前端·javascript·vue.js
crazyme_633 分钟前
前端自学入门:HTML 基础详解与学习路线指引
前端·学习·html
撸猫79142 分钟前
HttpSession 的运行原理
前端·后端·cookie·httpsession
亦世凡华、1 小时前
Rollup入门与进阶:为现代Web应用构建超小的打包文件
前端·经验分享·rollup·配置项目·前端分享
镜舟科技1 小时前
湖仓一体架构在金融典型数据分析场景中的实践
starrocks·金融·架构·数据分析·湖仓一体·物化视图·lakehouse
Bl_a_ck1 小时前
【React】Craco 简介
开发语言·前端·react.js·typescript·前端框架
Ramseyuu2 小时前
Mybatis-plus
微服务·云原生·架构
charlie1145141912 小时前
内核深入学习3——分析ARM32和ARM64体系架构下的Linux内存区域示意图与页表的建立流程
linux·学习·架构·内存管理
augenstern4162 小时前
webpack重构优化
前端·webpack·重构