基于vue2.0的一个系统

时间:2022-11-01 11:21:35

前言

这是一个用vue做的单页面管理系统,这里只是介绍架子搭建思路

前端架构

沿用Vue全家桶系列开发,主要技术栈:vue2.x+vue-router+vuex+element-ui1.x+axios

基于vue2.0的一个系统

工程目录

基于vue2.0的一个系统

基于vue2.0的一个系统

项目浅析

webpack打包配置

webpack的配置主要是vue-cli生成的,经过一些简化修改如下

webpack.config

 const path = require('path');
const webpack = require('webpack');
const cssnext = require('postcss-cssnext');
const atImport = require('postcss-import');
const cssvariables = require('postcss-css-variables');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const devSrc = 'http://localhost:8099/static/';
const devOutputPath = '../dist/static';
const prodSrc = './static/';
const prodOutputPath = '../dist/static'; const Util = require('./util') const PATH_DIST = {
font: 'font/',
img: 'image/',
css: 'css/',
js: 'js/'
};
const isProduction = process.env.NODE_ENV === 'production'; //环境,dev、production
console.log('isProduction',isProduction)
const host = isProduction ? prodSrc : devSrc;
const outputPath = isProduction ? prodOutputPath : devOutputPath;
const extractElementUI = new ExtractTextPlugin(PATH_DIST.css + 'element.css' + (isProduction ? '?[contenthash:8]' : ''));
const extractCSS = new ExtractTextPlugin(PATH_DIST.css + 'app.css' + (isProduction ? '?[contenthash:8]' : '')); module.exports = function (env) {
let Config = {
entry: {
element: ['element-ui'],
vue: ['vue', 'axios', 'vue-router', 'vuex'],
app: './src/main.js'
},
output: {
path: path.resolve(__dirname, outputPath),
publicPath: host,
filename: PATH_DIST.js + '[name].js' + (isProduction ? '?[chunkhash:8]' : '')
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
scss:Util.generateSassResourceLoader(),
sass:Util.generateSassResourceLoader(),
css: extractCSS.extract({
use: 'css-loader!postcss-loader',
fallback: 'vue-style-loader'
})
}
}
},
{
test: function (path) {
if (/\.css$/.test(path) && (/element-ui/).test(path)) {
return true;
} else {
return false;
}
},
loader: extractElementUI.extract({
use: 'css-loader!postcss-loader'
})
},
{
test: function (path) {
if (/\.css$/.test(path) && !(/element-ui/).test(path)) {
return true;
} else {
return false;
}
},
loader: extractCSS.extract({
use: 'css-loader!postcss-loader'
})
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(woff|svg|eot|ttf)\??.*$/, //字体文件
loader: 'file-loader',
options: {
publicPath:'../font/',
outputPath:PATH_DIST.font,
name: '[name].[ext]'
}
},
{
test: /\.(gif|jpg|png)\??.*$/, //图片
loader: 'file-loader',
options: {
name: PATH_DIST.img + '[name].[ext]'
}
},
{
test: /\.scss$/,
use: Util.generateSassResourceLoader()
},
{
test: /\.sass/,
use: Util.generateSassResourceLoader()
}, ]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['element', 'vue']
}),
extractElementUI,
extractCSS,
new webpack.LoaderOptionsPlugin({
options: {
postcss: function () {
return [atImport({
path: [path.resolve(__dirname, '../src')]
}), cssnext, cssvariables];
}
},
minimize: isProduction
}),
new HtmlWebpackPlugin({
title: 'JD唯品会运营后台',
template: 'index.html',
filename: '../index.html',
inject: false,
chunks: ['element', 'vue', 'app']
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': isProduction ? '"production"' : '"development"'
})
],
performance: {
hints: isProduction ? 'warning' : false
},
devtool: isProduction ? false : '#eval-source-map',
resolve: {
alias: {
'src': path.resolve(__dirname, '../src'),
'scss':path.resolve(__dirname,'../src/scss/'),
'config':path.resolve(__dirname, '../src/config/'),
}
}
}; if (isProduction) {
Config.plugins = Config.plugins.concat([
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: false
}
})
]);
} else {
Config.devServer = {
historyApiFallback: true,
publicPath: '/static/',
disableHostCheck: true,
noInfo: true,
hot: true,
host: 'localhost',
port: 8099,
watchOptions: {
poll: false,
ignored: ['node_modules/**', 'config/**', 'common/**', 'dist/**']
},
headers: {
'Access-Control-Allow-Origin': '*'
}
};
}
return Config;
};

