Vue入门之Web端CURD前端项目示例

Vue入门之Web端CURD前端项目示例

随着vue.js越来越火,很多不太懂前端的小伙伴想要入坑。而入坑最好的办法就是上手实际操作,因此本文将重点放在实际操作上,对理论不做过多解释,不明白的地方小伙伴们可以去看官方文档或者相关书籍。

目录

本文的示例项目操作环境如下:

平台/工具说明
操作系统Mac OS 10.13.6 (17G65)
浏览器Chrome 77.0.3865.120
nvm下nodejs版本10.13.0
编辑器Visual Studio Code 1.39.2

一、安装npm

npm有多种安装方式,这里推荐使用nvm进行安装。

1.1 安装nvm

  • windows

地址:https://github.com/coreybutler/nvm-windows/releases

根据文档说明安装,记得设置淘宝的镜像。

nvm node_mirror https://npm.taobao.org/mirrors/node/
nvm npm_mirror https://npm.taobao.org/mirrors/npm/

或者修改settings.txt文件,在后面加上淘宝镜像设置。

node_mirror: https://npm.taobao.org/mirrors/node/
npm_mirror: https://npm.taobao.org/mirrors/npm/
  • linux/mac

地址:https://github.com/creationix/nvm

根据文档说明安装,记得设置淘宝的镜像。

# bash_profile
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node

1.2 配置npm

npm config set registry https://registry.npm.taobao.org
npm config set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass

1.3 安装yarn

npm install -g yarn

1.4 配置yarn

已配置过npm则无需再次配置。

yarn config set registry https://registry.npm.taobao.org -g
yarn config set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass -g

二、安装vue-cli

细心的小伙伴会发现vue官网是不推荐入门直接上vue-cli的,这里考虑到很多小伙伴不是前端工程师,而仅仅是需要了解vue项目的基本结构,所以直接使用vue-cli创建项目吧。

使用npm或者yarn全局安装vue-cli:

注意:这里的@vue/cli不要写成了vue-cli,vue-cli是旧版本。

npm install -g @vue/cli
# OR
yarn global add @vue/cli

三、使用vue-cli创建项目

现在可以开始使用vue-cli创建项目了,这里项目名称为example-curd

这里也可以通过vue ui来创建,使用方法是控制台输入vue ui,具体内容本文不做展示

vue create example-curd

这时会有一个选项,第一项为默认配置,这里我们选择第二项手动配置,回车确定。

在这里插入图片描述

之后会出现一个多选,这时的操作是空格键选择,空格选择完所有需要的配置之后,最后按回车确定。

本文用于演示基本的项目流程,只选择3项,其它项小伙伴们自行查阅资料按需选择

在这里插入图片描述

选好依赖项后,进入vue-router设置提示,是否使用history mode,输入n然后回车确定。

这里的区别小伙伴们自己查阅资料

在这里插入图片描述

然后会问如何处理Babel、ESLint之类的配置文件,这里为了便于修改,不与package.json混在一起,选择第一项。

在这里插入图片描述

最后问你是否记录下这套配置,由于本文只是演示,选择的配置项并不适合正式项目,所以选择不记录。

在这里插入图片描述

选择包管理器,这里推荐使用yarn。

在这里插入图片描述

项目配置完成。

在这里插入图片描述

四、修改项目配置

生成的项目目录结构如图:
在这里插入图片描述
为了避免默认端口被占用的情况,我们新建一个名为vue.config.js的配置文件,指定hostport配置参考

module.exports = {
  devServer: {
    open: true,
    host: '0.0.0.0',
    port: 9000,
  },
};

五、启动项目

可以在package.json中修改脚本命令,例如增加一个与serve功能相同的dev命令:

{
  "name": "example-curd",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "dev": "vue-cli-service serve"
  },
  "dependencies": {
    "core-js": "^3.1.2",
    "vue": "^2.6.10",
    "vue-router": "^3.0.6",
    "vuex": "^3.0.1"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.0.0",
    "@vue/cli-plugin-router": "^4.0.0",
    "@vue/cli-plugin-vuex": "^4.0.0",
    "@vue/cli-service": "^4.0.0",
    "vue-template-compiler": "^2.6.10"
  }
}

启动项目

yarn dev

在这里插入图片描述

