vue 使用element-ui基础布局
首先创建项目,在此之前,我们需要全局安装npm
接着执行如下指令:
npm install -g vue-cli
vue init webpack vueDemo
cd vueDemo
npm install
npm run dev
webpack为官方推荐的模板,还可以执行如下代码创建vue项目(vue3.0),但是这种方法初始化后没有this.$emit()等方法,具体原因不清楚。
vue create vueDemo
访问http://localhost:8080就可以看到我们新创建的vue项目了
然后安装element-ui
npm i element-ui -S
接着在main.js文件中集成element-ui
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import App from './App' Vue.use(ElementUI) Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', components: { App }, template: '<App/>' })
接下来我们在App.vue中增加一个顶级的router-view
<template> <div id="app"> <router-view></router-view> </div> </template> <script> import router from '@/js/router' export default { name: 'App', router, components: { }, created () { }, data () { return { } }, methods: { } } </script> <style> </style>
写router.js
import Vue from 'vue' import VueRouter from 'vue-router' import Layout from '@/layout/Layout' Vue.use(VueRouter) const routes = [ { path: '/', component: Layout, redirect: '/student', children: [ { path: 'student', name: 'student', component: () => import('@/components/student/student') } ] } ] // 路由配置 const RouterConfig = { mode: 'history', // require service support scrollBehavior: () => ({ y: 0 }), routes } // export const router = new Router(RouterConfig) const createRouter = () => new VueRouter(RouterConfig) // 创建路由实例 const router = createRouter() // 添加动态路由 // addAsyncRouter() export default router
我们可以看到定义的路由组件有子组件,这样我们就可以直接在App.vue中定义一个router-view,然后其它什么也不用写,将路由全部路由到Layout组件中,这样就可以所有页面统一样式了,每一个菜单的特定路由定义在children路由中
当然也可以在App.vue中引入Layout组件,如果这样,所有的菜单路由就不需要统一路由到Layout组件了。
然后我们写一个布局文件Layout.vue
<template> <div> <el-row> <el-col :span="3"> <side-bar></side-bar></el-col> <el-col :span="21"> <router-view></router-view></el-col> </el-row> </div> </template> <script> import sideBar from '../components/SideBar/sideBar' export default { name: '', components: { sideBar } } </script> <style scoped> </style>
其中sideBar是我们自定义的左侧导航
by the way 封装以下http请求
http.js
import axios from 'axios' // 引入axios // 响应拦截器 // 响应拦截器 axios.interceptors.response.use( response => { // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据 // 否则的话抛出错误 if (response.status === 200) { return Promise.resolve(response) } else { return Promise.reject(response) } }, // 服务器状态码不是2开头的的情况 // 这里可以跟你们的后台开发人员协商好统一的错误状态码 // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等 // 下面列举几个常见的操作,其他需求可自行扩展 error => { if (error.response.status) { return Promise.reject(error.response) } }) /** * get方法,对应get请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function get (url, params) { return new Promise((resolve, reject) => { axios.get(url, { params: params }).then(res => { resolve(res.data) }).catch(err => { reject(err.data) }) }) } /** * post方法,对应post请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function post (url, params) { return new Promise((resolve, reject) => { axios.post(url, params) .then(res => { resolve(res.data) }) .catch(err => { reject(err.data) }) }) }
api.js
/** * api接口统一管理 */ import { get, post } from './http' const preUrl = 'http://localhost:8081/' export const apiAddress = p => get(preUrl + '/stuUser/list', p) export const apiAddressa = p => post(preUrl + '/stuUser/list', p)
然后写一个router.js中定义的student组件
<template> <div> <el-table v-loading="listLoading" :data="tableData" style="width: 100%"> <el-table-column label="照片" width="180"> <template slot-scope="scope"> <i class="el-icon-time"></i> <span style="margin-left: 10px"> <el-image style="width: 100px; height: 100px" :src="scope.row.picture" fit="fit"></el-image></span> </template> </el-table-column> <el-table-column label="学生姓名" width="180"> <template slot-scope="scope"> <el-popover trigger="hover" placement="top"> <p>学生姓名: {{ scope.row.studentName }}</p> <p>学生学号: {{ scope.row.studentNumber }}</p> <div slot="reference" class="name-wrapper"> <el-tag size="medium">{{ scope.row.studentName }}</el-tag> </div> </el-popover> </template> </el-table-column> <el-table-column label="手机号" width="180"> <template slot-scope="scope"> <span style="margin-left: 10px">{{ scope.row.phoneNumber }}</span> </template> </el-table-column> <el-table-column label="家庭地址" width="180"> <template slot-scope="scope"> <span style="margin-left: 10px">{{ scope.row.address }}</span> </template> </el-table-column> <el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" :page.sync="pageNum" :rows.sync="pageSize" @pagination="getList" /> <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible"> <el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="80px" style="width: 400px; margin-left:50px;"> <el-form-item label="照片" prop="studentName"> <uploader @setImage="setImage" :url.sync="temp.picture"/> <el-input v-model="temp.picture" /> </el-form-item> <el-form-item label="学生姓名" prop="studentName"> <el-input v-model="temp.studentName" /> </el-form-item> <el-form-item label="学生学号" prop="studentNumber"> <el-input v-model="temp.studentNumber" /> </el-form-item> <el-form-item label="手机号" prop="phoneNumber"> <el-input v-model="temp.phoneNumber" /> </el-form-item> <el-form-item label="家庭地址" prop="address"> <el-input v-model="temp.address" /> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogFormVisible = false"> Cancel </el-button> <el-button type="primary" @click="dialogStatus==='create'?createData():updateData()"> Confirm </el-button> </div> </el-dialog> </div> </template> <script> import { apiAddress } from '@/js/api' import Pagination from '@/components/Pagination' // secondary package based on el-pagination import uploader from '../uploader/uploader' export default { components: {Pagination, uploader}, data () { return { picture: '', tableData: [], listLoading: true, total: 0, pageNum: 1, pageSize: 20, temp: {studentNumber: '', studentName: '', phoneNumber: '', address: '', id: ''}, dialogFormVisible: false, dialogStatus: '', textMap: { update: '编辑', create: '新增' }, rules: { address: [{ required: true, message: 'address is required', trigger: 'change' }], phoneNumber: [{ required: true, message: 'phoneNumber is required', trigger: 'change' }], studentName: [{ required: true, message: 'studentName is required', trigger: 'blur' }], picture: [{ required: true, message: 'picture is required', trigger: 'blur' }], studentNumber: [{ required: true, message: 'title is required', trigger: 'blur' }] } } }, mounted () { this.getList() }, methods: { setImage (val) { console.log(val) }, getList () { this.listLoading = true apiAddress({ page: this.pageNum, rows: this.pageSize }).then(res => { if (res.list.length > 0) { res.list.forEach(item => { item.picture = 'http://localhost:8081/' + item.picture }) this.tableData = res.list this.total = res.total this.pageNum = res.pageNum this.pageSize = res.pageSize } this.listLoading = false }) }, handleEdit (index, row) { this.temp = Object.assign({}, row) // copy obj this.dialogStatus = 'update' this.dialogFormVisible = true /* this.$nextTick(() => { this.$refs['dataForm'].clearValidate() }) */ }, handleDelete (index, row) { this.$notify({ title: 'Success', message: 'Delete Successfully', type: 'success', duration: 2000 }) this.list.splice(index, 1) }, updateData () { this.$refs['dataForm'].validate((valid) => { if (valid) { const tempData = Object.assign({}, this.temp) tempData.timestamp = +new Date(tempData.timestamp) // change Thu Nov 30 2017 16:41:05 GMT+0800 (CST) to 1512031311464 } }) } } } </script>
其中涉及Pagenation分页组件和uploader图片上传组件
Pagenation index.vue
<template> <div :class="{'hidden':hidden}" class="pagination-container"> <el-pagination :background="background" :current-page.sync="currentPage" :page-size.sync="pageSize" :layout="layout" :page-sizes="pageSizes" :total="total" v-bind="$attrs" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </template> <script> import { scrollTo } from './scroll-to' export default { name: 'Pagination', props: { total: { required: true, type: Number }, page: { type: Number, default: 1 }, rows: { type: Number, default: 20 }, pageSizes: { type: Array, default () { return [10, 20, 30, 50] } }, layout: { type: String, default: 'total, sizes, prev, pager, next, jumper' }, background: { type: Boolean, default: true }, autoScroll: { type: Boolean, default: true }, hidden: { type: Boolean, default: false } }, computed: { currentPage: { get () { return this.page }, set (val) { this.$emit('update:page', val) } }, pageSize: { get () { return this.rows }, set (val) { this.$emit('update:rows', val) } } }, methods: { handleSizeChange (val) { this.$emit('pagination', { page: this.currentPage, rows: val }) if (this.autoScroll) { scrollTo(0, 800) } }, handleCurrentChange (val) { this.$emit('pagination', { page: val, rows: this.pageSize }) if (this.autoScroll) { scrollTo(0, 800) } } } } </script> <style scoped> .pagination-container { background: #fff; padding: 32px 16px; } .pagination-container.hidden { display: none; } </style>
scroll-to.js
Math.easeInOutQuad = function (t, b, c, d) { t /= d / 2 if (t < 1) { return c / 2 * t * t + b } t-- return -c / 2 * (t * (t - 2) - 1) + b } // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts var requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60) } })() /** * Because it's so fucking difficult to detect the scrolling element, just move them all * @param {number} amount */ function move (amount) { document.documentElement.scrollTop = amount document.body.parentNode.scrollTop = amount document.body.scrollTop = amount } function position () { return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop } /** * @param {number} to * @param {number} duration * @param {Function} callback */ export function scrollTo (to, duration, callback) { const start = position() const change = to - start const increment = 20 let currentTime = 0 duration = (typeof (duration) === 'undefined') ? 500 : duration var animateScroll = function () { // increment the time currentTime += increment // find the value with the quadratic in-out easing function var val = Math.easeInOutQuad(currentTime, start, change, duration) // move the document.body move(val) // do the animation unless its over if (currentTime < duration) { requestAnimFrame(animateScroll) } else { if (callback && typeof (callback) === 'function') { // the animation is done so lets callback callback() } } } animateScroll() }
uploader.vue
<template> <el-upload class="avatar-uploader" action="http://localhost:8081/uploadFileToFast" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"> <img v-if="url" :src="url" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </template> <style> .avatar-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; } .avatar-uploader .el-upload:hover { border-color: #409EFF; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; line-height: 178px; text-align: center; } .avatar { width: 178px; height: 178px; display: block; } </style> <script> export default { props: { url: { required: true, type: String } }, /* data () { return { imageUrl: '' } }, */ /* computed: { imageUrl: { get () { return this.url }, set (val) { this.$emit('update:url', val) } } }, */ methods: { handleAvatarSuccess (res, file) { this.$emit('update:url', 'http://localhost:8081/' + res) this.$emit('setImage', { url: res }) }, beforeAvatarUpload (file) { const isJPG = file.type === 'image/jpeg' const isLt2M = file.size / 1024 / 1024 < 2 if (!isJPG) { this.$message.error('上传头像图片只能是 JPG 格式!') } if (!isLt2M) { this.$message.error('上传头像图片大小不能超过 2MB!') } return isJPG && isLt2M } } } </script>
组件的prop不可以直接修改,如果要响应式双向绑定,正如代码中的样子,子组件定义prop,父组件使用子组件时,绑定自身相对应的data,如下代码:
<uploader @setImage="setImage" :url.sync="temp.picture"/>
注意到需要加上.sync,然后子组件中不可以直接修改,需要触发$emit方法
this.$emit('update:url', 'http://localhost:8081/' + res) //修改url 直接this.url=xxx会报错 不允许在子组件中直接修改prop 执行此步骤父组件相对应的temp.picture就会响应式被修改 this.$emit('setImage', { url: res }) //调用父组件方法,传参为url(此处传参是以数组形式,父组件接收参数使用一个参数即可,如果是执行了上一步代码,实现了双向绑定,这一步就可以省略啦)
//还有一种方式是定义计算属性
computed: {
imageUrl: {
get () {
return this.url
},
set (val) {
this.$emit('update:url', val)
}
}
}
给imageUrl赋值的时候,自动就更新了子组件的prop url,父组件与之对应的temp.picture