add 站内通知、私信、websocket服务

This commit is contained in:
yxh 2024-01-02 17:51:46 +08:00
parent ab1d7bee42
commit f00cf6db39
12 changed files with 1672 additions and 108 deletions

View File

@ -0,0 +1,80 @@
import request from '/@/utils/request'
// 查询通知私信列表
export function listSysNotice(query:object) {
return request({
url: '/api/v1/system/sysNotice/list',
method: 'get',
params: query
})
}
export function listShowNotice(query:object) {
return request({
url: '/api/v1/system/sysNotice/listShow',
method: 'get',
params: query
})
}
// 查询通知私信详细
export function getSysNotice(id:number) {
return request({
url: '/api/v1/system/sysNotice/get',
method: 'get',
params: {
id: id.toString()
}
})
}
// 新增通知私信
export function addSysNotice(data:object) {
return request({
url: '/api/v1/system/sysNotice/add',
method: 'post',
data: data
})
}
// 修改通知私信
export function updateSysNotice(data:object) {
return request({
url: '/api/v1/system/sysNotice/edit',
method: 'put',
data: data
})
}
// 删除通知私信
export function delSysNotice(ids:number[]) {
return request({
url: '/api/v1/system/sysNotice/delete',
method: 'delete',
data:{
ids:ids
}
})
}
export function getIndexData(){
return request(
{
url:'/api/v1/system/sysNotice/getIndexData',
method:'get'
}
)
}
//获取维度通知私信公告信息
export function unReadCount(){
return request({
url:'/api/v1/system/sysNotice/unReadCount',
method:'get'
})
}
//获取私信待选择的用户
export function getUserList(query:string){
return request({
url:'/api/v1/system/sysNotice/userList',
method:'get',
params:{userNickname:query}
})
}

View File

@ -0,0 +1,52 @@
import request from '/@/utils/request'
// 查询已读记录列表
export function listSysNoticeRead(query:object) {
return request({
url: '/api/v1/system/sysNoticeRead/list',
method: 'get',
params: query
})
}
// 查询已读记录详细
export function getSysNoticeRead(id:number) {
return request({
url: '/api/v1/system/sysNoticeRead/get',
method: 'get',
params: {
id: id.toString()
}
})
}
// 新增已读记录
export function addSysNoticeRead(data:object) {
return request({
url: '/api/v1/system/sysNoticeRead/add',
method: 'post',
data: data
})
}
export function readNotice(data:object) {
return request({
url: '/api/v1/system/sysNoticeRead/readNotice',
method: 'post',
data: data
})
}
// 修改已读记录
export function updateSysNoticeRead(data:object) {
return request({
url: '/api/v1/system/sysNoticeRead/edit',
method: 'put',
data: data
})
}
// 删除已读记录
export function delSysNoticeRead(ids:number[]) {
return request({
url: '/api/v1/system/sysNoticeRead/delete',
method: 'delete',
data:{
ids:ids
}
})
}

View File

