Mimikyu 开发笔记

330 阅读9分钟

Mimikyu 开发笔记

步骤

github

  • 新建仓库:README,.gitignore:Node,LICENSE:MIT
  • git clone git@github.com:xx/xx

nest-admin 后台管理接口部分实现

  • 参考网址:docs.nestjs.com/first-steps
  • 全局安装 nest:yarn global add @nestjs/cli
  • 创建新 nest 项目:nest new 项目名 For example:nest new server
  • 推荐选择 yarn 安装项目依赖
  • 进入 server 项目:cd server,创建子应用,用于管理后台接口:nest g app 应用名 For example:nest g app admin
  • 启动子应用 admin 服务:nest start -w admin
  • 创建 lib 库,用于数据库相关公用库:nest g lib db,(default:@app)推荐输入使用@libs
  • server\apps\admin\src\app.module.ts 在引入 DbModule
  • 需要连接数据库,安装所需依赖:yarn add nestjs-typegoose @typegoose/typegoose mongoose @types/mongoose
  • server\libs\db\src\db.module.ts中使用TypegooseModule.forRoot链接数据库

server\libs\db\src\db.module.ts

  // 连接数据库
  imports: [
    TypegooseModule.forRoot('mongodb://localhost/mimikyu', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
      useFindAndModify: false,
    }),
  ],

附:下载安装配置Mongodb:www.cnblogs.com/wuweixiong/…

downloads.mongodb.org/win32/mongo…

  • 新建 models 文件夹存放所有数据库模型:server\libs\db\src\models
  • 新建server\libs\db\src\models\user.mdel.ts存放用户数据库模型

server\libs\db\src\models\user.mdel.ts

import { prop } from "@typegoose/typegoose"; // 使用prop装饰数据库模型属性

export class User {
  @prop()
  username: string; // 属性
  @prop()
  password: string; // 属性
}
  • 可以通过在server\libs\db\src\db.module.ts在使用@Global()将其标志为全局,并将 server\libs\db\src\db.module.ts导入,以通过全局使用
  • 在 admin 层级下创建 users 子模块,去管理 user 模型:nest g mo -p admin users
  • 在 admin 层级下创建 users 控制器:nest g co -p admin users
  • 为方便使用数据库的增删改查,安装依赖:yarn add nestjs-mongoose-crud
  • server\apps\admin\src\users\users.controller.ts中使用@InjectModel(User) private readonly model输入模型,引入 User 模型
  • 使用 Crud 为对应模型快捷建立增删改查

server\apps\admin\src\users\users.controller.ts

@Crud({
    model: User
})
  • 安装接口文档依赖:yarn add @nestjs/swagger swagger-ui-express
  • 可以尝试 GET 请求http://localhost:3000/users测试是否正常
  • 参考:https://docs.nestjs.com/recipes/swaggerserver\apps\admin\src\main.ts中使用 swagger

server\apps\admin\src\main.ts

import { Module, Global } from "@nestjs/common";
import { DbService } from "./db.service";
import { TypegooseModule } from "nestjs-typegoose";
import { User } from "./models/user.mdel";

const models = TypegooseModule.forFeature([User]);

@Global() // 使用Global将当前文件标志为全局使用
@Module({
  // 连接数据库
  imports: [
    TypegooseModule.forRoot("mongodb://localhost/mimikyu", {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
      useFindAndModify: false
    }),
    models // 导入模型
  ],
  providers: [DbService],
  exports: [DbService, models] // 导出模型
})
export class DbModule {}
  • 可以使用 @ApiUseTagsserver\apps\admin\src\users\users.controller.ts的控制器添加标签

server\apps\admin\src\users\users.controller.ts

@ApiUseTags('用户') // 标签,用于Swagger
  • 可以使用@ApiModelPropertyserver\libs\db\src\models\user.mdel.ts添加描述

server\libs\db\src\models\user.mdel.ts

  @ApiModelProperty({ description: '用户名', example: 'testName' }) // 添加描述,作用于swagger文档
  • 可以在模型中使用@modelOptions为模型中的各个属性都添加指定的额外属性

server\libs\db\src\models\user.mdel.ts

@modelOptions({
  schemaOptions:{
    timestamps:true
  }
})
  • 由于课程中可以包含多个课时,所有可以新建课程模型(server\libs\db\src\models\course.model.ts)及课时模型(server\libs\db\src\models\episodes.model.ts),将课时模型引入并内嵌到课程模型中,作为其下的一个属性。

