完成留言管理功能
diff --git a/frontend/src/api/feedback.js b/frontend/src/api/feedback.js
new file mode 100644
index 0000000..016794e
--- /dev/null
+++ b/frontend/src/api/feedback.js
@@ -0,0 +1,24 @@
+import request from '@/utils/request'
+
+export function getFeedbackList(query) {
+  return request({
+    url: '/feedback/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getFeedbackReply(fbid) {
+  return request({
+    url: '/feedback/reply/' + fbid,
+    method: 'get'
+  })
+}
+
+export function saveFeedbackReply(data) {
+  return request({
+    url: '/feedback/reply/save',
+    method: 'post',
+    data
+  })
+}
diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js
index b8b8741..aba990c 100644
--- a/frontend/src/api/user.js
+++ b/frontend/src/api/user.js
@@ -2,15 +2,25 @@
 
 export function login(data) {
   return request({
-    url: '/vue-element-admin/user/login',
+    url: '/login',
     method: 'post',
-    data
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded'
+    },
+    data,
+    transformRequest: [function(data) {
+      let ret = ''
+      for (const it in data) {
+        ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
+      }
+      return ret
+    }]
   })
 }
 
 export function getInfo(token) {
   return request({
-    url: '/vue-element-admin/user/info',
+    url: '/user/info',
     method: 'get',
     params: { token }
   })
@@ -18,7 +28,15 @@
 
 export function logout() {
   return request({
-    url: '/vue-element-admin/user/logout',
+    url: '/user/logout',
     method: 'post'
   })
 }
+
+export function getAuthMenu(token) {
+  return request({
+    url: '/user/resource',
+    method: 'get',
+    params: { token }
+  })
+}
diff --git a/frontend/src/components/Breadcrumb/index.vue b/frontend/src/components/Breadcrumb/index.vue
index e224ff7..8283946 100644
--- a/frontend/src/components/Breadcrumb/index.vue
+++ b/frontend/src/components/Breadcrumb/index.vue
@@ -2,7 +2,7 @@
   <el-breadcrumb class="app-breadcrumb" separator="/">
     <transition-group name="breadcrumb">
       <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
-        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1||index==levelList.length-2" class="no-redirect">{{ item.meta.title }}</span>
         <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
       </el-breadcrumb-item>
     </transition-group>
@@ -37,7 +37,7 @@
       const first = matched[0]
 
       if (!this.isDashboard(first)) {
-        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
+        matched = [{ path: '/', meta: { title: '首页' }}].concat(matched)
       }
 
       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
diff --git a/frontend/src/components/HeaderSearch/index.vue b/frontend/src/components/HeaderSearch/index.vue
index 6026ebb..9a30d1a 100644
--- a/frontend/src/components/HeaderSearch/index.vue
+++ b/frontend/src/components/HeaderSearch/index.vue
@@ -8,7 +8,7 @@
       filterable
       default-first-option
       remote
-      placeholder="Search"
+      placeholder="搜索功能"
       class="header-search-select"
       @change="change"
     >
diff --git a/frontend/src/components/RightPanel/index.vue b/frontend/src/components/RightPanel/index.vue
index 55e8c1e..f437c0c 100644
--- a/frontend/src/components/RightPanel/index.vue
+++ b/frontend/src/components/RightPanel/index.vue
@@ -2,9 +2,6 @@
   <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
     <div class="rightPanel-background" />
     <div class="rightPanel">
-      <div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
-        <i :class="show?'el-icon-close':'el-icon-setting'" />
-      </div>
       <div class="rightPanel-items">
         <slot />
       </div>
diff --git a/frontend/src/layout/components/Navbar.vue b/frontend/src/layout/components/Navbar.vue
index 37bc1e6..0b0ebeb 100644
--- a/frontend/src/layout/components/Navbar.vue
+++ b/frontend/src/layout/components/Navbar.vue
@@ -8,36 +8,20 @@
       <template v-if="device!=='mobile'">
         <search id="header-search" class="right-menu-item" />
 
-        <error-log class="errLog-container right-menu-item hover-effect" />
-
-        <screenfull id="screenfull" class="right-menu-item hover-effect" />
-
-        <el-tooltip content="Global Size" effect="dark" placement="bottom">
-          <size-select id="size-select" class="right-menu-item hover-effect" />
-        </el-tooltip>
-
       </template>
-
-      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
+      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
         <div class="avatar-wrapper">
-          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
-          <i class="el-icon-caret-bottom" />
+          <span v-html="getOperName()" />
         </div>
         <el-dropdown-menu slot="dropdown">
-          <router-link to="/profile/index">
-            <el-dropdown-item>Profile</el-dropdown-item>
+          <router-link to="/">
+            <el-dropdown-item>个人信息</el-dropdown-item>
           </router-link>
           <router-link to="/">
-            <el-dropdown-item>Dashboard</el-dropdown-item>
+            <el-dropdown-item>修改密码</el-dropdown-item>
           </router-link>
-          <a target="_blank" href="https://github.com/PanJiaChen/vue-element-admin/">
-            <el-dropdown-item>Github</el-dropdown-item>
-          </a>
-          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
-            <el-dropdown-item>Docs</el-dropdown-item>
-          </a>
           <el-dropdown-item divided @click.native="logout">
-            <span style="display:block;">Log Out</span>
+            <span style="display:block;">退出</span>
           </el-dropdown-item>
         </el-dropdown-menu>
       </el-dropdown>
@@ -47,20 +31,15 @@
 
 <script>
 import { mapGetters } from 'vuex'
+import user from '@/store/modules/user'
 import Breadcrumb from '@/components/Breadcrumb'
 import Hamburger from '@/components/Hamburger'
-import ErrorLog from '@/components/ErrorLog'
-import Screenfull from '@/components/Screenfull'
-import SizeSelect from '@/components/SizeSelect'
 import Search from '@/components/HeaderSearch'
 
 export default {
   components: {
     Breadcrumb,
     Hamburger,
-    ErrorLog,
-    Screenfull,
-    SizeSelect,
     Search
   },
   computed: {
@@ -75,8 +54,14 @@
       this.$store.dispatch('app/toggleSideBar')
     },
     async logout() {
-      await this.$store.dispatch('user/logout')
-      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+      this.$store.dispatch('user/logout').then(() => {
+        this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+      }).catch((response) => {
+        console.log(response)
+      })
+    },
+    getOperName() {
+      return user.state.name
     }
   }
 }
diff --git a/frontend/src/layout/components/Sidebar/Logo.vue b/frontend/src/layout/components/Sidebar/Logo.vue
index ac0c8d8..4979dc2 100644
--- a/frontend/src/layout/components/Sidebar/Logo.vue
+++ b/frontend/src/layout/components/Sidebar/Logo.vue
@@ -24,7 +24,7 @@
   },
   data() {
     return {
-      title: 'Vue Element Admin',
+      title: '门户系统',
       logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
     }
   }