至此,基本的项目创建与启动演示完毕,接下来进入实例演示。

六、安装依赖

本文主要目的是演示创建基本CURD的前端WEB页面,因此不做后端API的创建,这里使用mock.js拦截请求并返回模拟数据

本文需要用到的依赖有:

使用yarn安装以上依赖

如果已经启动了项目,可以先ctrl + c退出,安装完依赖后再启动。

yarn add element-ui mockjs axios

七、建立基本视图框架

修改App.vuemain.jsrouter.js,同时删除项目创建时的默认文件About.vueHome.vueHelloWorld.vue,在src/views目录下创建一个layout目录和page目录,分别用于存放布局和页面。

7.1 修改main.js文件

引入element组件库及其样式

这里的@在vue-cli默认语法中代表项目中的src目录

src/main.js:

import Vue from 'vue';
import App from '@/App.vue';
import router from '@/router';
import store from '@/store';
import ElementUI from 'element-ui'; // ElementUI组件库
import 'element-ui/packages/theme-chalk/lib/index.css'; // ElementUI组件库样式

// 注册饿了么UI
Vue.use(ElementUI);

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

7.2 修改App.vue

同时调整一些必要的全局html样式。

src/App.vue:

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

<style>
html,
body {
  padding: 0;
  margin: 0;
}
#app {
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
    Microsoft YaHei, SimSun, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
a:focus,
a:active {
  outline: none;
}

a,
a:focus,
a:hover {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
}
</style>

7.3 创建布局文件

layout目录下创建一个default.vue作为默认布局文件,初学可以使用element的Container 布局容器组件。

src/views/layout/default.vue:

<template>
  <el-container>
    <el-aside width="200px">Aside</el-aside>
    <el-container>
      <el-header>Header</el-header>
      <el-main>
      	<router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

7.4 修改router

指定src/views/page/index.vue为本项目默认首页,且作用于默认布局下。

() => import(‘xxx’) 为动态加载组件,这样做的好处是用到哪个页面加载哪个页面,避免导致首页加载时间过长。

src/router/index.js:

import Vue from 'vue'
import VueRouter from 'vue-router'
import DefaultLayout from '@/views/layout/default'

Vue.use(VueRouter)

const routes = [
  {
    path: '',
    component: DefaultLayout,
    redirect: 'index',
    children: [{
      path: 'index',
      name: 'index',
      component: () => import('@/views/page/index'),
    }],
  },
]

const router = new VueRouter({
  routes
})

export default router;

7.5 修改index.vue

修改首页内容,测试布局是否生效。

src/views/page/index.vue:

<template>
  <div>
    <el-button type="primary">首页的按钮</el-button>
  </div>
</template>

7.6 查看基本布局是否生效

可以看到首页的ElementUI的按钮组件正常显示,且首页位于默认布局之下。
在这里插入图片描述

此时项目结构如下:

在这里插入图片描述

八、完善CURD功能

最后只剩下界面优化及基本的业务功能页面了。

8.1 优化界面

增加左侧菜单以及右侧上方的导航栏。可以使用ElementUI的NavMenu 导航菜单组件来实现。

此时可以将菜单栏和标题栏拆分成独立组件引入默认布局,创建src/views/layout/components目录并新建header.vuemenu.vue

此时我们希望菜单根据router配置自动生成,所以需要修改router,可以将router配置单独提取成routes.js便于其它页面调用。同时新建一个CURD页面curd.vue

[标题栏] src/views/layout/components/header.vue:

<template>
  <div class="layout-header">
    <i class="icon-collapse" :class="`el-icon-s-${collapse ? 'unfold' : 'fold'}`" @click="collapseMenu"></i>
    <div v-if="$route.name === 'index'" class="el-page-header">
      <div class="el-page-header__content">首页</div>
    </div>
    <el-page-header v-else @back="goBack" :content="content"></el-page-header>
  </div>
</template>

<script>
export default {
  props: {
    collapse: {
      type: Boolean,
    }
  },
  data: () => ({
    content: ''
  }),
  watch: {
    $route: {
      handler(to) {
        this.content = to.meta.title;
      },
      immediate: true
    }
  },
  methods: {
    goBack() {
      this.$router.back();
    },
    collapseMenu() {
      this.$emit('collapse-menu');
    }
  },
}
</script>