server\libs\db\src\models\course.model.ts

  @ApiModelProperty({ description: '课程' }) // 添加描述,作用于swagger文档
  @arrayProp({ itemsRef: 'Episode' }) // 使用arrayProp表示该属性为数组字段,通过itemsRef可以指定属性的参考模型
  episodes: Ref<Episode>[]; // Ref表示参考类型,定义为泛型,此处引入Episode模型,将其(课时)内嵌在课程中
  • 类似创建 users 子模块的方法,使用nest g mo -p admin courses创建 courses 子模块,用于管理 course 模型
  • 同样地,使用nest g co -p admin courses创建 courses 子模块的控制器
  • 同样地,在server\apps\admin\src\courses\courses.controller.ts中添加@Crud,快捷添加增删改查

server\apps\admin\src\courses\courses.controller.ts

@Crud({
    model: Course
})
  • server\apps\admin\src\courses\courses.controller.ts控制器中注入 Course 模型

server\apps\admin\src\courses\courses.controller.ts

    @InjectModel(Course) private readonly model: ReturnModelType<typeof Course>,
  ) {} // 使用@InjectModel注入模型,最后也可以使用ReturnModelType<typeof Course>定义改模型类型
  • 可以为该控制器添加@ApiUseTags('课程')作为其 swagger 的分类
  • 同样地,需要在server\libs\db\src\db.module.ts引入上面创建的 Course 及 Episode 模型才能正常使用

server\libs\db\src\db.module.ts

const models = TypegooseModule.forFeature([User, Course, Episode]); // 引入模型
  • 和创建课程子模块一样,使用nest g mo -p admin episodes创建课时模块
  • 和创建课程控制器一样,使用nest g co -p admin episodes创建课时控制器
  • 同样地,添加@Crud({model: Episode})为 Episode 模型快捷添加增删改查操作
  • 同样地,通过注入的方式,引入 Episode 模型

server\apps\admin\src\episodes\episodes.controller.ts

constructor(
    @InjectModel(Episode) private readonly model:ReturnModelType<typeof Episode>
){}
  • 最后,可以使用@ApiUseTags('课时')为该控制器添加 swagger 标签

vue-admin 后台管理界面部分实现

温馨提示:遇事不决,重启为上。新建了 xx.d.ts,还是提示找不到模块?重启项目吧。console.log()报错?试试 global.console.log()

  • vue create admin 使用默认方式创建即可
  • cd admin
  • 使用 ts 的方式编写项目:vue add typescript
  • 添加 element:vue add elementStill proced? y Fully import SCSS variables? No zh-CN
  • vue add router,Use history ... ? n
  • vue add typescript,Use class-style ...syntax? Yes Use Babel ...JSX)? Yes Convert all .js ...ts? Yes Allow ...compiled? No
  • 注意修改admin\src\main.ts中的引入 element 为引入 ts 而不是 ts,import './plugins/element'即可
  • 适当修改admin\src\App.vue

admin\src\App.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script lang="ts">
  import { Component, Vue } from "vue-property-decorator";

  @Component({})
  export default class App extends Vue {}
</script>

<style>
  body {
    padding: 0;
    margin: 0;
  }
</style>
  • 创建主要布局容器admin\src\views\Main.vue,可以通过在 vscode 中安装Element UI Snippets插件,输入elcon即可扩展为 element 的默认布局,输入elmen则可扩展出 element 的菜单。
vue add element

admin\src\views\Main.vue

<template>
  <el-container>
    <el-aside width="200px">
      <!-- 此处可以使用elmen扩展出element菜单 -->
      菜单
      <!-- Aside content -->
    </el-aside>
    <el-container>
      <el-header height>
        Mimkyue - 后台管理界面
        <!-- Header content -->
      </el-header>
      <el-main height>
        <!-- Main content -->
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script lang="ts">
  // 上面使用lang="ts"标记为ts语法
  import { Vue, Component } from "vue-property-decorator";

  @Component({}) // 使用@Component标注其为组件
  export default class Main extends Vue {}
</script>

<style></style>
  • admin\src\views\Main.vue中使用router-view后,需要到admin\src\router\index.ts中加上且作为其他路由的父级
vue add router

admin\src\router\index.ts

import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";

Vue.use(VueRouter);

const routes: RouteConfig[] = [
  {
    path: "/",
    component: () => import("../views/Main.vue"), // 懒加载,加快页面加载速度
    children: [
      {
        path: "/",
        name: "home",
        component: () => import("../views/Home.vue")
      }
    ]
  }
];

const router = new VueRouter({
  routes
});

