好好学习,认真工作

基于vue2.0的一个系统

前言

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

前端架构

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

工程目录

 

 

 

项目浅析

webpack打包配置

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

webpack.config

  1 const path = require('path');
  2 const webpack = require('webpack');
  3 const cssnext = require('postcss-cssnext');
  4 const atImport = require('postcss-import');
  5 const cssvariables = require('postcss-css-variables');
  6 const ExtractTextPlugin = require('extract-text-webpack-plugin');
  7 const HtmlWebpackPlugin = require('html-webpack-plugin');
  8 const CopyWebpackPlugin = require('copy-webpack-plugin');
  9 
 10 const devSrc = 'http://localhost:8099/static/';
 11 const devOutputPath = '../dist/static';
 12 const prodSrc = './static/';
 13 const prodOutputPath = '../dist/static';
 14 
 15 const Util = require('./util')
 16 
 17 const PATH_DIST = {
 18     font: 'font/',
 19     img: 'image/',
 20     css: 'css/',
 21     js: 'js/'
 22 };
 23 const isProduction = process.env.NODE_ENV === 'production';   //环境,dev、production
 24 console.log('isProduction',isProduction)
 25 const host = isProduction ? prodSrc : devSrc;
 26 const outputPath = isProduction ? prodOutputPath : devOutputPath;
 27 const extractElementUI = new ExtractTextPlugin(PATH_DIST.css + 'element.css' + (isProduction ? '?[contenthash:8]' : ''));
 28 const extractCSS = new ExtractTextPlugin(PATH_DIST.css + 'app.css' + (isProduction ? '?[contenthash:8]' : ''));
 29 
 30 module.exports = function (env) {
 31     let Config = {
 32         entry: {
 33             element: ['element-ui'],
 34             vue: ['vue', 'axios', 'vue-router', 'vuex'],
 35             app: './src/main.js'
 36         },
 37         output: {
 38             path: path.resolve(__dirname, outputPath),
 39             publicPath: host,
 40             filename: PATH_DIST.js + '[name].js' + (isProduction ? '?[chunkhash:8]' : '')
 41         },
 42         module: {
 43             rules: [
 44                 {
 45                     test: /\.vue$/,
 46                     loader: 'vue-loader',
 47                     options: {
 48                         loaders: {
 49                             scss:Util.generateSassResourceLoader(),
 50                             sass:Util.generateSassResourceLoader(),
 51                             css: extractCSS.extract({
 52                                 use: 'css-loader!postcss-loader',
 53                                 fallback: 'vue-style-loader'
 54                             })
 55                         }
 56                     }
 57                 },
 58                 {
 59                     test: function (path) {
 60                         if (/\.css$/.test(path) && (/element-ui/).test(path)) {
 61                             return true;
 62                         } else {
 63                             return false;
 64                         }
 65                     },
 66                     loader: extractElementUI.extract({
 67                         use: 'css-loader!postcss-loader'
 68                     })
 69                 },
 70                 {
 71                     test: function (path) {
 72                         if (/\.css$/.test(path) && !(/element-ui/).test(path)) {
 73                             return true;
 74                         } else {
 75                             return false;
 76                         }
 77                     },
 78                     loader: extractCSS.extract({
 79                         use: 'css-loader!postcss-loader'
 80                     })
 81                 },
 82                 {
 83                     test: /\.js$/,
 84                     loader: 'babel-loader',
 85                     exclude: /node_modules/
 86                 },
 87                 {
 88                     test: /\.(woff|svg|eot|ttf)\??.*$/,  //字体文件
 89                     loader: 'file-loader',
 90                     options: {
 91                         publicPath:'../font/',
 92                         outputPath:PATH_DIST.font,
 93                         name: '[name].[ext]'
 94                     }
 95                 },
 96                 {
 97                     test: /\.(gif|jpg|png)\??.*$/,  //图片
 98                     loader: 'file-loader',
 99                     options: {
100                         name: PATH_DIST.img + '[name].[ext]'
101                     }
102                 },
103                 {
104                     test: /\.scss$/,
105                     use: Util.generateSassResourceLoader()
106                 },
107                 {
108                     test: /\.sass/,
109                     use: Util.generateSassResourceLoader()
110                 },
111 
112             ]
113         },
114         plugins: [
115             new webpack.optimize.CommonsChunkPlugin({
116                 name: ['element', 'vue']
117             }),
118             extractElementUI,
119             extractCSS,
120             new webpack.LoaderOptionsPlugin({
121                 options: {
122                     postcss: function () {
123                         return [atImport({
124                             path: [path.resolve(__dirname, '../src')]
125                         }), cssnext, cssvariables];
126                     }
127                 },
128                 minimize: isProduction
129             }),
130             new HtmlWebpackPlugin({
131                 title: 'JD唯品会运营后台',
132                 template: 'index.html',
133                 filename: '../index.html',
134                 inject: false,
135                 chunks: ['element', 'vue', 'app']
136             }),
137             new webpack.DefinePlugin({
138                 'process.env.NODE_ENV': isProduction ? '"production"' : '"development"'
139             })
140         ],
141         performance: {
142             hints: isProduction ? 'warning' : false
143         },
144         devtool: isProduction ? false : '#eval-source-map',
145         resolve: {
146             alias: {
147                 'src': path.resolve(__dirname, '../src'),
148                 'scss':path.resolve(__dirname,'../src/scss/'),
149                 'config':path.resolve(__dirname, '../src/config/'),
150             }
151         }
152     };
153 
154     if (isProduction) {
155         Config.plugins = Config.plugins.concat([
156             new webpack.optimize.UglifyJsPlugin({
157                 sourceMap: true,
158                 compress: {
159                     warnings: false
160                 }
161             })
162         ]);
163     } else {
164         Config.devServer = {
165             historyApiFallback: true,
166             publicPath: '/static/',
167             disableHostCheck: true,
168             noInfo: true,
169             hot: true,
170             host: 'localhost',
171             port: 8099,
172             watchOptions: {
173                 poll: false,
174                 ignored: ['node_modules/**', 'config/**', 'common/**', 'dist/**']
175             },
176             headers: {
177                 'Access-Control-Allow-Origin': '*'
178             }
179         };
180     }
181     return Config;
182 };
View Code

