Merge pull request #822 from jumpserver/pr@dev@feat_subscription

feat: 系统消息订阅页面
This commit is contained in:
老广
2021-06-08 11:23:14 +08:00
committed by GitHub
13 changed files with 366 additions and 66 deletions

BIN
dump.rdb Normal file

Binary file not shown.

View File

@@ -46,6 +46,7 @@
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0", "lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0", "lodash.values": "^4.3.0",
"moment": "^2.29.1",
"moment-parseformat": "^3.0.0", "moment-parseformat": "^3.0.0",
"normalize.css": "7.0.0", "normalize.css": "7.0.0",
"npm": "^7.8.0", "npm": "^7.8.0",

View File

@@ -74,7 +74,7 @@ export default {
} }
.dialog-footer { .dialog-footer {
padding-right: 50px; padding-right: 20px;
} }
</style> </style>

View File

@@ -649,7 +649,8 @@
"Users": "用户管理", "Users": "用户管理",
"WebFTP": "文件管理", "WebFTP": "文件管理",
"WebTerminal": "Web终端", "WebTerminal": "Web终端",
"Notifications": "通知" "Notifications": "通知",
"SiteMessageList": "站内信"
}, },
"sessions": { "sessions": {
"StorageConfiguration": "存储配置", "StorageConfiguration": "存储配置",
@@ -1034,7 +1035,15 @@
"MessageType": "消息类型", "MessageType": "消息类型",
"Receivers": "接收人", "Receivers": "接收人",
"Subscription": "消息订阅", "Subscription": "消息订阅",
"ChangeReceiver": "修改消息接收人" "ChangeReceiver": "修改消息接收人",
"Subject": "主题",
"Message": "消息",
"DeliveryTime": "发送时间",
"HasRead": "是否已读",
"Sender": "发送人",
"MarkAsRead": "标记已读",
"NoUnreadMsg": "暂无未读消息",
"SiteMessage": "站内信"
}, },
"xpack": { "xpack": {
"Admin": "管理员", "Admin": "管理员",

View File

@@ -647,7 +647,8 @@
"Users": "Users", "Users": "Users",
"WebFTP": "WebFTP", "WebFTP": "WebFTP",
"WebTerminal": "Web Terminal", "WebTerminal": "Web Terminal",
"Notifications": "Notifications" "Notifications": "Notifications",
"SiteMessageList": "Site message"
}, },
"sessions": { "sessions": {
"StorageConfiguration": "Storage configuration", "StorageConfiguration": "Storage configuration",
@@ -1028,7 +1029,15 @@
"MessageType": "Message Type", "MessageType": "Message Type",
"Receivers": "Receivers", "Receivers": "Receivers",
"Subscription": "Subscription", "Subscription": "Subscription",
"ChangeReceiver": "Change Receivers" "ChangeReceiver": "Change Receivers",
"Subject": "Subject",
"Message": "Message",
"DeliveryTime": "Delivery time",
"HasRead": "Has read",
"Sender": "Sender",
"MarkAsRead": "Mark as read",
"NoUnreadMsg": "No unread messages",
"SiteMessage": "Site messages"
}, },
"xpack": { "xpack": {
"Admin": "Admin", "Admin": "Admin",

View File

@@ -10,7 +10,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'Language', name: 'Language',
data() { data() {
@@ -47,11 +46,22 @@ export default {
} }
}, },
mounted() { mounted() {
if (this.currentLang.code !== this.$i18n.locale) { this.changeLang()
this.changeLangTo(this.currentLang) this.changeMomentLang()
}
}, },
methods: { methods: {
changeLang() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
},
changeMomentLang() {
if (this.currentLang.code.indexOf('en') > -1) {
this.$moment.locale('en')
} else {
this.$moment.locale('zh-cn')
}
},
changeLangTo(item) { changeLangTo(item) {
this.$i18n.locale = item.code this.$i18n.locale = item.code
localStorage.setItem('lang', item.code) localStorage.setItem('lang', item.code)

View File

@@ -0,0 +1,245 @@
<template>
<div>
<el-badge :value="unreadMsgCount" :hidden="unreadMsgCount === 0" :max="99" size="mini" type="primary">
<a style="color: #606266 !important; width: 30px" @click="toggleDrawer">
<i class="el-icon-message" style="font-size: 18px" />
</a>
</el-badge>
<el-drawer
:visible.sync="show"
:before-close="handleClose"
:modal="false"
:title="$t('notifications.SiteMessage')"
custom-class="site-msg"
size="25%"
@open="getMessages"
>
<div v-if="unreadMsgCount !== 0" class="msg-list">
<div
v-for="msg of messages"
:key="msg.id"
class="msg-item"
@mouseover="hoverMsgId = msg.id"
@mouseleave="hoverMsgId = ''"
@click="showMsgDetail(msg)"
>
<div class="msg-item-head">
<span class="msg-item-head-type">{{ msg.subject }}</span>
<span v-if="hoverMsgId !== msg.id" class="msg-item-head-time">
{{ formatDate(msg.date_created) }}
</span>
<div v-else class="msg-item-read-btn" @click.stop="markAsRead(msg)">
<a>{{ $t('notifications.MarkAsRead') }}</a>
</div>
</div>
<div class="msg-item-txt">
{{ msg.message }}
</div>
</div>
</div>
<div v-else class="no-msg">
{{ $t('notifications.NoUnreadMsg') }}
</div>
</el-drawer>
<Dialog
v-if="msgDetailVisible"
:visible.sync="msgDetailVisible"
:title="''"
:close-on-click-modal="false"
:confirm-title="$t('notifications.MarkAsRead')"
@confirm="markAsRead(currentMsg)"
@cancel="cancelRead"
>
<div class="msg-detail">
<div class="msg-detail-head">
<h3>{{ currentMsg.subject }}</h3>
<h5>
<span class="msg-detail-time">{{ formatDate(currentMsg.date_created) }}</span>
</h5>
</div>
<div class="msg-detail-txt">
{{ currentMsg.message }}
</div>
</div>
</Dialog>
</div>
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import Dialog from '@/components/Dialog'
export default {
name: 'SiteMessages',
components: { Dialog },
data() {
return {
show: false,
messages: [],
hoverMsgId: '',
msgDetailVisible: false,
currentMsg: null,
unreadMsgCount: 0
}
},
mounted() {
this.enablePullMsgCount()
},
methods: {
handleClose() {
this.show = false
},
toggleDrawer() {
this.show = !this.show
},
showMsgDetail(msg) {
this.currentMsg = msg
this.msgDetailVisible = true
},
getMessages() {
const url = '/api/v1/notifications/site-message/?offset=0&limit=15&has_read=false'
this.$axios.get(url).then(resp => {
this.messages = [...resp.results]
this.unreadMsgCount = resp.count
})
},
formatDate(s) {
if (!s) {
return ''
}
const d = new Date(s)
const now = new Date()
if (now.getTime() - d.getTime() > (3600 * 24 * 7) * 1000) {
return toSafeLocalDateStr(s)
} else {
return this.$moment(d).fromNow()
}
},
markAsRead(msg) {
console.log(`${msg}`)
const url = `/api/v1/notifications/site-message/mark-as-read/`
this.$axios.patch(url, { ids: [msg.id] }).then(res => {
this.msgDetailVisible = false
this.getMessages()
}).catch(err => {
this.$message(err.detail)
})
},
cancelRead() {
this.msgDetailVisible = false
},
pullMsgCount() {
const url = '/api/v1/notifications/site-message/unread-total/'
this.$axios.get(url).then(res => {
this.unreadMsgCount = res.total
}).catch(err => {
this.$message(err.detail)
})
},
enablePullMsgCount() {
this.pullMsgCount()
setInterval(() => {
this.pullMsgCount()
}, 10000)
}
}
}
</script>
<style lang="scss" scoped>
.el-badge ::v-deep .el-badge__content.is-fixed{
top:10px;
}
.msg-list {
padding: 0 20px 20px;
}
>>> .site-msg .el-drawer__header {
border-bottom: solid 1px rgb(231, 234, 239);
margin-bottom: 0;
padding-top: 10px;
font-size: 16px;
}
>>> .site-msg {
margin-top: 50px;
}
.msg-item {
border-bottom: solid 1px rgb(231, 234, 239);
padding: 15px 0 10px;
position: relative;
border-bottom: 1px solid #ddd;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
padding: 15px 20px 10px;
margin: 0 -20px;
border-bottom: 1px solid #fff;
}
}
.msg-item-head {
line-height: 20px;
color: #888;
font-size: 12px;
&:after {
clear: both;
content: ".";
display: block;
height: 0;
overflow: hidden;
}
.msg-item-head-type {
float: left;
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.msg-item-head-time {
float: right;
}
.msg-item-read-btn {
float: right;
}
}
.msg-item-txt {
overflow: hidden;
color: #000;
padding: 4px 0 0;
line-height: 21px;
max-height: 21px;
display: -webkit-box;
font-size: 12px;
display: block;
}
.msg-detail {
padding-left: 20px;
.msg-detail-time {
font-weight: 400;
font-size: 12px;
line-height: 1.1;
}
.msg-detail-txt {
margin-bottom: 20px;
line-height: 25px;
}
}
.no-msg {
padding-top: 20px;
text-align: center;
}
>>> :focus{ outline:0; }
</style>

View File

@@ -3,30 +3,26 @@
<div class="navbar-header"> <div class="navbar-header">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
</div> </div>
<div class="navbar-right"> <ul class="navbar-right">
<div class="header-item"> <li class="header-item header-icon">
<SiteMessages />
</li>
<li class="header-item" style="margin-left: 10px">
<Help /> <Help />
</div> </li>
<div class="header-item"> <li class="header-item">
<Language /> <Language />
</div> </li>
<div <li v-if="showTickets" class="header-item">
v-if="
publicSettings.TICKETS_ENABLED
&& publicSettings.XPACK_LICENSE_IS_VALID
&& !isOrgAuditor
"
class="header-item"
>
<Tickets /> <Tickets />
</div> </li>
<div class="header-item"> <li class="header-item">
<WebTerminal /> <WebTerminal />
</div> </li>
<div class="header-item header-profile"> <li class="header-item header-profile">
<AccountDropdown /> <AccountDropdown />
</div> </li>
</div> </ul>
</div> </div>
</template> </template>
@@ -34,6 +30,7 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import AccountDropdown from './AccountDropdown' import AccountDropdown from './AccountDropdown'
import SiteMessages from './SiteMessages'
import Help from './Help' import Help from './Help'
import Language from './Language' import Language from './Language'
import WebTerminal from './WebTerminal' import WebTerminal from './WebTerminal'
@@ -48,7 +45,8 @@ export default {
Language, Language,
Help, Help,
Tickets, Tickets,
WebTerminal WebTerminal,
SiteMessages
}, },
data() { data() {
return { return {
@@ -60,13 +58,17 @@ export default {
]), ]),
isOrgAuditor() { isOrgAuditor() {
return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor') return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor')
},
showTickets() {
return this.publicSettings.TICKETS_ENABLED &&
this.publicSettings.XPACK_LICENSE_IS_VALID &&
!this.isOrgAuditor
} }
}, },
methods: { methods: {
toggleSideBar() { toggleSideBar() {
this.$store.dispatch('app/toggleSideBar') this.$store.dispatch('app/toggleSideBar')
} }
} }
} }
</script> </script>
@@ -91,12 +93,19 @@ export default {
.navbar-right { .navbar-right {
float: right; float: right;
margin-right: 10px; margin-right: 10px;
}
.header-item { .header-item {
line-height: 50px; line-height: 50px;
display: inline-block; display: inline-block;
padding-right: 20px; padding-right: 10px;
padding-left: 10px;
}
.header-icon {
&:hover {
background-color: #e6e6e6;
}
}
} }
.breadcrumb-container { .breadcrumb-container {
@@ -108,5 +117,9 @@ export default {
.el-header { .el-header {
background-color: #ffffff; background-color: #ffffff;
} }
ul {
margin: 0;
}
</style> </style>

View File

@@ -40,8 +40,13 @@ Vue.config.productionTip = false
import VueCookie from 'vue-cookie' import VueCookie from 'vue-cookie'
Vue.use(VueCookie) Vue.use(VueCookie)
window.$cookie = VueCookie window.$cookie = VueCookie
import VueMoment from 'vue-moment'
Vue.use(VueMoment) const moment = require('moment')
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
moment
})
// logger // logger
import VueLogger from 'vuejs-logger' import VueLogger from 'vuejs-logger'
import loggerOptions from './utils/logger' import loggerOptions from './utils/logger'

View File

@@ -64,7 +64,7 @@ export default {
const selectedUsers = this.selectedUsers.map(item => { const selectedUsers = this.selectedUsers.map(item => {
return { return {
id: item.id, id: item.id,
label: `${item.name}(${item.username})` label: item.name
} }
}) })
setTimeout(() => { setTimeout(() => {

View File

@@ -67,7 +67,7 @@ export default {
} }
this.$axios.patch( this.$axios.patch(
`/api/v1/notifications/system/subscriptions/${sub.id}/`, `/api/v1/notifications/system-msg-subscription/${sub.id}/`,
{ receive_backends: backends } { receive_backends: backends }
).catch(err => { ).catch(err => {
this.$log.error(err) this.$log.error(err)
@@ -78,11 +78,10 @@ export default {
onDialogSelectSubmit(userIds) { onDialogSelectSubmit(userIds) {
this.dialogVisible = false this.dialogVisible = false
this.$axios.patch( this.$axios.patch(
`/api/v1/notifications/system/subscriptions/${this.currentEditSub.id}/`, `/api/v1/notifications/system-msg-subscription/${this.currentEditSub.id}/`,
{ users: userIds } { users: userIds }
).then(newSub => { ).then(newSub => {
const msgType = this.idMessageTypeMapper[newSub.id] const msgType = this.idMessageTypeMapper[newSub.message_type]
msgType.users = newSub.users
msgType.receivers = newSub.receivers msgType.receivers = newSub.receivers
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
@@ -97,40 +96,37 @@ export default {
this.receiveBackends = await this.$axios.get('/api/v1/notifications/backends/') this.receiveBackends = await this.$axios.get('/api/v1/notifications/backends/')
}, },
async initSubscriptions() { async initSubscriptions() {
const subscriptions = await this.$axios.get('/api/v1/notifications/system/subscriptions/') const subscriptions = await this.$axios.get('/api/v1/notifications/system-msg-subscription/')
// 根据 app 分组 const trans_subscriptions = []
const appMessageTypesMapper = {}
subscriptions.forEach(sub => {
if (!(sub.message_category in appMessageTypesMapper)) {
appMessageTypesMapper[sub.message_category] = {
id: sub.message_category,
value: sub.message_category_label,
children: [sub]
}
} else {
appMessageTypesMapper[sub.message_category].children.push(sub)
}
})
// sub 改成需要的数据结构 for (const category of subscriptions) {
for (const app of Object.values(appMessageTypesMapper)) { const children = []
app.children = app.children.map(sub => { trans_subscriptions.push({
id: category.category,
value: category.category_label,
children: children
})
for (const sub of category.children) {
const backendsChecked = {} const backendsChecked = {}
this.receiveBackends.forEach(backend => { this.receiveBackends.forEach(backend => {
backendsChecked[backend.name] = sub.receive_backends.indexOf(backend.name) > -1 backendsChecked[backend.name] = sub.receive_backends.indexOf(backend.name) > -1
}) })
const subObj = {
id: sub.id, const trans_sub = {
value: sub.message_label, id: sub.message_type,
value: sub.message_type_label,
receivers: sub.receivers, receivers: sub.receivers,
receive_backends: backendsChecked receive_backends: backendsChecked
} }
this.idMessageTypeMapper[sub.id] = subObj children.push(trans_sub)
return subObj
}) this.idMessageTypeMapper[trans_sub.id] = trans_sub
}
} }
this.tableData = Object.values(appMessageTypesMapper)
this.tableData = trans_subscriptions
} }
} }
} }

View File

@@ -69,6 +69,10 @@ export default {
title: this.$t('setting.DingTalk'), title: this.$t('setting.DingTalk'),
name: 'DingTalk' name: 'DingTalk'
}, },
{
title: this.$t('setting.SystemMessageSubscription'),
name: 'SystemMessageSubscription'
},
{ {
title: this.$t('setting.Terminal'), title: this.$t('setting.Terminal'),
name: 'Terminal' name: 'Terminal'
@@ -119,6 +123,9 @@ export default {
case 'License': case 'License':
this.activeMenu = 'License' this.activeMenu = 'License'
break break
case 'SystemMessageSubscription':
this.activeMenu = 'SystemMessageSubscription'
break
default: default:
this.activeMenu = 'Basic' this.activeMenu = 'Basic'
break break

View File

@@ -7339,6 +7339,11 @@ moment@^2.19.2:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.npm.taobao.org/moment/download/moment-2.29.1.tgz?cache=0&sync_timestamp=1601983320283&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmoment%2Fdownload%2Fmoment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha1-sr52n6MZQL6e7qZGnAdeNQBvo9M=
move-concurrently@^1.0.1: move-concurrently@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"