export default router;
  • 适当改造admin\src\views\Main.vue

admin\src\views\Main.vue

<template>
  <el-container>
    <el-aside width="200px">
      <el-menu
        mode="vertical"
        style="height:100vh"
        :default-active="$router.path"
        router
      >
        <el-submenu
          v-for="(item, index) in menu.items"
          :index="`menu-item-${index}`"
          :key="`menu-item-${index}`"
        >
          <template slot="title">{{ item.title }}</template>
          <el-menu-item
            v-for="(subItem, subIndex) in item.items"
            :key="`menu-item-${index}-${subIndex}`"
            :index="subItem.path"
            >{{ subItem.title }}</el-menu-item
          >
        </el-submenu>
      </el-menu>

      <!-- Aside content -->
    </el-aside>
    <el-container>
      <el-header height>
        <h3>Mimkyue - 后台管理界面</h3>
        <!-- Header content -->
      </el-header>
      <el-main height>
        <!-- Main content -->
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script lang="ts">
  // 使用ts
  import { Vue, Component } from "vue-property-decorator";

  @Component({}) // 使用@Component标注其为组件
  export default class Main extends Vue {
    menu = {
      items: [
        {
          title: "内容管理",
          items: [
            {
              title: "首页",
              path: "/"
            },
            {
              title: "课程管理",
              path: "/courses/list"
            },
            {
              title: "课时管理",
              path: "/courses/create"
            }
          ]
        },
        {
          title: "运营管理",
          items: [
            {
              title: "用户管理",
              path: "/users/list"
            }
          ]
        }
      ]
    };
  }
</script>

<style></style>
  • 在 admin 中yarn add axios @types/axios,因为 axios 在 js 的包,所以需要加上@types/axios以起到代码提示作用,并在admin\src\main.ts中引入 axios 及创建其对应的全局变量$http

admin\src\main.ts

import Vue from "vue";
import App from "./App.vue";
import "./plugins/element";
import router from "./router";
import axios from "axios"; // 引入axios

Vue.config.productionTip = false;

// 使axios以$http的形式在全局使用,通过创建实例,将axios的请求与服务端地址对接
Vue.prototype.$http = axios.create({
  baseURL: "http://localhost:3000"
});

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");
  • 新建课程列表组件admin\src\views\courses\CourseList.vue。请求列表的时候,若提示跨域,需要在server\apps\admin\src\main.ts中加上app.enableCors();以允许跨域

admin\src\views\courses\CourseList.vue

<template>
  <div>
    <h3>课程列表</h3>
    <el-table :data="data.data" border stripe>
      <el-table-column
        v-for="(field, name) in fields"
        :prop="name"
        :key="name"
        :label="field.label"
        :width="field.width"
      >
      </el-table-column>
    </el-table>
  </div>
</template>

<script lang="ts">
  import { Vue, Component } from "vue-property-decorator";

  @Component({})
  export default class CourseList extends Vue {
    data = {};
    fields = {
      _id: { label: "ID" },
      name: { label: "课程名称" },
      cover: { label: "课程封面图" }
    };

    async fetch() {
      const res = await this.$http.get("courses");
      this.data = res.data;
    }

    created() {
      this.fetch();
    }
  }
</script>

<style></style>

server\apps\admin\src\main.ts

import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  const options = new DocumentBuilder()
    .setTitle("Mimikyu后台管理API")
    .setDescription("供后台界面调用的服务端API")
    .setVersion("1.0")
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup("api-docs", app, document);

  await app.listen(3000);
  console.log("api-docs is working  http://localhost:3000/api-docs");
}
bootstrap();
  • 在路由中加入课程列表组件作为与 home 同级的子路由

admin\src\router\index.ts

...
{
  name: "course-list",
  path: "/course/list",
  component: () => import("../views/courses/CourseList.vue")
}
...
  • 解决当前admin\src\main.tsadmin\src\router\index.ts等文件报错,在admin\src\shims-vue.d.ts同级目录下,新建admin\src\custom-vue.d.ts,代码如下

admin\src\shims-vue.d.ts

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

// 该部分copy到admin\src\custom-vue.d.ts中去
// import Vue from "vue";
// import { AxiosInstance } from "axios";
// declare module "vue/types/vue" {
//   interface Vue {
//     $http: AxiosInstance;
//   }
// }

admin\src\custom-vue.d.ts