<style>
.layout-header {
  position: relative;
  height: 100%;
  padding: 0 20px;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,21,41,.08);
  display: flex;
  align-items: center;
}
.layout-header .icon-collapse {
  cursor: pointer;
  font-size: 20px;
  padding-right: 20px;
}
</style>

[导航菜单] src/views/layout/components/menu.vue:

<template>
  <el-menu
    ref="menu"
    class="layout-menu"
    background-color="#001529"
    text-color="#fff"
    active-text-color="#1890ff"
    :collapse="collapse"
    :default-active="$route.name"
  >
    <router-link v-for="(menu, index) in menus" :key="index" :to="menu.path">
      <el-menu-item :index="menu.name">
        <i :class="`el-icon-${menu.meta.icon || 'menu'}`"></i>
        <span slot="title">{{ menu.meta.title }}</span>
      </el-menu-item>
    </router-link>
  </el-menu>
</template>

<script>
import routes from '@/router/routes';

export default {
  props: {
    collapse: Boolean
  },
  computed: {
    menus() {
      return routes[0].children || [];
    }
  },
}
</script>

<style>
.layout-menu {
  height: 100%;
  width: 100%;
}
</style>

[路由设置] src/router/index.js:

import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'

Vue.use(VueRouter)

const router = new VueRouter({
  routes
})

export default router;

[路由表] src/router/routes.js:

import DefaultLayout from '@/views/layout/default';

export default [
  {
    path: '',
    component: DefaultLayout,
    redirect: 'index',
    children: [
      {
        path: 'index',
        name: 'index',
        component: () => import('@/views/page/index'),
        meta: { title: '首页', icon: 's-home' }
      },
      {
        path: 'curd',
        name: 'curd',
        component: () => import('@/views/page/curd'),
        meta: { title: '增删改查', icon: 's-opportunity' }
      }
    ],
  },
]

src/views/page/curd.vue:

<template>
  <div>CURD页面</div>
</template>

src/views/layout/default.vue

<template>
  <el-container class="layout-default">
    <el-aside class="layout-default__menu" :style="{ width: `${collapse ? 64 : 256}px` }">
      <layout-menu :collapse="collapse"></layout-menu>
    </el-aside>
    <el-container class="layout-default__main" :style="{ 'margin-left': `${collapse ? 64 : 256}px` }">
      <el-header>
        <layout-header @collapse-menu="collapse = !collapse" :collapse="collapse"></layout-header>
      </el-header>
      <el-main><router-view></router-view></el-main>
    </el-container>
  </el-container>
</template>

<script>
import LayoutMenu from './components/menu';
import LayoutHeader from './components/header';

export default {
  components: { LayoutMenu, LayoutHeader },
  data: () => ({
    collapse: false,
  }),
}
</script>

<style>
.layout-default .el-header {
  padding: 0;
}
.layout-default__menu {
  position: fixed;
  overflow-x: hidden;
  background-color: #001529;
  transition: width 300ms;
  overflow-x: hidden !important;
  height: 100vh;
}
.layout-default__main {
  transition: margin 300ms;
}
.layout-default__menu, .layout-default__menu .el-menu {
  border: 0 !important;
}
</style>

此时的目录结构是这样的:
在这里插入图片描述
这时可以看到页面已经有了较为美观的样式:

在这里插入图片描述
在这里插入图片描述
且菜单支持收起和展开:
在这里插入图片描述

8.2 修改curd页面结构

一个基本的CURD页面应该包括:查询条件操作按钮数据列表列表分页编辑表单详情展示等几个基本模块。

因此我们可以修改原有的curd.vue为一个curd目录,目录下包括index.vue及其相关组件。由于表单页面相对而言重复性较高,代码较为冗长,所以我们将其拆分为index.vuesearch.vueform.vue三个组件,其中index.vue为列表分页展示及数据控制,search.vue和form.vue为表单组件。

search.vue搜索表单我们可以参照element的行内表单示例进行修改:

src/views/curd/search.vue:

