完成留言管理功能
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>