config用到的util

 1 let path = require('path')
 2 let ExtractTextPlugin = require('extract-text-webpack-plugin')
 3 function resolveResouce(name) {
 4   let src  = path.resolve(__dirname, '../src/assets/' + name);
 5   return src;
 6 }
 7 let cssLoader = {
 8   loader: 'css-loader',
 9   options: {
10     minimize: process.env.NODE_ENV === 'production',
11     sourceMap: process.env.NODE_ENV !== 'production'
12   }
13 }
14 exports.generateSassResourceLoader = function (options) {
15 
16   let loaders = [
17     'css-loader',
18     'postcss-loader',
19     'sass-loader', {
20       loader: 'sass-resources-loader',
21       options: {
22         // it need a absolute path
23         resources: [resolveResouce('mixin.scss')]
24       }
25     }
26   ];
27   return ExtractTextPlugin.extract({
28       use: loaders,
29       fallback: 'vue-style-loader'
30     })
31 }
View Code

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

ajax.js配置axios

 1 import axios from 'axios'
 2 import qs from 'qs'
 3 import store from './../store'
 4 import { Message } from 'element-ui'
 5 
 6 
 7 function getCookie(name) {
 8     var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
 9     if (arr = document.cookie.match(reg)){
10         return unescape(arr[2]);
11     }
12     else{
13         return null;
14     }
15 }
16 
17 const X_CSRF_TOKEN = getCookie("pmsTp_token");
18 
19 const service = axios.create({
20   timeout: 30000,                  // 请求超时时间
21 })
22 
23 service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded '; 
24 
25 //请求拦截
26 service.interceptors.request.use((config) => {
27   if (config.data&&config.data.noLoading) {
28     delete config.data.noLoading;
29   }else{    
30     store.commit(STORE_TYPE.IS_LOADING,true);
31   }
32     //config.headers.X_CSRF_TOKEN  = X_CSRF_TOKEN
33     //在发送请求之前做某件事
34     if(config.method  === 'post'){
35         config.data = qs.stringify(config.data);
36     }
37     return config;
38 },(error) =>{
39      console.log("错误的传参", 'fail');
40     return Promise.reject(error);
41 });
42 //返回拦截
43 service.interceptors.response.use((res) =>{
44     store.commit(STORE_TYPE.IS_LOADING,false);
45     if (res.data.code ==-300) {
46       window.VM&&VM.$router.push({name:'redownload'});
47       return Promise.reject(res);
48     }
49     //对响应数据做些事
50     if(res.data.code<0){
51       if (res.data&&res.data.msg) {
52            Message({
53             message: res.data.msg,
54             type: 'error',
55             showClose:true,
56             duration: 5 * 1000
57           })
58 
59         return Promise.reject(res);   
60         }
61     }
62     return res;
63 }, (error) => {
64     store.commit(STORE_TYPE.IS_LOADING,false);
65     console.log("网络异常", 'fail');
66     return Promise.reject(error);
67 });
68 
69 export default service
View Code

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

 1 let install = (Vue, options= {}) => {
 2 
 3     //权限指令
 4     Vue.directive("permission", {
 5         bind:  function(el, binding) {
 6             let permission = binding.value
 7             let btnCodeList = localStorage.getItem('btnCodeList') || [];  
 8             if (btnCodeList.indexOf(permission) < 0 ) {
 9                 el.remove() 
10             }
11         } 
12     });
13 
14 }
15 
16 export default {
17     install
18 }
View Code

 

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

 1 let jqMixin = {
 2     methods: {
 3         goto(path) {
 4             if (path && typeof path === 'string') {
 5                 if (/^\//.test(path)) {
 6                     this.$router.push(path);
 7                 } else {
 8                     this.$router.push([this.$route.fullPath, path].join('/'));
 9                 }
10             } else if (typeof path === 'number') {
11                 this.$router.go(path);
12             }
13         },
14         logout(){
15              window.location.href = window.location.protocol + '//' + window.location.host + '/' + "logout";
16          },
17         login(){
18               window.location.reload();            
19          },
20         queryVauleByMap(map, value, returnKey = "name", key = "id") {
21             for (var i = map.length - 1; i >= 0; i--) {
22                 if (map[i][key] == value) {
23                     return map[i][returnKey]
24                 }
25             }
26         },
27         formatDate(val, format) {
28             if (!val) return '';
29             let d;
30             if (typeof val == 'number') {
31                 d = new Date(val);
32             } else if (val instanceof Date) {
33                 d = val;
34             }
35             var o = {
36                 "m+": d.getMonth() + 1, //月份
37                 "d+": d.getDate(), //
38                 "h+": d.getHours(), //小时
39                 "i+": d.getMinutes(), //
40                 "s+": d.getSeconds(), //
41                 "q+": Math.floor((d.getMonth() + 3) / 3), //季度
42                 "S": d.getMilliseconds() //毫秒
43             };
44             if (/(y+)/.test(format))
45                 format = format.replace(RegExp.$1, (d.getFullYear() + "").substr(4 - RegExp.$1.length));
46             for (var k in o)
47                 if (new RegExp("(" + k + ")").test(format))
48                     format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
49             return format;
50         },
51         getStringfy(obj) {
52             let str = '';
53             for (let key in obj) {
54                 str += ('&' + key + '=' + obj[key]);
55             }
56             return str.substr(1);
57         },
58         copyObj(obj) {
59             var o = {}
60             for (var i in obj) {
61                 if (Object.prototype.toString.call(obj[i]) == '[object Object]' || Object.prototype.toString.call(obj[i]) == '[object Array]' ) {
62                     o[i] = this.copyObj(obj[i]);
63                 }else{
64                     o[i] = obj[i];
65                 }
66             }
67             return o;
68         }, 
69         handleSizeChange(val) {
70             this.$store.commit(this.paramType,{
71               pageIndex:1,
72               pageSize:val,
73             });
74             this.$store.dispatch(this.queryType);
75           },
76         handleCurrentChange(val) {
77               this.$store.commit(this.paramType,{
78                 pageIndex:val,
79               });
80               this.$store.dispatch(this.queryType);
81           },
82         clear() {
83             for (let key in this.queryParam) {
84                 if (typeof this.queryParam[key] == "string") {
85                     this.queryParam[key] = ""
86                 } else {
87                     this.queryParam[key] = -1
88                 }
89             }
90         },
91     },
92 }
93 let install = (Vue, options = {}) => {
94     Vue.mixin(jqMixin);
95 };
96 
97 export default {
98     install
99 };
View Code