<template>
  <el-card shadow="never">
    <el-form inline :model="model" size="small">
      <el-form-item label="姓名">
        <el-input v-model="model.name" placeholder="请输入姓名"></el-input>
      </el-form-item>
      <el-form-item label="性别">
        <el-select v-model="model.gender" placeholder="请选择性别">
          <el-option label="小姐姐" value="famale"></el-option>
          <el-option label="小哥哥" value="male"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSubmit">查询</el-button>
        <el-button plain @click="handleReset">重置</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
  export default {
    props: {
      value: {
        type: Object,
        default: () => {
          return {};
        }
      },
    },
    data: () => ({
      model: {}
    }),
    watch: {
      value: {
        handler(val) {
          if (val) {
            Object.keys(this.model).forEach(key => {
              this.model[key] = val[key];
            });
          } else {
            Object.keys(this.model).forEach(key => {
              this.model[key] = null;
            });
          }
        },
        immediate: true,
      },
      model: {
        handler(val) {
          this.$emit("input", val);
        },
        deep: true
      }
    },
    methods: {
      handleSubmit() {
        this.$emit('search', this.model);
      },
      handleReset() {
        Object.keys(this.model).forEach(key => {
          this.model[key] = null;
        });
      }
    }
  }
</script>

form.vue搜索表单我们可以参照element的表单验证示例进行修改:

src/views/curd/form.vue:

<template>
  <el-form ref="form" :model="model" :rules="rules" size="small" label-width="100px">
    <el-form-item label="姓名" prop="name">
      <el-input v-model="model.name"></el-input>
    </el-form-item>
    <el-form-item label="性别" prop="gender">
      <el-select v-model="model.gender" placeholder="请选择性别">
        <el-option label="小姐姐" value="famale"></el-option>
        <el-option label="小哥哥" value="male"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="年龄" prop="age">
      <el-input-number v-model="model.age"></el-input-number>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" size="small" @click="handleSubmit">确定</el-button>
      <el-button plain size="small" @click="handleCancel" style="margin-left: 8px">取消</el-button>
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  props: {
    value: {
      type: Object,
      default: () => {
        return {};
      }
    },
  },
  data: () => ({
    model: {
      name: '',
      gender: null,
      age: 18,
    },
    rules: {
      name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
      gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
      age: [{ required: true, message: '请输入年龄', trigger: 'change' }],
    }
  }),
  watch: {
    value: {
      handler(val) {
        if (val) {
          Object.keys(this.model).forEach(key => {
            this.model[key] = val[key];
          });
        } else {
          Object.keys(this.model).forEach(key => {
            this.model[key] = null;
          });
        }
      },
      immediate: true,
    },
    model: {
      handler(val) {
        this.$emit("input", val);
      },
      deep: true
    }
  },
  methods: {
    // 点击确定提交表单的操作
    handleSubmit(name) {
      this.$refs.form.validate(valid => {
        if (valid) {
          const result = this.submitPure ? this.getPureModel() : JSON.parse(JSON.stringify(this.model));
          this.$emit("submit", result);
        }
      });
    },
    // 校验表单
    validate() {
      this.$refs.form.validate(valid => {
        this.$emit("validate", valid);
      });
    },
    // 重置表单
    reset() {
      Object.keys(this.model).forEach(key => {
        this.model[key] = undefined;
      });
      this.$nextTick(() => {
        this.$refs.form.clearValidate();
      });
    },
    // 点击取消的操作
    handleCancel() {
      this.$emit("cancel");
    }
  }
};
</script>

index.vue则是对以上组件的整合以及列表数据的展示操作。

src/views/curd/index.vue:

<template>
  <div class="page-curd">
    <curd-search @search="handleSearch"></curd-search>
    <div class="page-curd__action-bar">
      <el-button type="primary" icon="el-icon-plus" size="small" @click="handleNew">新增</el-button>
      <el-button icon="el-icon-delete" size="small" :disabled="tableSelection && tableSelection.length <= 0" @click="handleDeleteMul">批量删除</el-button>
    </div>
    <el-table size="mini" :data="tableData" class="page-curd__table" border @selection-change="handleTableSelectionChange">
      <el-table-column type="selection" min-width="35"></el-table-column>
      <el-table-column label="姓名" prop="name" min-width="100">
        <template slot-scope="{ row }">
          <span><i class="el-icon-user-solid"></i>{{ row.name }}</span>
        </template>
      </el-table-column>
      <el-table-column label="性别" prop="gender">
        <template slot-scope="{ row }">
          <el-tag v-if="row.gender === 'male'" size="small">小哥哥 ♂</el-tag>
          <el-tag v-else type="danger" size="small">小姐姐 ♀</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="年龄" prop="age"></el-table-column>
      <el-table-column label="操作" min-width="140" fixed="right">
        <template slot-scope="slotScope">
          <el-button class="eagle-scheme__table-btn" type="text" icon="el-icon-view" title="详情" @click="handleView(slotScope)"></el-button>
          <el-button class="eagle-scheme__table-btn" type="text" icon="el-icon-edit" title="编辑" @click="handleEdit(slotScope)"></el-button>
          <el-button type="text" icon="el-icon-delete" title="删除" @click="handleDelete(slotScope)"></el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      class="page-curd__pagination"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="currentPage"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      background
    >
    </el-pagination>
    <el-dialog :visible.sync="dialogVisible" :title="title" width="450px" :close-on-click-modal="dialogMode === 'view'" append-to-body>
      <template v-if="dialogMode === 'view'">
        <el-row :gutter="20">
          <el-col :span="12">姓名:{{ formModel.name }}</el-col>
          <el-col :span="12">
            性别:
            <template v-if="formModel.gender">
              <el-tag v-if="formModel.gender === 'male'" size="small">小哥哥 ♂</el-tag>
              <el-tag v-else type="danger" size="small">小姐姐 ♀</el-tag>
            </template>
          </el-col>
          <el-col :span="12" style="margin-top: 20px;">年龄:{{ formModel.age }}</el-col>
        </el-row>
      </template>
      <template v-else>
        <curd-form ref="curdForm" v-model="formModel" @submit="handleSubmit" @cancel="handleCancel"></curd-form>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import CurdSearch from './search';
import CurdForm from './form';

export default {
  components: { CurdSearch, CurdForm },
  data: () => ({
    dialogMode: 'new',
    dialogVisible: false,
    tableData: [
      { name: '古力娜扎', age: 27, gender: 'famale' },
      { name: '迪丽热巴', age: 27, gender: 'famale' },
      { name: '尼格买提', age: 36, gender: 'male' },
    ],
    currentPage: 1,
    pageSize: 10,
    total: 3,
    tableSelection: [],
    formModel: {}
  }),
  computed: {
    title() {
      if (this.dialogMode === 'view') {
        return '查看';
      }
      return this.dialogMode === 'edit' ? '编辑' : '新增';
    }
  },
  methods: {
    async handleSearch(value, needReset) {
      if (needReset) {
        this.currentPage = 1;
      }
      const param = {
        ...value,
        currentPage: this.currentPage,
        pageSize: this.pageSize,
      };
      console.log(param);
    },
    handleNew() {
      if (this.$refs['curdForm']) { this.$refs['curdForm'].reset(); }
      this.dialogMode = 'new';
      this.dialogVisible = true;
      this.formModel = {};
    },
    handleView({ row }) {
      this.dialogMode = 'view';
      this.dialogVisible = true;
      this.formModel = { ...row };
    },
    handleEdit({ row }) {
      if (this.$refs['curdForm']) { this.$refs['curdForm'].reset(); }
      this.dialogMode = 'edit';
      this.dialogVisible = true;
      this.formModel = { ...row };
    },
    handleDelete({ row }) {
      console.log('delete:', row);
    },
    handleDeleteMul() {
      console.log('mul delete:', this.tableSelection);
    },
    handleSubmit() {
      if (this.dialogMode === 'new') {
        console.log('new', this.formModel);
      } else if (this.dialogMode === 'edit') {
        console.log('edit', this.formModel);
      }
    },
    handleCancel() {
      this.dialogVisible = false;
    },
    handleTableSelectionChange(selection) {
      this.tableSelection = selection;
    },
    handleSizeChange(val) {
      this.currentPage = 1;
      this.pageSize = val;
      this.handleSearch();
    },
    handleCurrentChange(val) {
      this.currentPage = val;
      this.handleSearch();
    },
  }
}
</script>

<style>
.page-curd__action-bar {
  margin: 20px 0px;
}
.page-curd__table {
  margin-bottom: 20px;
}
.page-curd .el-icon-user-solid {
  padding-right: 10px;
}
.page-curd__pagination {
  text-align: right;
}
</style>

假如你有强迫症的话,那就顺便把首页略微修改一下。

src/views/page/index.vue:

<template>
  <div class="page-index">
    Vue入门项目CURD示例
  </div>
</template>

<style>
.page-index {
  text-align: center;
  padding-top: calc(50vh - 60px - 24px);
  font-size: 24px;
}
</style>

此时的项目效果如下:

首页
在这里插入图片描述
CURD页面
在这里插入图片描述
新增
在这里插入图片描述
编辑
在这里插入图片描述
查看详情
在这里插入图片描述

8.3 模拟接口数据

本示例项目为纯前端项目,所以使用mockjs来模拟增删改查的基本API。

在项目中新建一个mock目录用于存放模拟mockjs配置文件。包括index.jsutil.jsmodule/user.js

src/mock/index.js:

import Mock from 'mockjs';
import { parseUrl } from './util';
import userAPI from './module/user';

// 临时缓存mockjs数据
let MockCache = {};

const MockBot = {
  // 通用模板API
  fastAPI: {
    // 获取数据列表
    page: (base) => config => {
      const list = MockCache[base] || [];
      const param = parseUrl(config.url) || {};
      const { page = 1, size = 10, ...query } = param;
      // 计算有几个搜索条件
      let queryCount = false;
      for (let key in query) {
        if (query[key]) {
          queryCount += 1;
          break;
        }
      }
      // 根据搜索条件过滤结果
      const filteredList = queryCount > 0 ? list.filter(data => {
        let result = false;
        for (let key in query) {
          if (data[key] === query[key]) {
            result = true;
            break;
          }
        }
        return result;
      }) : list;
      // 根据结果处理分页数据
      const _page = Number(page);
      const _limit = Number(size);
      const pageList = filteredList.filter((item, index) => index < _limit * _page && index >= _limit * (_page - 1));
      const response = {
        page: _page,
        size: _limit,
        result: {
          list: pageList,
          total: filteredList.length,
        },
        success: true,
      };
      return response;
    },
    // 查询数据详情
    get: (base) => config => {
      const list = MockCache[base] || [];
      const param = parseUrl(config.url) || {};
      const id = param.id;
      const result = list.find((item) => item.id == id);
      return {
        result: result,
        success: true,
      };
    },
    // 新增数据
    add: (base) => config => {
      const param = JSON.parse(config.body);
      MockCache[base].unshift(param);
      return {
        success: true,
      };
    },
    // 编辑数据
    update: (base) => config => {
      const param = JSON.parse(config.body);
      const index = MockCache[base].findIndex(item => item.id === param.id);
      MockCache[base][index] = param;
      return {
        success: true,
      };
    },
    // 删除数据
    delete: (base) => config => {
      const ids = JSON.parse(config.body);
      ids.forEach(id => {
        MockCache[base] = MockCache[base].filter(item => item.id !== id);
      });
      return {
        success: true,
      };
    }
  },
  // 根据通用模板API快速创建模拟接口
  fastMock: (url, list) => {
    MockCache[url] = list;
    Mock.mock(new RegExp(`\/${url}\/page`), 'get', MockBot.fastAPI.page(url));
    Mock.mock(new RegExp(`\/${url}\/get`), 'get', MockBot.fastAPI.get(url));
    Mock.mock(new RegExp(`\/${url}\/add`), 'post', MockBot.fastAPI.add(url));
    Mock.mock(new RegExp(`\/${url}\/update`), 'post', MockBot.fastAPI.update(url));
    Mock.mock(new RegExp(`\/${url}\/delete`), 'post', MockBot.fastAPI.delete(url));
  },
}

// 产品管理
MockBot.fastMock('user', userAPI.userList);

src/mock/util.js:

import Mock from 'mockjs';

export const parseUrl = (url) => {
  let obj = {};// 创建一个Object
  let reg = /[?&][^?&]+=[^?&]+/g;// 正则匹配 ?&开始 =拼接  非?&结束  的参数
  let arr = url.match(reg);// match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
  // arr数组形式 ['?id=12345','&a=b']
  if (arr) {
    arr.forEach((item) => {
      /**
       * tempArr数组    ['id','12345']和['a','b']
      * 第一个是key,第二个是value
      * */
      let tempArr = item.substring(1).split('=');
      let key = decodeURIComponent(tempArr[0]);
      let val = decodeURIComponent(tempArr[1]);
      obj[key] = val;
    });
  }
  return obj;
}