config用到的util

 let path = require('path')
let ExtractTextPlugin = require('extract-text-webpack-plugin')
function resolveResouce(name) {
let src = path.resolve(__dirname, '../src/assets/' + name);
return src;
}
let cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: process.env.NODE_ENV !== 'production'
}
}
exports.generateSassResourceLoader = function (options) { let loaders = [
'css-loader',
'postcss-loader',
'sass-loader', {
loader: 'sass-resources-loader',
options: {
// it need a absolute path
resources: [resolveResouce('mixin.scss')]
}
}
];
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
}

src文件夹下的confg放一些东西

ajax.js配置axios

 import axios from 'axios'
import qs from 'qs'
import store from './../store'
import { Message } from 'element-ui' function getCookie(name) {
var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if (arr = document.cookie.match(reg)){
return unescape(arr[2]);
}
else{
return null;
}
} const X_CSRF_TOKEN = getCookie("pmsTp_token"); const service = axios.create({
timeout: 30000, // 请求超时时间
}) service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded '; //请求拦截
service.interceptors.request.use((config) => {
if (config.data&&config.data.noLoading) {
delete config.data.noLoading;
}else{
store.commit(STORE_TYPE.IS_LOADING,true);
}
//config.headers.X_CSRF_TOKEN = X_CSRF_TOKEN
//在发送请求之前做某件事
if(config.method === 'post'){
config.data = qs.stringify(config.data);
}
return config;
},(error) =>{
console.log("错误的传参", 'fail');
return Promise.reject(error);
});
//返回拦截
service.interceptors.response.use((res) =>{
store.commit(STORE_TYPE.IS_LOADING,false);
if (res.data.code ==-300) {
window.VM&&VM.$router.push({name:'redownload'});
return Promise.reject(res);
}
//对响应数据做些事
if(res.data.code<0){
if (res.data&&res.data.msg) {
Message({
message: res.data.msg,
type: 'error',
showClose:true,
duration: 5 * 1000
}) return Promise.reject(res);
}
}
return res;
}, (error) => {
store.commit(STORE_TYPE.IS_LOADING,false);
console.log("网络异常", 'fail');
return Promise.reject(error);
}); export default service

directive.js 一些自定义指令(暂时只是权限控制)

 let install = (Vue, options= {}) => {

     //权限指令
Vue.directive("permission", {
bind: function(el, binding) {
let permission = binding.value
let btnCodeList = localStorage.getItem('btnCodeList') || [];
if (btnCodeList.indexOf(permission) < 0 ) {
el.remove()
}
}
}); } export default {
install
}