constant.js 一些定死的枚举值

1 const CONSTANT = {
2     
3 }
4 export default CONSTANT
View Code

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

structure.js 定义路由

 1 const structure = [{
 2     path: '/index',
 3     name: 'index',
 4     // redirect: '/scheduleList', //默认重定向跳转配置,暂时不开启 
 5     title: '首页',
 6     icon: 'fa-home',
 7     component: require('./../page/index/index.vue'),
 8     children: []
 9 },{
10     path: '/redownload',
11     name: 'redownload',
12     // redirect: '/scheduleList', //默认重定向跳转配置,暂时不开启 
13     title: '重新登录',
14     icon: 'fa-home',
15     component: require('./../page/redownload.vue'),
16     children: []
17 }, {
18     path: '',
19     name: 'promotions',
20     title: '促销活动',
21     children: [{
22         path: '/promotions/fullMinus',
23         name: 'promotions/fullMinus',
24         title: '满减促销活动',
25         component: require('./../page/promotions/list.vue'),
26         children: [{
27             path: '/viewPromotions/fullMinus/:actId/:actNo',
28             name: 'viewPromotions/fullMinus',
29             title: '查看满减促销活动',
30             component: require('./../page/promotions/view.vue')
31         }]
32     },{
33         path: '/promotions/discount',
34         name: 'promotions/discount',
35         title: '折扣促销活动',
36         component: require('./../page/promotions/list.vue'),
37         children: [{
38             path: '/viewPromotions/discount/:actId/:actNo',
39             name: 'viewPromotions/discount',
40             title: '查看折扣促销活动',
41             component: require('./../page/promotions/view.vue')
42         }]
43     },{
44         path: '/promotions/buyFree',
45         name: 'promotions/buyFree',
46         title: '买免促销活动',
47         component: require('./../page/promotions/list.vue'),
48         children: [{
49             path: '/viewPromotions/buyFree/:actId/:actNo',
50             name: 'viewPromotions/buyFree',
51             title: '查看买免促销活动',
52             component: require('./../page/promotions/view.vue')
53         }]
54     },{
55         path: '/promotions/nOptional',
56         name: 'promotions/nOptional',
57         title: 'N元任选促销活动',
58         component: require('./../page/promotions/list.vue'),
59         children: [{
60             path: '/viewPromotions/nOptional/:actId/:actNo',
61             name: 'viewPromotions/nOptional',
62             title: '查看N元任选促销活动',
63             component: require('./../page/promotions/view.vue')
64         }]
65     },{
66         path: '/promotions/syncAct',
67         name: 'promotions/syncAct',
68         title: '同步结果页',
69         component: require('./../page/promotions/syncAct.vue'),
70         children: []
71     },{
72         path: '/promotions/innerpro',
73         name: 'promotions/innerpro',
74         title: '内部工具页面',
75         component: require('./../page/promotions/innerpro.vue'),
76         children: []
77     }]
78 }];
79 export default {
80     structure: structure
81 };
View Code

 

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