export default {
  // 根据配置文件和数量快速生成数据列表
  fastList: (config, count) => {
    const list = []
    for (let i = 0; i < count; i++) {
      list.push(Mock.mock(config));
    }
    return list;
  },
}

src/mock/module/user.js:

import MockUtil from '../util';

export default {
  // 用户列表
  userList: MockUtil.fastList({
    id: '@guid()',
    name: '@cname()',
    'gender|1': ['male', 'famale'],
    'age|16-40': 1,
  }, 43),
}

8.4 对接接口数据

模拟好接口,接下来就需要进行接口对接了,首先我们修改main.js,将axios创建一个实例并设置为Vue的prototype,这样可以在每个Vue页面或组件中直接通过this进行访问。

创建axios实例后可以设置request和response拦截器,具体用法请查看axios官方文档

src/main.js:

import Vue from 'vue';
import axios from 'axios';
import App from '@/App.vue';
import router from '@/router';
import store from '@/store';
import ElementUI from 'element-ui'; // ElementUI组件库
import 'element-ui/packages/theme-chalk/lib/index.css'; // ElementUI组件库样式
import '@/mock';

const request = axios.create({
  baseURL: 'http://localhost',
  timeout: 1000 * 60,
  withCredentials: true,
});

// respone 拦截器
request.interceptors.response.use(
  response => {
    const { data = {} } = response;
    const { success } = data;
    if (success) {
      return data;
    } else {
      return { success };
    }
  },
  error => {
    return { success: false };
  });

Vue.prototype.$request = request;

// 注册饿了么UI
Vue.use(ElementUI);

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

配置好axios之后,我们就可以在curd页面中进行对接了。

src/views/page/curd/index.vue:

<template>
  <div class="page-curd">
    <curd-search @search="handleSearch"></curd-search>
    <div class="page-curd__action-bar">
      <el-button type="primary" icon="el-icon-plus" size="small" @click="handleNew">新增</el-button>
      <el-button icon="el-icon-delete" size="small" :disabled="tableSelection && tableSelection.length <= 0" @click="handleDeleteMul">批量删除</el-button>
    </div>
    <el-table size="mini" :data="tableData" class="page-curd__table" border @selection-change="handleTableSelectionChange">
      <el-table-column type="selection" min-width="35"></el-table-column>
      <el-table-column label="姓名" prop="name" min-width="100">
        <template slot-scope="{ row }">
          <span><i class="el-icon-user-solid"></i>{{ row.name }}</span>
        </template>
      </el-table-column>
      <el-table-column label="性别" prop="gender">
        <template slot-scope="{ row }">
          <el-tag v-if="row.gender === 'male'" size="small">小哥哥 ♂</el-tag>
          <el-tag v-else type="danger" size="small">小姐姐 ♀</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="年龄" prop="age"></el-table-column>
      <el-table-column label="操作" min-width="140" fixed="right">
        <template slot-scope="slotScope">
          <el-button class="eagle-scheme__table-btn" type="text" icon="el-icon-view" title="详情" @click="handleView(slotScope)"></el-button>
          <el-button class="eagle-scheme__table-btn" type="text" icon="el-icon-edit" title="编辑" @click="handleEdit(slotScope)"></el-button>
          <el-button type="text" icon="el-icon-delete" title="删除" @click="handleDelete(slotScope)"></el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      class="page-curd__pagination"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="currentPage"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      background
    >
    </el-pagination>
    <el-dialog :visible.sync="dialogVisible" :title="title" width="450px" :close-on-click-modal="dialogMode === 'view'" append-to-body>
      <template v-if="dialogMode === 'view'">
        <el-row :gutter="20">
          <el-col :span="12">姓名:{{ formModel.name }}</el-col>
          <el-col :span="12">
            性别:
            <template v-if="formModel.gender">
              <el-tag v-if="formModel.gender === 'male'" size="small">小哥哥 ♂</el-tag>
              <el-tag v-else type="danger" size="small">小姐姐 ♀</el-tag>
            </template>
          </el-col>
          <el-col :span="12" style="margin-top: 20px;">年龄:{{ formModel.age }}</el-col>
        </el-row>
      </template>
      <template v-else>
        <curd-form ref="curdForm" v-model="formModel" @submit="handleSubmit" @cancel="handleCancel"></curd-form>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import CurdSearch from './search';