mixin.js 只是定义一些全局用的方法

 let jqMixin = {
methods: {
goto(path) {
if (path && typeof path === 'string') {
if (/^\//.test(path)) {
this.$router.push(path);
} else {
this.$router.push([this.$route.fullPath, path].join('/'));
}
} else if (typeof path === 'number') {
this.$router.go(path);
}
},
logout(){
window.location.href = window.location.protocol + '//' + window.location.host + '/' + "logout";
},
login(){
window.location.reload();
},
queryVauleByMap(map, value, returnKey = "name", key = "id") {
for (var i = map.length - 1; i >= 0; i--) {
if (map[i][key] == value) {
return map[i][returnKey]
}
}
},
formatDate(val, format) {
if (!val) return '';
let d;
if (typeof val == 'number') {
d = new Date(val);
} else if (val instanceof Date) {
d = val;
}
var o = {
"m+": d.getMonth() + 1, //月份
"d+": d.getDate(), //日
"h+": d.getHours(), //小时
"i+": d.getMinutes(), //分
"s+": d.getSeconds(), //秒
"q+": Math.floor((d.getMonth() + 3) / 3), //季度
"S": d.getMilliseconds() //毫秒
};
if (/(y+)/.test(format))
format = format.replace(RegExp.$1, (d.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(format))
format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return format;
},
getStringfy(obj) {
let str = '';
for (let key in obj) {
str += ('&' + key + '=' + obj[key]);
}
return str.substr(1);
},
copyObj(obj) {
var o = {}
for (var i in obj) {
if (Object.prototype.toString.call(obj[i]) == '[object Object]' || Object.prototype.toString.call(obj[i]) == '[object Array]' ) {
o[i] = this.copyObj(obj[i]);
}else{
o[i] = obj[i];
}
}
return o;
},
handleSizeChange(val) {
this.$store.commit(this.paramType,{
pageIndex:1,
pageSize:val,
});
this.$store.dispatch(this.queryType);
},
handleCurrentChange(val) {
this.$store.commit(this.paramType,{
pageIndex:val,
});
this.$store.dispatch(this.queryType);
},
clear() {
for (let key in this.queryParam) {
if (typeof this.queryParam[key] == "string") {
this.queryParam[key] = ""
} else {
this.queryParam[key] = -1
}
}
},
},
}
let install = (Vue, options = {}) => {
Vue.mixin(jqMixin);
}; export default {
install
};

constant.js 一些定死的枚举值

 const CONSTANT = {

 }
export default CONSTANT

src文件夹下的router放路由配置

structure.js 定义路由

 const structure = [{
path: '/index',
name: 'index',
// redirect: '/scheduleList', //默认重定向跳转配置,暂时不开启
title: '首页',
icon: 'fa-home',
component: require('./../page/index/index.vue'),
children: []
},{
path: '/redownload',
name: 'redownload',
// redirect: '/scheduleList', //默认重定向跳转配置,暂时不开启
title: '重新登录',
icon: 'fa-home',
component: require('./../page/redownload.vue'),
children: []
}, {
path: '',
name: 'promotions',
title: '促销活动',
children: [{
path: '/promotions/fullMinus',
name: 'promotions/fullMinus',
title: '满减促销活动',
component: require('./../page/promotions/list.vue'),
children: [{
path: '/viewPromotions/fullMinus/:actId/:actNo',
name: 'viewPromotions/fullMinus',
title: '查看满减促销活动',
component: require('./../page/promotions/view.vue')
}]
},{
path: '/promotions/discount',
name: 'promotions/discount',
title: '折扣促销活动',
component: require('./../page/promotions/list.vue'),
children: [{
path: '/viewPromotions/discount/:actId/:actNo',
name: 'viewPromotions/discount',
title: '查看折扣促销活动',
component: require('./../page/promotions/view.vue')
}]
},{
path: '/promotions/buyFree',
name: 'promotions/buyFree',
title: '买免促销活动',
component: require('./../page/promotions/list.vue'),
children: [{
path: '/viewPromotions/buyFree/:actId/:actNo',
name: 'viewPromotions/buyFree',
title: '查看买免促销活动',
component: require('./../page/promotions/view.vue')
}]
},{
path: '/promotions/nOptional',
name: 'promotions/nOptional',
title: 'N元任选促销活动',
component: require('./../page/promotions/list.vue'),
children: [{
path: '/viewPromotions/nOptional/:actId/:actNo',
name: 'viewPromotions/nOptional',
title: '查看N元任选促销活动',
component: require('./../page/promotions/view.vue')
}]
},{
path: '/promotions/syncAct',
name: 'promotions/syncAct',
title: '同步结果页',
component: require('./../page/promotions/syncAct.vue'),
children: []
},{
path: '/promotions/innerpro',
name: 'promotions/innerpro',
title: '内部工具页面',
component: require('./../page/promotions/innerpro.vue'),
children: []
}]
}];
export default {
structure: structure
};

受之前angular的配置影响,这里配置的children属性是基于业务逻辑下的子页面,并不是vue-router的children含义,因此需要遍历structure对象将路由配置全部提取出来

因此有个index.js

index.js生成配置路由

 import Vue from 'vue'
import VueRouter from 'vue-router'
import structure from './structure.js';
import store from './../store'; Vue.use(VueRouter)
let getRoutes = ((items = [], fbreadcrumbs = []) => {
let routes = [];
let structureRoutes = [];
items.forEach(item => {
let route = {}
let breadcrumbs = item.title ? fbreadcrumbs.concat({
title: item.title,
path: item.path,
}) : fbreadcrumbs;
if (item.path || item.path === '') {
route.path = item.path;
route.name = item.name;
route.title = item.title;
route.meta = {
breadcrumbs: breadcrumbs,
};
if (item.component) {
route.component = item.component;
}
if (item.redirect) {
route.redirect = item.redirect;
}
if (item.children && item.children.length) {
routes = routes.concat(getRoutes(item.children, breadcrumbs));
}
routes.push(route);
} else {
if (item.children && item.children.length) {
routes = routes.concat(getRoutes(item.children, breadcrumbs));
}
} });
return routes;
}) const router = new VueRouter()
router.afterEach(route => {
store.commit(STORE_TYPE.COMMON_BREADCRUMBS_UPDATE, route.meta.breadcrumbs);
});
let routerData = getRoutes(structure.structure);
router.addRoutes(routerData)
export default {
router: router
}

src文件夹下的store放公共或者自己喜欢放的数据

type.js 一些行为的事件名字(很鸡肋)

 let STORE_TYPE = {
IS_LOADING: 'IS_LOADING',
COMMON_PERMISSION: 'COMMON_PERMISSION',
COMMON_BREADCRUMBS_UPDATE: 'COMMON_BREADCRUMBS_UPDATE',
DEPT_LIST: 'DEPT_LIST',
USER_INFO: 'USER_INFO',
USER_CSRF_TOKEN: 'USER_CSRF_TOKEN',
USER_PERMISSION: 'USER_PERMISSION',
/*
promotions
*/
PROMOTIONS_DATA_INIT: 'PROMOTIONS_DATA_INIT',
PROMOTIONS_PARAM: 'PROMOTIONS_PARAM',
PROMOTIONS_PARAM_START: 'PROMOTIONS_PARAM_START',
PROMOTIONS_CACHEPARAM: 'PROMOTIONS_CACHEPARAM',
PROMOTIONS_LISTDATA: 'PROMOTIONS_LISTDATA',
PROMOTIONS_REMOVE: 'PROMOTIONS_REMOVE',
PROMOTIONS_ACTIVITYINFO: 'PROMOTIONS_ACTIVITYINFO',
PROMOTIONS_GOODSINFO: 'PROMOTIONS_GOODSINFO',
PROMOTIONS_GOODSPARAM: 'PROMOTIONS_GOODSPARAM',
PROMOTIONS_SYNCSTATUS: 'PROMOTIONS_SYNCSTATUS',
PROMOTIONS_SYNC: 'PROMOTIONS_SYNC',
PROMOTIONS_BATCHSYNC: 'PROMOTIONS_BATCHSYNC',
PROMOTIONS_SYNCACT: 'PROMOTIONS_SYNCACT', };
if (window && !window.STORE_TYPE) {
window.STORE_TYPE = STORE_TYPE;
}
export default {
STORE_TYPE
};

index.js 暴露给外部的vuex对象

 import Vue from 'vue'
import Vuex from 'vuex'
import './types'
import common from './modules/common'
import user from './modules/user'
import promotions from './modules/promotions' Vue.use(Vuex) export default new Vuex.Store({
modules: {
common,
user,
promotions,
}
})

modules 放置各个vuex对象定义的文件夹

这儿只帖个common.js的定义吧

 import axios from 'config/ajax.js'

 const state = {
isLoading: false,
permission: {},
dept_list:[],
breadcrumbs: [],
} const getters = {
} const mutations = {
[STORE_TYPE.COMMON_PERMISSION](state, data) {
if (JSON.stringify(state.permission) == '{}') {
state.permission = data;
}
},
[STORE_TYPE.IS_LOADING](state, data) {
state.isLoading = data;
},
[STORE_TYPE.DEPT_LIST](state, data) {
state.dept_list = data;
},
[STORE_TYPE.COMMON_BREADCRUMBS_UPDATE](state, breadcrumbs) {
state.breadcrumbs = breadcrumbs;
},
} const actions = {
[STORE_TYPE.COMMON_PERMISSION]({
commit
}) {
return axios.get("/common/getUserPermissions").then(res => {
if (res.data.data) {
commit(STORE_TYPE.COMMON_PERMISSION, res.data.data);
return res.data.data;
}
});
}, [STORE_TYPE.DEPT_LIST]({
commit
}) {
return axios.get("/common/getDepartment").then(res => {
if (res.data.data) {
commit(STORE_TYPE.DEPT_LIST, res.data.data.children);
return res.data.data.children;
}
});
}, } export default {
state,
getters,
mutations,
actions
}

全局vue对象模板 app.vue

 <template>
<div id="app">
<div id="loading" v-show="isLoading">
<i class="loading-icon fa fa-spinner fa-spin"></i>
</div>
<bread></bread>
<router-view class="mainInfor__inner"></router-view>
</div>
</template> <script>
import bread from './common/bread.vue'
export default {
name: 'app',
data() {
return { }
},
computed: {
isLoading() {
return this.$store.state.common.isLoading
}
},
mounted() {
window.store = this.$store;
},
components:{
bread
} }
</script> <style rel="stylesheet/scss" lang="scss" scoped>
#loading{
position: fixed;
width: 100%;
height: 100%;
z-index: 10000;
top: 0;
left: 0;
.loading-icon{
height: 100px;
width: 100px;
display: block;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -50px;
font-size: 80px;
color: #0072c5;
position: fixed;
}
}
</style>