因此有个index.js

 index.js生成配置路由

 1 import Vue from 'vue'
 2 import VueRouter from 'vue-router'
 3 import structure from './structure.js';
 4 import store from './../store';
 5 
 6 Vue.use(VueRouter)
 7 let getRoutes = ((items = [], fbreadcrumbs = []) => {
 8     let routes = [];
 9     let structureRoutes = [];
10     items.forEach(item => {
11         let route = {}
12         let breadcrumbs = item.title ? fbreadcrumbs.concat({
13             title: item.title,
14             path: item.path,
15         }) : fbreadcrumbs;
16         if (item.path || item.path === '') {
17             route.path = item.path;
18             route.name = item.name;
19             route.title = item.title;
20             route.meta = {
21                 breadcrumbs: breadcrumbs,
22             };
23             if (item.component) {
24                 route.component = item.component;
25             }
26             if (item.redirect) {
27                 route.redirect = item.redirect;
28             }
29             if (item.children && item.children.length) {
30                 routes = routes.concat(getRoutes(item.children, breadcrumbs));
31             }
32             routes.push(route);
33         } else {
34             if (item.children && item.children.length) {
35                 routes = routes.concat(getRoutes(item.children, breadcrumbs));
36             }
37         }
38 
39     });
40     return routes;
41 })
42 
43 const router = new VueRouter()
44 router.afterEach(route => {
45     store.commit(STORE_TYPE.COMMON_BREADCRUMBS_UPDATE, route.meta.breadcrumbs);
46 });
47 let routerData = getRoutes(structure.structure);
48 router.addRoutes(routerData)
49 export default {
50     router: router
51 }
View Code

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

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

 1 let STORE_TYPE = {
 2     IS_LOADING: 'IS_LOADING',
 3     COMMON_PERMISSION: 'COMMON_PERMISSION',
 4     COMMON_BREADCRUMBS_UPDATE: 'COMMON_BREADCRUMBS_UPDATE',
 5     DEPT_LIST: 'DEPT_LIST',
 6     USER_INFO: 'USER_INFO',
 7     USER_CSRF_TOKEN: 'USER_CSRF_TOKEN',
 8     USER_PERMISSION: 'USER_PERMISSION',
 9     /*
10         promotions
11     */
12     PROMOTIONS_DATA_INIT: 'PROMOTIONS_DATA_INIT',
13     PROMOTIONS_PARAM: 'PROMOTIONS_PARAM',
14     PROMOTIONS_PARAM_START: 'PROMOTIONS_PARAM_START',
15     PROMOTIONS_CACHEPARAM: 'PROMOTIONS_CACHEPARAM',
16     PROMOTIONS_LISTDATA: 'PROMOTIONS_LISTDATA',
17     PROMOTIONS_REMOVE: 'PROMOTIONS_REMOVE',
18     PROMOTIONS_ACTIVITYINFO: 'PROMOTIONS_ACTIVITYINFO',
19     PROMOTIONS_GOODSINFO: 'PROMOTIONS_GOODSINFO',
20     PROMOTIONS_GOODSPARAM: 'PROMOTIONS_GOODSPARAM',
21     PROMOTIONS_SYNCSTATUS: 'PROMOTIONS_SYNCSTATUS',
22     PROMOTIONS_SYNC: 'PROMOTIONS_SYNC',
23     PROMOTIONS_BATCHSYNC: 'PROMOTIONS_BATCHSYNC',
24     PROMOTIONS_SYNCACT: 'PROMOTIONS_SYNCACT',
25 
26 
27 };
28 if (window && !window.STORE_TYPE) {
29     window.STORE_TYPE = STORE_TYPE;
30 }
31 export default {
32     STORE_TYPE
33 };
View Code

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

 1 import Vue from 'vue'
 2 import Vuex from 'vuex'
 3 import './types'
 4 import common from './modules/common'
 5 import user from './modules/user'
 6 import promotions from './modules/promotions'
 7 
 8 Vue.use(Vuex)
 9 