diff --git a/frontend/src/main.js b/frontend/src/main.js
index b5fa135..e750af9 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -6,7 +6,7 @@
 
 import Element from 'element-ui'
 import './styles/element-variables.scss'
-import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
+// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
 
 import '@/styles/index.scss' // global css
 
@@ -34,8 +34,7 @@
 }
 
 Vue.use(Element, {
-  size: Cookies.get('size') || 'medium', // set element-ui default size
-  locale: enLang // 如果使用中文,无需设置,请删除
+  size: Cookies.get('size') || 'medium' // set element-ui default size
 })
 
 // register global utility filters
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 2be959d..bce74c5 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -7,10 +7,6 @@
 import Layout from '@/layout'
 
 /* Router Modules */
-import componentsRouter from './modules/components'
-import chartsRouter from './modules/charts'
-import tableRouter from './modules/table'
-import nestedRouter from './modules/nested'
 
 /**
  * Note: sub-menu only appear when route children.length >= 1
@@ -72,55 +68,7 @@
   },
   {
     path: '/',
-    component: Layout,
-    redirect: '/dashboard',
-    children: [
-      {
-        path: 'dashboard',
-        component: () => import('@/views/dashboard/index'),
-        name: 'Dashboard',
-        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
-      }
-    ]
-  },
-  {
-    path: '/documentation',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/documentation/index'),
-        name: 'Documentation',
-        meta: { title: 'Documentation', icon: 'documentation', affix: true }
-      }
-    ]
-  },
-  {
-    path: '/guide',
-    component: Layout,
-    redirect: '/guide/index',
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/guide/index'),
-        name: 'Guide',
-        meta: { title: 'Guide', icon: 'guide', noCache: true }
-      }
-    ]
-  },
-  {
-    path: '/profile',
-    component: Layout,
-    redirect: '/profile/index',
-    hidden: true,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/profile/index'),
-        name: 'Profile',
-        meta: { title: 'Profile', icon: 'user', noCache: true }
-      }
-    ]
+    component: Layout
   }
 ]
 
@@ -128,261 +76,7 @@
  * asyncRoutes
  * the routes that need to be dynamically loaded based on user roles
  */