import Vue from "vue";
import { AxiosInstance } from "axios";
declare module "vue/types/vue" {
  interface Vue {
    $http: AxiosInstance;
  }
}
  • 完成上述两个文件的编程后,若admin\src\views\courses\CourseList.vue中的$http报错,可以按F1键,输入重新加载窗口并回车确认,即可。

  • 新建编辑(创建)课程组件admin\src\views\courses\CourseEdit.vue,其中使用了vue-ele-form组件,需要yarn add vue-ele-form,并在admin\src\main.ts中注册使用,注册 vue-ele-form 后,由于 vue-ele-form 不是基于 ts 语言开发的,因此若遇到提示无法找到模块“vue-ele-form”的声明文件。,则需要去声明一下。在admin\src\main.ts同级目录下新建admin\src\packages.d.ts,输入下方代码去声明该模块,每当调整 x.d.ts 文件时,推荐重启项目。

yarn add vue-ele-form

admin\src\main.ts

...
import EleForm from 'vue-ele-form' // 引入
...
Vue.use(EleForm) // 注册
...

admin\src\packages.d.ts

declare module "vue-ele-form" {
  export const install: () => any;
}

admin\src\views\courses\CourseEdit.vue

<template>
  <div>
    <h3>{{ isNew ? "创建" : "编辑" }}课程</h3>
    <ele-form :form-data="data" :form-desc="fields" :request-fn="submit">
    </ele-form>
  </div>
</template>

<script lang="ts">
  import { Vue, Component, Prop } from "vue-property-decorator";

  @Component({})
  export default class CourseEdit extends Vue {
    // 可通过在路由中定义props:true,且带上:id作为参数,则可在当前页面通过@Prop获取到路由携带的id参数(注意:由于id初始化时没有赋值,需要在冒号前加上!,形成id!:string)
    @Prop(String) id!: string;
    data = {};
    fields = {
      name: { label: "课程名称", type: "input" },
      cover: { label: "课程封面图", type: "input" }
    };

    // --- 计算属性 ---
    // 没有id,则为创建,为new,相反有id,则为编辑。
    get isNew() {
      return !this.id;
    }

    async submit(data) {
      // 在此无法使用console.log(),故加上global,全局使用
      // global.console.log("data:", data);

      const url = this.isNew ? `courses` : `courses/${this.id}`; // 区分创建还是编辑的url
      const method = this.isNew ? "post" : "put"; // 区分创建还是编辑的请求方式
      await this.$http[method](url, data);
      this.$message.success("保存成功!");
      this.data = {};
      this.$router.go(-1);
    }

    async fetch() {
      const res = await this.$http.get(`courses/${this.id}`); // 获取路由中携带的id作为查询条件,获取该id对应的数据,作为编辑操作时的默认值
      this.data = res.data;
    }

    created() {
      !this.isNew && this.fetch(); // 若不能创建,则请求数据作为默认值
    }
  }
</script>

<style></style>
  • admin\src\router\index.ts路由文件中加入上述组件作为与course-list同级的路由。

admin\src\router\index.ts

...
{
  name: "course-list",
  path: "/courses/list",
  component: () => import("../views/courses/CourseList.vue")
},
{
  name: "course-edit",
  path: "/courses/edit/:id", // 在路由带上id作为参数,以确定编辑哪个列表
  component: () => import("../views/courses/CourseEdit.vue"),
  props:true, // 允许当前路由接受参数
},
{
  name: "course-create",
  path: "/courses/create",
  component: () => import("../views/courses/CourseEdit.vue")
}
...
  • 此时,由于上述course-editcourse-create引用了同一个组件,可能回导致两个路由切换时不会更新数据,因此需要修改一下admin\src\views\Main.vue

在 router-view 中,以$route.path 为 key,防止切换不同路由,相同组件间出现数据不更新问题

...
<el-main height>
  <!-- Main content -->
  <router-view :key="$route.path"></router-view>
</el-main>
...
  • admin\src\views\courses\CourseList.vue加上编辑删除模块,输入eltc可扩展成,其他如下代码所示,主要新增的有 3 点。

admin\src\views\courses\CourseList.vue

