本教学文章分三部分:
- 第一篇:项目搭建、布局
- 第二篇:用户登录、身份认证、用户权限
- 第三篇:权限管理、商品管理、广告管理
$ vue create project-name
? Please pick a preset: // 选择预设
> Manually select features // 手动选择功能特性
?Check the features needed for your project:
◯ Choose // Vue version 默认vue2 选择后可选vue3
◉ Babel // 使用babel es6、ts转换
◉ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router // vue-router 管理路由
◉ Vuex // vuex 管理共享数据容器
◉ CSS Pre-processors // css预处理器
◉ Linter / Formatter // 代码格式校验
◯ Unit Testing // 测试相关
◯ E2E Testing // 测试相关
? Use class-style component syntax? (Y/n) y // 若使用ts,是否使用class风格的组件语法
? Use Babel alongside TypeScript(Y/n) y // 是否让babel和ts结合起来编译,使ts只负责转换类型相关的功能特性
? Use history mode for router?(Y/n) n // 是否使用history路由模式 history路由模式兼容不太好所以不选择,默认hash模式
? Pick a CSS pre-processor: // 选择对应的css预处理器
> Sass/SCSS (with dart-sass) // 此次选择
Sass/SCSS (with node-sass) //老版scss
Less
Stylus
? Pick a linter / formatter config: // 选择代码的格式校验
ESLint with error prevention only
ESLint + Airbnb config
> ESLint + Standard config // 此次选择
ESLint + Prettier
TSLint (deprecated)
? Pick additional lint features: // 代码格式校验触发时机
◉ Lint on save // 保存文件时
◉ Lint and fix on commit // 执行git commit提交时
? Where do you prefer placing config for Babel, ESLint, etc.?
// Babel, ESLint等工具生成的配置信息如何存放
> In dedicated config files // 存放到单独的配置文件中 此次选择
In package.json // 全都写在package.json
?Save this as a preset for future projects? (y/N) n
// 是否把刚才的配置选项保存起来
dependencies 依赖:
vue-class-component // 提供使用 Class 语法写 Vue 组件
vue-property-decorator // 在 Class 语法基础之上提供了一些辅助装饰器
devDependencies 依赖:
@typescript-eslint/eslint-plugin // 使用 ESLint 校验 TypeScript 代码
@typescript-eslint/parser // 将 TypeScript 转为 AST 供 ESLint 校验使用
@vue/cli-plugin-typescript // 使用 TypeScript + ts-loader + fork-ts-checker-webpack-plugin进行更快的类型检查.
@vue/eslint-config-typescript // 兼容 ESLint 的 TypeScript 校验规则
typescript // TypeScript编译器,提供类型校验和转换 JavaScript 功能
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
shims-tsx.d.ts
// 为 jsx 组件模板补充类型声明
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}
shims-vue.d.ts
// 主要用于 Typescrip 识别 .vue 文件模块
// Typescript 默认不支持导入.vue 模块,这个文件告诉 Typescript 导入.vue 文件模块都按 Vueconstructor类型识别处理
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
.eslintrc.js配置文件
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
// 自定义校验规则
rules: {
// 查阅eslint文档进行自己项目代码规则配置
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
}
}
安装 element-ui
$ npm i element-ui -S
// 完整引入
// mian.js文件中加入
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
src/styles
├── index.scss # 全局样式(在入口模块被加载生效)
├── mixin.scss # 公共mixin混入(可以把重复的样式封装为mixin混入到复用的地方)
├── reset.scss # 重置基础样式
└── variables.scss # 公共样式变量
index.scss
@import './variables.scss';
html {
font-family: $font-family;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
// better Font Rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
background-color: $body-bg;
}
/* custom element theme */
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
/* change font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
/* import element default theme */
@import '~element-ui/packages/theme-chalk/src/index';
.el-pagination {
color: #868e96;
}
.status {
display: inline-block;
cursor: pointer;
width: .875rem;
height: .875rem;
vertical-align: middle;
border-radius: 50%;
&-primary {
background: $--color-primary;
}
&-success {
background: $--color-success;
}
&-warning {
background: $--color-warning;
}
&-danger {
background: $--color-danger;
}
&-info {
background: $--color-info;
}
}
variables.scss
$primary-color: #40586F;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #868e96; // #22b8cf;
$body-bg: #E9EEF3; // #f5f5f9;
$sidebar-bg: #F8F9FB;
$navbar-bg: #F8F9FB;
$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
// 在vue.config.js中配置共享全局样式变量文件
scss: {
additionalData: `@import "~@/variables.scss"`
}
常用跨域方法:CORS(需服务端配合)、proxy代理(此次使用)
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: '', //转发地址
changeOrigin: true // 把请求头中的host配置为targte
}
}
}
}
页面基本内容,首页为例
<template>
<div class="home">
首页
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'HomeIndex',
data () {
return {
username: ''
}
},
methods: {
}
})
</script>
<style lang="scss" scoped>
</style>
src/router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
import store from '@/store'
Vue.use(VueRouter)
// 路由配置规则
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/register',
name: 'register',
component: () => import(/* webpackChunkName: 'register' */ '@/views/login/register.vue')
},
{
path: '', // 默认子路由
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
},
{
path: '/role',
name: 'role',
component: () => import(/* webpackChunkName: 'login' */ '@/views/role/index.vue')
},
{
path: '/menu',
name: 'menu',
component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
},
{
path: '/resource',
name: 'resource',
component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
},
{
path: '/product',
name: 'product',
component: () => import(/* webpackChunkName: 'course' */ '@/views/product/index.vue')
},
{
path: '/user',
name: 'user',
component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
},
{
path: '/advert',
name: 'advert',
component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
},
{
path: '/advert-space',
name: 'advert-space',
component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
},
{
path: '/menu/create',
name: 'menu-create',
component: () => import(/* webpackChunkName: 'menu-create' */ '@/views//menu/create.vue')
}
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
export default router
后台管理系统一般大多页面都有很多公共部分,例如头部、侧边栏…,因此我们需要创建一个layout模版组件,使用嵌套路由,只切换页面内容部分
src/layout/compontens/app-header.vue
<template>
<div class="header">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>活动管理</el-breadcrumb-item>
<el-breadcrumb-item>活动列表</el-breadcrumb-item>
<el-breadcrumb-item>活动详情</el-breadcrumb-item>
</el-breadcrumb>
<el-dropdown>
<span class="el-dropdown-link">
<el-avatar shape="square" :size="30" :src="@/assets/portrait.png"></el-avatar>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>xxxx</el-dropdown-item>
<el-dropdown-item divided>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'AppHeader',
})
</script>
<style lang="scss" scoped>
.header {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown-link {
display: flex;
align-items: center
}
}
</style>
src/layout/compontens/app-aside.vue
<template>
<div class="aside">
<div>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="rgb(48, 65, 86)"
text-color="#fff"
active-text-color="#ffd04b"
router
>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>权限管理</span>
</template>
<el-menu-item-group>
<el-menu-item index="/role">
<i class="el-icon-menu"></i>
<span slot="title">角色管理</span>
</el-menu-item>
<el-menu-item index="/menu">
<i class="el-icon-menu"></i>
<span slot="title">菜单管理</span>
</el-menu-item>
<el-menu-item index="/resource">
<i class="el-icon-menu"></i>
<span slot="title">资源管理</span>
</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-menu-item index="/product">
<i class="el-icon-menu"></i>
<span slot="title">商品管理</span>
</el-menu-item>
<el-menu-item index="/user">
<i class="el-icon-document"></i>
<span slot="title">用户管理</span>
</el-menu-item>
<el-submenu index="4">
<template slot="title">
<i class="el-icon-location"></i>
<span>广告管理</span>
</template>
<el-menu-item-group>
<el-menu-item index="/advert">
<i class="el-icon-menu"></i>
<span slot="title">广告列表</span>
</el-menu-item>
<el-menu-item index="/advert-space">
<i class="el-icon-menu"></i>
<span slot="title">广告位列表</span>
</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'AppAside',
methods: {
handleOpen (key: string, keyPath: string): void {
console.log(key, keyPath)
},
handleClose (key: string, keyPath: string): void {
console.log(key, keyPath)
}
}
})
</script>
<style lang="scss">
.aside {
width: 201px;
}
.el-submenu__title:hover{
background-color: #263445 !important;
}
.el-menu-item:hover {
background: #001528!important;
}
</style>
src/layout/index.vue
<template>
<el-container>
<div class="page-left">
<h1 class="logo">
boat管理系统
</h1>
<el-aside>
<app-aside/>
</el-aside>
</div>
<el-container>
<el-header>
<app-header />
</el-header>
<el-main>
<!-- 子路由出口 -->
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script lang="ts">
import Vue from 'vue'
import appAside from './components/app-aside.vue'
import appHeader from './components/app-header.vue'
export default Vue.extend({
name: 'layoutIndex',
components: {
appAside,
appHeader
}
})
</script>
<style lang="scss" scoped>
.el-container {
max-height: 100vh;
min-width: 980px;
}
.logo {
padding: 20px 0;
text-align: center;
margin: 0;
color: #fff;
background: rgb(48, 65, 86);
box-sizing: border-box;
}
.el-aside {
flex: 1;
background: rgb(48, 65, 86);
}
.el-header {
background: #fff;
}
.el-main {
background: #e9eef3;
}
.page-left {
width: 200px;
min-height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>
修改src/router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
import store from '@/store'
Vue.use(VueRouter)
// 路由配置规则
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/register',
name: 'register',
component: () => import(/* webpackChunkName: 'register' */ '@/views/login/register.vue')
},
{
path: '/',
component: Layout,
meta: {
requiresAuth: true // 自定义数据
},
children: [
{
path: '', // 默认子路由
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
},
{
path: '/role',
name: 'role',
component: () => import(/* webpackChunkName: 'login' */ '@/views/role/index.vue')
},
{
path: '/menu',
name: 'menu',
component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
},
{
path: '/resource',
name: 'resource',
component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
},
{
path: '/product',
name: 'product',
component: () => import(/* webpackChunkName: 'course' */ '@/views/product/index.vue')
},
{
path: '/user',
name: 'user',
component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
},
{
path: '/advert',
name: 'advert',
component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
},
{
path: '/advert-space',
name: 'advert-space',
component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
},
{
path: '/menu/create',
name: 'menu-create',
component: () => import(/* webpackChunkName: 'menu-create' */ '@/views//menu/create.vue')
}
]
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
export default router
登录页:src/views/login/index.vue
<template>
<div class="login">
<div class="login-main">
<div class="login-left">
<h1>boat管理系统</h1>
</div>
<el-form
class="login-form"
label-position="top"
ref="form"
:model="formData"
label-width="80px"
:rules="formRule"
>
<h2>登录</h2>
<el-form-item label="手机号" prop="userName">
<el-input v-model="formData.userName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pwd">
<el-input v-model="formData.pwd" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button
class="login-btn"
:loading="isLoginLoading"
type="primary"
@click="onSubmit"
>
登录
</el-button>
</el-form-item>
<div class="login-link">
<el-link class="login-link-item" @click="linkHandle('0')">立即注册</el-link>
<el-link class="login-link-item" @click="linkHandle('1')">忘记密码?</el-link>
</div>
</el-form>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { Form } from 'element-ui'
export default Vue.extend({
name: 'LoginIndex',
data () {
return {
isLoginLoading: false,
formData: {
userName: '',
pwd: ''
},
formRule: {
userName: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
pwd: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
]
}
}
},
methods: {
linkHandle (target: string): void {
if (target === '0') { // 注册
this.$router.push({
name: 'register'
})
} else { // 忘记密码
}
},
async onSubmit () {
try {
// 1. 表单验证
await (this.$refs.form as Form).validate()
// 登录按钮 loading
this.isLoginLoading = true
// 2. 验证通过 -> 提交表单
interface User {
userName: string,
pwd: string
}
const params:User = this.formData
const { data } = await login(params)
console.log(data)
if (data.status !== 200) {
this.$message.error(data.msg)
} else {
. // 登录成功
}
} catch (err) {
this.$message.error('登录失败')
console.log(err)
}
this.isLoginLoading = false
}
}
})
</script>
<style lang="scss" scoped>
.login {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 100px 500px;
.login-main {
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
overflow: hidden;
.login-left {
align-items: center;
padding: 170px 20px;
width: 380px;
height: 600px;
background: url(../../assets/login.png);
background-size: 100% 100%;
color: #fff;
box-sizing: border-box;
}
.login-form {
flex:1;
height: 600px;
background: #fff;
padding: 100px;
border-radius: 5px;
box-sizing: border-box;
}
.login-btn {
width: 100%;
}
.login-link {
display: flex;
justify-content: flex-end;
.login-link-item:nth-child(1) {
margin-right: 10px;
}
}
}
}
</style>
注册页:src/views/login/register.vue
<template>
<div class="register">
<h1>boat 管理系统</h1>
<el-form
class="register-form"
label-position="top"
ref="form"
:model="formData"
label-width="80px"
:rules="formRule"
>
<h2>注册</h2>
<el-form-item label="手机号" prop="userName">
<el-input v-model="formData.userName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pwd">
<el-input v-model="formData.pwd"></el-input>
</el-form-item>
<el-form-item>
<el-button
class="register-btn"
:loading="isLoginLoading"
type="primary"
@click="onSubmit"
>
注册
</el-button>
</el-form-item>
<div>
<el-link @click="goLogin">< 返回登陆</el-link>
</div>
</el-form>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { register } from '@/services/user'
import { Form } from 'element-ui'
export default Vue.extend({
name: 'Register',
data () {
return {
isLoginLoading: false,
formData: {
userName: '',
pwd: ''
},
formRule: {
userName: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
pwd: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
]
}
}
},
methods: {
goLogin () {
this.$router.go(-1)
},
async onSubmit () {
try {
// 1. 表单验证
await (this.$refs.form as Form).validate()
// 登录按钮 loading
this.isLoginLoading = true
// 2. 验证通过 -> 提交表单
const { data } = await register(this.formData)
if (data !== 200) {
this.$message.error(data.msg)
} else {
// 成功:跳转回原来页面或首页
this.$router.push(this.$route.query.redirect as string || '/')
this.$message.success('登录成功')
}
} catch (err) {
this.$message.error('登录失败')
console.log(err)
}
this.isLoginLoading = false
}
}
})
</script>
<style lang="scss" scoped>
.register {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.register-form {
width: 300px;
background: #fff;
padding: 20px;
border-radius: 5px;
}
.register-btn {
width: 100%;
}
}
</style>
这篇内容主要是详细讲述如何从0搭建、完善vue项目结构,引入ui,完成了公共样式,路由,以及登录页面的简单功能开发。下一篇的内容会有,登录时身份认证、如何根据不同的用户权限,显示不同的菜单、功能。(可以看我之前的文章,试着自己用nodejs和mysql写数据接口,最终所有代码最后一篇会公开让大家参考)
如有问题或建议欢迎留言或私信我一起讨论