-export const asyncRoutes = [
-  {
-    path: '/permission',
-    component: Layout,
-    redirect: '/permission/page',
-    alwaysShow: true, // will always show the root menu
-    name: 'Permission',
-    meta: {
-      title: 'Permission',
-      icon: 'lock',
-      roles: ['admin', 'editor'] // you can set roles in root nav
-    },
-    children: [
-      {
-        path: 'page',
-        component: () => import('@/views/permission/page'),
-        name: 'PagePermission',
-        meta: {
-          title: 'Page Permission',
-          roles: ['admin'] // or you can only set roles in sub nav
-        }
-      },
-      {
-        path: 'directive',
-        component: () => import('@/views/permission/directive'),
-        name: 'DirectivePermission',
-        meta: {
-          title: 'Directive Permission'
-          // if do not set roles, means: this page does not require permission
-        }
-      },
-      {
-        path: 'role',
-        component: () => import('@/views/permission/role'),
-        name: 'RolePermission',
-        meta: {
-          title: 'Role Permission',
-          roles: ['admin']
-        }
-      }
-    ]
-  },
-
-  {
-    path: '/icon',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/icons/index'),
-        name: 'Icons',
-        meta: { title: 'Icons', icon: 'icon', noCache: true }
-      }
-    ]
-  },
-
-  /** when your routing map is too long, you can split it into small modules **/
-  componentsRouter,
-  chartsRouter,
-  nestedRouter,
-  tableRouter,
-
-  {
-    path: '/example',
-    component: Layout,
-    redirect: '/example/list',
-    name: 'Example',
-    meta: {
-      title: 'Example',
-      icon: 'el-icon-s-help'
-    },
-    children: [
-      {
-        path: 'create',
-        component: () => import('@/views/example/create'),
-        name: 'CreateArticle',
-        meta: { title: 'Create Article', icon: 'edit' }
-      },
-      {
-        path: 'edit/:id(\\d+)',
-        component: () => import('@/views/example/edit'),
-        name: 'EditArticle',
-        meta: { title: 'Edit Article', noCache: true, activeMenu: '/example/list' },
-        hidden: true
-      },
-      {
-        path: 'list',
-        component: () => import('@/views/example/list'),
-        name: 'ArticleList',
-        meta: { title: 'Article List', icon: 'list' }
-      }
-    ]
-  },
-
-  {
-    path: '/tab',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/tab/index'),
-        name: 'Tab',
-        meta: { title: 'Tab', icon: 'tab' }
-      }
-    ]
-  },
-
-  {
-    path: '/error',
-    component: Layout,
-    redirect: 'noRedirect',
-    name: 'ErrorPages',
-    meta: {
-      title: 'Error Pages',
-      icon: '404'
-    },
-    children: [
-      {
-        path: '401',
-        component: () => import('@/views/error-page/401'),
-        name: 'Page401',
-        meta: { title: '401', noCache: true }
-      },
-      {
-        path: '404',
-        component: () => import('@/views/error-page/404'),
-        name: 'Page404',
-        meta: { title: '404', noCache: true }
-      }
-    ]
-  },
-
-  {
-    path: '/error-log',
-    component: Layout,
-    children: [
-      {
-        path: 'log',
-        component: () => import('@/views/error-log/index'),
-        name: 'ErrorLog',
-        meta: { title: 'Error Log', icon: 'bug' }
-      }
-    ]
-  },
-
-  {
-    path: '/excel',
-    component: Layout,
-    redirect: '/excel/export-excel',
-    name: 'Excel',
-    meta: {
-      title: 'Excel',
-      icon: 'excel'
-    },
-    children: [
-      {
-        path: 'export-excel',
-        component: () => import('@/views/excel/export-excel'),
-        name: 'ExportExcel',
-        meta: { title: 'Export Excel' }
-      },
-      {
-        path: 'export-selected-excel',
-        component: () => import('@/views/excel/select-excel'),
-        name: 'SelectExcel',
-        meta: { title: 'Export Selected' }
-      },
-      {
-        path: 'export-merge-header',
-        component: () => import('@/views/excel/merge-header'),
-        name: 'MergeHeader',
-        meta: { title: 'Merge Header' }
-      },
-      {
-        path: 'upload-excel',
-        component: () => import('@/views/excel/upload-excel'),
-        name: 'UploadExcel',
-        meta: { title: 'Upload Excel' }
-      }
-    ]
-  },
-
-  {
-    path: '/zip',
-    component: Layout,
-    redirect: '/zip/download',
-    alwaysShow: true,
-    name: 'Zip',
-    meta: { title: 'Zip', icon: 'zip' },
-    children: [
-      {
-        path: 'download',
-        component: () => import('@/views/zip/index'),
-        name: 'ExportZip',
-        meta: { title: 'Export Zip' }
-      }
-    ]
-  },
-
-  {
-    path: '/pdf',
-    component: Layout,
-    redirect: '/pdf/index',
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/pdf/index'),
-        name: 'PDF',
-        meta: { title: 'PDF', icon: 'pdf' }
-      }
-    ]
-  },
-  {
-    path: '/pdf/download',
-    component: () => import('@/views/pdf/download'),
-    hidden: true
-  },
-
-  {
-    path: '/theme',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/theme/index'),
-        name: 'Theme',
-        meta: { title: 'Theme', icon: 'theme' }
-      }
-    ]
-  },
-
-  {
-    path: '/clipboard',
-    component: Layout,
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/clipboard/index'),
-        name: 'ClipboardDemo',
-        meta: { title: 'Clipboard', icon: 'clipboard' }
-      }
-    ]
-  },
-
-  {
-    path: 'external-link',
-    component: Layout,
-    children: [
-      {
-        path: 'https://github.com/PanJiaChen/vue-element-admin',
-        meta: { title: 'External Link', icon: 'link' }
-      }
-    ]
-  },
-
+export let asyncRoutes = [
   // 404 page must be placed at the end !!!
   { path: '*', redirect: '/404', hidden: true }
 ]