<template>
  <div>
    <h3>课程列表</h3>
    <!-- 1.添加跳转至新增 -->
    <div>
      <el-button
        type="success"
        size="small"
        @click="$router.push('/courses/create')"
        >创建课程</el-button
      >
    </div>
    <el-table :data="data.data" border stripe>
      <el-table-column
        v-for="(field, name) in fields"
        :prop="name"
        :key="name"
        :label="field.label"
        :width="field.width"
      >
      </el-table-column>
      <!-- 2. 添加“编辑”,“删除” -->
      <el-table-column label="操作" :width="200">
        <template v-slot="{ row }">
          <el-button
            size="small"
            type="success"
            @click="$router.push(`/courses/edit/${row._id}`)"
            >编辑</el-button
          >
          <el-button size="small" type="danger" @click="remove(row)"
            >删除</el-button
          >
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script lang="ts">
  import { Vue, Component } from "vue-property-decorator";

  @Component({})
  export default class CourseList extends Vue {
    data = {};
    fields = {
      _id: { label: "ID" },
      name: { label: "课程名称" },
      cover: { label: "课程封面图" }
    };

    async fetch() {
      const res = await this.$http.get("courses");
      this.data = res.data;
    }
    // 3. 加上对应的“删除”功能
    async remove(row) {
      try {
        await this.$confirm("是否确认删除?");
      } catch (e) {
        return;
      }
      await this.$http.delete(`courses/${row._id}`);
      this.$message.success("删除成功");
      this.fetch();
    }

    created() {
      this.fetch();
    }
  }
</script>

<style></style>
  • 使用Avue改造 CRUD,先安装 avue,yarn add @smallwei/avue,可以先创建admin\src\plugins\avue.ts,输入下方代码,若报“找不到模块”错误,则到admin\src\packages.d.ts加入下方代码,最后到admin\src\main.ts引入。在admin\src\main.ts,由于使用了 Avue 进行改造,因此,可以去掉 vue-ele-form,代码如下。完成了 avue 的引入,则可以参考Avue 文档来创建admin\src\views\courses\CourseCrud.vue,最后,admin\src\router\index.ts路由也需要改造,代码如下。

admin\src\plugins\avue.ts

import Vue from "vue";
import Avue from "@smallwei/avue";
import "@smallwei/avue/lib/index.css";
Vue.use(Avue);

admin\src\packages.d.ts

declare module "vue-ele-form" {
  export const install: () => any;
}

// 由于引入了基于js的@smallwei/avue,所以加上这行代码
declare module "@smallwei/avue" {
  export const install: () => any;
}

admin\src\main.ts

import Vue from "vue";
import App from "./App.vue";
import "./plugins/element";
import router from "./router";
import axios from "axios"; // 引入axios
// import EleForm from "vue-ele-form"; // 引入了Avue,因此不需要
import "./plugins/avue"; // 引入Avue

Vue.config.productionTip = false;

// 使axios以$http的形式在全局使用,通过创建实例,将axios的请求与服务端地址对接
Vue.prototype.$http = axios.create({
  baseURL: "http://localhost:3000"
});

// 注册 vue-ele-form
// Vue.use(EleForm); // 引入了Avue,因此不需要

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

admin\src\views\courses\CourseCrud.vue

<template>
  <div>
    <avue-crud
      :data="data.data"
      :option="option"
      @row-save="create"
      @row-update="update"
      @row-del="remove"
    ></avue-crud>
  </div>
</template>