@ -36,7 +36,7 @@
<i class="fa-trash fa" title="清除缓存"></i> <i class="fa-trash fa" title="清除缓存"></i>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false"> <el-popover ref="newPopoverRef" placement="bottom" trigger="click" transition="el-zoom-in-top" :width="500" :persistent="false">
<template #reference> <template #reference>
<el-badge :is-dot="true"> <el-badge :is-dot="true">
<el-icon :title="$t('message.user.title4')"> <el-icon :title="$t('message.user.title4')">
@ -45,7 +45,7 @@
</el-badge> </el-badge>
</template> </template>
<template #default> <template #default>
<UserNews /> <UserNews @hideNews="hideNews" />
</template> </template>
</el-popover> </el-popover>
</div> </div>
@ -80,9 +80,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, getCurrentInstance, computed, reactive, toRefs, onMounted, defineComponent } from 'vue'; import {ref, getCurrentInstance, computed, reactive, toRefs, onMounted, defineComponent, watch} from 'vue';
import { useRouter } from 'vue-router'; import { useRoute,useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus'; import {ElMessageBox, ElMessage, ElNotification} from 'element-plus';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@ -94,6 +94,8 @@ import UserNews from '/@/layout/navBars/breadcrumb/userNews.vue';
import Search from '/@/layout/navBars/breadcrumb/search.vue'; import Search from '/@/layout/navBars/breadcrumb/search.vue';
import {logout} from "/@/api/login"; import {logout} from "/@/api/login";
import {removeCache} from "/@/api/system/cache"; import {removeCache} from "/@/api/system/cache";
import {noticeStore} from "/@/stores/noticeStore";
export default defineComponent({ export default defineComponent({
name: 'layoutBreadcrumbUser', name: 'layoutBreadcrumbUser',
@ -102,11 +104,13 @@ export default defineComponent({
const { t } = useI18n(); const { t } = useI18n();
const { proxy } = <any>getCurrentInstance(); const { proxy } = <any>getCurrentInstance();
const router = useRouter(); const router = useRouter();
const route = useRoute();
const stores = useUserInfo(); const stores = useUserInfo();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { userInfos } = storeToRefs(stores); const { userInfos } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const searchRef = ref(); const searchRef = ref();
const newPopoverRef=ref();
const state = reactive({ const state = reactive({
isScreenfull: false, isScreenfull: false,
disabledI18n: 'zh-cn', disabledI18n: 'zh-cn',
@ -194,6 +198,10 @@ export default defineComponent({
const onSearchClick = () => { const onSearchClick = () => {
searchRef.value.openSearch(); searchRef.value.openSearch();
}; };
const hideNews=()=>{
debugger
newPopoverRef.value.hide()
}
// //
const onComponentSizeChange = (size: string) => { const onComponentSizeChange = (size: string) => {
Local.remove('themeConfig'); Local.remove('themeConfig');
@ -253,6 +261,32 @@ export default defineComponent({
initComponentSize(); initComponentSize();
} }
}); });
const noticeStoreAct = noticeStore()
const getMessages = computed(() => {
return noticeStoreAct.message;
});
watch(getMessages,(nv,ov)=>{
if (!nv || !nv.id) {
return;
}
showNotice(nv)
},{ immediate: true, deep: true })
const showNotice = (data:any) => {
const eln = ElNotification({
title: '新消息',
message: `您有一条新消息:【${data.title}】,请点击查看详情。`,
type: 'warning',
duration:3600000,
onClick(){
if(route.fullPath=="/system/sysNotice/show?type="+data.type){
router.go(0)
}else{
router.push("/system/sysNotice/show?type="+data.type)
}
eln.close()
}
})
}
return { return {
userInfos, userInfos,
onLayoutSetingClick, onLayoutSetingClick,
@ -262,6 +296,7 @@ export default defineComponent({
onComponentSizeChange, onComponentSizeChange,
onLanguageChange, onLanguageChange,
removeCacheClick, removeCacheClick,
hideNews,
searchRef, searchRef,
layoutUserFlexNum, layoutUserFlexNum,
...toRefs(state), ...toRefs(state),

View File

@ -1,57 +1,246 @@
<template> <template>
<div>
<el-tabs v-model="tabsActive" @tab-change="handleTabChange">
<el-tab-pane :name="1">
<template v-slot:label>
<span style="font-size: 18px;font-weight: 500;">通知</span>
<el-badge type="warning" :value="count.notify" style="margin-left: 10px"/>
</template>
<div class="layout-navbars-breadcrumb-user-news"> <div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">{{ $t('message.user.newTitle') }}</div>
<div class="head-box-btn" v-if="newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div>
</div>
<div class="content-box"> <div class="content-box">
<template v-if="newsList.length > 0"> <template v-if="noticeList.length > 0">
<div class="content-box-item" v-for="(v, k) in newsList" :key="k"> <div class="content-box-item" v-for="(v, k) in noticeList" :key="k">
<div>{{ v.label }}</div> <div @click="handleRead(v)">
<div>{{ v.title }}</div>
<div class="content-box-msg"> <div class="content-box-msg">
{{ v.value }} {{ v.content }}
</div>
<span class="content-box-time">{{ v.createdAt }}</span>
<span style="float: right">
<el-tag type="success" effect="plain" v-if="v.isRead==true">已读</el-tag>
<el-tag type="danger" effect="plain" v-else>未读</el-tag>
</span>
</div> </div>
<div class="content-box-time">{{ v.time }}</div>
</div> </div>
</template> </template>
<el-empty :description="$t('message.user.newDesc')" v-else></el-empty> <el-empty :description="$t('message.user.newDesc')" v-else></el-empty>
</div> </div>
<div class="foot-box" @click="onGoToGiteeClick" v-if="newsList.length > 0">{{ $t('message.user.newGo') }}</div> <div class="foot-box" v-if="noticeList.length > 0">
<!-- <el-button @click="hendleClear('公告')" size="80">清空</el-button>-->
<!-- <el-button @click="hendleAllread('公告')" size="80">全部已读</el-button>-->
<el-button @click="hendleShowMore()" size="80">查看更多</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane :name="2">
<template v-slot:label>
<span style="font-size: 18px;font-weight: 500;"> 私信</span>
<el-badge type="danger" :value="count.notice" style="margin-left: 10px"/>
</template>
<div class="layout-navbars-breadcrumb-user-news">
<div class="content-box">
<template v-if="noticeList.length > 0">
<div class="content-box-item" v-for="(v, k) in noticeList" :key="k">
<div @click="handleRead(v)">
<div>{{ v.title }}</div>
<div class="content-box-msg" v-html="v.content">
</div>
<span class="content-box-time">{{ v.createdAt }}</span>
<span style="float: right">
<el-tag type="success" effect="plain" v-if="v.isRead==true">已读</el-tag>
<el-tag type="danger" effect="plain" v-else>未读</el-tag>
</span>
</div>
</div>
</template>
<el-empty :description="$t('message.user.newDesc')" v-else></el-empty>
</div>
<div class="foot-box" v-if="noticeList.length > 0">
<!-- <el-button @click="hendleClear('通知')" size="80">清空</el-button>-->
<!-- <el-button @click="hendleAllread('通知')" size="80">全部已读</el-button>-->
<el-button @click="hendleShowMore()" size="80">查看更多</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
<!--查看更多弹出框-->
<el-dialog v-model="isShowDialog" width="1200px" :close-on-click-modal="false" :destroy-on-close="true">
<el-tabs v-model="activeInfo" type="border-card">
<el-tab-pane :name="1">
<template v-slot:label>
<span style="font-weight: 600;font-size: 18px;color: #6c6c6c ">通知</span>
</template>
</el-tab-pane>
<el-tab-pane label="Config" :name="2">
<template v-slot:label>
<span style="font-weight: 600;font-size: 18px;color: #6c6c6c ">私信</span>
</template>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { reactive, toRefs, defineComponent } from 'vue'; import {reactive, toRefs, defineComponent, computed, watch} from 'vue';
import {
listShowNotice,
unReadCount,
} from "/@/api/system/notice/sysNotice";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import {readNotice} from "/@/api/system/notice/sysNoticeRead";
import {SysNoticeInfoData} from "/@/views/system/sysNotice/list/component/model";
import {useRouter} from "vue-router";
export default defineComponent({ export default defineComponent({
name: 'layoutBreadcrumbUserNews', name: 'layoutBreadcrumbUserNews',
created() {
this.getUnReadCount()
this.getData(1)
},
setup() { setup() {
const router = useRouter();
const state = reactive({ const state = reactive({
newsList: [ type1Num: 0,
{ type2Num: 0,
label: '关于版本发布的通知', // notifyList: [],
value: 'vue-next-admin基于 vue3 + CompositionAPI + typescript + vite + element plus正式发布时间2021年02月28日', noticeList: [],
time: '2020-12-08', count: {
notify: 0,
notice: 0
}, },
{ tabsActive: 1,
label: '关于学习交流的通知', activeInfo: 1,
value: 'QQ群号码 665452019欢迎小伙伴入群学习交流探讨', isShowDialog: false,
time: '2020-12-08', barName: "通知"
},
],
}); });
/** 改变tab*/
const handleTabChange = (tabName: number) => {
if (tabName === 1) {
state.barName = "通知"
} else {
state.barName = "公告"
}
getData(tabName)
};
//
const getUnReadCount = () => {
unReadCount().then((res: any) => {
if (res.data != null) {
state.count.notice = res.data.noticeCount || 0
state.count.notify = res.data.notifyCount || 0
}
})
}
//
const getData = (barName: number | undefined) => {
/* let notifyParam = {
pageNum: 1,
pageSize: 5,
type: barName,
}
listSysNotice(notifyParam).then((res: any) => {
console.log("listSysNotice",res)
state.notifyList = res.data.list || []
})*/
let noticeParam = {
pageNum: 1,
pageSize: 5,
type: barName,
}
listShowNotice(noticeParam).then((res: any) => {
state.noticeList = res.data.list || []
})
};
const readAllItem = () => {
}
// //
const onAllReadClick = () => { const onAllReadClick = () => {
state.newsList = []; state.noticeList = [];
}; };
// //
const onGoToGiteeClick = () => { const onGoToGiteeClick = () => {
window.open('https://gitee.com/tiger1103/gfast'); /*window.open('https://gitee.com/tiger1103/gfast');*/
state.isShowDialog = true
}; };
const hendleClear = (type: string) => {
ElMessageBox.confirm("是否清除全部" + type + "!", '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {/*
delSysNotice(id).then(() => {
ElMessage.success('删除成功');
})*/
let typeIndex = (type == "通知") ? 1 : 2
let query = {
type: typeIndex,
}
/* clearNews(query).then((res: any) => {
console.log(res)
ElMessage.success('清空成功');
})*/
})
.catch(() => {
});
};
const hendleAllread = (type: string) => {
ElMessageBox.confirm("是否将全部" + type + "标记为已读!", '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {/*
delSysNotice(id).then(() => {
ElMessage.success('删除成功');
})*/
})
.catch(() => {
});
};
const hendleShowMore = () => {
// console.log(emit)
router.push("/system/sysNotice/show")
};
const handleRead = (item: SysNoticeInfoData) => {
// console.log("handleRead", item)
let query = {
noticeId: item.id
}
readNotice(query).then(() => {
// console.log(res)
getData(item.type)
ElMessage.success("已读");
})
}
return { return {
getData,
readAllItem,
onAllReadClick, onAllReadClick,
onGoToGiteeClick, onGoToGiteeClick,
hendleClear,
hendleAllread,
hendleShowMore,
getUnReadCount,
handleRead,
...toRefs(state), ...toRefs(state),
handleTabChange
}; };
}, },
}); });
@ -59,41 +248,31 @@ export default defineComponent({
<style scoped lang="scss"> <style scoped lang="scss">
.layout-navbars-breadcrumb-user-news { .layout-navbars-breadcrumb-user-news {
.head-box {
display: flex;
border-bottom: 1px solid var(--el-border-color-lighter);
box-sizing: border-box;
color: var(--el-text-color-primary);
justify-content: space-between;
height: 35px;
align-items: center;
.head-box-btn {
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
.content-box { .content-box {
font-size: 13px; font-size: 13px;
.content-box-item { .content-box-item {
padding-top: 12px; padding-top: 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-of-type { &:last-of-type {
padding-bottom: 12px; padding-bottom: 12px;
} }
.content-box-msg { .content-box-msg {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px;
} }
.content-box-time { .content-box-time {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
} }
} }
} }
.foot-box { .foot-box {
height: 35px; height: 35px;
color: var(--el-color-primary); color: var(--el-color-primary);
@ -103,13 +282,27 @@ export default defineComponent({
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-top: 1px solid var(--el-border-color-lighter); /* border-top: 1px solid var(--el-border-color-lighter);*/
margin-top: 10px;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
} }
:deep(.el-empty__description p) { :deep(.el-empty__description p) {
font-size: 13px; font-size: 13px;
} }
} }
:deep(.el-tabs__active-bar) {
padding-left: 100px;
padding-right: 100px;
}
:deep(.el-tabs__item) {
width: 200px;
}
</style> </style>

View File

@ -13,6 +13,7 @@ import '/@/theme/index.scss';
import mitt from 'mitt'; import mitt from 'mitt';
import VueGridLayout from 'vue-grid-layout'; import VueGridLayout from 'vue-grid-layout';
import {getUpFileUrl, handleTree, parseTime, selectDictLabel} from '/@/utils/gfast'; import {getUpFileUrl, handleTree, parseTime, selectDictLabel} from '/@/utils/gfast';
import Websocket from '/@/utils/websocket';
import {useDict} from '/@/api/system/dict/data'; import {useDict} from '/@/api/system/dict/data';
import {getItems, setItems, getOptionValue, isEmpty} from '/@/api/items' import {getItems, setItems, getOptionValue, isEmpty} from '/@/api/items'
// 分页组件 // 分页组件
@ -27,6 +28,16 @@ import VueUeditorWrap from 'vue-ueditor-wrap';
const app = createApp(App); const app = createApp(App);
// 全局websocket
const onMessageList: Array<Function> = [];
app.provide('onMessageList', onMessageList);
const onMessage = (event: any) => {
onMessageList.forEach((f) => {
f.call(null, event);
});
};
Websocket(onMessage);
directive(app); directive(app);
other.elSvg(app); other.elSvg(app);

View File

@ -95,3 +95,13 @@ export interface ThemeConfigStates {
export interface bigUploadStates { export interface bigUploadStates {
panelShow : boolean panelShow : boolean
} }
export interface NoticeMessage{
id:number;
title:string;
tag:number;
type:number;
}
export interface NoticeStates{
message:NoticeMessage;
}

19
src/stores/noticeStore.ts Normal file
View File

@ -0,0 +1,19 @@
import { defineStore } from 'pinia';
import {NoticeMessage, NoticeStates} from "/@/stores/interface";
export const noticeStore = defineStore({
id: 'noticeStore',
state: (): NoticeStates => <NoticeStates>({
message: {}
}),
getters: {
getMessages(): NoticeMessage {
return this.message;
},
},
actions: {
setMessages(messages:NoticeMessage) {
this.message = messages;
},
},
});

169
src/utils/websocket.ts Normal file
View File

@ -0,0 +1,169 @@
import {getToken} from "/@/utils/gfast"
import {noticeStore} from "/@/stores/noticeStore";
const noticeStoreAct = noticeStore()
const webSocketURL = import.meta.env.VITE_WEBSOCKET_URL
let socket: WebSocket;
let isActive: boolean;
export function getSocket(): WebSocket {
if (socket === undefined) {
location.reload();
}
return socket;
}
export function getActive(): boolean {
return isActive;
}
export function sendMsg(event: string, data = null, isRetry = true) {
if (socket === undefined || !isActive) {
if (!isRetry) {
console.log('socket连接异常发送失败');
return;
}
console.log('socket连接异常等待重试..');
setTimeout(function () {
sendMsg(event, data);
}, 200);
return;
}
try {
socket.send(
JSON.stringify({
event: event,
data: data,
})
);
} catch (err) {
// @ts-ignore
console.log('ws发送消息失败等待重试err' + err.message);
if (!isRetry) {
return;
}
setTimeout(function () {
sendMsg(event, data);
}, 100);
}
}
export function addOnMessage(onMessageList: any, func: Function) {
let exist = false;
for (let i = 0; i < onMessageList.length; i++) {
if (onMessageList[i].name == func.name) {
onMessageList[i] = func;
exist = true;
}
}
if (!exist) {
onMessageList.push(func);
}
}
export default (onMessage: Function) => {
const heartCheck = {
timeout: 10000,
timeoutObj: setTimeout(() => {}),
serverTimeoutObj: setInterval(() => {}),
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function () {
socket.send(
JSON.stringify({
event: 'ping',
})
);
self.serverTimeoutObj = setTimeout(function () {
console.log('关闭服务');
socket.close();
}, self.timeout);
}, this.timeout);
},
};
let lockReconnect = false;
let timer: ReturnType<typeof setTimeout>;
const createSocket = () => {
console.log('createSocket...');
try {
if (getToken() === '') {
throw new Error('用户未登录,稍后重试...');
}
socket = new WebSocket(webSocketURL+'/api/v1/websocket?token='+encodeURIComponent(getToken()));
init();
} catch (e) {
console.log('createSocket err:' + e);
reconnect();
}
if (lockReconnect) {
lockReconnect = false;
}
};
const reconnect = () => {
console.log('lockReconnect:' + lockReconnect);
if (lockReconnect) return;
lockReconnect = true;
clearTimeout(timer);
timer = setTimeout(() => {
createSocket();
}, 30000);
};
const init = () => {
socket.onopen = function (_) {
console.log('WebSocket:已连接');
heartCheck.reset().start();
isActive = true;
};
socket.onmessage = function (event) {
isActive = true;
// console.log('WebSocket:收到一条消息', event.data);
let isHeart = false;
const message = JSON.parse(event.data);
if (message.event === 'ping') {
isHeart = true;
}
// 通知私信
if (message.event === 'notice') {
noticeStoreAct.setMessages(message.data)
return;
}
if (onMessage && !isHeart) {
onMessage.call(null, event);
}
heartCheck.reset().start();
};
socket.onerror = function (_) {
console.log('WebSocket:发生错误');
reconnect();
isActive = false;
};
socket.onclose = function (_) {
console.log('WebSocket:已关闭');
heartCheck.reset();
reconnect();
isActive = false;
};
window.onbeforeunload = function () {
socket.close();
isActive = false;
};
};
createSocket();
};

View File

@ -0,0 +1,300 @@
<template>
<div class="system-sysNotice-edit">
<!-- 添加或修改私信对话框 -->
<el-dialog v-model="isShowDialog" width="800px" :close-on-click-modal="false" :destroy-on-close="true">
<template #header>
<div v-drag="['.system-sysNotice-edit .el-dialog', '.system-sysNotice-edit .el-dialog__header']">
<span style="font-weight: 500;font-size: 18px;color: rgb(31 34 37);"> {{
(!formData.id || formData.id == 0 ? '发送' : '修改') + title
}} </span>
</div>
</template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题"/>
</el-form-item>
<!-- <el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型">
<el-option label="请选择字典生成" value=""/>
</el-select>
</el-form-item>-->
<el-form-item v-if="formData.type==2" label="接收用户">
<el-select
style="width: 100%;"
v-model="formData.receiver"
multiple
filterable
remote
reserve-keyword
placeholder="可输入要指定的用户名称搜索"
:remote-method="remoteUserMethod"
:loading="loading"
>
<el-option
v-for="item in userListOptions"
:key="item.id"
:label="item.userNickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="标签" prop="tag">
<el-select v-model="formData.tag" placeholder="请选择标签">
<el-option
v-for="dict in tagOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="内容">
<gf-ueditor editorId="ueSysNoticeContent" v-model="formData.content"
@setEditContent="setContentEditContent"></gf-ueditor>
</el-form-item>
<el-form-item label="状态" prop="status">
<!-- <el-radio-group v-model="formData.status">
<el-radio>请选择字典生成</el-radio>
</el-radio-group>-->
<el-switch
v-model="formData.status"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
:active-value="1"
:inactive-value="0"
active-text="正常"
inactive-text="停用"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input show-word-limit maxlength="200" v-model="formData.remark" type="textarea" placeholder="请输入备注"/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number type="num" v-model="formData.sort"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
<el-button @click="onCancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import {reactive, onMounted, toRefs, defineComponent, ref, unref, getCurrentInstance} from 'vue';
import { ElMessage} from 'element-plus';
import {
getSysNotice,
addSysNotice,
updateSysNotice, getUserList,
} from "/@/api/system/notice/sysNotice";
import GfUeditor from "/@/components/ueditor/index.vue"
import {
SysNoticeInfoData,
SysNoticeEditState
} from "/@/views/system/sysNotice/list/component/model"
export default defineComponent({
name: "NoticeMessageEdit",
components: {
GfUeditor,
},
props: {
tagOptions: {
type: Array,
default: () => []
},
},
setup(props, {emit}) {
const {proxy} = <any>getCurrentInstance()
const formRef = ref<HTMLElement | null>(null);
const menuRef = ref();
const state = reactive<SysNoticeEditState>({
loading: false,
isShowDialog: false,
title: "",
userListOptions: [],
formData: {
id: undefined,
receiver: undefined,
title: undefined,
type: undefined,
tag: undefined,
content: undefined,
remark: undefined,
sort: undefined,
status: 0,
createdBy: undefined,
updatedBy: undefined,
createdAt: undefined,
updatedAt: undefined,
deletedAt: undefined,
},
//
rules: {
id: [
{required: true, message: "ID不能为空", trigger: "blur"}
],
title: [
{required: true, message: "标题不能为空", trigger: "blur"}
],
type: [
{required: true, message: "类型不能为空", trigger: "blur"}
],
content: [
{required: true, message: "内容不能为空", trigger: "blur"}
],
status: [
{required: true, message: "状态不能为空", trigger: "blur"}
],
}
});
onMounted(() => {
remoteUserMethod("");
});
//
const openDialog = (row?: SysNoticeInfoData) => {
resetForm();
if (row) {
getSysNotice(row.id!).then((res: any) => {
const data = res.data;
data.type = parseInt(data.type)
if(data.type===2&&data.receiverUser){
const userListOptions = [...state.userListOptions,...data.receiverUser]
let uniqueSet = new Set(userListOptions.map(item => item.id));
state.userListOptions = userListOptions.filter((value, index, self) => {
return uniqueSet.has(value.id) && uniqueSet.delete(value.id)
});
}
data.tag = '' + data.tag
data.status = parseInt(data.status)
state.formData = data;
})
}
state.isShowDialog = true;
};
const remoteUserMethod = (query: string) => {
//console.log("remoteMethod", query)
state.userListOptions = []
getUserList(query).then((res: any) => {
/*console.log(res)*/
// let list:object[]
//list=res.data
state.userListOptions = res.data.userList
})
/* if (query) {
loading.value = true
setTimeout(() => {
loading.value = false
options.value = list.value.filter((item) => {
return item.label.toLowerCase().includes(query.toLowerCase())
})
}, 200)
} else {
options.value = []
}*/
}
//
const setType = (type: number) => {
state.formData.type = type
if (type == 1) {
state.title = "通知"
} else if (type == 2) {
state.title = "私信"
}
};
//
const closeDialog = () => {
state.isShowDialog = false;
};
//
const onCancel = () => {
closeDialog();
};
//
const onSubmit = () => {
const formWrap = unref(formRef) as any;
if (!formWrap) return;
formWrap.validate((valid: boolean) => {
if (valid) {
state.loading = true;
if (!state.formData.id || state.formData.id === 0) {
//
addSysNotice(state.formData).then(() => {
ElMessage.success('添加成功');
closeDialog(); //
emit('sysNoticeList')
}).finally(() => {
state.loading = false;
})
} else {
//
updateSysNotice(state.formData).then(() => {
ElMessage.success('修改成功');
closeDialog(); //
emit('sysNoticeList')
}).finally(() => {
state.loading = false;
})
}
}
});
};
const resetForm = () => {
state.formData = {
receiver: undefined,
type: undefined,
id: undefined,
title: undefined,
tag: undefined,
content: undefined,
remark: undefined,
sort: 0,
status: 1,
createdBy: undefined,
updatedBy: undefined,
createdAt: undefined,
updatedAt: undefined,
deletedAt: undefined
}
};
//
const setContentEditContent = (data: string) => {
state.formData.content = data
}
return {
proxy,
openDialog,
setType,
closeDialog,
onCancel,
onSubmit,
remoteUserMethod,
menuRef,
formRef,
//
setContentEditContent,
...toRefs(state),
};
}
})
</script>
<style scoped>
.kv-label {
margin-bottom: 15px;
font-size: 14px;
}
.mini-btn i.el-icon {
margin: unset;
}
.kv-row {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,61 @@
export interface SysNoticeTableColumns {
id:number; // ID
title:string; // 标题
type:number; // 类型
tag:number; // 标签
content:string; // 内容
remark:string; // 备注
sort:number; // 排序
status:number; // 状态
createdBy:string; // 发送人
createdAt:string; // 创建时间
}
export interface SysNoticeInfoData {
id:number|undefined; // ID
receiver:number[]|undefined;//用户id
title:string|undefined; // 标题
type:number|undefined; // 类型
tag:number|undefined; // 标签
content:string|undefined; // 内容
remark:string|undefined; // 备注
sort:number|undefined; // 排序
status:number; // 状态
createdBy:number|undefined; // 发送人
updatedBy:number|undefined; // 修改人
createdAt:string|undefined; // 创建时间
updatedAt:string|undefined; // 更新时间
deletedAt:string|undefined; // 删除时间
}
export interface SysNoticeTableDataState {
ids:any[];
tableData: {
data: Array<SysNoticeTableColumns>;
total: number;
loading: boolean;
param: {
pageNum: number;
pageSize: number;
id: number|undefined;
title: string|undefined;
type: number|undefined;
tag: number|undefined;
status: number|undefined;
createdAt: string|undefined;
dateRange: string[];
};
};
}
export interface SysNoticeEditState{
loading:boolean;
isShowDialog: boolean;
userListOptions:object[];
title:string;
formData:SysNoticeInfoData;
rules: object;
}

View File

@ -0,0 +1,375 @@
<template>
<div class="system-sysNotice-container">
<el-card shadow="hover">
<div class="system-sysNotice-search mb15">
<el-form :model="tableData.param" ref="queryRef" :inline="true" label-width="100px">
<el-row>
<el-col :span="5" class="colBlock">
<el-form-item label="标题" prop="title">
<el-input
v-model="tableData.param.title"
placeholder="请输入标题"
clearable
@keyup.enter.native="sysNoticeList"
/>
</el-form-item>
</el-col>
<el-col :span="5" class="colBlock">
<el-form-item label="状态" prop="status">
<el-select v-model="tableData.param.status" placeholder="请选择状态" clearable>
<el-option label="正常" :value="1"/>
<el-option label="停用" :value="0"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5" class="colBlock">
<el-form-item label="类型" prop="type">
<el-select v-model="tableData.param.type" placeholder="请选择类型" clearable>
<el-option label="通知" value="1"/>
<el-option label="公告" value="2"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5" class="colBlock">
<el-form-item label="标签" prop="tag">
<el-select v-model="tableData.param.tag" placeholder="请选择标签" clearable>
<el-option
v-for="dict in notice_tag"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4" class="colBlock">
<el-form-item>
<el-button type="primary" @click="sysNoticeList">
<el-icon>
<ele-Search/>
</el-icon>
搜索
</el-button>
<el-button @click="resetQuery(queryRef)">
<el-icon>
<ele-Refresh/>
</el-icon>
重置
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
@click="handleAdd(1)"
v-auth="'api/v1/system/sysNotice/noticeAdd'"
>
<el-icon>
<ele-Plus/>
</el-icon>
发通知
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
@click="handleAdd(2)"
v-auth="'api/v1/system/sysNotice/messageAdd'"
>
<el-icon>
<ele-Plus/>
</el-icon>
发私信
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
:disabled="single"
@click="handleUpdate(null)"
v-auth="'api/v1/system/sysNotice/edit'"
>
<el-icon>
<ele-Edit/>
</el-icon>
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
:disabled="multiple"
@click="handleDelete(null)"
v-auth="'api/v1/system/sysNotice/delete'"
>
<el-icon>
<ele-Delete/>
</el-icon>
删除
</el-button>
</el-col>
</el-row>
</div>
<el-table v-loading="loading" :data="tableData.data" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"/>
<el-table-column label="ID" align="center" prop="id" min-width="50px"/>
<el-table-column label="标题" align="left" prop="title" min-width="150px"/>
<el-table-column label="类型" align="center" prop="type" min-width="150px">
<template #default="scope">
<el-tag type="warning" v-if="scope.row.type=='1'">通知</el-tag>
<el-tag type="danger" v-if="scope.row.type=='2'">私信</el-tag>
</template>
</el-table-column>
<el-table-column label="标签" align="center" prop="tag" :formatter="tagFormat" min-width="80px">
<template #default="scope">
<el-tag type="info" v-if="scope.row.tag==0">无标签</el-tag>
<el-tag type="success" v-if="scope.row.tag==1">提醒</el-tag>
<el-tag type="success" v-if="scope.row.tag==2">一般</el-tag>
<el-tag type="warning" v-if="scope.row.tag==3">次要</el-tag>
<el-tag type="danger" v-if="scope.row.tag==4">重要</el-tag>
<el-tag type="danger" v-if="scope.row.tag==5">紧急</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="内容" align="center" prop="content" min-width="150px"/>-->
<el-table-column label="备注" align="center" prop="remark" min-width="150px"/>
<el-table-column label="阅读次数" align="center" prop="clickNumber" min-width="150px"/>
<el-table-column label="排序" align="center" prop="sort" min-width="50px"/>
<el-table-column label="状态" align="center" prop="status" min-width="80px">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status==1">正常</el-tag>
<el-tag type="danger" v-if="scope.row.status==0">停用</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="发送人" align="center" prop="createdBy" min-width="150px"/>-->
<el-table-column label="发送时间" align="center" prop="createdAt" min-width="100px">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createdAt, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding" min-width="160px" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
@click="handleUpdate(scope.row)"
v-auth="'api/v1/system/sysNotice/edit'"
>
<el-icon>
<ele-EditPen/>
</el-icon>
修改
</el-button>
<el-button
type="primary"
link
@click="handleDelete(scope.row)"
v-auth="'api/v1/system/sysNotice/delete'"
>
<el-icon>
<ele-DeleteFilled/>
</el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="tableData.total>0"
:total="tableData.total"
v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize"
@pagination="sysNoticeList"
/>
</el-card>
<NoticeMessageEdit
ref="editRef"
:tagOptions="notice_tag"
@sysNoticeList="sysNoticeList"
></NoticeMessageEdit>
</div>
</template>
<script lang="ts">
import {toRefs, reactive, onMounted, ref, defineComponent, computed, getCurrentInstance, toRaw} from 'vue';
import {ElMessageBox, ElMessage, FormInstance} from 'element-plus';
import {
listSysNotice,
delSysNotice,
} from "/src/api/system/notice/sysNotice";
import {
SysNoticeTableColumns,
SysNoticeInfoData,
SysNoticeTableDataState,
} from "/@/views/system/sysNotice/list/component/model"
import NoticeMessageEdit from "/@/views/system/sysNotice/list/component/NoticeMessageEdit.vue"
export default defineComponent({
name: "apiV1SystemSysNoticeList",
components: {
NoticeMessageEdit
},
setup() {
const {proxy} = <any>getCurrentInstance()
const loading = ref(false)
const queryRef = ref()
const editRef = ref();
//
const showAll = ref(false)
//
const single = ref(true)
//
const multiple = ref(true)
const word = computed(() => {
if (showAll.value === false) {
//
return "展开搜索";
} else {
return "收起搜索";
}
})
//
const {
notice_tag,
} = proxy.useDict(
'notice_tag',
)
const state = reactive<SysNoticeTableDataState>({
ids: [],
tableData: {
data: [],
total: 0,
loading: false,
param: {
pageNum: 1,
pageSize: 10,
id: undefined,
title: undefined,
type: undefined,
tag: undefined,
status: undefined,
createdAt: undefined,
dateRange: []
},
},
});
//
onMounted(() => {
initTableData();
});
//
const initTableData = () => {
sysNoticeList()
};
/** 重置按钮操作 */
const resetQuery = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
sysNoticeList()
};
//
const sysNoticeList = () => {
loading.value = true
listSysNotice(state.tableData.param).then((res: any) => {
let list = res.data.list ?? [];
list.map((item: any) => {
item.createdBy = item.createdUser?.userNickname
})
state.tableData.data = list;
state.tableData.total = res.data.total;
loading.value = false
})
};
const toggleSearch = () => {
showAll.value = !showAll.value;
}
//
const tagFormat = (row: SysNoticeTableColumns) => {
return proxy.selectDictLabel(notice_tag.value, row.tag);
}
//
const handleSelectionChange = (selection: Array<SysNoticeInfoData>) => {
state.ids = selection.map(item => item.id)
single.value = selection.length != 1
multiple.value = !selection.length
}
const handleAdd = (type: number) => {
editRef.value.openDialog()
editRef.value.setType(type)
}
const handleUpdate = (row: SysNoticeTableColumns) => {
if (!row) {
row = state.tableData.data.find((item: SysNoticeTableColumns) => {
return item.id === state.ids[0]
}) as SysNoticeTableColumns
}
editRef.value.openDialog(toRaw(row));
};
const handleDelete = (row: SysNoticeTableColumns) => {
let msg = '你确定要删除所选数据?';
let id: number[] = [];
if (row) {
msg = `此操作将永久删除数据,是否继续?`
id = [row.id]
} else {
id = state.ids
}
if (id.length === 0) {
ElMessage.error('请选择要删除的数据。');
return
}
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
delSysNotice(id).then(() => {
ElMessage.success('删除成功');
sysNoticeList();
})
})
.catch(() => {
});
}
return {
proxy,
editRef,
showAll,
loading,
single,
multiple,
word,
queryRef,
resetQuery,
sysNoticeList,
toggleSearch,
tagFormat,
notice_tag,
handleSelectionChange,
handleAdd,
handleUpdate,
handleDelete,
...toRefs(state),
}
}
})
</script>
<style lang="scss" scoped>
.colBlock {
display: block;
}
.colNone {
display: none;
}
.ml-2 {
margin: 3px;
}
</style>

View File

@ -0,0 +1,259 @@
<template>
<div class="system-sysNotice-container">
<el-card shadow="hover">
<el-tabs
v-model="tableData.param.type"
type="card"
@tab-click="handleTabsClick"
>
<el-tab-pane label="通知" :name="1">
<el-table :data="tableData.data" border style="width: 100%">
<el-table-column>
<template #default="scope">
<el-row @click="handleRead(scope.row)">
<el-col :span="0.8">
<el-icon class="el_icon">
<ele-Bell class="icon"/>
</el-icon>
</el-col>
<el-col :span="23">
<span class="title" style="">{{ scope.row.title }}</span>
<span style="margin-left: 6px">
<el-tag type="success" effect="plain" v-if="scope.row.isRead==true">已读</el-tag>
<el-tag type="danger" effect="plain" v-else>未读</el-tag>
</span>
<span style="float: right">
<el-tag type="info" v-if="scope.row.tag==0">无标签</el-tag>
<el-tag type="success" v-if="scope.row.tag==1">提醒</el-tag>
<el-tag type="success" v-if="scope.row.tag==2">一般</el-tag>
<el-tag type="warning" v-if="scope.row.tag==3">次要</el-tag>
<el-tag type="danger" v-if="scope.row.tag==4">重要</el-tag>
<el-tag type="danger" v-if="scope.row.tag==5">紧急</el-tag>
</span>
<br>
<span>{{ scope.row.createdAt }}</span>
<br>
<span v-html="scope.row.content"></span>
<br>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="私信" :name="2">
<el-table :data="tableData.data" border style="width: 100%">
<el-table-column>
<template #default="scope">
<el-row @click="handleRead(scope.row)">
<el-col :span="0.8">
<el-icon class="el_icon">
<ele-ChatDotRound class="icon"/>
</el-icon>
</el-col>
<el-col :span="23">
<span class="title" style="">{{ scope.row.title }}</span>
<span style="margin-left: 6px">
<el-tag type="success" effect="plain" v-if="scope.row.isRead==true">已读</el-tag>
<el-tag type="danger" effect="plain" v-else>未读</el-tag>
</span>
<span style="float: right">
<el-tag type="info" v-if="scope.row.tag==0">无标签</el-tag>
<el-tag type="success" v-if="scope.row.tag==1">提醒</el-tag>
<el-tag type="success" v-if="scope.row.tag==2">一般</el-tag>
<el-tag type="warning" v-if="scope.row.tag==3">次要</el-tag>
<el-tag type="danger" v-if="scope.row.tag==4">重要</el-tag>
<el-tag type="danger" v-if="scope.row.tag==5">紧急</el-tag>
</span>
<br>
<span>{{ scope.row.createdAt }}</span>
<span v-html="scope.row.content"></span>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<pagination
v-show="tableData.total>0"
:total="tableData.total"
v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize"
@pagination="sysNoticeList"
/>
</el-card>
</div>
</template>
<script lang="ts">
import {useRoute} from "vue-router";
import {toRefs, reactive, onMounted, ref, defineComponent, getCurrentInstance} from 'vue';
import {ElMessageBox, ElMessage, FormInstance} from 'element-plus';
import {
delSysNotice, listShowNotice,
} from "/@/api/system/notice/sysNotice";
import {
SysNoticeTableColumns,
SysNoticeTableDataState,
} from "/@/views/system/sysNotice/list/component/model"
import {readNotice} from "/@/api/system/notice/sysNoticeRead";
export default defineComponent({
name: "",
components: {},
setup() {
const route = useRoute();
const {proxy} = <any>getCurrentInstance()
const loading = ref(false)
const state = reactive<SysNoticeTableDataState>({
ids: [],
tableData: {
data: [],
total: 0,
loading: false,
param: {
pageNum: 1,
pageSize: 3,
id: undefined,
title: undefined,
type: 1,
tag: undefined,
status: undefined,
createdAt: undefined,
dateRange: []
},
},
});
//
onMounted(() => {
if (route.query.type){
state.tableData.param.type = parseInt(route.query.type as string)
}
initTableData();
});
//
const initTableData = () => {
sysNoticeList()
};
/** 重置按钮操作 */
const resetQuery = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
sysNoticeList()
};
//
const sysNoticeList = () => {
loading.value = true
listShowNotice(state.tableData.param).then((res: any) => {
let list = res.data.list ?? [];
list.map((item: any) => {
item.createdBy = item.createdUser?.userNickname
})
state.tableData.data = list;
state.tableData.total = res.data.total;
loading.value = false
})
};
const handleTabsClick = (e: any) => {
//console.log(e.props.name)
state.tableData.param.type = e.props.name
sysNoticeList()
}
const handleDelete = (row: SysNoticeTableColumns) => {
let msg = '你确定要删除所选数据?';
let id: number[] = [];
if (row) {
msg = `此操作将永久删除数据,是否继续?`
id = [row.id]
} else {
id = state.ids
}
if (id.length === 0) {
ElMessage.error('请选择要删除的数据。');
return
}
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
delSysNotice(id).then(() => {
ElMessage.success('删除成功');
sysNoticeList();
})
})
.catch(() => {
});
}
// eslint-disable-next-line no-undef
const handleRead = (item: any) => {
// console.log("handleRead", item)
let query = {
noticeId: item.id
}
readNotice(query).then(() => {
sysNoticeList()
ElMessage.success("已读");
})
}
return {
proxy,
loading,
resetQuery,
sysNoticeList,
handleTabsClick,
handleDelete,
handleRead,
...toRefs(state),
}
}
})
</script>
<style lang="scss" scoped>
.el_icon {
width: 28px;
height: 28px;
border-radius: 10px;
background-color: #2d8cf0;
margin-right: 10px;
}
.title {
font-size: 16px;
font-weight: 500;
color: #0a0a0a
}
.icon {
width: 28px;
height: 28px;
color: #F8F8FF
}
.colBlock {
display: block;
}
.colNone {
display: none;
}
.ml-2 {
margin: 3px;
}
:deep(.el-table__header-wrapper) {
display: none;
}
</style>