首先創建項目,在此之前,我們需要全局安裝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