新增消息中心

This commit is contained in:
zhigang.li@tendcloud.com 2018-11-08 15:15:25 +08:00
parent e82724c16f
commit a72be82a9b
13 changed files with 480 additions and 15 deletions

View File

@ -10,7 +10,8 @@ module.exports = {
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }],
'no-undef': 'off'
'no-undef': 'off',
'camelcase': 'off'
},
parserOptions: {
parser: 'babel-eslint'

View File

@ -28,3 +28,50 @@ export const logout = (token) => {
method: 'post'
})
}
export const getMessage = () => {
return axios.request({
url: 'message/init',
method: 'get'
})
}
export const getContentByMsgId = msg_id => {
return axios.request({
url: 'message/content',
method: 'get',
params: {
msg_id
}
})
}
export const hasRead = msg_id => {
return axios.request({
url: 'message/has_read',
method: 'post',
data: {
msg_id
}
})
}
export const removeReaded = msg_id => {
return axios.request({
url: 'message/remove_readed',
method: 'post',
data: {
msg_id
}
})
}
export const restoreTrash = msg_id => {
return axios.request({
url: 'message/restore',
method: 'post',
data: {
msg_id
}
})
}

View File

@ -5,5 +5,8 @@
// height: 64px;
vertical-align: middle;
// line-height: 64px;
.ivu-badge-dot{
top: 16px;
}
}
}

View File

