vue create edu-boss-fed
git init
git add .
git commit -a -m "项目初始化"
git remote add orign 地址
git push origin -u master
默认生成的目录结构不满足我们的开发需求,所以需要做一些自定义改动
这里主要处理下面的内容:
修改App.vue
:
<template>
<div id="app">
<h1>Apph1>
<router-view />
div>
template>
<style lang="scss" scoped>
style>
修改router/index.ts
:
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
Vue.use(VueRouter)
const routes: Array<RouteConfig> = [
]
const router = new VueRouter({
routes
})
export default router
删除views/About.vue
删除views/Home.vue
删除components/HelloWorld.vue
新增utils目录
新增styles目录
新增services目录
在Vue项目中启用TypeScript支持
(1)全新项目:使用Vue CLI脚手架工具创建Vue项目
(2)已有项目:添加Vue官方配置的TypeScript适配插件
vue add @vue/typescript
(1)安装了TypeScript相关的依赖项
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编译器,提供类型校验和转换js功能 |
(2)shims-vue.d.ts文件的作用
// 主要用于 TypeScript 识别 .vue文件模块
// TypeScript 默认不支持导入 .vue 模块,这个文件告诉 TypeScript
// 导入 .vue 文件模块都按 VueConstructor 类型识别处理
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
(3)shims-tsx.d.ts文件的作用
为jsx组件模板补充类型声明
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
// 类型推断已启用
data(){
return{}
}
})
</script>
如果您在声明组件时更喜欢基于类的 API,则可以使用官方维护的 vue-class-component 装饰器:
教程文档
import Vue from 'vue'
import Component from 'vue-class-component'
// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
// 所有的组件选项都可以放在这里
template: ''
})
export default class MyComponent extends Vue {
// 初始数据可以直接声明为实例的 property
message: string = 'Hello!'
// 组件方法也可以直接声明为实例的方法
onClick (): void {
window.alert(this.message)
}
}
装饰器是ES草案中的一个新特性,不过这个草案最近有可能发生重大调整,所以不建议生成环境使用
装饰器是一种特殊类型的声明,它能够加到类、方法,属性、参数上添加额外的功能
通俗的讲装饰器就是一个方法;在不修改类的情况下拓展方法;已是Es7标准特性之一
function testable(target){
target.isTestable = true
}
@testable
class MytestableClass {
// ...
}
console.log(MytestableClass.isTestable) // true
这种方式继续放大了Class这种组件定义方法
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class Button extends Vue {
private count: number = 1
private text: string = 'Click me'
@Prop() readonly size?:string
get content () {
return `${this.text} ${this.count} `
}
increment () {
this.count++
}
mounted () {
console.log('button is mounted')
}
}
个人建议:No Class APIS, 只用 Options APIs。
Class 语法仅仅是一种写法而已,最终还是要转换为普通的组件数据结构
装饰器语法还没有正式定稿发布,建议了解即可,正式发布以后在选择使用也可以
使用Options APIs最好是使用export default Vue.extend({...})
而不是export default {...}
没有绝对的标准,下面是一些大厂根据多数开发这的编码习惯定制的一些编码规范,仅供参考
vue代码风格指南
module.exports = {
root: true,
env: {
node: true
},
// 使用插件的编码规则
extends: [
'plugin:vue/essential', // eslint-plugin-vue
'@vue/standard', // @vue/eslint-config-standard
'@vue/typescript/recommended' // @vue/eslint-config-typescript
],
parserOptions: {
ecmaVersion: 2020
},
// 自定义编码校验规则
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
ESLint 附带有大量的规则。你可以使用注释或配置文件修改你项目中要使用的规则。要改变一个规则设置,你必须将规则 ID 设置为下列值之一:
"off"
或 0
- 关闭规则
"warn"
或 1
- 开启规则,使用警告级别的错误:warn (不会导致程序退出)
"error"
或 2
- 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)
'semi': ['error','always'] // 第一项是错误的级别,第二项是参数
注意:修改完后需要删除node_modules里的.cache缓存文件
ESLint Ts校验规则
例:ts接口中不需要加分号
'@typescript-eslint/member-delimiter-style':['error',
{
"multiline": { // 多行
"delimiter": "none", // semi || comma || none
"requireLast": true
}
}
]
npm i element-ui -S
在main.ts中添加
import ElementUI from 'element-ui'
Vue.use(ElementUI)
// 公共样式变量
$primary-color: #40586f;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #e9eef3; // #22b8cf;
$body-bg: #e9eef3; // #f5f5f9;
$sidebar-bg: #f8f9fb;
$navbar-bg: #f8f9fb;
$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
index.scss
@import './variables.scss';
// 设置html样式
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样式
body {
margin: 0;
background-color: $body-bg;
}
// 更改 element 主题色(写法参考element官方文档)
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
// 改变 icon 字体路径变量,必需
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// 引入 element 默认主题文件(样式文件)
@import '~element-ui/packages/theme-chalk/src/index';
// node_modules/element-ui/packages/theme-chalk/src/common/var.scss
// overrides
// .el-menu-item, .el-submenu__title {
// height: 50px;
// line-height: 50px;
// }
.el-pagination {
color: #868e96;
}
// components
.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;
}
}
在main.ts中引入
import './styles/index.scss'
module.exports = {
css: {
// 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
// 因为 `scss` 语法在内部也是由 sass-loader 处理的
// 但是在配置 `prependData` 选项的时候
// `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
// 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
loaderOptions: {
scss: {
prependData: '@import "~@/styles/variables.scss";'
}
}
}
}
示例:
module.exports = {
devServer: {
proxy: {
'/api': {
target: '' ,
ws: true,
changeOrigin: true
}
}
}
}
npm i axios
在utils
下新建request.ts
import axios from 'axios'
const request = axios.create({
// 配置选项
// baseURL,
// timeout
})
// 请求拦截器
// 响应拦截器
export default request
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/index.vue')
}
创建layout目录index.vue
{
path: '/',
component: Layout,
children: [
{
path: '/',
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
}
]
}
layout/index.vue
<template>
<el-container>
<el-aside width="200px">
<app-aside />
el-aside>
<el-container>
<el-header>
<app-header />
el-header>
<el-main>
<router-view>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 {
min-height: 100vh;
min-width: 980px; // 避免挤压导致不好看
}
.el-aside {
background-color: #d3dce6;
}
.el-header {
background-color: #ffffff;
}
.el-main {
background-color: #e9eef3;
}
style>
layout/components/app-aside.vue
<template>
<div class="aside">
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#545c64"
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 index="/role">
<i class="el-icon-setting">i>
<span slot="title">角色管理span>
el-menu-item>
<el-menu-item index="/menu">
<i class="el-icon-setting">i>
<span slot="title">菜单管理span>
el-menu-item>
<el-menu-item index="/resource">
<i class="el-icon-setting">i>
<span slot="title">资源管理span>
el-menu-item>
el-submenu>
<el-menu-item index="/course">
<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-setting">i>
<span>广告管理span>
template>
<el-menu-item index="/advert">
<i class="el-icon-setting">i>
<span slot="title">广告列表span>
el-menu-item>
<el-menu-item index="/advert-space">
<i class="el-icon-setting">i>
<span slot="title">广告位列表span>
el-menu-item>
el-submenu>
el-menu>
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" scoped>
.aside {
.el-menu {
min-height: 100vh;
}
}
style>
layout/components/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="https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png"
>el-avatar>
<i class="el-icon-arrow-down el-icon--right">i>
span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>用户IDel-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>
views/login.vue
<template>
<div class="login">
<el-form
class="login-form"
label-position="top"
ref="form"
:model="form"
label-width="80px"
:rules="rules"
>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone">el-input>
el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password">el-input>
el-form-item>
<el-button
class="login-btn"
type="primary"
@click="onSubmit"
:loading="isLoginLoading"
>登录el-button
>
el-form>
div>
template>
<script lang="ts">
import Vue from 'vue'
import request from '@/utils/request'
import qs from 'qs'
import { Form } from 'element-ui'
export default Vue.extend({
name: 'LoginIndex',
data () {
return {
form: {
phone: '',
password: ''
},
rules: {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1\d{10}$/,
message: '请输入正确的手机号',
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 18, message: '长度在6到18个字符', trigger: 'blur' }
]
},
isLoginLoading: false
}
},
methods: {
async onSubmit () {
try {
// 1. 表单验证
await (this.$refs.form as Form).validate() // 若不传入回调函数,则返回一个promise
// 2. 验证通过 -> 提交表单
this.isLoginLoading = true
const { data } = await request({
method: 'POST',
url: '/front/user/login',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(this.form) // axios 默认发送的是application/json格式的数据
})
console.log(data)
// 3. 处理请求结果
// 失败:给出提示
if (data.state !== 1) {
this.$message.error(data.message)
}else {
// 成功:跳转到首页
this.$router.push({
name: 'home'
})
this.$message.success(data.message)
}
} catch (error) {
console.log('登录失败', error)
}
// 结束登录按钮的 loading
this.isLoginLoading = false
}
}
})
script>
<style lang="scss" scoped>
.login {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.login-form {
width: 300px;
background-color: #ffffff;
padding: 20px;
border-radius: 5px;
}
.login-btn {
width: 100%;
}
}
style>
新建services/user.ts
/**
* 用户相关请求模块
*/
import request from '@/utils/request'
import qs from 'qs'
interface User {
phone: string
password: string
}
export const login = (data: User) => {
return request({
method: 'POST',
url: '/front/user/login',
// headers: { 'content-type': 'application/x-www-form-urlencoded' },
// 如果 data 是普通对象,则Content-Type 是 application/json
// 如果 data 是 qs.stringify(data) 转换之后的数据: key=value&key=value
// 则Content-Type会被设置为 application/x-www-form-urlencoded
// 如果 data 是 FormData 对象,则Content-Type 是 multipart/form-data
data: qs.stringify(data) // axios 默认发送的是application/json格式的数据
})
}
router/index.ts
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/',
component: Layout,
meta: {
requiresAuth: true // 自定义数据
}, // meta 默认就是一空对象
children: [
{
path: '/',
name: 'home',
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
// meta: {
// requiresAuth: true // 自定义数据
// } // meta 默认就是一空对象
}
]
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/index.vue')
}
]
const router = new VueRouter({
routes
})
// 全局前置守卫:任何页面的访问都要经过这里
// to:要去哪里的路由信息
// from:从哪里来的路由信息
// next:通行的标志
router.beforeEach((to, from, next) => {
// console.log('进入了全局路由守卫')
console.log('to=>', to)
console.log('from=>', from)
// to.matched 是一个数组(匹配到是路由父级即以下的)
// 如果当前孙辈||父辈有requiresAuth则匹配有没有登录
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!store.state.user) {
// 跳转到登录页面
next({ name: 'login' })
} else {
next()
}
} else {
next()
}
// 路由守卫中一定要调用next, 否则页码无法展示
// if (to.path !== 'login') {} // 校验登录状态
})
export default router
router/index.ts
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!store.state.user) {
// 跳转到登录页面
next({
name: 'login',
query: { // 通过 url 传递查询字符串参数
redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页
}
})
} else {
next()
}
} else {
next()
}
}
views/login/index.vue
// 成功:跳转到之前访问的页面或首页
this.$router.push(this.$route.query.redirect as string || '/')
import axios from 'axios'
import store from '@/store'
const request = axios.create({
// 配置选项
// baseURL,
// timeout
})
// 请求拦截器
request.interceptors.request.use((config: any) => {
// 我们就在这里通过改写 config 配置信息来实现业务功能的统一处理
const { user } = store.state
if (user?.access_token) {
config.headers.Authorization = user.access_token
}
// 注意:这里一定要返回 config,否则请求发不出去了
return config
}, (error) => Promise.reject(error)) // 请求本身就出错了走这里
// 响应拦截器
export default request
穿透组件,将该函数注册到根元素
<el-dropdown-item divided @click.native="handleLogout">退出el-dropdown-item>
// 清除登录状态
this.$store.commit('setUser', null)
// 跳转到登录页面
this.$router.push({
name: 'login'
})