10 export default new Vuex.Store({
11   modules: {
12     common,
13     user,
14     promotions,
15   }
16 })
View Code

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

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

 1 import axios from 'config/ajax.js'
 2 
 3 const state = {
 4     isLoading: false,
 5     permission: {},
 6     dept_list:[],
 7     breadcrumbs: [],
 8 }
 9 
10 const getters = {
11 }
12 
13 const mutations = {
14     [STORE_TYPE.COMMON_PERMISSION](state, data) {
15         if (JSON.stringify(state.permission) == '{}') {
16             state.permission = data;
17         }
18     },
19     [STORE_TYPE.IS_LOADING](state, data) {
20         state.isLoading = data;
21     },
22     [STORE_TYPE.DEPT_LIST](state, data) {
23         state.dept_list = data;
24     },
25     [STORE_TYPE.COMMON_BREADCRUMBS_UPDATE](state, breadcrumbs) {
26         state.breadcrumbs = breadcrumbs;
27     },
28 }
29 
30 const actions = {  
31     [STORE_TYPE.COMMON_PERMISSION]({
32         commit
33     }) {
34         return axios.get("/common/getUserPermissions").then(res => {
35             if (res.data.data) {
36                 commit(STORE_TYPE.COMMON_PERMISSION, res.data.data);
37                 return res.data.data;
38             }
39         });
40     },
41   
42     [STORE_TYPE.DEPT_LIST]({
43         commit
44     }) {
45         return axios.get("/common/getDepartment").then(res => {
46             if (res.data.data) {
47                 commit(STORE_TYPE.DEPT_LIST, res.data.data.children);
48                 return res.data.data.children;
49             }
50         });
51     },
52   
53 }
54 
55 export default {
56     state,
57     getters,
58     mutations,
59     actions
60 }
View Code

全局vue对象模板 app.vue

 1 <template>
 2   <div id="app">
 3       <div id="loading" v-show="isLoading">
 4           <i class="loading-icon fa fa-spinner fa-spin"></i>
 5       </div>
 6       <bread></bread>
 7       <router-view class="mainInfor__inner"></router-view>  
 8     </div>
 9 </template>
10 
11 <script>
12 import bread from './common/bread.vue'
13 export default {
14   name: 'app',
15   data() {
16     return {
17       
18     }
19   },
20   computed: {
21     isLoading() {
22       return this.$store.state.common.isLoading
23     }
24   },
25   mounted() {
26     window.store = this.$store;
27   },
28   components:{
29     bread
30   }
31 
32 }
33 </script>
34 
35 <style rel="stylesheet/scss" lang="scss" scoped>
36 #loading{ 
37   position: fixed;
38   width: 100%;
39   height: 100%;
40   z-index: 10000;
41   top: 0;
42   left: 0;
43   .loading-icon{
44     height: 100px;
45     width: 100px; 
46     display: block;
47     top: 50%;
48     left: 50%;
49     margin-top: -50px;
50     margin-left: -50px;
51     font-size: 80px;
52     color: #0072c5;
53     position: fixed;
54   }
55 }
56 </style>
View Code

 

废话了这么多,下面就来生成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 喜欢就去看看吧

 

posted on 2017-12-20 16:54  peace_1  阅读(936)  评论(0编辑  收藏  举报