<script lang="ts">
  import { Vue, Component } from "vue-property-decorator";

  @Component({})
  export default class CourseList extends Vue {
    data = {};
    option = {
      title: "课程管理",
      column: [
        {
          prop: "name",
          label: "课程名称"
        },
        {
          prop: "cover",
          label: "课程封面图"
        }
      ]
    };
    async fetch() {
      const res = await this.$http.get("courses");
      this.data = res.data;
    }

    async create(row, done, loading) {
      await this.$http.post("courses", row);
      this.$message.success("创建成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async update(row, index, done, loading) {
      const data = JSON.parse(JSON.stringify(row)); //先把对象转字符串,再把字符串转对象,达到复制row而不影响row的效果。
      delete data.$index; // update请求会把index作为数据,类似{ $index:1,... }发送到接口,因此请求前需要清除该字段
      await this.$http.put(`courses/${row._id}`, data);
      this.$message.success("更新成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async remove(row) {
      try {
        await this.$confirm("是否确认删除?");
      } catch (e) {
        return;
      }
      await this.$http.delete(`courses/${row._id}`);
      this.$message.success("删除成功");
      this.fetch();
    }

    created() {
      this.fetch();
    }
  }
</script>

<style></style>

admin\src\router\index.ts

import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";

Vue.use(VueRouter);

const routes: RouteConfig[] = [
  {
    path: "/",
    component: () => import("../views/Main.vue"), // 懒加载,加快页面加载速度
    children: [
      {
        name: "home",
        path: "/",
        component: () => import("../views/Home.vue")
      },
      {
        name: "course-crud",
        path: "/course/list",
        component: () => import("../views/courses/CourseCrud.vue")
      }
      // {
      //   name: "course-list",
      //   path: "/courses/list",
      //   component: () => import("../views/courses/CourseList.vue")
      // },
      // {
      //   name: "course-edit",
      //   path: "/courses/edit/:id", // 在路由带上id作为参数,以确定编辑哪个列表
      //   component: () => import("../views/courses/CourseEdit.vue"),
      //   props: true // 允许当前路由接受参数
      // },
      // {
      //   name: "course-create",
      //   path: "/courses/create",
      //   component: () => import("../views/courses/CourseEdit.vue")
      // }
    ]
  }
];

const router = new VueRouter({
  routes
});

export default router;
  • 进一步使用 Avue 改造组件,从上述的admin\src\plugins\avue.ts中可知,可以通过改变option的默认值及接口请求地址来提高复用性,该步骤先改变 option,先在服务端的server\apps\admin\src\courses\courses.controller.ts加上 option 的 GET 请求,代码如下。在修改接口请求地址前,需要先改造admin\src\router\index.ts,代码如下。然后为admin\src\views\courses\CourseCrud.vue加上对应请求并将请求地址根据当前页面路由地址动态改变,最终代码如下。

server\apps\admin\src\courses\courses.controller.ts

import { Controller, Get } from "@nestjs/common";
import { Crud } from "nestjs-mongoose-crud";
import { Course } from "@libs/db/models/course.model";
import { InjectModel } from "nestjs-typegoose";
import { ReturnModelType } from "@typegoose/typegoose";
import { ApiUseTags } from "@nestjs/swagger";

@Crud({
  model: Course
})
@Controller("courses")
@ApiUseTags("课程") // 为其添加分类,作用于swagger
export class CoursesController {
  constructor(
    @InjectModel(Course) private readonly model: ReturnModelType<typeof Course>
  ) {} // 使用@InjectModel注入模型,最后也可以使用ReturnModelType<typeof Course>定义改模型类型
  @Get("option") // courses的子路由,请求方式为get,即是localhost:3000/courses/option
  option() {
    // 函数名称不影响,类型@Watch()写法
    return {
      title: "课程管理",
      column: [
        {
          prop: "name",
          label: "课程名称"
        },
        {
          prop: "cover",
          label: "课程封面图"
        }
      ]
    };
  }
}

admin\src\router\index.ts

import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";

Vue.use(VueRouter);

const routes: RouteConfig[] = [
  {
    path: "/",
    component: () => import("../views/Main.vue"), // 懒加载,加快页面加载速度
    children: [
      {
        name: "home",
        path: "/",
        component: () => import("../views/Home.vue")
      },
      {
        name: "resource-crud",
        path: "/:resource/list", // :resource 可以被Prop获取到
        component: () => import("../views/courses/CourseCrud.vue"),
        props: true
      }
    ]
  }
];

const router = new VueRouter({
  routes
});

export default router;

admin\src\views\courses\CourseCrud.vue

<template>
  <div>
    <!-- 有option.column再显示,即请求接口获取option所需数据再显示 -->
    <avue-crud
      v-if="option.column"
      :data="data.data"
      :option="option"
      @row-save="create"
      @row-update="update"
      @row-del="remove"
    ></avue-crud>
  </div>
</template>

<script lang="ts">
  import { Vue, Component, Prop } from "vue-property-decorator";

  @Component({})
  export default class ResourceList extends Vue {
    @Prop(String) resource: string;

    data = {};
    option = {};
    async fetch() {
      const res = await this.$http.get(`${this.resource}`);
      this.data = res.data;
    }

    async fetchOption() {
      const res = await this.$http.get(`${this.resource}/option`);
      this.option = res.data;
    }

    async create(row, done, loading) {
      await this.$http.post(`${this.resource}`, row);
      this.$message.success("创建成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async update(row, index, done, loading) {
      const data = JSON.parse(JSON.stringify(row)); //先把对象转字符串,再把字符串转对象,达到复制row而不影响row的效果。
      delete data.$index; // update请求会把index作为数据,类似{ $index:1,... }发送到接口,因此请求前需要清除该字段
      await this.$http.put(`${this.resource}/${row._id}`, data);
      this.$message.success("更新成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async remove(row) {
      try {
        await this.$confirm("是否确认删除?");
      } catch (e) {
        return;
      }
      await this.$http.delete(`${this.resource}/${row._id}`);
      this.$message.success("删除成功");
      this.fetch();
    }

    created() {
      this.fetchOption();
      this.fetch();
    }
  }
</script>

<style></style>
  • 同样地,按照上述步骤,可以增加课时管理的组件,首先改造admin\src\views\Main.vue,将其中的请求地址修正。然后在服务端server\apps\admin\src\episodes\episodes.controller.ts增加类似上述的接口,同样地,也可以修改server\apps\admin\src\users\users.controller.ts。此时,可以修改一下admin\src\views\courses\CourseCrud.vueadmin\src\views\ResourceCrud.vue,并修改其对应的路由admin\src\router\index.ts

admin\src\views\Main.vue

... { title: "课时管理", path: "/episodes/list" } ...

server\apps\admin\src\episodes\episodes.controller.ts

import { Controller, Get } from "@nestjs/common";
import { Crud } from "nestjs-mongoose-crud";
import { Episode } from "@libs/db/models/episodes.model";
import { InjectModel } from "nestjs-typegoose";
import { ReturnModelType } from "@typegoose/typegoose";
import { ApiUseTags } from "@nestjs/swagger";

@Crud({
  model: Episode
})
@Controller("episodes")
@ApiUseTags("课时") // 添加swagger标签
export class EpisodesController {
  constructor(
    @InjectModel(Episode)
    private readonly model: ReturnModelType<typeof Episode>
  ) {}

  @Get("option")
  option() {
    return {
      title: "课时管理",
      column: [
        {
          prop: "name",
          label: "课时名称"
        },
        {
          prop: "file",
          label: "课时文件"
        }
      ]
    };
  }
}

server\apps\admin\src\users\users.controller.ts

...
  @Get('option')
  option() {
    return {
      title: '用户管理',
      column: [
        {
          prop: 'username',
          label: '用户名称',
        },
      ],
    };
  }
...
  • 为组件增加分页效果,需要增加@on-load="changePage"query及修改fetch请求,修改admin\src\views\ResourceCrud.vue

admin\src\views\ResourceCrud.vue

<template>
  <div>
    <!-- 有option.column再显示,即请求接口获取option所需数据再显示 -->
    <avue-crud
      v-if="option.column"
      :page="page"
      :data="data.data"
      :option="option"
      @row-save="create"
      @row-update="update"
      @row-del="remove"
      @on-load="changePage"
    ></avue-crud>
  </div>
</template>

<script lang="ts">
  import { Vue, Component, Prop } from "vue-property-decorator";

  @Component({})
  export default class ResourceList extends Vue {
    @Prop(String) resource: string;

    data: any = {};
    option: any = {};
    page: any = {
      pageSize: 2, // 默认每页显示
      pageSizes: [2, 5, 10], // 下拉选项。2条每页,5条每页,10条每页
      total: 0
    };
    query: any = {
      limit: 2 // 限制每页显示条数
    };

    async fetch() {
      const res = await this.$http.get(`${this.resource}`, {
        params: {
          query: this.query
        }
      });
      this.page.total = res.data.total; // 获取总数
      this.data = res.data;
    }

    async fetchOption() {
      const res = await this.$http.get(`${this.resource}/option`);
      this.option = res.data;
    }

    async changePage({ pageSize, currentPage }) {
      this.query.page = currentPage;
      this.query.limit = pageSize;
      this.fetch();
    }

    async create(row, done, loading) {
      await this.$http.post(`${this.resource}`, row);
      this.$message.success("创建成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async update(row, index, done, loading) {
      const data = JSON.parse(JSON.stringify(row)); //先把对象转字符串,再把字符串转对象,达到复制row而不影响row的效果。
      delete data.$index; // update请求会把index作为数据,类似{ $index:1,... }发送到接口,因此请求前需要清除该字段
      await this.$http.put(`${this.resource}/${row._id}`, data);
      this.$message.success("更新成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async remove(row) {
      try {
        await this.$confirm("是否确认删除?");
      } catch (e) {
        return;
      }
      await this.$http.delete(`${this.resource}/${row._id}`);
      this.$message.success("删除成功");
      this.fetch();
    }

    created() {
      this.fetchOption();
      this.fetch();
    }
  }
</script>

<style></style>
  • 为组件增加排序功能,首先由于定义表格在后端,因此需要修改服务端,如server\apps\admin\src\courses\courses.controller.ts中加上sortable:true,然后修改admin\src\views\ResourceCrud.vue,为其增加@sort-change="changeSort"及对应变量及函数。

server\apps\admin\src\courses\courses.controller.ts

...
option() {
  // 函数名称不影响,类型@Watch()写法
  return {
    title: '课程管理',
    column: [
      {
        prop: 'name',
        label: '课程名称',
        sortable:true // 允许排序
      },
      {
        prop: 'cover',
        label: '课程封面图',
      },
    ],
  };
}
...

admin\src\views\ResourceCrud.vue

<template>
  <div>
    <!-- 有option.column再显示,即请求接口获取option所需数据再显示 -->
    <avue-crud
      v-if="option.column"
      :page="page"
      :data="data.data"
      :option="option"
      @row-save="create"
      @row-update="update"
      @row-del="remove"
      @on-load="changePage"
      @sort-change="changeSort"
    ></avue-crud>
  </div>
</template>

<script lang="ts">
  import { Vue, Component, Prop } from "vue-property-decorator";

  @Component({})
  export default class ResourceList extends Vue {
    @Prop(String) resource: string;

    data: any = {};
    option: any = {};
    page: any = {
      pageSize: 2, // 默认每页显示
      pageSizes: [2, 5, 10], // 下拉选项。2条每页,5条每页,10条每页
      total: 0
    };
    query: any = {
      sort: {},
      // sort: {
      //   _id: -1
      // }, // 按_id排序,-1为倒序,1为正序
      limit: 2 // 限制每页显示条数
    };

    async fetch() {
      const res = await this.$http.get(`${this.resource}`, {
        params: {
          query: this.query
        }
      });
      this.page.total = res.data.total; // 获取总数
      this.data = res.data;
    }

    async fetchOption() {
      const res = await this.$http.get(`${this.resource}/option`);
      this.option = res.data;
    }

    async changePage({ pageSize, currentPage }) {
      this.query.page = currentPage;
      this.query.limit = pageSize;
      this.fetch();
    }

    async changeSort({ prop, order }) {
      if (!order) {
        this.query.sort = null;
      } else {
        this.query.sort = {
          [prop]: order === "ascending" ? 1 : -1
        };
      }
      this.fetch();
    }

    async create(row, done, loading) {
      await this.$http.post(`${this.resource}`, row);
      this.$message.success("创建成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async update(row, index, done, loading) {
      const data = JSON.parse(JSON.stringify(row)); //先把对象转字符串,再把字符串转对象,达到复制row而不影响row的效果。
      delete data.$index; // update请求会把index作为数据,类似{ $index:1,... }发送到接口,因此请求前需要清除该字段
      await this.$http.put(`${this.resource}/${row._id}`, data);
      this.$message.success("更新成功");
      this.fetch();
      done(); // 停止加载,关闭弹窗
    }

    async remove(row) {
      try {
        await this.$confirm("是否确认删除?");
      } catch (e) {
        return;
      }
      await this.$http.delete(`${this.resource}/${row._id}`);
      this.$message.success("删除成功");
      this.fetch();
    }

    created() {
      this.fetchOption();
      this.fetch();
    }
  }
</script>

<style></style>
  • 为组件增加搜索及模糊查询功能。依然需要先在后端加上search:true,也可以按需加上regex: true,,例如:server\apps\admin\src\courses\courses.controller.ts。然后在组件中增加监听搜索功能@search-change="search"

server\apps\admin\src\courses\courses.controller.ts

import { Controller, Get } from "@nestjs/common";
import { Crud } from "nestjs-mongoose-crud";
import { Course } from "@libs/db/models/course.model";
import { InjectModel } from "nestjs-typegoose";
import { ReturnModelType } from "@typegoose/typegoose";
import { ApiUseTags } from "@nestjs/swagger";

@Crud({
  model: Course
})
@Controller("courses")
@ApiUseTags("课程") // 为其添加分类,作用于swagger
export class CoursesController {
  constructor(
    @InjectModel(Course) private readonly model: ReturnModelType<typeof Course>
  ) {} // 使用@InjectModel注入模型,最后也可以使用ReturnModelType<typeof Course>定义改模型类型
  @Get("option") // courses的子路由,请求方式为get,即是localhost:3000/courses/option
  option() {
    // 函数名称不影响,类型@Watch()写法
    return {
      title: "课程管理",
      column: [
        {
          prop: "name",
          label: "课程名称",
          sortable: true, // 允许排序
          search: true, // 增加搜索框
          regex: true, // 允许通过正则表达式搜索,即支持模糊查询
        },
        {
          prop: "cover",
          label: "课程封面图"
        }
      ]
    };
  }

admin\src\views\ResourceCrud.vue

--- 待更新-2020-02-28 ---

Q&A

参考资料:全栈之巅