@@ -398,6 +92,7 @@
 // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
 export function resetRouter() {
   const newRouter = createRouter()
+  asyncRoutes = []
   router.matcher = newRouter.matcher // reset router
 }
 
diff --git a/frontend/src/settings.js b/frontend/src/settings.js
index 1ebc7f2..0f32b7f 100644
--- a/frontend/src/settings.js
+++ b/frontend/src/settings.js
@@ -1,5 +1,5 @@
 module.exports = {
-  title: 'Vue Element Admin',
+  title: '门户系统',
 
   /**
    * @type {boolean} true | false
@@ -23,7 +23,7 @@
    * @type {boolean} true | false
    * @description Whether show the logo in sidebar
    */
-  sidebarLogo: false,
+  sidebarLogo: true,
 
   /**
    * @type {string | array} 'production' | ['production', 'development']
diff --git a/frontend/src/store/modules/permission.js b/frontend/src/store/modules/permission.js
index aeb5ee5..f1d5583 100644
--- a/frontend/src/store/modules/permission.js
+++ b/frontend/src/store/modules/permission.js
@@ -1,4 +1,6 @@
 import { asyncRoutes, constantRoutes } from '@/router'
+import { getAuthMenu } from '@/api/user'
+import Layout from '@/layout'
 
 /**
  * Use meta.role to determine if the current user has permission
@@ -14,6 +16,28 @@
 }
 
 /**
+ * 后台查询的菜单数据拼装成路由格式的数据
+ * @param routes
+ */
+export function generaMenu(routes, data) {
+  data.forEach(item => {
+    // alert(JSON.stringify(item))
+    const menu = {
+      path: item.respath === '#' ? item.resid + '_key' : item.respath,
+      component: item.respath === '#' ? Layout : (resolve) => require([`@/views${item.respath}/index`], resolve),
+      // hidden: true,
+      children: [],
+      name: 'menu_' + item.resid,
+      meta: { title: item.resname, id: item.resid, roles: ['admin'], icon: item.icon }
+    }
+    if (item.children) {
+      generaMenu(menu.children, item.children)
+    }
+    routes.push(menu)
+  })
+}
+
+/**
  * Filter asynchronous routing tables by recursion
  * @param routes asyncRoutes
  * @param roles
@@ -49,14 +73,26 @@
 const actions = {
   generateRoutes({ commit }, roles) {
     return new Promise(resolve => {
-      let accessedRoutes
-      if (roles.includes('admin')) {
-        accessedRoutes = asyncRoutes || []
-      } else {
-        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
-      }
-      commit('SET_ROUTES', accessedRoutes)
-      resolve(accessedRoutes)
+      const loadMenuData = []
+      // 先查询后台并返回左侧菜单数据并把数据添加到路由
+      getAuthMenu(state.token).then(response => {
+        const data = response.resource
+        Object.assign(loadMenuData, data)
+        generaMenu(asyncRoutes, loadMenuData)
+        let accessedRoutes
+        if (roles.includes('admin')) {
+          // alert(JSON.stringify(asyncRoutes))
+          accessedRoutes = asyncRoutes || []
+        } else {
+          accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
+        }
+        commit('SET_ROUTES', accessedRoutes)
+        resolve(accessedRoutes)
+
+        // generaMenu(asyncRoutes, data)
+      }).catch(error => {
+        console.log(error)
+      })
     })
   }
 }
diff --git a/frontend/src/store/modules/user.js b/frontend/src/store/modules/user.js
index 7800941..b8348bf 100644
--- a/frontend/src/store/modules/user.js
+++ b/frontend/src/store/modules/user.js
@@ -7,7 +7,8 @@
   name: '',
   avatar: '',
   introduction: '',
-  roles: []
+  roles: [],
+  url: ''
 }
 
 const mutations = {
@@ -25,6 +26,9 @@
   },
   SET_ROLES: (state, roles) => {
     state.roles = roles
+  },
+  SET_URL: (state, url) => {
+    state.url = url
   }
 }
 
@@ -33,8 +37,7 @@
   login({ commit }, userInfo) {
     const { username, password } = userInfo
     return new Promise((resolve, reject) => {
-      login({ username: username.trim(), password: password }).then(response => {
-        const { data } = response
+      login({ username: username.trim(), password: password }).then(data => {
         commit('SET_TOKEN', data.token)
         setToken(data.token)
         resolve()
@@ -51,20 +54,13 @@
         const { data } = response
 
         if (!data) {
-          reject('Verification failed, please Login again.')
+          reject('认证失败,请稍后重试')
         }
 
-        const { roles, name, avatar, introduction } = data
-
-        // roles must be a non-empty array
-        if (!roles || roles.length <= 0) {
-          reject('getInfo: roles must be a non-null array!')
-        }
-
+        const { name, roles, url } = data
         commit('SET_ROLES', roles)
         commit('SET_NAME', name)
-        commit('SET_AVATAR', avatar)
-        commit('SET_INTRODUCTION', introduction)
+        commit('SET_URL', url + '/')
         resolve(data)
       }).catch(error => {
         reject(error)
@@ -78,6 +74,7 @@
       logout(state.token).then(() => {
         commit('SET_TOKEN', '')
         commit('SET_ROLES', [])
+        commit('SET_URL', '')
         removeToken()
         resetRouter()
 
@@ -97,6 +94,7 @@
     return new Promise(resolve => {
       commit('SET_TOKEN', '')
       commit('SET_ROLES', [])
+      commit('SET_URL', '')
       removeToken()
       resolve()
     })
diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js
index 2fb95ac..7288f82 100644
--- a/frontend/src/utils/request.js
+++ b/frontend/src/utils/request.js
@@ -1,5 +1,6 @@
 import axios from 'axios'
-import { MessageBox, Message } from 'element-ui'
+import { Message } from 'element-ui'
+import router from '@/router'
 import store from '@/store'
 import { getToken } from '@/utils/auth'
 
@@ -19,7 +20,7 @@
       // let each request carry token
       // ['X-Token'] is a custom headers key
       // please modify it according to the actual situation
-      config.headers['X-Token'] = getToken()
+      config.headers['Authorization'] = 'Bearer ' + getToken()
     }
     return config
   },
@@ -44,41 +45,24 @@
    */
   response => {
     const res = response.data
-
     // if the custom code is not 20000, it is judged as an error.
-    if (res.code !== 20000) {
-      Message({
-        message: res.message || 'Error',
-        type: 'error',
-        duration: 5 * 1000
-      })
-
-      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
-      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
-        // to re-login
-        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
-          confirmButtonText: 'Re-Login',
-          cancelButtonText: 'Cancel',
-          type: 'warning'
-        }).then(() => {
-          store.dispatch('user/resetToken').then(() => {
-            location.reload()
-          })
-        })
-      }
-      return Promise.reject(new Error(res.message || 'Error'))
-    } else {
+    if (res.code === 200) {
       return res
+    } else {
+      return Promise.reject(res || 'Error')
     }
   },
   error => {
-    console.log('err' + error) // for debug
-    Message({
-      message: error.message,
-      type: 'error',
-      duration: 5 * 1000
-    })
-    return Promise.reject(error)
+    if (error.response.status === 401) {
+      Message({
+        message: '当前登录信息已过期,请重新登录',
+        type: 'error',
+        duration: 5 * 1000
+      })
+      router.push(`/login`)
+    } else {
+      return Promise.reject(error)
+    }
   }
 )
 