@ -1,9 +1,14 @@
<template>
<div class="user-avator-dropdown">
<Dropdown @on-click="handleClick">
<Avatar :src="userAvator"/>
<Badge :dot="!!messageUnreadCount">
<Avatar :src="userAvator"/>
</Badge>
<Icon :size="18" type="md-arrow-dropdown"></Icon>
<DropdownMenu slot="list">
<DropdownItem name="message">
消息中心<Badge style="margin-left: 10px" :count="messageUnreadCount"></Badge>
</DropdownItem>
<DropdownItem name="logout">退出登录</DropdownItem>
</DropdownMenu>
</Dropdown>
@ -19,20 +24,33 @@ export default {
userAvator: {
type: String,
default: ''
},
messageUnreadCount: {
type: Number,
default: 0
}
},
methods: {
...mapActions([
'handleLogOut'
]),
logout () {
this.handleLogOut().then(() => {
this.$router.push({
name: 'login'
})
})
},
message () {
this.$router.push({
name: 'message_page'
})
},
handleClick (name) {
switch (name) {
case 'logout':
this.handleLogOut().then(() => {
this.$router.push({
name: 'login'
})
})
case 'logout': this.logout()
break
case 'message': this.message()
break
}
}

View File

@ -12,7 +12,7 @@
<Layout>
<Header class="header-con">
<header-bar :collapsed="collapsed" @on-coll-change="handleCollapsedChange">
<user :user-avator="userAvator"/>
<user :message-unread-count="messageUnreadCount" :user-avator="userAvator"/>
<language v-if="$config.useI18n" @on-lang-change="setLocal" style="margin-right: 10px;" :lang="local"/>
<error-store v-if="$config.plugin['error-store'] && $config.plugin['error-store'].showInHeader" :has-read="hasReadErrorPage" :count="errorCount"></error-store>
<fullscreen v-model="isFullscreen" style="margin-right: 10px;"/>
@ -68,7 +68,8 @@ export default {
},
computed: {
...mapGetters([
'errorCount'
'errorCount',
'messageUnreadCount'
]),
tagNavList () {
return this.$store.state.app.tagNavList

View File

@ -35,5 +35,6 @@ export default {
error_logger_page: 'Error Logger',
query: 'Query',
params: 'Params',
cropper_page: 'Cropper'
cropper_page: 'Cropper',
message_page: 'Message Center'
}

View File

@ -35,5 +35,6 @@ export default {
error_logger_page: '错误日志',
query: '带参路由',
params: '动态路由',
cropper_page: '图片裁剪'
cropper_page: '图片裁剪',
message_page: '消息中心'
}

View File

@ -35,5 +35,6 @@ export default {
error_logger_page: '錯誤日誌',
query: '帶參路由',
params: '動態路由',
cropper_page: '圖片裁剪'
cropper_page: '圖片裁剪',
message_page: '消息中心'
}

View File

@ -1,6 +1,12 @@
import Mock from 'mockjs'
import { login, logout, getUserInfo } from './login'
import { getTableData, getDragList, uploadImage } from './data'
import { getMessageInit, getContentByMsgId, hasRead, removeReaded, restoreTrash } from './user'
// 配置Ajax请求延时可用来测试网络延迟大时项目中一些效果
Mock.setup({
timeout: 1000
})
// 登录相关和获取用户信息
Mock.mock(/\/login/, login)
@ -10,5 +16,10 @@ Mock.mock(/\/get_table_data/, getTableData)
Mock.mock(/\/get_drag_list/, getDragList)
Mock.mock(/\/save_error_logger/, 'success')
Mock.mock(/\/image\/upload/, uploadImage)
Mock.mock(/\/message\/init/, getMessageInit)
Mock.mock(/\/message\/content/, getContentByMsgId)
Mock.mock(/\/message\/has_read/, hasRead)
Mock.mock(/\/message\/remove_readed/, removeReaded)
Mock.mock(/\/message\/restore/, restoreTrash)
export default Mock

51
src/mock/user.js Normal file
View File

@ -0,0 +1,51 @@
import Mock from 'mockjs'
import { doCustomTimes } from '@/libs/util'
const Random = Mock.Random
export const getMessageInit = () => {
let unreadList = []
doCustomTimes(3, () => {
unreadList.push(Mock.mock({
title: Random.cword(10, 15),
create_time: '@date',
msg_id: Random.increment(100)
}))
})
let readedList = []
doCustomTimes(4, () => {
readedList.push(Mock.mock({
title: Random.cword(10, 15),
create_time: '@date',
msg_id: Random.increment(100)
}))
})
let trashList = []
doCustomTimes(2, () => {
trashList.push(Mock.mock({
title: Random.cword(10, 15),
create_time: '@date',
msg_id: Random.increment(100)
}))
})
return {
unread: unreadList,
readed: readedList,
trash: trashList
}
}
export const getContentByMsgId = () => {
return `<divcourier new',="" monospace;font-weight:="" normal;font-size:="" 12px;line-height:="" 18px;white-space:="" pre;"=""><div>&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="font-size: medium;">这是消息内容,这个内容是使用<span style="color: rgb(255, 255, 255); background-color: rgb(28, 72, 127);">富文本编辑器</span>编辑的,所以你可以看到一些<span style="text-decoration-line: underline; font-style: italic; color: rgb(194, 79, 74);">格式</span></span></div><ol><li>你可以查看Mock返回的数据格式和api请求的接口来确定你的后端接口的开发</li><li>使用你的真实接口后,前端页面基本不需要修改即可满足基本需求</li><li>快来试试吧</li></ol><p>${Random.csentence(100, 200)}</p></divcourier>`
}
export const hasRead = () => {
return true
}
export const removeReaded = () => {
return true
}
export const restoreTrash = () => {
return true
}

View File

@ -78,6 +78,26 @@ export default [
}
]
},
{
path: '/message',
name: 'message',
component: Main,
meta: {
hideInBread: true,
hideInMenu: true
},
children: [
{
path: 'message_page',
name: 'message_page',
meta: {
icon: 'md-notifications',
title: '消息中心'
},
component: () => import('@/view/single-page/message/index.vue')
}
]
},
{
path: '/components',
name: 'components',

View File

@ -1,4 +1,13 @@
import { login, logout, getUserInfo } from '@/api/user'
import {
login,
logout,
getUserInfo,
getMessage,
getContentByMsgId,
hasRead,
removeReaded,
restoreTrash
} from '@/api/user'
import { setToken, getToken } from '@/libs/util'
export default {
@ -8,7 +17,11 @@ export default {
avatorImgPath: '',
token: getToken(),
access: '',
hasGetInfo: false
hasGetInfo: false,
messageUnreadList: [],
messageReadedList: [],
messageTrashList: [],
messageContentStore: {}
},
mutations: {
setAvator (state, avatorPath) {
@ -29,8 +42,31 @@ export default {
},
setHasGetInfo (state, status) {
state.hasGetInfo = status
},
setMessageUnreadList (state, list) {
state.messageUnreadList = list
},
setMessageReadedList (state, list) {
state.messageReadedList = list
},
setMessageTrashList (state, list) {
state.messageTrashList = list
},
updateMessageContentStore (state, { msg_id, content }) {
state.messageContentStore[msg_id] = content
},
moveMsg (state, { from, to, msg_id }) {
const index = state[from].findIndex(_ => _.msg_id === msg_id)
const msgItem = state[from].splice(index, 1)[0]
msgItem.loading = false
state[to].unshift(msgItem)
}
},
getters: {
messageUnreadCount: state => state.messageUnreadList.length,
messageReadedCount: state => state.messageReadedList.length,
messageTrashCount: state => state.messageTrashList.length
},
actions: {
// 登录
handleLogin ({ commit }, {userName, password}) {
@ -83,6 +119,86 @@ export default {
reject(error)
}
})
},
// 获取消息列表,其中包含未读、已读、回收站三个列表
getMessageList ({ state, commit }) {
return new Promise((resolve, reject) => {
getMessage().then(res => {
const { unread, readed, trash } = res.data
commit('setMessageUnreadList', unread.sort((a, b) => new Date(b.create_time) - new Date(a.create_time)))
commit('setMessageReadedList', readed.map(_ => {
_.loading = false
return _
}).sort((a, b) => new Date(b.create_time) - new Date(a.create_time)))
commit('setMessageTrashList', trash.map(_ => {
_.loading = false
return _
}).sort((a, b) => new Date(b.create_time) - new Date(a.create_time)))
resolve()
}).catch(error => {
reject(error)
})
})
},
// 根据当前点击的消息的id获取内容
getContentByMsgId ({ state, commit }, { msg_id }) {
return new Promise((resolve, reject) => {
let contentItem = state.messageContentStore[msg_id]
if (contentItem) {
resolve(contentItem)
} else {
getContentByMsgId(msg_id).then(res => {
const content = res.data
commit('updateMessageContentStore', { msg_id, content })
resolve(content)
})
}
})
},
// 把一个未读消息标记为已读
hasRead ({ commit }, { msg_id }) {
return new Promise((resolve, reject) => {
hasRead(msg_id).then(() => {
commit('moveMsg', {
from: 'messageUnreadList',
to: 'messageReadedList',
msg_id
})
resolve()
}).catch(error => {
reject(error)
})
})
},
// 删除一个已读消息到回收站
removeReaded ({ commit }, { msg_id }) {
return new Promise((resolve, reject) => {
removeReaded(msg_id).then(() => {
commit('moveMsg', {
from: 'messageReadedList',
to: 'messageTrashList',
msg_id
})
resolve()
}).catch(error => {
reject(error)
})
})
},
// 还原一个已删除消息到已读消息
restoreTrash ({ commit }, { msg_id }) {
return new Promise((resolve, reject) => {
restoreTrash(msg_id).then(() => {
commit('moveMsg', {
from: 'messageTrashList',
to: 'messageReadedList',
msg_id
})
resolve()
}).catch(error => {
reject(error)
})
})
}
}
}

View File

@ -0,0 +1,194 @@
<template>
<Card shadow>
<div>
<div class="message-page-con message-category-con">
<Menu width="auto" active-name="unread" @on-select="handleSelect">
<MenuItem name="unread">
<span class="category-title">未读消息</span><Badge style="margin-left: 10px" :count="messageUnreadCount"></Badge>
</MenuItem>
<MenuItem name="readed">
<span class="category-title">已读消息</span><Badge style="margin-left: 10px" class-name="gray-dadge" :count="messageReadedCount"></Badge>
</MenuItem>
<MenuItem name="trash">
<span class="category-title">回收站</span><Badge style="margin-left: 10px" class-name="gray-dadge" :count="messageTrashCount"></Badge>
</MenuItem>
</Menu>
</div>
<div class="message-page-con message-list-con">
<Spin fix v-if="listLoading" size="large"></Spin>
<Menu
width="auto"
active-name=""
:class="titleClass"
@on-select="handleView"
>
<MenuItem v-for="item in messageList" :name="item.msg_id" :key="`msg_${item.msg_id}`">
<div>
<p class="msg-title">{{ item.title }}</p>
<Badge status="default" :text="item.create_time" />
<Button
style="float: right;margin-right: 20px;"
:style="{ display: item.loading ? 'inline-block !important' : '' }"
:loading="item.loading"
size="small"
:icon="currentMessageType === 'readed' ? 'md-trash' : 'md-redo'"
:title="currentMessageType === 'readed' ? '删除' : '还原'"
type="text"
v-show="currentMessageType !== 'unread'"
@click.native.stop="removeMsg(item)"></Button>
</div>
</MenuItem>
</Menu>
</div>
<div class="message-page-con message-view-con">
<Spin fix v-if="contentLoading" size="large"></Spin>
<div class="message-view-header">
<h2 class="message-view-title">{{ showingMsgItem.title }}</h2>
<time class="message-view-time">{{ showingMsgItem.create_time }}</time>
</div>
<div v-html="messageContent"></div>
</div>
</div>
</Card>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
const listDic = {
unread: 'messageUnreadList',
readed: 'messageReadedList',
trash: 'messageTrashList'
}
export default {
name: 'message_page',
data () {
return {
listLoading: true,
contentLoading: false,
currentMessageType: 'unread',
messageContent: '',
showingMsgItem: {}
}
},
computed: {
...mapState({
messageUnreadList: state => state.user.messageUnreadList,
messageReadedList: state => state.user.messageReadedList,
messageTrashList: state => state.user.messageTrashList,
messageList () {
return this[listDic[this.currentMessageType]]
},
titleClass () {
return {
'not-unread-list': this.currentMessageType !== 'unread'
}
}
}),
...mapGetters([
'messageUnreadCount',
'messageReadedCount',
'messageTrashCount'
])
},
methods: {
...mapMutations([
//
]),
...mapActions([
'getContentByMsgId',
'getMessageList',
'hasRead',
'removeReaded',
'restoreTrash'
]),
stopLoading (name) {
this[name] = false
},
handleSelect (name) {
this.currentMessageType = name
},
handleView (msg_id) {
this.contentLoading = true
this.getContentByMsgId({ msg_id }).then(content => {
this.messageContent = content
const item = this.messageList.find(item => item.msg_id === msg_id)
if (item) this.showingMsgItem = item
if (this.currentMessageType === 'unread') this.hasRead({ msg_id })
this.stopLoading('contentLoading')
}).catch(() => {
this.stopLoading('contentLoading')
})
},
removeMsg (item) {
item.loading = true
const msg_id = item.msg_id
if (this.currentMessageType === 'readed') this.removeReaded({ msg_id })
else this.restoreTrash({ msg_id })
}
},
mounted () {
this.listLoading = true
//
this.getMessageList().then(() => this.stopLoading('listLoading')).catch(() => this.stopLoading('listLoading'))
}
}
</script>
<style lang="less">
.message-page{
&-con{
height: ~"calc(100vh - 176px)";
display: inline-block;
vertical-align: top;
position: relative;
&.message-category-con{
border-right: 1px solid #e6e6e6;
width: 200px;
}
&.message-list-con{
border-right: 1px solid #e6e6e6;
width: 230px;
}
&.message-view-con{
position: absolute;
left: 446px;
top: 16px;
right: 16px;
bottom: 16px;
overflow: auto;
padding: 12px 20px 0;
.message-view-header{
margin-bottom: 20px;
.message-view-title{
display: inline-block;
}
.message-view-time{
margin-left: 20px;
}
}
}
.category-title{
display: inline-block;
width: 65px;
}
.gray-dadge{
background: gainsboro;
}
.not-unread-list{
.msg-title{
color: rgb(170, 169, 169);
}
.ivu-menu-item{
.ivu-btn.ivu-btn-text.ivu-btn-small.ivu-btn-icon-only{
display: none;
}
&:hover{
.ivu-btn.ivu-btn-text.ivu-btn-small.ivu-btn-icon-only{
display: inline-block;
}
}
}
}
}
}
</style>