preface

This blog- Admin background management project is based on vue family bucket + Element UI development

rendering

See sdjblog.cn :8080/ for the full effect

Functional description

Implemented functions

  • Log in to register
  • The personal data
  • Subject switch
  • Data statistics
  • The article lists
  • Comment on the list
  • The tag list
  • A list of items
  • link
  • The message list
  • The menu features
  • User roles

The front-end technology

  • vue
  • vuex
  • vue-route
  • axios
  • scss
  • element-ui
  • moment
  • highlight.js
  • echarts
  • wangeditor
  • mavon-editor
  • xlsx

Main project structure

- API AXIos package and API interface - Assets picture and CSS font resources - Components package - TagsView Route label navigation - ChartCard card - EmptyShow Empty message for data - MyEcharts Echarts Chart Wrapper - MyForm Wrapper - MyTable Wrapper - TreeSelect drop-down tree structure - UploadFile file upload - WangEnduit WangEnduit Rich text editor - Router Router encapsulation - Store Vuex state management - utils encapsulation of common methods, such as form validation, Excel export - views - article list, article comments and article labels - errorPage For example, 404-HOME statistics (statistics of visitors, users, articles and messages) - Layout Head navigation and side navigation - Link friendship link list - login login registration - Menu menu function - Message message list - Project project list - user User role (roles include import permissions and batch import and export users) - redirect Route redirection - app.vue root component - main.js entry file, instantiate vue and initialize plug-in - permission.js Route permission interception, Load the corresponding route through the background return permissionCopy the code

instructions

  • You can log in using the username or email address and password. Test account: Username: test Password: 123456
  • Registered users from the background registration page for the blogger administrator, can publish their own articles and add ordinary users and other permissions
  • The system has realized the menu function permission and data permission, according to the user role has the permission to load the menu route and button operation, the background through the user role and permission API request interception and request data to obtain the user under the data list

Function implementation

Code encapsulation

General part of the code for encapsulation, so as to facilitate unified management and maintenance

The chart to encapsulate

Initialize the chart by configuring option chart parameters (same as echarts Options configuration), width and height, and listen for option parameter changes and window changes

