mirror of
https://github.com/jumpserver/lina.git
synced 2025-08-31 22:48:27 +00:00
Merge pull request #822 from jumpserver/pr@dev@feat_subscription
feat: 系统消息订阅页面
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"lodash.values": "^4.3.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-parseformat": "^3.0.0",
|
||||
"normalize.css": "7.0.0",
|
||||
"npm": "^7.8.0",
|
||||
|
@@ -74,7 +74,7 @@ export default {
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding-right: 50px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -649,7 +649,8 @@
|
||||
"Users": "用户管理",
|
||||
"WebFTP": "文件管理",
|
||||
"WebTerminal": "Web终端",
|
||||
"Notifications": "通知"
|
||||
"Notifications": "通知",
|
||||
"SiteMessageList": "站内信"
|
||||
},
|
||||
"sessions": {
|
||||
"StorageConfiguration": "存储配置",
|
||||
@@ -1034,7 +1035,15 @@
|
||||
"MessageType": "消息类型",
|
||||
"Receivers": "接收人",
|
||||
"Subscription": "消息订阅",
|
||||
"ChangeReceiver": "修改消息接收人"
|
||||
"ChangeReceiver": "修改消息接收人",
|
||||
"Subject": "主题",
|
||||
"Message": "消息",
|
||||
"DeliveryTime": "发送时间",
|
||||
"HasRead": "是否已读",
|
||||
"Sender": "发送人",
|
||||
"MarkAsRead": "标记已读",
|
||||
"NoUnreadMsg": "暂无未读消息",
|
||||
"SiteMessage": "站内信"
|
||||
},
|
||||
"xpack": {
|
||||
"Admin": "管理员",
|
||||
|
@@ -647,7 +647,8 @@
|
||||
"Users": "Users",
|
||||
"WebFTP": "WebFTP",
|
||||
"WebTerminal": "Web Terminal",
|
||||
"Notifications": "Notifications"
|
||||
"Notifications": "Notifications",
|
||||
"SiteMessageList": "Site message"
|
||||
},
|
||||
"sessions": {
|
||||
"StorageConfiguration": "Storage configuration",
|
||||
@@ -1028,7 +1029,15 @@
|
||||
"MessageType": "Message Type",
|
||||
"Receivers": "Receivers",
|
||||
"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": {
|
||||
"Admin": "Admin",
|
||||
|
@@ -10,7 +10,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Language',
|
||||
data() {
|
||||
@@ -47,11 +46,22 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.changeLang()
|
||||
this.changeMomentLang()
|
||||
},
|
||||
methods: {
|
||||
changeLang() {
|
||||
if (this.currentLang.code !== this.$i18n.locale) {
|
||||
this.changeLangTo(this.currentLang)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeMomentLang() {
|
||||
if (this.currentLang.code.indexOf('en') > -1) {
|
||||
this.$moment.locale('en')
|
||||
} else {
|
||||
this.$moment.locale('zh-cn')
|
||||
}
|
||||
},
|
||||
changeLangTo(item) {
|
||||
this.$i18n.locale = item.code
|
||||
localStorage.setItem('lang', item.code)
|
||||
|
245
src/layout/components/NavHeader/SiteMessages.vue
Normal file
245
src/layout/components/NavHeader/SiteMessages.vue
Normal 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>
|
@@ -3,30 +3,26 @@
|
||||
<div class="navbar-header">
|
||||
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div class="header-item">
|
||||
<ul class="navbar-right">
|
||||
<li class="header-item header-icon">
|
||||
<SiteMessages />
|
||||
</li>
|
||||
<li class="header-item" style="margin-left: 10px">
|
||||
<Help />
|
||||
</div>
|
||||
<div class="header-item">
|
||||
</li>
|
||||
<li class="header-item">
|
||||
<Language />
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
publicSettings.TICKETS_ENABLED
|
||||
&& publicSettings.XPACK_LICENSE_IS_VALID
|
||||
&& !isOrgAuditor
|
||||
"
|
||||
class="header-item"
|
||||
>
|
||||
</li>
|
||||
<li v-if="showTickets" class="header-item">
|
||||
<Tickets />
|
||||
</div>
|
||||
<div class="header-item">
|
||||
</li>
|
||||
<li class="header-item">
|
||||
<WebTerminal />
|
||||
</div>
|
||||
<div class="header-item header-profile">
|
||||
</li>
|
||||
<li class="header-item header-profile">
|
||||
<AccountDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,6 +30,7 @@
|
||||
import { mapGetters } from 'vuex'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import AccountDropdown from './AccountDropdown'
|
||||
import SiteMessages from './SiteMessages'
|
||||
import Help from './Help'
|
||||
import Language from './Language'
|
||||
import WebTerminal from './WebTerminal'
|
||||
@@ -48,7 +45,8 @@ export default {
|
||||
Language,
|
||||
Help,
|
||||
Tickets,
|
||||
WebTerminal
|
||||
WebTerminal,
|
||||
SiteMessages
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -60,13 +58,17 @@ export default {
|
||||
]),
|
||||
isOrgAuditor() {
|
||||
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: {
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -91,12 +93,19 @@ export default {
|
||||
.navbar-right {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header-item {
|
||||
line-height: 50px;
|
||||
display: inline-block;
|
||||
padding-right: 20px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
&:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
@@ -108,5 +117,9 @@ export default {
|
||||
.el-header {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -40,8 +40,13 @@ Vue.config.productionTip = false
|
||||
import VueCookie from 'vue-cookie'
|
||||
Vue.use(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
|
||||
import VueLogger from 'vuejs-logger'
|
||||
import loggerOptions from './utils/logger'
|
||||
|
@@ -64,7 +64,7 @@ export default {
|
||||
const selectedUsers = this.selectedUsers.map(item => {
|
||||
return {
|
||||
id: item.id,
|
||||
label: `${item.name}(${item.username})`
|
||||
label: item.name
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
|
@@ -67,7 +67,7 @@ export default {
|
||||
}
|
||||
|
||||
this.$axios.patch(
|
||||
`/api/v1/notifications/system/subscriptions/${sub.id}/`,
|
||||
`/api/v1/notifications/system-msg-subscription/${sub.id}/`,
|
||||
{ receive_backends: backends }
|
||||
).catch(err => {
|
||||
this.$log.error(err)
|
||||
@@ -78,11 +78,10 @@ export default {
|
||||
onDialogSelectSubmit(userIds) {
|
||||
this.dialogVisible = false
|
||||
this.$axios.patch(
|
||||
`/api/v1/notifications/system/subscriptions/${this.currentEditSub.id}/`,
|
||||
`/api/v1/notifications/system-msg-subscription/${this.currentEditSub.id}/`,
|
||||
{ users: userIds }
|
||||
).then(newSub => {
|
||||
const msgType = this.idMessageTypeMapper[newSub.id]
|
||||
msgType.users = newSub.users
|
||||
const msgType = this.idMessageTypeMapper[newSub.message_type]
|
||||
msgType.receivers = newSub.receivers
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
@@ -97,40 +96,37 @@ export default {
|
||||
this.receiveBackends = await this.$axios.get('/api/v1/notifications/backends/')
|
||||
},
|
||||
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 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)
|
||||
}
|
||||
const trans_subscriptions = []
|
||||
|
||||
for (const category of subscriptions) {
|
||||
const children = []
|
||||
trans_subscriptions.push({
|
||||
id: category.category,
|
||||
value: category.category_label,
|
||||
children: children
|
||||
})
|
||||
|
||||
// sub 改成需要的数据结构
|
||||
for (const app of Object.values(appMessageTypesMapper)) {
|
||||
app.children = app.children.map(sub => {
|
||||
for (const sub of category.children) {
|
||||
const backendsChecked = {}
|
||||
this.receiveBackends.forEach(backend => {
|
||||
backendsChecked[backend.name] = sub.receive_backends.indexOf(backend.name) > -1
|
||||
})
|
||||
const subObj = {
|
||||
id: sub.id,
|
||||
value: sub.message_label,
|
||||
|
||||
const trans_sub = {
|
||||
id: sub.message_type,
|
||||
value: sub.message_type_label,
|
||||
receivers: sub.receivers,
|
||||
receive_backends: backendsChecked
|
||||
}
|
||||
this.idMessageTypeMapper[sub.id] = subObj
|
||||
return subObj
|
||||
})
|
||||
children.push(trans_sub)
|
||||
|
||||
this.idMessageTypeMapper[trans_sub.id] = trans_sub
|
||||
}
|
||||
this.tableData = Object.values(appMessageTypesMapper)
|
||||
}
|
||||
|
||||
this.tableData = trans_subscriptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -69,6 +69,10 @@ export default {
|
||||
title: this.$t('setting.DingTalk'),
|
||||
name: 'DingTalk'
|
||||
},
|
||||
{
|
||||
title: this.$t('setting.SystemMessageSubscription'),
|
||||
name: 'SystemMessageSubscription'
|
||||
},
|
||||
{
|
||||
title: this.$t('setting.Terminal'),
|
||||
name: 'Terminal'
|
||||
@@ -119,6 +123,9 @@ export default {
|
||||
case 'License':
|
||||
this.activeMenu = 'License'
|
||||
break
|
||||
case 'SystemMessageSubscription':
|
||||
this.activeMenu = 'SystemMessageSubscription'
|
||||
break
|
||||
default:
|
||||
this.activeMenu = 'Basic'
|
||||
break
|
||||
|
@@ -7339,6 +7339,11 @@ moment@^2.19.2:
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
|
Reference in New Issue
Block a user