├── .browserslistrc
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ ├── logo.png
│ ├── index.css
│ └── index.less
├── api
│ ├── user.js
│ ├── http.js
│ ├── index.js
│ └── page.js
├── App.vue
├── components
│ ├── BreadCrumb.vue
│ ├── Main.vue
│ ├── main
│ │ ├── TopBar.vue
│ │ └── Card.vue
│ ├── Aside.vue
│ └── Header.vue
├── main.js
├── plugins
│ └── element.js
├── store
│ └── index.js
├── views
│ ├── NotFound.vue
│ ├── Home.vue
│ ├── Login.vue
│ └── Page1.vue
├── mock
│ ├── my-radom.js
│ └── index.js
└── router
│ └── index.js
├── .editorconfig
├── babel.config.js
├── .gitignore
├── .eslintrc.js
├── package.json
└── README.md
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aizener/admin-permission/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aizener/admin-permission/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 | * {
6 | box-sizing: border-box;
7 | }
8 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import http from './http'
2 |
3 | export const login = data => {
4 | return http('/login', 'post', data)
5 | }
--------------------------------------------------------------------------------
/src/assets/index.less:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | * {
7 | box-sizing: border-box;
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ],
5 | plugins: [
6 | [
7 | 'component',
8 | {
9 | libraryName: 'element-ui',
10 | styleLibraryName: 'theme-chalk'
11 | }
12 | ]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/http.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios'
2 |
3 | const axios = Axios.create({
4 | // baseURL: process.env.NODE_ENV === 'development' ? '' : '',
5 | })
6 |
7 | export default (url, method = 'get', data = {}) => {
8 | return axios({
9 | url,
10 | method,
11 | data
12 | })
13 | }
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | const context = require.context('./', false, /.js$/)
2 |
3 | const modules = {}
4 | context.keys().forEach(fileName => {
5 | if (!['./index.js', './http.js'].includes(fileName)) {
6 | Object.assign(modules, context(fileName))
7 | }
8 | })
9 |
10 | export default modules
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | '@vue/standard'
9 | ],
10 | parserOptions: {
11 | parser: 'babel-eslint'
12 | },
13 | rules: {
14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
16 | 'eol-last': 'off'
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/BreadCrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.name }}
5 |
6 |
7 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import router from './router'
4 | import store from './store'
5 | import './plugins/element.js'
6 | import './assets/index.less'
7 | import Api from './api'
8 | import './mock'
9 | import { MessageBox, Message } from 'element-ui'
10 | import 'animate.css'
11 |
12 | Vue.config.productionTip = false
13 | Vue.prototype.$api = Api
14 | Vue.prototype.$confirm = MessageBox.confirm
15 | Vue.prototype.$message = Message
16 |
17 | new Vue({
18 | router,
19 | store,
20 | render: h => h(App)
21 | }).$mount('#app')
22 |
--------------------------------------------------------------------------------
/src/api/page.js:
--------------------------------------------------------------------------------
1 | import http from './http'
2 |
3 | export const getList = data => {
4 | return http('/list', 'get', data)
5 | }
6 |
7 | export const getTotal = () => {
8 | return http('/list/total')
9 | }
10 |
11 | export const getListByValue = data => {
12 | return http('/list/value', 'get', data)
13 | }
14 |
15 | export const addList = data => {
16 | return http('/list/add', 'post', data)
17 | }
18 |
19 | export const updateList = data => {
20 | return http('/list/update', 'put', data)
21 | }
22 |
23 | export const deleteList = data => {
24 | return http('/list/delete', 'delete', data)
25 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/plugins/element.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import {
3 | Container,
4 | Header,
5 | Aside,
6 | Main,
7 | Button,
8 | Menu,
9 | MenuItem,
10 | Submenu,
11 | Breadcrumb,
12 | BreadcrumbItem,
13 | Form,
14 | FormItem,
15 | Table,
16 | TableColumn,
17 | Input,
18 | Icon,
19 | Pagination,
20 | Loading
21 | } from 'element-ui'
22 |
23 | Vue
24 | .use(Container)
25 | .use(Header)
26 | .use(Aside)
27 | .use(Main)
28 | .use(Button)
29 | .use(Menu)
30 | .use(MenuItem)
31 | .use(Submenu)
32 | .use(Breadcrumb)
33 | .use(BreadcrumbItem)
34 | .use(Form)
35 | .use(FormItem)
36 | .use(Table)
37 | .use(TableColumn)
38 | .use(Input)
39 | .use(Icon)
40 | .use(Pagination)
41 | .use(Loading)
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | Vue.use(Vuex)
5 |
6 | export default new Vuex.Store({
7 | state: {
8 | breads: [],
9 | user: {}
10 | },
11 | mutations: {
12 | addBread (state, bread) {
13 | const index = state.breads.findIndex(_bread => _bread.name === bread.name)
14 | if (index > -1) {
15 | state.breads.splice(index + 1, state.breads.length - index - 1)
16 | } else {
17 | state.breads.push(bread)
18 | }
19 | },
20 | removeBread (state, bread) {
21 | state.breads = state.breads.filter(_bread => _bread !== bread)
22 | },
23 | setUser (state, user) {
24 | state.user = user
25 | }
26 | },
27 | actions: {
28 | },
29 | modules: {
30 | }
31 | })
32 |
--------------------------------------------------------------------------------
/src/components/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
26 |
27 |
37 |
--------------------------------------------------------------------------------
/src/views/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
啊哦!找不到相关页面o(╥﹏╥)o。
4 |
5 | 返回上一级页面
6 |
7 |
8 |
9 |
10 |
15 |
16 |
36 |
--------------------------------------------------------------------------------
/src/components/main/TopBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 搜索
6 |
7 |
8 | 添加商品
9 |
10 |
11 |
12 |
26 |
27 |
--------------------------------------------------------------------------------
/src/mock/my-radom.js:
--------------------------------------------------------------------------------
1 | // 使用 Mock
2 | var Mock = require('mockjs')
3 |
4 | Mock.Random.extend({
5 | likes: function () {
6 | const likes = [
7 | '喜欢打游戏,看电影,尤其是英雄联盟和欧美大片。',
8 | '喜欢做饭,尤其是西餐,喜欢做甜点,自己每次都吃得饱饱的。',
9 | '我最爱去游泳了,当然也喜欢潜水,在海底下看各种好看的鱼鱼。',
10 | '我最最喜欢的就是去旅游了,看沿途的风景,真是美呆了。',
11 | '我的爱好是打篮球,我很喜欢打篮球,我的偶像是科比。',
12 | '我超喜欢去蹦迪了,感觉整个身体都在那里放松了。',
13 | '哈哈哈,我喜欢的是和女孩子一起玩,因为男女搭配,干活不累嘛。',
14 | '我没啥爱好,唯一的爱好就是宅。',
15 | '我喜欢看动漫,更喜欢日漫,我可是一个二次元哦。',
16 | '我喜欢cosplay,喜欢cos动漫里的每一个角色。'
17 | ]
18 | return this.pick(likes)
19 | }
20 | })
21 | Mock.Random.extend({
22 | address: function () {
23 | const address = [
24 | '深圳市南山区科技园南区R2-B三楼',
25 | '深圳南山区科技园汇景豪苑海欣阁',
26 | '深圳市南山区白石洲中信红树湾',
27 | '上海市普陀区金沙江路 1517 弄',
28 | '四川成都市中德英伦联邦C区',
29 | '北京市中南海老四合院靠左',
30 | '广州市中心中央银行33号'
31 | ]
32 | return this.pick(address)
33 | }
34 | })
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
33 |
34 |
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "permission-demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "animate.css": "^4.1.0",
12 | "axios": "^0.19.2",
13 | "core-js": "^3.6.5",
14 | "element-ui": "^2.4.5",
15 | "vue": "^2.6.11",
16 | "vue-router": "^3.2.0",
17 | "vuex": "^3.4.0"
18 | },
19 | "devDependencies": {
20 | "@vue/cli-plugin-babel": "~4.4.0",
21 | "@vue/cli-plugin-eslint": "~4.4.0",
22 | "@vue/cli-plugin-router": "~4.4.0",
23 | "@vue/cli-plugin-vuex": "~4.4.0",
24 | "@vue/cli-service": "~4.4.0",
25 | "@vue/eslint-config-standard": "^5.1.2",
26 | "babel-eslint": "^10.1.0",
27 | "babel-plugin-component": "^1.1.1",
28 | "eslint": "^6.7.2",
29 | "eslint-plugin-import": "^2.20.2",
30 | "eslint-plugin-node": "^11.1.0",
31 | "eslint-plugin-promise": "^4.2.1",
32 | "eslint-plugin-standard": "^4.0.0",
33 | "eslint-plugin-vue": "^6.2.2",
34 | "less": "^3.0.4",
35 | "less-loader": "^5.0.0",
36 | "mockjs": "^1.1.0",
37 | "vue-cli-plugin-element": "~1.0.1",
38 | "vue-template-compiler": "^2.6.11"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import Home from '../views/Home.vue'
4 | import Login from '../views/Login.vue'
5 | import NotFound from '../views/NotFound.vue'
6 |
7 | Vue.use(VueRouter)
8 | const routes = [
9 | {
10 | path: '/',
11 | name: 'Home',
12 | component: Home,
13 | redirect: '/menu/one',
14 | children: [
15 | {
16 | path: '/menu/one',
17 | component: () => import('@/views/Page1.vue')
18 | }, {
19 | path: '/menu/two',
20 | component: () => import('@/views/Page1.vue')
21 | }, {
22 | path: '/menu/three',
23 | component: () => import('@/views/Page1.vue')
24 | }, {
25 | path: '/menu/four',
26 | component: () => import('@/views/Page1.vue')
27 | }, {
28 | path: '/menu/five',
29 | component: () => import('@/views/Page1.vue')
30 | }
31 | ]
32 | },
33 | {
34 | path: '/login',
35 | name: 'Login',
36 | component: Login
37 | },
38 | {
39 | path: '*',
40 | name: 'NotFound',
41 | component: NotFound
42 | }
43 | ]
44 |
45 | const router = new VueRouter({
46 | routes
47 | })
48 |
49 | const originalPush = VueRouter.prototype.push
50 | // 重写了原型上的push方法,统一的处理了错误信息
51 | VueRouter.prototype.push = function push (location) {
52 | return originalPush.call(this, location).catch(err => err)
53 | }
54 |
55 | export default router
56 |
--------------------------------------------------------------------------------
/src/components/Aside.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 | 导航一
11 |
12 | 菜单栏1
13 | 菜单栏2
14 | 菜单栏3
15 |
16 |
17 |
18 |
19 | 导航二
20 |
21 | 菜单栏4
22 | 菜单栏5
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### 介绍
2 |
3 | > 这是一个用来练习Vue后台权限管理的简单模板,是我看了一个[B站视频](https://www.bilibili.com/video/BV15Q4y1K79c?t=5306)时,苦于没资料,于是自己花了点时间搭了一个,这里做来分享,分为两版:
4 |
5 | * 基础模板:有基本操作,mock等功能了,没实现权限判定;
6 | * 实现模板:基于基础模板做了权限的判定。
7 |
8 | ### 安装&运行
9 |
10 | 1. 克隆项目
11 |
12 | `git clone git@github.com:Aizener/admin-permission.git`
13 |
14 | 这个直接把基础模板和实现的模板都克隆下来的,develop的是基础模板,master的实现模板。
15 |
16 | 2. 安装依赖:
17 |
18 | 执行:`yarn`或者`yarn install`;
19 |
20 | 3. 基础模板:
21 |
22 | 基础模板在`develop`分支,可以通过`git checkout develop`来切换;
23 |
24 | 4. 实现模板:
25 |
26 | 基础模板在`master`分支, 可以通过`git checkout master`来切换;
27 |
28 | 5. 启动服务
29 |
30 | `yarn serve`
31 |
32 | 6. 效果图:
33 |
34 |
35 |
36 | ### 基本实现
37 |
38 | 这里说一下这个Demo的实现,通过什么技术,用的哪些知识点完成开发。以及,关于对于权限管理的一些理解与实现,大体分为以下几类:
39 |
40 | #### 视图
41 |
42 | 模板是通过[ElementUI](https://element.eleme.cn/)来搭建的,这个基于的UI组件库非常好用。
43 |
44 | #### 数据Mock
45 |
46 | 数据是通过[mockjs](http://mockjs.com/)来实现的,但是因为这个库好像不能模仿响应状态码,不过用起来还是挺方便的。
47 |
48 | #### 请求
49 |
50 | 请求数据的库,我使用的是[axios](http://www.axios-js.com/)这个库,这个库使用也非常的方便简单。
51 |
52 | #### 数据保存
53 |
54 | 数据的保存通过Vuex和sesstionStorage来实现的。
55 |
56 | ### 权限判断
57 |
58 | 基本实现知识完成了一些基本的东西,权限判断的话,前端主要是分为一下四类:
59 |
60 | #### 路由级别的判定
61 |
62 | 通过**beforeEach**全局守卫钩子,来进行token验证,是否能通过路由来登入其他页面;
63 |
64 | #### 菜单级别的判定
65 |
66 | 通过后端返回的`json`菜单权限数据,进行动态渲染,这里通过`router.addRoutes`动态添加菜单,没有的就不会出现了;
67 |
68 | #### 元素级别的判定
69 |
70 | 通过后端返回的`json`操作权限数据,绑定在`router`元信息上,再通过`this.$route.meta`在页面取出对应权限。再通过自定义指令的实现完成禁止、移除等。
71 |
72 | #### 请求级别的判定
73 |
74 | 通过axios的拦截器,判定某个用户在某个页面的操作是否有权限,通过`router.currentRoute`获取元信息来判定。
75 |
76 | ### 补充(关于代码的一些东西)
77 |
78 | 用户有两个,一个是普通用户,一个是管理员
79 |
80 | - 普通用户用户名就是:普通用户,密码是:normal;
81 | - 管理员用户名就是:管理员,密码是:admin。
82 |
83 | 权限判定的指令使用:
84 |
85 | `v-permission.disabled="condition"` 如果condition为true,则禁用当前绑定指令的标签;
86 |
87 | `v-permission.remove="condition"` 如果condition为true,则移除当前绑定指令的标签。
88 |
89 | 结尾:大体就这些了,接口方面已经写好交互了。
90 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
43 |
44 |
86 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 用户登录
5 |
6 | :
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | :
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 登录
23 |
24 |
25 |
26 |
27 |
28 |
77 |
78 |
--------------------------------------------------------------------------------
/src/components/main/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 取消
24 | {{ isAdd ? '添加' : '修改' }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
82 |
83 |
--------------------------------------------------------------------------------
/src/mock/index.js:
--------------------------------------------------------------------------------
1 | // 使用 Mock
2 | const Mock = require('mockjs')
3 | require('./my-radom')
4 | const Random = Mock.Random
5 | Mock.setup({
6 | timeout: '500-1000'
7 | })
8 |
9 | const list = []
10 |
11 | for (let i = 0; i < 20; i++) {
12 | list.push({
13 | id: i + 1,
14 | date: Random.date(),
15 | name: Random.cname(),
16 | address: Random.address(),
17 | likes: Random.likes()
18 | })
19 | }
20 |
21 | const users = [
22 | {
23 | id: 1,
24 | username: '普通用户',
25 | password: 'normal',
26 | token: 'abcdefghijklmnopqrstuvwxyz',
27 | rights: [{
28 | id: 1,
29 | authName: '一级菜单',
30 | icon: 'icon-menu',
31 | children: [{
32 | id: 11,
33 | authName: '一级项目1',
34 | path: '/',
35 | rights: ['view', 'edit', 'add', 'delete']
36 | }, {
37 | id: 11,
38 | authName: '一级项目2',
39 | path: '/',
40 | rights: ['view']
41 | }]
42 | }]
43 | },
44 | {
45 | id: 2,
46 | username: '管理员',
47 | password: 'admin',
48 | token: 'abcdefghijklmnopqrstuvwxyz'.split('').reverse().join(''),
49 | rights: [{
50 | id: 1,
51 | authName: '一级菜单',
52 | icon: 'icon-menu',
53 | children: [{
54 | id: 11,
55 | authName: '一级项目1',
56 | path: '/',
57 | rights: ['view', 'edit', 'add', 'delete']
58 | }, {
59 | id: 11,
60 | authName: '一级项目2',
61 | path: '/',
62 | rights: ['view', 'edit', 'add', 'delete']
63 | }]
64 | }, {
65 | id: 2,
66 | authName: '二级菜单',
67 | icon: 'icon-menu',
68 | children: [{
69 | id: 22,
70 | authName: '二级项目1',
71 | path: '/',
72 | rights: ['view', 'edit', 'add', 'delete']
73 | }]
74 | }]
75 | }
76 | ]
77 |
78 | // 获取列表
79 | Mock.mock('/list', 'get', options => {
80 | const { current } = JSON.parse(options.body)
81 | return list.slice(((current - 1) * 10), current * 10)
82 | })
83 |
84 | // 总数
85 | Mock.mock('/list/total', 'get', () => {
86 | return list.length
87 | })
88 |
89 | // 查询
90 | Mock.mock('/list/value', 'get', options => {
91 | const { value } = JSON.parse(options.body)
92 | const _list = list.filter(item => {
93 | if (item.name.includes(value) || item.address.includes(value) || item.likes.includes(value)) {
94 | return true
95 | }
96 | return false
97 | })
98 | return {
99 | list: _list,
100 | total: _list.length
101 | }
102 | })
103 |
104 | // 添加
105 | Mock.mock('/list/add', 'post', options => {
106 | const { rowData } = JSON.parse(options.body)
107 | rowData.id = list[list.length - 1].id + 1
108 | rowData.date = new Date().toLocaleDateString().replace(/\//g, '-')
109 | list.unshift(rowData)
110 | return rowData
111 | })
112 |
113 | // 修改
114 | Mock.mock('/list/update', 'put', options => {
115 | const { rowData } = JSON.parse(options.body)
116 | let _rowData = {}
117 | list.forEach((item, idx) => {
118 | if (item.id === rowData.id) {
119 | _rowData = rowData
120 | list[idx] = rowData
121 | }
122 | })
123 | return _rowData
124 | })
125 |
126 | // 删除
127 | Mock.mock('/list/delete', 'delete', options => {
128 | const { id } = JSON.parse(options.body)
129 | const index = list.findIndex(item => item.id === id)
130 | const item = index > 0 ? list[index] : {}
131 | list.splice(index, 1)
132 | return item
133 | })
134 |
135 | // 用户登录
136 | Mock.mock('/login', 'post', options => {
137 | const { username, password } = JSON.parse(options.body)
138 | const user = users.find(item => {
139 | return item.username === username && item.password === password
140 | })
141 | return user
142 | })
--------------------------------------------------------------------------------
/src/views/Page1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
14 |
18 |
19 |
23 |
24 |
27 |
28 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 |
197 |
198 |
--------------------------------------------------------------------------------