<template> <div class="echarts" :id="id" :style="style"> </div> </template> <script> export default { props: { width: { type: String, default: "100%" }, height: { type: String }, option: { type: Object } }, data() { return { id: ', MyEcharts: "" //echarts instance}; }, computed: { style() { return { height: this.height, width: this.width }; }}, watch: {// Because echarts is data-driven, we need to listen for changes in the data to redraw the chart option: { handler(newVal, oldVal) { if (this.MyEcharts) { if (newVal) { this.MyEcharts.setOption(newVal, true); } else { this.MyEcharts.setOption(oldVal, true); } } else { this.InitCharts(); }}, deep: true // Listen on internal attributes of objects, critical. } }, created(){ // this.id = Number(Math.random().toString().substr(3,length) + Date.now()).toString(36); this.id = this.uuid(); }, mounted() { this.InitCharts(); }, methods: {/ / generates a unique identifier uuid () {return 'xxxxxx4xxxyxxxxxx. Replace (/ [y] / g, function (c) {let r = Math. The random () * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, // The design of a common component of the chart, whether the first method or the second method to write data according to the official website format, InitCharts() {this.myecharts = this.$echarts.init(document.getelementById (this.id)); /** * This method is applicable to all projects of the chart, but each configuration needs to be passed in the parent component, equivalent to each chart configuration needs to be written again, not special save code, mainly is flexible * echarts configuration items, you can directly configure outside, Just throw in this.option */ this.myecharts.clear (); SetOption (this.myecharts.setoption (this.option, true); // Set to true to re-render when the chart switches data // When a page has multiple charts, there is a bug that only one chart changes with the window size. // window.onresize = () => { // this.MyEcharts.resize(); / /}; // The following is a solution to the above bug. In the future use this plan, abandon the previous one. window.addEventListener("resize", ()=> { this.MyEcharts.resize(); }); }}}; </script>Copy the code

Forms and table encapsulation

Forms and tables are frequently used components in background systems. Through secondary encapsulation of Element UI, common JSON data format can be used for transmission. Form subcomponents bind data and update via V-model by defining prop pass properties as objects (base types cannot be changed directly), initialize some of the default actions (empty buttons, word limits, etc.), and use lots of slots to make components more flexible. The table is rendered by render mode, increasing the flexibility of data display.

ArticleForm: {ref: 'articleRef', labelWidth: '80px', marginBottom: '30px', requiredAsterisk: FormItemList: [{type: "text", prop: "title", width: '400px', label: 'placeholder ', placeholder: "placeholder"}, {type: "Text ", prop: "description", width: '400px', label: 'placeholder ', placeholder:"}, {type: "select", prop: "Tags ", multiple: true, width: '400px', label: 'placeholder ', arrList: []}, {label:' placeholder ', slot: 'upload'}, {type: "radio", prop: "contentType", label: 'article type ', arrList: [{label:' rich text editing ', value: '0'}, {label: 'MarkDown edit ', value: '1'}]}], formModel: {title: '', description: '', tags: [], contentType: '0'}, rules: {title: [{required: true, validator: form.formvalidate.form (' article title ').noempty, trigger: 'blur'}], description: [{required: true, validator: form.formvalidate.form (' article description ').noempty, trigger: 'blur'}], tags: [{required: true, validator: form.formvalidate.form (' article description '). Form(' article tags ').typeselect, trigger: 'change'}]}}Copy the code

File upload and encapsulation

Verify file size, format, file upload request and file upload progress bar display in beforeUpload

<template> <div class='slot-upload'> <div class="upload-square" v-if='type === "square"'> <el-upload class='upload-file'  enctype='multipart/form-data' :accept="accept" :limit='limit' :multiple="multiple" :list-type="listType" :file-list="fileList" :before-upload='beforeUpload' :before-remove='beforeRemove' :on-exceed='handleExceed' action=""> <i class="el-icon-plus avatar-uploader-icon"></i> </el-upload> <div class="file-progress" v-if='progressObj.show'> <el-progress :percentage="progressObj.percentage" :status="progressObj.percentage == 100? 'success':'exception'"> </el-progress> </div> </div> <div class="upload-avatar" v-else-if='type === "avatar"'> <el-upload class='avatar-slot' enctype='multipart/form-data' :accept="accept" :limit='limit' :multiple="multiple" :list-type="listType" :file-list="fileList" :before-upload='beforeUpload' :before-remove='beforeRemove' :on-exceed='handleExceed' action=""> <slot :name="avatarSlot" v-if='avatarSlot' /> </el-upload> <div class="file-progress" v-if='progressObj.show' :style='{width: progress.width,margin: progress.margin}'> <el-progress :percentage="progressObj.percentage" :status="progressObj.percentage == 100? 'success':'exception'"> </el-progress> </div> </div> </div> </template> <script> export default { name: 'UploadFile', props: {// Props type: {type: String, default: "square"}, // Accept: {type: String, default: Multiple: {type: Boolean, default: false}, // File display type listType: {type: String, default: "Text"}, // Limit Number limit: {type: Number, default: 1}, // File display list fileList: {type: Array, default() {return []; }}, // fileSize: {type: Number, default: 1048576}, // Progress: {type: Object, default() {return {}; }}, // custom content avatarSlot: {type: String, default: ""}}, data() {return {progressObj: {show: false, percentage: 0 } } }, methods: {handleExceed(files, fileList) {this. $message. Warning (' handleExceed(files, fileList) {handleExceed(files, fileList) {this. ${files.length + filelist. length}; }, beforeUpload(file){ if(file.size > this.fileSize){ let sizeLimit = this.fileSize/1024/1024 This $Message. Warning (` size limit within the ${sizeLimit} Mb `) return} this. ProgressObj. Percentage = 0; this.progressObj.show = true; let fd = new FormData() fd.append('file', file) this.$api.upload.uploadFile(fd,(upload)=>{ let complete = (upload.loaded / upload.total * 100 | 0) this.progressObj.percentage = complete; if(this.progressObj.percentage == 100){ setTimeout(()=>{ this.progressObj = { show: false, percentage: 0 } },1000) } }).then((res) => { let code = res.code if(code === this.$constant.reqSuccess){ let fileData = res.data this.$emit('uploadEvent',fileData) }else{ this.progressObj = { show: false, percentage: 0} this.$message.warning(' File upload failed '); } }) return false }, beforeRemove(file, fileList){ if(file.status === 'ready'){ return true }else{ this.$api.upload.fileDel(file.sourceId).then((res)=>{ let code = res.code if(code === this.$constant.reqSuccess){ this.$emit('removeEvent',file) }else{ This.$message.warning(' File deletion failed '); return false } }) } } } } </script> <style lang="scss" scoped> .slot-upload { width: 100%; .upload-file { .el-upload { border: 1px dashed $color-G70; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; &:hover { border-color: #409eff; } } /deep/ .el-upload--picture-card, /deep/ .el-upload-list__item{ width: 120px; height: 120px; line-height: 120px; } .avatar-uploader-icon { font-size: 20px; color: #8c939d; width: 48px; height: 48px; line-height: 48px; text-align: center; } } .upload-avatar{ .avatar-slot{ display: flex; align-items: center; justify-content: center; } } .file-progress { width: 400px; margin-top: 10px; } } </style>Copy the code

API package

Request and response are intercepted, token data is configured, and request interfaces are processed in a unified file

// Const project = {projectList (params) {return axios.get('/project/list',{params})}, projectAdd (params) { return axios.post('/project/add',params) }, projectUpdate (params) { return axios.put('/project/update',params) }, projectDel(id){ return axios.delete('/project/del/'+id) } } export default { project }Copy the code

Routing to intercept

Dynamically add routes by comparing the role menu ID returned in the background with the route menu ID

import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' import 'nprogress/nprogress.css' NProgress.configure({ showSpinner: False}) const whiteList = ['/login'] // No redirection whiteList route. beforeEach(async(to, from, Next) => {nprogress.start () // Set the page title if (to.meta. Title) {document.title = to.meta. Title} if (sessionStorage.getitem ('token')) {if (to.path === '/login') {// If (to.path === '/login') {// If (to.path === '/login') {// If (to.path === '/login') {// If (to.path === '/login') {// If (to.path === '/login') {next({path: '/' ,query: { t: + new Date ()}}) NProgress. Done ()} else {/ / to determine whether the user access to the user permission information const hasAuths = store. The getters. GetAuthList && store.getters.getAuthList.length > 0; If (hasAuths) {next()} else {try {// get user information let auths = await store.dispatch('actAuthList') // Generate an accessible routing table let accessRoutes = await store.dispatch('generateRoutes', Auths) if(accessRoutes. Length > 0){let pathTo = accessRoutes[0] Let routeList = [] accessroutes. forEach((item)=>{if(item.children){for(let I = 0; i < item.children.length; i++){ routeList.push(item.children[i].path) } } }) if(routeList.includes(to.path)){ next({ ... to, replace: true }) }else{ next({path: pathTo, replace: True})}} else {/ / delete the token, enter the login page to login sessionStorage. RemoveItem (" token ") the Message. The error 'no permission to view the next (` / login? Redirect =${to.path} ') nprogress.done ()}} catch (error) {redirect=${to.path} ') nprogress.done ()}} catch (error) { Enter the login page to login sessionStorage. RemoveItem (" token ") the Message. The error (error | | 'from the error') next (` / login? Redirect =${to.path} ') nprogress.done ()}}}} else {/* No token exists */ if (whitelist.indexof (to.path)! == -1) {// In the free login whitelist, go directly to next()} else {// Other pages without access are redirected to the login page next(' /login? redirect=${to.path}`) NProgress.done() } } }) router.afterEach((to) => { NProgress.done(); window.scrollTo(0, 0); })Copy the code

Subject switch

LocalStorage stores the selected theme, dynamically adds the data-theme property to the body, and uses scs@mixin to configure different themes

GetThemeColor (){let propTheme = localstorage.getitem ('propTheme'); if(propTheme){ document.body.setAttribute('data-theme',propTheme); }else{ document.body.setAttribute('data-theme','custom-light'); Localstorage.setitem ('propTheme','custom-light')}} transparent, $darkColor: transparent){ background-color: $lightColor; [data-theme='custom-light'] & { background-color: $lightColor; } [data-theme='custom-dark'] & { background-color: $darkColor; } } .product-item{ @include bg-color($color-W20,$color-D20); }Copy the code

Common tool function encapsulation

Form verification, Excel export, time format encapsulation

Export function diffDay(time) {let currentTime = moment(); let endTime = moment(time); let day = endTime.diff(currentTime, Export function currentDay(type = 'time') {if(type === 'day'){return Format ('YYYY-MM-DD')}else{return moment(). Format ('YYYY-MM-DD HH: MM :ss')} objProp = (data, path) => { if (! data || ! path) { return null } let tempArr = path.split('.'); for (let i = 0; i < tempArr.length; i++) { let key = tempArr[i] if (data[key]) { data = data[key] } else { return null } } return data }Copy the code

Build Setup

# install dependencies
npm install

# serve with hot reload at localhost: 8090
npm run dev

# build for production with minification
npm run build
Copy the code

If you want to see the full effect, you need to run it with the background project blog- Node, otherwise the interface request will fail.

Project Address:

Front Desk Presentation: https://gitee.com/sdj_work/blog-page (Vue/Nuxt/ UNI-app)

Management background: https://gitee.com/sdj_work/blog-admin (Vue/React)

Back-end Node: https://gitee.com/sdj_work/blog-node (Express/Koa)

Blog: https://sdjBlog.cn/

Project series:

Vue+Nuxt blog display

Vue+ UniAPP blog display

Vue+ElementUI backstage blog management

Node + KOA + mongodb blog interface development

Node + Express + mongodb blog interface development