import CurdForm from './form';

export default {
  components: { CurdSearch, CurdForm },
  data: () => ({
    dialogMode: 'new',
    dialogVisible: false,
    tableData: [],
    currentPage: 1,
    pageSize: 10,
    total: 3,
    tableSelection: [],
    formModel: {},
    currentRow: {},
  }),
  computed: {
    title() {
      if (this.dialogMode === 'view') {
        return '查看';
      }
      return this.dialogMode === 'edit' ? '编辑' : '新增';
    }
  },
  created() {
    this.handleSearch();
  },
  methods: {
    async handleSearch(value, needReset) {
      if (needReset) {
        this.currentPage = 1;
      }
      const params = {
        ...value,
        page: this.currentPage,
        size: this.pageSize,
      };
      this.$request.get('/user/page', { params }).then(response => {
        const { result: { list, total } = {} } = response;
        this.tableData = list;
        this.total = total;
      });
    },
    handleNew() {
      if (this.$refs['curdForm']) { this.$refs['curdForm'].reset(); }
      this.dialogMode = 'new';
      this.dialogVisible = true;
      this.formModel = {};
    },
    handleView({ row }) {
      this.dialogMode = 'view';
      this.dialogVisible = true;
      this.formModel = { ...row };
    },
    handleEdit({ row }) {
      if (this.$refs['curdForm']) { this.$refs['curdForm'].reset(); }
      this.dialogMode = 'edit';
      this.$request.get('/user/get', { params: { id: row.id } }).then(response => {
        const { result } = response;
        this.formModel = result;
        this.currentRow = result;
        this.dialogVisible = true;
      });
    },
    handleDelete({ row }) {
      this.$request.post('/user/delete', [row.id]).then(response => {
        const { success } = response;
        if (success) {
          this.$message.success('删除成功');
          this.handleSearch();
        }
      });
    },
    handleDeleteMul() {
      const ids = this.tableSelection.map(selection => selection.id);
      this.$request.post('/user/delete', ids).then(response => {
        const { success } = response;
        if (success) {
          this.$message.success('删除成功');
          this.handleSearch();
        }
      });
    },
    handleSubmit() {
      if (this.dialogMode === 'new') {
        this.$request.post('/user/add', { ...this.formModel, id: undefined }).then(response => {
          const { success } = response;
          if (success) {
            this.$message.success('新增成功');
            this.handleSearch();
            this.dialogVisible = false;
          }
        });
      } else if (this.dialogMode === 'edit') {
        this.$request.post('/user/update', { ...this.formModel, id: this.currentRow.id }).then(response => {
          const { success } = response;
          if (success) {
            this.$message.success('编辑成功');
            this.handleSearch();
            this.dialogVisible = false;
          }
        });
      }
    },
    handleCancel() {
      this.dialogVisible = false;
    },
    handleTableSelectionChange(selection) {
      this.tableSelection = selection;
    },
    handleSizeChange(val) {
      this.currentPage = 1;
      this.pageSize = val;
      this.handleSearch();
    },
    handleCurrentChange(val) {
      this.currentPage = val;
      this.handleSearch();
    },
  }
}
</script>

<style>
.page-curd__action-bar {
  margin: 20px 0px;
}
.page-curd__table {
  margin-bottom: 20px;
}
.page-curd .el-icon-user-solid {
  padding-right: 10px;
}
.page-curd__pagination {
  text-align: right;
}
</style>

至此,一个基本的CURD示例就完成了。

在这里插入图片描述

九、总结

在本实例项目中,并没有使用多少依赖项,只有基本的UI组件库element、基于promise的HTTP库axios、前端模拟数据的mockjs,至于本文开始时,创建项目选择的vuex并没有提到,这个建议小伙伴们自己查阅相关资料去学习。

在我的学习理念中,每当学习到一个新的技术时,可以先查阅学习技术的简单教程,然后上手实践,从模仿到举一反三再到创新,千万不要只学习理论而不主动去实践,这样效果并不深刻。

本文中有一些ES6以上的语法并没有过多提及,有许多技巧也并没有过多的给大家解释,更多的技术细节我会在以后的文章中提到,如果有BUG或者不对的地方,欢迎各位小伙伴留言指正!

谢谢。

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值