废话了这么多,下面就来生成vue对象吧

main.js如下

import Vue from 'vue'
import App from './App.vue'
import store from './store/index.js'
import router from './router/index.js'
import ElementUI from 'element-ui' import jqMixin from './config/mixin.js'
import jqDirective from './config/directive.js'
import LazyRender from 'vue-lazy-render'
import './config/constant.js'
import './config/ajax.js' import '../element-ui/index.css' //element-ui默认主题UI
import '../font-awesome/css/font-awesome.min.css' //font-awesome字体插件: http://fontawesome.io/icons/
import './assets/ui.scss'
Vue.config.devtools = true;
Vue.config.productionTip = false; Vue.use(ElementUI)
Vue.use(jqMixin)
Vue.use(jqDirective)
Vue.use(LazyRender)
localStorage.removeItem('newRoutePath');
localStorage.removeItem('oldRoutePath');
store.dispatch(STORE_TYPE.COMMON_PERMISSION).then((user) => {
localStorage.setItem('btnCodeList', user.btnCodeList);
window.VM = new Vue({
el: '#app',
store: store,
router: router.router,
render: h => h(App),
watch:{
$route:function (newVal,val) {
localStorage.setItem('newRoutePath',newVal.path);
localStorage.setItem('oldRoutePath',val.path);
}
}
})
},(data)=>{
window.VM = new Vue({
el: '#app',
store: store,
router: router.router,
render: h => h(App),
})
// VM.$router.push({name:'redownload'}) }).catch(e=>{
console.log(e);
})

剩下的就是page文件夹放置各个页面的业务代码了,这儿就贴了

源码都在这https://github.com/houpeace/vueProject 喜欢就去看看吧,记得加星哟~