diff --git a/frontend/src/views/feedback/index.vue b/frontend/src/views/feedback/index.vue
new file mode 100644
index 0000000..125e87f
--- /dev/null
+++ b/frontend/src/views/feedback/index.vue
@@ -0,0 +1,451 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">留言用户</div>
+      <el-input
+        v-model="formData.username"
+        placeholder="用户名"
+        style="width: 350px;margin-right:50px"
+        class="filter-item"
+      />
+      <div class="filter-item" style="margin-right:15px">留言内容</div>
+      <el-input
+        v-model="formData.content"
+        placeholder="留言关键字"
+        style="width: 300px;margin-right:50px"
+        class="filter-item"
+      />
+    </div>
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">留言日期</div>
+      <el-date-picker
+        v-model="queryDate"
+        type="daterange"
+        align="left"
+        style="width:350px;margin-right:50px"
+        unlink-panels
+        range-separator="至"
+        start-placeholder="开始日期"
+        end-placeholder="结束日期"
+        value-format="yyyyMMdd"
+        :picker-options="pickerOptions"
+      />
+      <div class="filter-item" style="margin-right:15px">留言状态</div>
+      <el-select
+        v-model="formData.replystatus"
+        style="width:200px;margin-right:100px"
+      >
+        <el-option
+          v-for="item in statusOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+      <el-button
+        class="filter-item"
+        type="primary"
+        icon="el-icon-search"
+        @click="handleFilter()"
+      >
+        搜索
+      </el-button>
+      <el-button class="filter-item" type="info" @click="clearFilter">
+        清空
+      </el-button>
+    </div>
+    <el-table
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;margin-top:10px"
+    >
+      <el-table-column label="留言用户" width="150">
+        <template slot-scope="{row}">
+          <span>{{ row.username }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言内容" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.content }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" width="100">
+        <template slot-scope="{row}">
+          <el-tag v-if="row.replystatus==='0'" size="medium">待回复</el-tag>
+          <el-tag v-else type="success" size="medium">已回复</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言时间" align="center" width="160">
+        <template slot-scope="{row}">
+          <span>{{ dateFormat(row.fbtime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="100">
+        <template slot-scope="{row}">
+          <el-tooltip class="item" effect="dark" content="查看详情" placement="bottom">
+            <el-button icon="el-icon-search" circle size="mini" @click="openDetailDialog(row)" />
+          </el-tooltip>
+          <el-tooltip class="item" effect="dark" content="回复" placement="bottom">
+            <el-button type="primary" icon="el-icon-edit" circle size="mini" @click="openReplyDialog(row)" />
+          </el-tooltip>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="formData.pageno"
+      :limit.sync="formData.pagesize"
+      style="margin-top:0;"
+      @pagination="getFeedbackList"
+    />
+
+    <el-dialog
+      title="留言详情"
+      :visible.sync="detailDialogVisible"
+      width="60%"
+      top="5vh"
+    >
+      <el-table
+        :data="detailContent"
+        border
+        fit
+        :span-method="mergeCells"
+        style="width: 100%"
+        :header-cell-style="{background:'white'}"
+      >
+        <el-table-column align="center">
+          <template slot="header" slot-scope="{}">
+            <span>当前留言状态:
+              <span v-if="currentFeedback.replystatus === '0'" style="color:#409EFF">待回复</span>
+              <span v-else style="color:#67C23A">已回复</span>
+            </span>
+          </template>
+          <el-table-column label="留言用户" align="center" width="120px">
+            <template slot-scope="{row}">
+              <span style="font-weight:bold;color:#909399">{{ row.title }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column>
+            <template slot="header" slot-scope="{}">
+              <span style="font-weight: normal;color:#606266">{{ currentFeedback.username }}</span>
+            </template>
+            <template slot-scope="{row}">
+              <div style="position:relative">
+                <div style="height:120px">{{ row.content }}</div>
+                <el-divider
+                  v-if="row.pictures && row.pictures.length!==0"
+                  style="margin:10px 0"
+                />
+                <div>
+                  <el-image
+                    v-for="(picture) in row.pictures"
+                    :key="picture.annexid"
+                    style="width: 100px; height: 100px;margin-left:10px"
+                    :src="picture.path"
+                    :preview-src-list="picture.previewList"
+                    fit="cover"
+                  >
+                    <div
+                      slot="error"
+                      style="text-align:center;
+                    vertical-align:middle;
+                    font-size:20px;
+                    padding-top:37px;
+                    width:100%;height:100%;background-color:#f4f7fa"
+                    >
+                      <i size="medium" class="el-icon-picture-outline" />
+                    </div>
+                  </el-image>
+                </div>
+                <div style="color:#909399;position:absolute;right:0;bottom:0;">{{ row.time }}</div>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="提交IP" align="center" />
+          <el-table-column>
+            <template slot="header" slot-scope="{}">
+              <span style="font-weight: normal;color:#606266">{{ currentFeedback.fbip }}</span>
+            </template>
+
+          </el-table-column>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+
+    <el-dialog
+      title="留言回复"
+      :visible.sync="replyDialogVisible"
+      width="45%"
+    >
+      <div>
+        <div class="filter-container">
+          <div class="filter-item" style="margin:10px 15px 0 0;vertical-align:top">回复内容</div>
+          <el-input
+            v-model="currentreply.replycontent"
+            :placeholder="'回复'+currentFeedback.username+':'"
+            :rows="7"
+            class="filter-item"
+            style="width:80%;"
+            type="textarea"
+            maxlength="180"
+            show-word-limit
+          />
+        </div>
+        <div style="text-align:center">
+          <el-button type="primary" @click="saveReply">保存</el-button>
+          <el-button @click="replyDialogVisible=false">取消</el-button>
+        </div>
+
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import {
+  getFeedbackList,
+  getFeedbackReply,
+  saveFeedbackReply
+} from '@/api/feedback'
+import moment from 'moment'
+import user from '@/store/modules/user'
+import Pagination from '@/components/Pagination'
+
+export default {
+  name: 'Feedback',
+  components: {
+    Pagination
+  },
+  data() {
+    return {
+      imageUrl: '',
+      imageList: [],
+      textarea: '',
+      listLoading: false,
+      tableKey: 0,
+      list: null,
+      currentFeedback: { username: '' },
+      detailContent: [
+        {
+          title: '留言内容',
+          content: '',
+          time: '',
+          pictures: []
+        },
+        {
+          title: '回复内容',
+          content: '',
+          time: ''
+        }
+      ],
+      total: 0,
+      detailDialogVisible: false,
+      replyDialogVisible: false,
+      queryDate: null,
+      formData: {
+        username: '',
+        content: '',
+        replystatus: '',
+        startdate: '',
+        enddate: '',
+        pageno: 1,
+        pagesize: 10
+      },
+      currentreply: {
+        replycontent: ''
+      },
+      statusOptions: [{
+        value: '',
+        label: '所有'
+      },
+      {
+        value: '0',
+        label: '待回复'
+      }, {
+        value: '1',
+        label: '已回复'
+      }],
+      pickerOptions: {
+        shortcuts: [{
+          text: '今天',
+          onClick(picker) {
+            const end = new Date()
+            const start = new Date()
+            picker.$emit('pick', [start, end])
+          }
+        }, {
+          text: '最近三天',
+          onClick(picker) {
+            const end = new Date()
+            const start = new Date()
+            start.setTime(start.getTime() - 3600 * 1000 * 24 * 3)
+            picker.$emit('pick', [start, end])
+          }
+        }, {
+          text: '最近一周',
+          onClick(picker) {
+            const end = new Date()
+            const start = new Date()
+            start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
+            picker.$emit('pick', [start, end])
+          }
+        }]
+      }
+    }
+  },
+  created() {
+    this.imageUrl = user.state.url
+    this.getFeedbackList()
+  },
+  methods: {
+    getFeedbackList() {
+      this.listLoading = true
+      const date = this.queryDate
+      if (date != null) {
+        this.formData.startdate = date[0]
+        this.formData.enddate = date[1]
+      } else {
+        this.formData.startdate = ''
+        this.formData.enddate = ''
+      }
+      getFeedbackList(this.formData).then(response => {
+        if (response.page) {
+          this.list = response.page.list
+          this.total = response.page.totalCount
+        } else {
+          this.list = null
+          this.total = 0
+        }
+        this.listLoading = false
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+        this.listLoading = false
+      })
+    },
+    handleFilter() {
+      this.formData.pageno = 1
+      this.getFeedbackList()
+    },
+    clearFilter() {
+      this.formData.username = ''
+      this.formData.content = ''
+      this.formData.replystatus = ''
+      this.queryDate = null
+    },
+    dateFormat(date) {
+      if (date === null) {
+        return ''
+      }
+      return moment(date, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss')
+    },
+    openDetailDialog(row) {
+      this.currentFeedback = row
+      const list = []
+      const previewList = []
+      for (let i = 0; i < row.pictures.length; i++) {
+        const item = {}
+        item.path = this.imageUrl + row.pictures[i].minpicid
+        list.push(item)
+        previewList.push(this.imageUrl + row.pictures[i].picid)
+      }
+      // 根据图片顺序(index)更改每张图片绑定的list的图片顺序
+      for (let i = 0; i < row.pictures.length; i++) {
+        const container = previewList
+        const frontArr = container.slice(0, i)
+        const behindArr = container.slice(i, row.pictures.length)
+        const concatList = behindArr.concat(frontArr)
+        list[i].previewList = concatList
+      }
+      const feedbackContent = {
+        title: '留言内容',
+        content: row.content,
+        time: row.fbtime === null ? '' : moment(row.fbtime, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss'),
+        pictures: list
+      }
+
+      this.$set(this.detailContent, 0, feedbackContent)
+
+      getFeedbackReply(row.fbid).then(response => {
+        let reply = {
+          replycontent: '',
+          updatetime: null
+        }
+        if (response.list) {
+          reply = response.list[0]
+        }
+        const replyContent = {
+          title: '回复内容',
+          content: reply.replycontent,
+          time: reply.updatetime === null ? '' : moment(reply.updatetime, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss')
+        }
+        this.$set(this.detailContent, 1, replyContent)
+        this.detailDialogVisible = true
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+    },
+    openReplyDialog(row) {
+      this.currentFeedback = row
+      this.currentreply = {
+        replycontent: '',
+        fbid: row.fbid,
+        replyid: null
+      }
+      getFeedbackReply(row.fbid).then(response => {
+        if (response.list) {
+          this.currentreply = response.list[0]
+        }
+        this.replyDialogVisible = true
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+    },
+    saveReply() {
+      if (this.currentreply.replycontent === '') {
+        this.$message('请输入回复内容')
+        return
+      }
+      saveFeedbackReply(this.currentreply).then(response => {
+        this.$notify({
+          title: '成功',
+          message: '回复留言成功!',
+          type: 'success',
+          duration: 2000
+        })
+        this.replyDialogVisible = false
+        this.getFeedbackList()
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+      })
+    },
+    mergeCells({ row, column, rowIndex, columnIndex }) {
+      if (columnIndex === 1) {
+        if (rowIndex === 0) {
+          return [1, 3]
+        } else {
+          return [2, 3]
+        }
+      }
+    }
+  }
+}
+
+</script>
+
diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue
index 2590640..2abcff9 100644
--- a/frontend/src/views/login/index.vue
+++ b/frontend/src/views/login/index.vue
@@ -45,7 +45,7 @@
         </el-form-item>
       </el-tooltip>
 
-      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
+      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
 
       <div style="position:relative">
         <div class="tips">
@@ -74,7 +74,6 @@
 </template>
 
 <script>
-import { validUsername } from '@/utils/validate'
 import SocialSign from './components/SocialSignin'
 
 export default {
@@ -82,11 +81,7 @@
   components: { SocialSign },
   data() {
     const validateUsername = (rule, value, callback) => {
-      if (!validUsername(value)) {
-        callback(new Error('Please enter the correct user name'))
-      } else {
-        callback()
-      }
+      callback()
     }
     const validatePassword = (rule, value, callback) => {
       if (value.length < 6) {
@@ -97,8 +92,8 @@
     }
     return {
       loginForm: {
-        username: 'admin',
-        password: '111111'
+        username: 'system',
+        password: '123456'
       },
       loginRules: {
         username: [{ required: true, trigger: 'blur', validator: validateUsername }],
@@ -156,12 +151,16 @@
       this.$refs.loginForm.validate(valid => {
         if (valid) {
           this.loading = true
-          this.$store.dispatch('user/login', this.loginForm)
+          this.$store.dispatch('user/login', { 'username': this.loginForm.username, 'password': this.loginForm.password })
             .then(() => {
               this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
               this.loading = false
             })
-            .catch(() => {
+            .catch((response) => {
+              this.$message({
+                message: response.msg || '请求异常',
+                type: 'error'
+              })
               this.loading = false
             })
         } else {
diff --git a/frontend/src/views/operator/index.vue b/frontend/src/views/operator/index.vue
new file mode 100644
index 0000000..5956c3f
--- /dev/null
+++ b/frontend/src/views/operator/index.vue
@@ -0,0 +1,9 @@
+
+<template>
+  <div class="app-container" /></template>
+<script>
+export default {
+  name: 'Operator'
+}
+</script>
+
diff --git a/frontend/src/views/pushmsg/index.vue b/frontend/src/views/pushmsg/index.vue
new file mode 100644
index 0000000..cd8e058
--- /dev/null
+++ b/frontend/src/views/pushmsg/index.vue
@@ -0,0 +1,8 @@
+<template>
+  <div>消息推送</div>
+</template>
+<script>
+export default {
+  name: 'PushMsg'
+}
+</script>