通知板块 及 功能调整
This commit is contained in:
+135
-5
@@ -12,6 +12,8 @@
|
|||||||
import { UserService } from './api/usersApi'
|
import { UserService } from './api/usersApi'
|
||||||
import { setThemeTransitionClass } from './utils/theme/animation'
|
import { setThemeTransitionClass } from './utils/theme/animation'
|
||||||
import { checkStorageCompatibility } from './utils/storage'
|
import { checkStorageCompatibility } from './utils/storage'
|
||||||
|
import { mittBus } from '@/utils/sys'
|
||||||
|
import appLogo from '@/assets/img/common/logox.png'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { language } = storeToRefs(userStore)
|
const { language } = storeToRefs(userStore)
|
||||||
@@ -32,14 +34,130 @@
|
|||||||
zh: zh,
|
zh: zh,
|
||||||
en: en
|
en: en
|
||||||
}
|
}
|
||||||
|
const isSupported = ref(false)
|
||||||
|
const permissionStatus = ref('default')
|
||||||
|
// 检查浏览器支持情况
|
||||||
|
const checkSupport = () => {
|
||||||
|
if ('Notification' in window) {
|
||||||
|
isSupported.value = true
|
||||||
|
console.log('浏览器支持原生Notification API')
|
||||||
|
} else {
|
||||||
|
isSupported.value = false
|
||||||
|
console.log('⚠️ 浏览器不支持原生Notification API')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 请求通知权限
|
||||||
|
const requestPermission = async () => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
console.log('浏览器不支持原生Notification API')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const permission = await Notification.requestPermission()
|
||||||
|
permissionStatus.value = permission
|
||||||
|
if (permission === 'granted') {
|
||||||
|
console.log('用户已授予通知权限')
|
||||||
|
} else if (permission === 'denied') {
|
||||||
|
console.log('用户拒绝了通知权限')
|
||||||
|
} else {
|
||||||
|
console.log('用户暂时未授予通知权限')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`请求权限时出错:`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示基本通知(支持自定义标题与内容)
|
||||||
|
const showBasicNotification = (
|
||||||
|
title: string = '基本通知',
|
||||||
|
body: string = '这是一个使用浏览器原生API的系统通知'
|
||||||
|
) => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
ElMessage({
|
||||||
|
message: '浏览器不支持原生Notification API',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
ElMessage({
|
||||||
|
message: '请先授予通知权限',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
requestPermission()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const iconUrl = appLogo || '/favicon.ico'
|
||||||
|
const notification = new Notification(title, {
|
||||||
|
body,
|
||||||
|
requireInteraction: true,
|
||||||
|
icon: iconUrl
|
||||||
|
})
|
||||||
|
notification.onclick = () => {
|
||||||
|
notification.close()
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
const showEventNotification = () => {
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
ElMessage({
|
||||||
|
message: '请先授予通知权限',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const notification = new Notification('带事件监听的通知', {
|
||||||
|
body: '这个通知演示各种事件监听',
|
||||||
|
icon: 'https://element-plus.org/images/element-plus-logo.svg'
|
||||||
|
})
|
||||||
|
notification.onshow = () => {
|
||||||
|
console.log('通知显示事件触发')
|
||||||
|
}
|
||||||
|
notification.onclick = () => {
|
||||||
|
console.log('通知点击事件触发')
|
||||||
|
notification.close()
|
||||||
|
}
|
||||||
|
notification.onclose = () => {
|
||||||
|
console.log('通知关闭事件触发')
|
||||||
|
}
|
||||||
|
notification.onerror = (error) => {
|
||||||
|
// console.log(`通知错误事件: ${error.message}`)
|
||||||
|
}
|
||||||
|
console.log('已发送带事件监听的通知')
|
||||||
|
} catch (error) {
|
||||||
|
// console.log(`发送通知失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
setThemeTransitionClass(true)
|
setThemeTransitionClass(true)
|
||||||
setCompanyFromUrl()
|
setCompanyFromUrl()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 页面刷新时重置WebSocket重连次数,同时清理事件监听
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
import('@/utils/sys/wsClient').then(({ resetWebSocketReconnect }) => {
|
||||||
|
resetWebSocketReconnect()
|
||||||
|
})
|
||||||
|
mittBus.off('ws:message')
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleWsMessageNotification = (data: any) => {
|
||||||
|
if (data && data.event === 'notify.receive') {
|
||||||
|
const notice = data?.data?.notice_data
|
||||||
|
const title = notice?.message?.title || '新通知'
|
||||||
|
const rawContent = notice?.message?.content || ''
|
||||||
|
const body = typeof rawContent === 'string' ? rawContent.replace(/<[^>]+>/g, '') : ''
|
||||||
|
showBasicNotification(title, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 检查存储兼容性
|
// 检查存储兼容性
|
||||||
checkStorageCompatibility()
|
checkStorageCompatibility()
|
||||||
|
checkSupport()
|
||||||
|
requestPermission()
|
||||||
// 提升暗黑主题下页面刷新视觉体验
|
// 提升暗黑主题下页面刷新视觉体验
|
||||||
setThemeTransitionClass(false)
|
setThemeTransitionClass(false)
|
||||||
// 系统升级
|
// 系统升级
|
||||||
@@ -48,11 +166,23 @@
|
|||||||
getUserInfo()
|
getUserInfo()
|
||||||
// 初始化 WebSocket
|
// 初始化 WebSocket
|
||||||
const { accessToken } = useUserStore()
|
const { accessToken } = useUserStore()
|
||||||
// if (accessToken) {
|
// showEventNotification()
|
||||||
// import('@/utils/sys/wsClient').then(({ initWebSocket }) => {
|
if (accessToken) {
|
||||||
// initWebSocket(accessToken)
|
import('@/utils/sys/wsClient').then(
|
||||||
// })
|
({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => {
|
||||||
// }
|
// 页面刷新时重置重连次数
|
||||||
|
resetWebSocketReconnect()
|
||||||
|
initWebSocket(accessToken)
|
||||||
|
// 添加调试功能,可以在控制台查看WebSocket状态
|
||||||
|
// @ts-ignore
|
||||||
|
window.getWebSocketStatus = getWebSocketStatus
|
||||||
|
// console.log('WebSocket状态查看: window.getWebSocketStatus()')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听ws消息,用于系统级通知
|
||||||
|
mittBus.on('ws:message', handleWsMessageNotification)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
|
|||||||
@@ -198,4 +198,11 @@ export class manageService {
|
|||||||
// showErrorMessage: false // 不显示错误消息
|
// showErrorMessage: false // 不显示错误消息
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//回收站 恢复
|
||||||
|
static customerRestore(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/crm/customer/restore',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
export class commonService {
|
||||||
|
//通知列表
|
||||||
|
static getNoticeList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/common/notice/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知详情
|
||||||
|
static getNoticeDetail(params: any) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/common/notice/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知已读
|
||||||
|
static readNotice(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/common/notice/read',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知删除
|
||||||
|
static delNotice(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/common/notice/destroy',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知未读列表
|
||||||
|
static getUnReadNoticeList() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/common/notice/unread'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,4 +191,12 @@ export class CrmService {
|
|||||||
// showErrorMessage: false // 不显示错误消息
|
// showErrorMessage: false // 不显示错误消息
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//客户删除
|
||||||
|
static crmCustomerDelete(params: any) {
|
||||||
|
return request.get<Api.Common.AddResponse>({
|
||||||
|
url: '/crm/customer/delete',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,4 +217,32 @@ export class systemService {
|
|||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//通知列表
|
||||||
|
static getNoticeList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/notice/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知详情
|
||||||
|
static getNoticeDetail(params: any) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/notice/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知删除
|
||||||
|
static delNotice(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/notice/destroy',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//通知推送
|
||||||
|
static sendNotice(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/notice/push',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,4 +92,12 @@ export class UserService {
|
|||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//客户删除
|
||||||
|
static crmCustomerDelete(params: any) {
|
||||||
|
return request.get<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/crm/customer/delete',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -54,6 +54,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
<!-- 通知 -->
|
||||||
|
<div class="btn-box notice-btn" @click="visibleNotice">
|
||||||
|
<div class="btn notice-button">
|
||||||
|
<i
|
||||||
|
class="iconfont-sys notice-btn"
|
||||||
|
:class="{ 'bell-shake': isBellShaking }"
|
||||||
|
:style="isBellShaking ? 'color: #5d87ff' : ''"
|
||||||
|
></i
|
||||||
|
>
|
||||||
|
<span class="count notice-btn" v-if="isBellShaking"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="btn-box screen-box" @click="clearCachefn">
|
<div class="btn-box screen-box" @click="clearCachefn">
|
||||||
<div class="btn" title="清除缓存">
|
<div class="btn" title="清除缓存">
|
||||||
<i class="iconfont-sys"></i>
|
<i class="iconfont-sys"></i>
|
||||||
@@ -69,13 +81,6 @@
|
|||||||
<i class="iconfont-sys">{{ isFullscreen ? '' : '' }}</i>
|
<i class="iconfont-sys">{{ isFullscreen ? '' : '' }}</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 通知 -->
|
|
||||||
<!-- <div class="btn-box notice-btn" @click="visibleNotice">
|
|
||||||
<div class="btn notice-button">
|
|
||||||
<i class="iconfont-sys notice-btn"></i>
|
|
||||||
<span class="count notice-btn"></span>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<!-- 聊天 -->
|
<!-- 聊天 -->
|
||||||
<!-- <div class="btn-box chat-btn" @click="openChat">
|
<!-- <div class="btn-box chat-btn" @click="openChat">
|
||||||
<div class="btn chat-button">
|
<div class="btn chat-button">
|
||||||
@@ -248,6 +253,10 @@
|
|||||||
const showNotice = ref(false)
|
const showNotice = ref(false)
|
||||||
const notice = ref(null)
|
const notice = ref(null)
|
||||||
const userMenuPopover = ref()
|
const userMenuPopover = ref()
|
||||||
|
// 控制铃铛是否晃动
|
||||||
|
const isBellShaking = ref(false)
|
||||||
|
// 轮询定时器句柄,防止重复创建
|
||||||
|
let notificationPollTimer: number | undefined
|
||||||
|
|
||||||
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
|
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
|
||||||
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
|
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
|
||||||
@@ -269,10 +278,20 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initLanguage()
|
initLanguage()
|
||||||
document.addEventListener('click', bodyCloseNotice)
|
document.addEventListener('click', bodyCloseNotice)
|
||||||
|
// 监听通知数据变化,控制铃铛晃动
|
||||||
|
watchNotificationData()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', bodyCloseNotice)
|
document.removeEventListener('click', bodyCloseNotice)
|
||||||
|
// 清理事件监听器
|
||||||
|
mittBus.off('notification-data-changed')
|
||||||
|
mittBus.off('ws:message')
|
||||||
|
// 清理通知轮询
|
||||||
|
if (notificationPollTimer) {
|
||||||
|
clearInterval(notificationPollTimer)
|
||||||
|
notificationPollTimer = undefined
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||||
@@ -373,8 +392,6 @@
|
|||||||
if (showSettingGuide.value) {
|
if (showSettingGuide.value) {
|
||||||
settingStore.hideSettingGuide()
|
settingStore.hideSettingGuide()
|
||||||
}
|
}
|
||||||
// 打开设置引导
|
|
||||||
// settingStore.openSettingGuide()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSearchDialog = () => {
|
const openSearchDialog = () => {
|
||||||
@@ -412,9 +429,72 @@
|
|||||||
userMenuPopover.value.hide()
|
userMenuPopover.value.hide()
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听通知数据变化,控制铃铛晃动
|
||||||
|
const watchNotificationData = () => {
|
||||||
|
// 定期检查通知数据
|
||||||
|
// const checkNotificationData = async () => {
|
||||||
|
// try {
|
||||||
|
// const { commonService } = await import('@/api/commonApi')
|
||||||
|
// const res = await commonService.getUnReadNoticeList()
|
||||||
|
// const noticeCount = res?.list?.length || 0
|
||||||
|
// isBellShaking.value = noticeCount > 0
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('获取通知数据失败:', error)
|
||||||
|
// isBellShaking.value = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // 立即检查一次
|
||||||
|
// checkNotificationData()
|
||||||
|
// // 每30秒检查一次通知数据,先清理旧定时器,避免重复
|
||||||
|
// if (notificationPollTimer) {
|
||||||
|
// clearInterval(notificationPollTimer)
|
||||||
|
// notificationPollTimer = undefined
|
||||||
|
// }
|
||||||
|
// notificationPollTimer = window.setInterval(checkNotificationData, 30000)
|
||||||
|
// 监听通知数据变化事件
|
||||||
|
mittBus.on('notification-data-changed', (count: unknown) => {
|
||||||
|
isBellShaking.value = typeof count === 'number' ? count > 0 : false
|
||||||
|
})
|
||||||
|
// 监听WebSocket消息,处理实时通知
|
||||||
|
mittBus.on('ws:message', (data: any) => {
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
data.event &&
|
||||||
|
(data.event.includes('notice') ||
|
||||||
|
data.event.includes('notification') ||
|
||||||
|
data.event.includes('message'))
|
||||||
|
) {
|
||||||
|
isBellShaking.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use './style';
|
@use './style';
|
||||||
@use './mobile';
|
@use './mobile';
|
||||||
|
// 铃铛晃动动画
|
||||||
|
.bell-shake {
|
||||||
|
animation: bellShake 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes bellShake {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70%,
|
||||||
|
90% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
40%,
|
||||||
|
60%,
|
||||||
|
80% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,43 +9,46 @@
|
|||||||
@click.stop=""
|
@click.stop=""
|
||||||
>
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="text">{{ $t('notice.title') }}</span>
|
<!-- <span class="text">{{ $t('notice.title') }}</span> -->
|
||||||
<span class="btn">{{ $t('notice.btnRead') }}</span>
|
<span class="text">消息</span>
|
||||||
|
<span class="btn" @click="readAll">{{ $t('notice.btnRead') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="bar">
|
<ul class="bar">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in barList"
|
v-for="(item, index) in barList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="{ active: barActiveIndex === index }"
|
:class="{ active: barActiveIndex === index }"
|
||||||
@click="changeBar(index)"
|
|
||||||
>
|
>
|
||||||
{{ item.name }} ({{ item.num }})
|
{{ item.name }} ({{ noticeList.length }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<!-- 通知 -->
|
<!-- 通知 -->
|
||||||
<ul class="notice-list" v-show="barActiveIndex === 0">
|
<ul class="notice-list">
|
||||||
<li v-for="(item, index) in noticeList" :key="index">
|
<!-- <ul class="notice-list" v-show="barActiveIndex === 0"> -->
|
||||||
|
<li v-for="(item, index) in noticeList" :key="index" @click="showfn(item)" v-ripple>
|
||||||
<div
|
<div
|
||||||
class="icon"
|
class="icon"
|
||||||
:style="{ background: getNoticeStyle(item.type).backgroundColor + '!important' }"
|
:style="{
|
||||||
|
background: getNoticeStyle(item.message.scene).backgroundColor + '!important'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="iconfont-sys"
|
class="iconfont-sys"
|
||||||
:style="{ color: getNoticeStyle(item.type).iconColor + '!important' }"
|
:style="{ color: getNoticeStyle(item.message.scene).iconColor + '!important' }"
|
||||||
v-html="getNoticeStyle(item.type).icon"
|
v-html="getNoticeStyle(item.message.scene).icon"
|
||||||
>
|
>
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<h4>{{ item.title }}</h4>
|
<h4>{{ item.message.title }}</h4>
|
||||||
<p>{{ item.time }}</p>
|
<p>{{ item.message.created_at }}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- 消息 -->
|
<!-- 消息 -->
|
||||||
<ul class="user-list" v-show="barActiveIndex === 1">
|
<!-- <ul class="user-list" v-show="barActiveIndex === 1">
|
||||||
<li v-for="(item, index) in msgList" :key="index">
|
<li v-for="(item, index) in msgList" :key="index">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<img :src="item.avatar" />
|
<img :src="item.avatar" />
|
||||||
@@ -55,27 +58,27 @@
|
|||||||
<p>{{ item.time }}</p>
|
<p>{{ item.time }}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul> -->
|
||||||
<!-- 待办 -->
|
<!-- 待办 -->
|
||||||
<ul class="base" v-show="barActiveIndex === 3">
|
<!-- <ul class="base" v-show="barActiveIndex === 3">
|
||||||
<li v-for="(item, index) in pendingList" :key="index">
|
<li v-for="(item, index) in pendingList" :key="index">
|
||||||
<h4>{{ item.title }}</h4>
|
<h4>{{ item.title }}</h4>
|
||||||
<p>{{ item.time }}</p>
|
<p>{{ item.time }}</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul> -->
|
||||||
|
|
||||||
<div class="empty-tips" v-show="barActiveIndex === 0 && noticeList.length === 0">
|
<div class="empty-tips" v-show="noticeList.length === 0">
|
||||||
<i class="iconfont-sys"></i>
|
<i class="iconfont-sys"></i>
|
||||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty-tips" v-show="barActiveIndex === 1 && msgList.length === 0">
|
<!-- <div class="empty-tips" v-show="barActiveIndex === 1 && msgList.length === 0">
|
||||||
<i class="iconfont-sys"></i>
|
<i class="iconfont-sys"></i>
|
||||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty-tips" v-show="barActiveIndex === 2 && pendingList.length === 0">
|
<div class="empty-tips" v-show="barActiveIndex === 2 && pendingList.length === 0">
|
||||||
<i class="iconfont-sys"></i>
|
<i class="iconfont-sys"></i>
|
||||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-wrapper">
|
<div class="btn-wrapper">
|
||||||
<el-button class="view-all" @click="handleViewAll" v-ripple>
|
<el-button class="view-all" @click="handleViewAll" v-ripple>
|
||||||
@@ -86,6 +89,25 @@
|
|||||||
|
|
||||||
<div style="height: 100px"></div>
|
<div style="height: 100px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 全局通知详情弹窗 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<ElDialog v-model="dialogVisible" title="通知详情" align-center :append-to-body="true">
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>发送人:{{ noticeDetail.sender_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>通知标题:{{ noticeDetail.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
通知内容:
|
||||||
|
<div class="message-text" v-html="noticeDetail.content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElDialog>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -97,7 +119,10 @@
|
|||||||
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
||||||
import AppConfig from '@/config'
|
import AppConfig from '@/config'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { commonService } from '@/api/commonApi'
|
||||||
|
import { mittBus } from '@/utils/sys'
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -123,85 +148,38 @@
|
|||||||
{
|
{
|
||||||
name: computed(() => t('notice.bar[0]')),
|
name: computed(() => t('notice.bar[0]')),
|
||||||
num: 1
|
num: 1
|
||||||
},
|
|
||||||
{
|
|
||||||
name: computed(() => t('notice.bar[1]')),
|
|
||||||
num: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: computed(() => t('notice.bar[2]')),
|
|
||||||
num: 0
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
const noticeList = ref<any>([])
|
||||||
const noticeList = [
|
const getNoticeList = async () => {
|
||||||
{
|
await commonService.getUnReadNoticeList().then((res: any) => {
|
||||||
title: '新增国际化',
|
noticeList.value = res.list
|
||||||
time: '2024-6-13 0:10',
|
// 通知头部组件更新铃铛状态
|
||||||
type: 'notice'
|
mittBus.emit('notification-data-changed', res.list?.length || 0)
|
||||||
},
|
})
|
||||||
{
|
|
||||||
title: '冷月呆呆给你发了一条消息',
|
|
||||||
time: '2024-4-21 8:05',
|
|
||||||
type: 'message'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '小肥猪关注了你',
|
|
||||||
time: '2020-3-17 21:12',
|
|
||||||
type: 'collection'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '新增使用文档',
|
|
||||||
time: '2024-02-14 0:20',
|
|
||||||
type: 'notice'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '小肥猪给你发了一封邮件',
|
|
||||||
time: '2024-1-20 0:15',
|
|
||||||
type: 'email'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '菜单mock本地真实数据',
|
|
||||||
time: '2024-1-17 22:06',
|
|
||||||
type: 'notice'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const msgList: any = [
|
|
||||||
{
|
|
||||||
title: '池不胖 关注了你',
|
|
||||||
time: '2021-2-26 23:50',
|
|
||||||
avatar: avatar1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '唐不苦 关注了你',
|
|
||||||
time: '2021-2-21 8:05',
|
|
||||||
avatar: avatar2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '中小鱼 关注了你',
|
|
||||||
time: '2020-1-17 21:12',
|
|
||||||
avatar: avatar3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '何小荷 关注了你',
|
|
||||||
time: '2021-01-14 0:20',
|
|
||||||
avatar: avatar4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '誶誶淰 关注了你',
|
|
||||||
time: '2020-12-20 0:15',
|
|
||||||
avatar: avatar5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '冷月呆呆 关注了你',
|
|
||||||
time: '2020-12-17 22:06',
|
|
||||||
avatar: avatar6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const changeBar = (index: number) => {
|
|
||||||
barActiveIndex.value = index
|
|
||||||
}
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getNoticeList()
|
||||||
|
// 监听WebSocket消息:收到新通知实时插入到列表首位
|
||||||
|
mittBus.on('ws:message', onWsMessage)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
mittBus.off('ws:message')
|
||||||
|
})
|
||||||
|
|
||||||
|
const onWsMessage = (data: any) => {
|
||||||
|
if (data && data.event === 'notify.receive') {
|
||||||
|
// 将新通知插入到列表首位。后端数据结构需包含 type 与 message 字段
|
||||||
|
// 若结构不同,可在此处做一次字段映射
|
||||||
|
console.log(data)
|
||||||
|
noticeList.value.unshift(data.data.notice_data)
|
||||||
|
console.log(noticeList.value)
|
||||||
|
mittBus.emit('notification-data-changed', noticeList.value.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const changeBar = (index: number) => {
|
||||||
|
// barActiveIndex.value = index
|
||||||
|
// }
|
||||||
|
|
||||||
const getRandomColor = () => {
|
const getRandomColor = () => {
|
||||||
const index = Math.floor(Math.random() * AppConfig.systemMainColor.length)
|
const index = Math.floor(Math.random() * AppConfig.systemMainColor.length)
|
||||||
@@ -209,27 +187,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const noticeStyleMap = {
|
const noticeStyleMap = {
|
||||||
email: {
|
4: {
|
||||||
icon: '',
|
icon: '',
|
||||||
iconColor: 'rgb(var(--art-warning))',
|
iconColor: 'rgb(var(--art-warning))',
|
||||||
backgroundColor: 'rgb(var(--art-bg-warning))'
|
backgroundColor: 'rgb(var(--art-bg-warning))'
|
||||||
},
|
},
|
||||||
message: {
|
2: {
|
||||||
icon: '',
|
icon: '',
|
||||||
iconColor: 'rgb(var(--art-success))',
|
iconColor: 'rgb(var(--art-success))',
|
||||||
backgroundColor: 'rgb(var(--art-bg-success))'
|
backgroundColor: 'rgb(var(--art-bg-success))'
|
||||||
},
|
},
|
||||||
collection: {
|
5: {
|
||||||
icon: '',
|
icon: '',
|
||||||
iconColor: 'rgb(var(--art-danger))',
|
iconColor: 'rgb(var(--art-danger))',
|
||||||
backgroundColor: 'rgb(var(--art-bg-danger))'
|
backgroundColor: 'rgb(var(--art-bg-danger))'
|
||||||
},
|
},
|
||||||
user: {
|
3: {
|
||||||
icon: '',
|
icon: '',
|
||||||
iconColor: 'rgb(var(--art-info))',
|
iconColor: 'rgb(var(--art-info))',
|
||||||
backgroundColor: 'rgb(var(--art-bg-info))'
|
backgroundColor: 'rgb(var(--art-bg-info))'
|
||||||
},
|
},
|
||||||
notice: {
|
1: {
|
||||||
icon: '',
|
icon: '',
|
||||||
iconColor: 'rgb(var(--art-primary))',
|
iconColor: 'rgb(var(--art-primary))',
|
||||||
backgroundColor: 'rgb(var(--art-bg-primary))'
|
backgroundColor: 'rgb(var(--art-bg-primary))'
|
||||||
@@ -243,7 +221,11 @@
|
|||||||
backgroundColor: getRandomColor()
|
backgroundColor: getRandomColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = noticeStyleMap[type as keyof typeof noticeStyleMap] || defaultStyle
|
// 先转成 unknown,再转成 keyof 类型,避免 TS 报错
|
||||||
|
const key = type as unknown as keyof typeof noticeStyleMap
|
||||||
|
const style = noticeStyleMap[key] || defaultStyle
|
||||||
|
|
||||||
|
// const style = noticeStyleMap[type as keyof typeof noticeStyleMap] || defaultStyle
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...style,
|
...style,
|
||||||
@@ -264,27 +246,43 @@
|
|||||||
}, 350)
|
}, 350)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const readAll = async () => {
|
||||||
|
const { message } = await commonService.readNotice({
|
||||||
|
msg_ids: 'all'
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getNoticeList()
|
||||||
|
}
|
||||||
// 查看全部
|
// 查看全部
|
||||||
const handleViewAll = () => {
|
const handleViewAll = () => {
|
||||||
switch (barActiveIndex.value) {
|
handleNoticeAll()
|
||||||
case 0:
|
}
|
||||||
handleNoticeAll()
|
const noticeDetail = ref<any>({})
|
||||||
break
|
const showfn = async (row: any) => {
|
||||||
case 1:
|
dialogVisible.value = true
|
||||||
handleMsgAll()
|
await commonService.getNoticeDetail({ id: row.id }).then((res: any) => {
|
||||||
break
|
noticeDetail.value = res.message
|
||||||
case 2:
|
showNotice(false) // 关闭通知组件
|
||||||
handlePendingAll()
|
getNoticeList()
|
||||||
break
|
})
|
||||||
}
|
}
|
||||||
|
const handleNoticeAll = () => {
|
||||||
|
router.push('/common/notice')
|
||||||
|
showNotice(false) // 关闭通知组件
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNoticeAll = () => {}
|
|
||||||
const handleMsgAll = () => {}
|
const handleMsgAll = () => {}
|
||||||
const handlePendingAll = () => {}
|
const handlePendingAll = () => {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use './style';
|
@use './style';
|
||||||
|
.el-button {
|
||||||
|
--el-button-border-color: #dcdfe6 !important;
|
||||||
|
--el-button-text-color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.message-text {
|
||||||
|
padding: 10px 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+151
-78
@@ -3,56 +3,38 @@ import { useUserStore } from '@/store/modules/user'
|
|||||||
|
|
||||||
type JsonValue = Record<string, any> | any[]
|
type JsonValue = Record<string, any> | any[]
|
||||||
|
|
||||||
const WS_BASE = 'wss://mzl.dev.hnxinyouhe.com/ws?token='
|
const WS_BASE = import.meta.env.VITE_API_URL_WSS + '/ws?token='
|
||||||
|
const MAX_RECONNECT_ATTEMPTS = 10
|
||||||
|
const MAX_RECONNECT_DELAY = 50000
|
||||||
|
const HEARTBEAT_INTERVAL = 50000
|
||||||
|
|
||||||
let socket: WebSocket | null = null
|
let socket: WebSocket | null = null
|
||||||
let reconnectTimer: any = null
|
let reconnectTimer: any = null
|
||||||
let heartbeatTimer: any = null
|
let heartbeatTimer: any = null
|
||||||
let reconnectAttempts = 0
|
let reconnectAttempts = 0
|
||||||
const maxReconnectDelayMs = 30000
|
let isInitialConnection = true
|
||||||
const heartbeatIntervalMs = 30000
|
|
||||||
|
|
||||||
// 生成全局唯一的 trace_id(优先使用 crypto.randomUUID)
|
// 生成唯一ID
|
||||||
const generateTraceId = (): string => {
|
const generateTraceId = (): string => {
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
||||||
// @ts-ignore
|
|
||||||
return `req-${crypto.randomUUID()}`
|
return `req-${crypto.randomUUID()}`
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
// Fallback:基于时间戳 + 随机数
|
// 忽略错误,使用fallback方案
|
||||||
const timestampPart = Date.now().toString(36)
|
|
||||||
let randomPart = ''
|
|
||||||
try {
|
|
||||||
// @ts-ignore
|
|
||||||
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
||||||
const buf = new Uint8Array(16)
|
|
||||||
// @ts-ignore
|
|
||||||
crypto.getRandomValues(buf)
|
|
||||||
randomPart = Array.from(buf)
|
|
||||||
.map((b) => b.toString(16).padStart(2, '0'))
|
|
||||||
.join('')
|
|
||||||
} else {
|
|
||||||
randomPart = Array.from({ length: 16 })
|
|
||||||
.map(() =>
|
|
||||||
Math.floor(Math.random() * 256)
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')
|
|
||||||
)
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
randomPart = Math.random().toString(36).slice(2)
|
|
||||||
}
|
}
|
||||||
return `req-${timestampPart}-${randomPart}`
|
const timestamp = Date.now().toString(36)
|
||||||
|
const random = Math.random().toString(36).slice(2)
|
||||||
|
return `req-${timestamp}-${random}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算重连延迟
|
||||||
const getReconnectDelay = () => {
|
const getReconnectDelay = () => {
|
||||||
const base = 1000 * Math.pow(2, reconnectAttempts)
|
const base = 1000 * Math.pow(2, reconnectAttempts)
|
||||||
return Math.min(base, maxReconnectDelayMs)
|
return Math.min(base, MAX_RECONNECT_DELAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (reconnectTimer) {
|
if (reconnectTimer) {
|
||||||
clearTimeout(reconnectTimer)
|
clearTimeout(reconnectTimer)
|
||||||
@@ -64,107 +46,198 @@ const cleanup = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动心跳
|
||||||
const startHeartbeat = () => {
|
const startHeartbeat = () => {
|
||||||
// 清理上一次的心跳
|
|
||||||
if (heartbeatTimer) {
|
if (heartbeatTimer) {
|
||||||
clearTimeout(heartbeatTimer)
|
clearTimeout(heartbeatTimer)
|
||||||
heartbeatTimer = null
|
|
||||||
}
|
}
|
||||||
// 递归 setTimeout,确保固定间隔执行
|
|
||||||
heartbeatTimer = setTimeout(() => {
|
heartbeatTimer = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
const msg = {
|
const msg = {
|
||||||
event: 'system.ping',
|
event: 'system.ping',
|
||||||
data: {},
|
data: {},
|
||||||
trace_id: generateTraceId() // 可选,用于请求-响应匹配
|
trace_id: generateTraceId()
|
||||||
}
|
}
|
||||||
socket.send(JSON.stringify(msg))
|
socket.send(JSON.stringify(msg))
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
// 下一次心跳
|
// 忽略心跳发送错误
|
||||||
|
}
|
||||||
startHeartbeat()
|
startHeartbeat()
|
||||||
}, heartbeatIntervalMs)
|
}, HEARTBEAT_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送心跳消息
|
||||||
|
const sendPing = () => {
|
||||||
|
try {
|
||||||
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
|
const msg = {
|
||||||
|
event: 'system.ping',
|
||||||
|
data: {},
|
||||||
|
trace_id: generateTraceId()
|
||||||
|
}
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略发送错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理WebSocket消息
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
let data: any = event.data
|
||||||
|
try {
|
||||||
|
data = JSON.parse(event.data)
|
||||||
|
} catch {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
console.log('WebSocket message:', data)
|
||||||
|
if (data.event == 'notify.receive') {
|
||||||
|
ElNotification({
|
||||||
|
title: data.data.notice_data.message.title || '通知',
|
||||||
|
message: data.data.notice_data.message.content,
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
mittBus.emit('notification-data-changed', 1)
|
||||||
|
}
|
||||||
|
mittBus.emit('ws:message', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置WebSocket事件处理器
|
||||||
|
const setupWebSocketHandlers = (ws: WebSocket) => {
|
||||||
|
ws.onopen = () => {
|
||||||
|
isInitialConnection = false
|
||||||
|
startHeartbeat()
|
||||||
|
mittBus.emit('ws:open')
|
||||||
|
sendPing()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = handleMessage
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
mittBus.emit('ws:error', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
mittBus.emit('ws:close')
|
||||||
|
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安排重连
|
||||||
const scheduleReconnect = () => {
|
const scheduleReconnect = () => {
|
||||||
|
if (!isInitialConnection) {
|
||||||
|
reconnectAttempts += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`)
|
||||||
|
mittBus.emit('ws:max-reconnect-reached')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
const delay = getReconnectDelay()
|
const delay = getReconnectDelay()
|
||||||
|
console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`)
|
||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
const { accessToken } = useUserStore()
|
const { accessToken } = useUserStore()
|
||||||
if (accessToken) initWebSocket(accessToken)
|
if (accessToken) {
|
||||||
|
try {
|
||||||
|
if (socket) {
|
||||||
|
try {
|
||||||
|
socket.close()
|
||||||
|
} catch {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${WS_BASE}${accessToken}`
|
||||||
|
socket = new WebSocket(url)
|
||||||
|
setupWebSocketHandlers(socket)
|
||||||
|
} catch (e) {
|
||||||
|
mittBus.emit('ws:error', e)
|
||||||
|
if (reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化WebSocket连接
|
||||||
export const initWebSocket = (token: string) => {
|
export const initWebSocket = (token: string) => {
|
||||||
try {
|
try {
|
||||||
if (!token) return
|
if (!token) return
|
||||||
if (socket) {
|
if (socket) {
|
||||||
try {
|
try {
|
||||||
socket.close()
|
socket.close()
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
socket = null
|
socket = null
|
||||||
}
|
}
|
||||||
cleanup()
|
cleanup()
|
||||||
|
reconnectAttempts = 0
|
||||||
|
isInitialConnection = true
|
||||||
|
|
||||||
const url = `${WS_BASE}${token}`
|
const url = `${WS_BASE}${token}`
|
||||||
socket = new WebSocket(url)
|
socket = new WebSocket(url)
|
||||||
|
setupWebSocketHandlers(socket)
|
||||||
socket.onopen = () => {
|
|
||||||
reconnectAttempts = 0
|
|
||||||
startHeartbeat()
|
|
||||||
mittBus.emit('ws:open')
|
|
||||||
try {
|
|
||||||
const msg = {
|
|
||||||
event: 'system.ping',
|
|
||||||
data: {},
|
|
||||||
trace_id: generateTraceId() // 可选,用于请求-响应匹配
|
|
||||||
}
|
|
||||||
socket?.send(JSON.stringify(msg))
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
|
||||||
const dataStr = event.data
|
|
||||||
let data: any = dataStr
|
|
||||||
try {
|
|
||||||
data = JSON.parse(dataStr)
|
|
||||||
} catch (_) {}
|
|
||||||
mittBus.emit('ws:message', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.onerror = (err) => {
|
|
||||||
mittBus.emit('ws:error', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.onclose = () => {
|
|
||||||
mittBus.emit('ws:close')
|
|
||||||
reconnectAttempts += 1
|
|
||||||
scheduleReconnect()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mittBus.emit('ws:error', e)
|
mittBus.emit('ws:error', e)
|
||||||
scheduleReconnect()
|
scheduleReconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭WebSocket连接
|
||||||
export const closeWebSocket = () => {
|
export const closeWebSocket = () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
try {
|
try {
|
||||||
socket?.close()
|
socket?.close()
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
socket = null
|
socket = null
|
||||||
|
reconnectAttempts = 0
|
||||||
|
isInitialConnection = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送WebSocket消息
|
||||||
export const sendWebSocket = (payload: string | JsonValue) => {
|
export const sendWebSocket = (payload: string | JsonValue) => {
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false
|
if (!socket || socket.readyState !== WebSocket.OPEN) return false
|
||||||
try {
|
try {
|
||||||
const data = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
const data = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
||||||
socket.send(data)
|
socket.send(data)
|
||||||
return true
|
return true
|
||||||
} catch (_) {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置重连次数
|
||||||
|
export const resetWebSocketReconnect = () => {
|
||||||
|
reconnectAttempts = 0
|
||||||
|
isInitialConnection = true
|
||||||
|
console.log('WebSocket重连次数已重置')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前重连次数
|
||||||
|
export const getWebSocketReconnectAttempts = () => reconnectAttempts
|
||||||
|
|
||||||
|
// 获取最大重连次数
|
||||||
|
export const getMaxReconnectAttempts = () => MAX_RECONNECT_ATTEMPTS
|
||||||
|
|
||||||
|
// 获取WebSocket连接状态
|
||||||
|
export const getWebSocketStatus = () => ({
|
||||||
|
socket,
|
||||||
|
readyState: socket?.readyState,
|
||||||
|
reconnectAttempts,
|
||||||
|
maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS,
|
||||||
|
isReconnecting: reconnectTimer !== null,
|
||||||
|
isHeartbeating: heartbeatTimer !== null,
|
||||||
|
isInitialConnection
|
||||||
|
})
|
||||||
@@ -175,6 +175,15 @@
|
|||||||
>
|
>
|
||||||
<i class="iconfont-sys"> </i>跟进记录
|
<i class="iconfont-sys"> </i>跟进记录
|
||||||
</el-link>
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="danger"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="delectfn(scope.row)"
|
||||||
|
v-if="hasAuth('customer@delete')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i>删除
|
||||||
|
</el-link>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
@@ -555,6 +564,22 @@
|
|||||||
plzyDialogVisible.value = false
|
plzyDialogVisible.value = false
|
||||||
gettableList()
|
gettableList()
|
||||||
}
|
}
|
||||||
|
const delectfn = (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'list' })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -0,0 +1,573 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<!-- <el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
|
||||||
|
<i class="iconfont-sys"></i> 导出
|
||||||
|
</el-button> -->
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="code"
|
||||||
|
label="编号"
|
||||||
|
align="center"
|
||||||
|
width="170px"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||||
|
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||||
|
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||||
|
<el-table-column prop="country_name" label="国家" align="center" />
|
||||||
|
<!-- <el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" /> -->
|
||||||
|
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<div
|
||||||
|
>WhatsApp:{{
|
||||||
|
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
|
||||||
|
}}</div
|
||||||
|
>
|
||||||
|
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
|
||||||
|
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||||
|
<!-- <el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.follow)">
|
||||||
|
{{ scope.row.follow_txt }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||||
|
<el-table-column prop="user_name" label="跟单人"></el-table-column>
|
||||||
|
<el-table-column prop="del_source" label="来源"></el-table-column>
|
||||||
|
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
|
||||||
|
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onDetail(scope.row)"
|
||||||
|
v-if="hasAuth('recycle@detail')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>详情
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="recoverfn(scope.row)"
|
||||||
|
v-if="hasAuth('recycle@recover')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>恢复
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="danger"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="delectfn(scope.row)"
|
||||||
|
v-if="hasAuth('recycle@delete')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i>删除
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||||
|
<div>电话:{{ detailRow.phone }}</div>
|
||||||
|
<div>国家:{{ detailRow.country_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>渠道:{{ detailRow.channel_name }}</div>
|
||||||
|
<div>邮箱:{{ detailRow.email }}</div>
|
||||||
|
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>发布人:{{ detailRow.enter_user_name }}</div>
|
||||||
|
<div>跟单人:{{ detailRow.user_name }}</div>
|
||||||
|
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
等级:
|
||||||
|
<span>
|
||||||
|
{{ detailRow.magnitude_txt }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
状态:<span>{{ detailRow.grade_txt }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
跟进状态:<span>{{ detailRow.follow_txt }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>备注:{{ detailRow.describe }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
|
||||||
|
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 图片预览对话框 -->
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="imageViewerVisible"
|
||||||
|
:url-list="imageList"
|
||||||
|
:initial-index="imageViewerIndex"
|
||||||
|
@close="closeImageViewer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const selectValue = ref<string[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const zyDialogVisible = ref(false)
|
||||||
|
const detailRow = ref<any>(null)
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
// 图片预览相关
|
||||||
|
const imageViewerVisible = ref(false)
|
||||||
|
const imageList = ref<string[]>([])
|
||||||
|
const imageViewerIndex = ref(0)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const zyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
onMounted(() => {
|
||||||
|
getuserList()
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
// tabs 切换联动
|
||||||
|
watch([keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
scene: 'recyclea'
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||||
|
tableData.value = data.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
//导出
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
function onDetail(row: any) {
|
||||||
|
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||||
|
detailRow.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const recoverfn = (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要恢复此条数据吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await manageService.customerRestore({ id: row.id })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const delectfn = (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const closeImageViewer = () => {
|
||||||
|
imageViewerVisible.value = false
|
||||||
|
imageList.value = []
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.timeline-img {
|
||||||
|
width: 180px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 450px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tags {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
width: 120px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-following {
|
||||||
|
color: #00a43f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dealed {
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-table {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.tag-items {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
.tag-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.tag-itemall {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.tag-active {
|
||||||
|
background: #3471ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tag-active :deep(.el-badge__content) {
|
||||||
|
color: #3471ff !important; /* 文字颜色 */
|
||||||
|
background-color: #fff !important; /* 背景色 */
|
||||||
|
}
|
||||||
|
.detail-info-table {
|
||||||
|
padding-left: 15px;
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dtitle {
|
||||||
|
line-height: 40px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
.detils-content {
|
||||||
|
padding-left: 15px;
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-record-header {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-record-list {
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
.list-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.record-type {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-time {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-user {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-footer {
|
||||||
|
.next-follow {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-records {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
:currentPage="pagination.currentPage"
|
:currentPage="pagination.currentPage"
|
||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn label="ID" prop="id" />
|
<ElTableColumn label="ID" prop="id" />
|
||||||
@@ -123,7 +125,15 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getWorkList()
|
getWorkList()
|
||||||
})
|
})
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
// 获取列表数据
|
// 获取列表数据
|
||||||
const getWorkList = async () => {
|
const getWorkList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
:currentPage="pagination.currentPage"
|
:currentPage="pagination.currentPage"
|
||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn label="ID" prop="id" width="80" />
|
<ElTableColumn label="ID" prop="id" width="80" />
|
||||||
@@ -209,7 +211,6 @@
|
|||||||
getcompanyList()
|
getcompanyList()
|
||||||
getsystemsList()
|
getsystemsList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取列表数据
|
// 获取列表数据
|
||||||
const getWorkList = async () => {
|
const getWorkList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -230,6 +231,15 @@
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
|
|
||||||
|
|||||||
@@ -496,7 +496,6 @@
|
|||||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
system_id: [{ required: true, message: '请选择系统', trigger: 'blur' }],
|
|
||||||
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
|
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
|
||||||
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
|
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<ElRow>
|
||||||
|
<div style="width: 12px"></div>
|
||||||
|
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||||
|
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@notice_add')"
|
||||||
|
>新增</ElButton
|
||||||
|
>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn label="编号" prop="msg_no" width="240px" />
|
||||||
|
<ElTableColumn label="通知标题" prop="title" />
|
||||||
|
<ElTableColumn label="通知内容" prop="content" />
|
||||||
|
<ElTableColumn label="推送类型" prop="push_method_name" />
|
||||||
|
<ElTableColumn label="场景" prop="scene_name" />
|
||||||
|
<ElTableColumn label="创建时间" prop="created_at" />
|
||||||
|
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElRow>
|
||||||
|
<el-button link @click="showfn(scope.row)"> 详情 </el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="deleteRole(scope.row)"
|
||||||
|
v-if="hasAuth('system@notice_destroy')"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
<ElDialog v-model="dialogVisible" title="新增通知" align-center>
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
|
||||||
|
<ElFormItem label="通知标题" prop="title">
|
||||||
|
<ElInput v-model="form.title" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="通知内容" prop="content">
|
||||||
|
<ElInput v-model="form.content" type="textarea" :rows="3" placeholder="请输入通知内容" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="通知用户" prop="recipient">
|
||||||
|
<el-select
|
||||||
|
v-model="form.recipient"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
placeholder="请选择要通知的用户"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in companyUserList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-text class="mx-1" type="warning">不选择用户,代表通知所有用户!</el-text>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
<ElDialog v-model="dialogVisiblexq" title="通知详情" align-center>
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>发送人:{{ noticeDetail.sender_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>通知标题:{{ noticeDetail.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
通知内容:
|
||||||
|
<div class="message-text" v-html="noticeDetail.content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
defineOptions({ name: 'Type ' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogVisiblexq = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
const isMobile = computed(() => width.value < 500)
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||||
|
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
|
||||||
|
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
const companyUserList = ref<any>()
|
||||||
|
// 获取用户列表
|
||||||
|
const getCompanyUserList = async () => {
|
||||||
|
const response = await manageService.presetusers()
|
||||||
|
companyUserList.value = response || []
|
||||||
|
}
|
||||||
|
const form = reactive<any>({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
recipient: []
|
||||||
|
})
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
|
||||||
|
// 表格实例引用
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
watch([keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
const noticeDetail = ref<any>({})
|
||||||
|
const showfn = async (row: any) => {
|
||||||
|
dialogVisiblexq.value = true
|
||||||
|
await systemService.getNoticeDetail({ id: row.id }).then((res: any) => {
|
||||||
|
noticeDetail.value = res
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
// 获取列表数据
|
||||||
|
const getWorkList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
|
||||||
|
const { records, current, size, total } = await systemService.getNoticeList({
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
})
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogType = ref('add')
|
||||||
|
|
||||||
|
const showDialog = (type: string) => {
|
||||||
|
getCompanyUserList()
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogType.value = type
|
||||||
|
form.title = ''
|
||||||
|
form.content = ''
|
||||||
|
form.recipient = []
|
||||||
|
}
|
||||||
|
const deleteRole = (row?: any) => {
|
||||||
|
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await systemService.delNotice({
|
||||||
|
id: row.id
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
const { message } = await systemService.sendNotice(form)
|
||||||
|
ElMessage.success(message)
|
||||||
|
dialogVisible.value = false
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.message-text {
|
||||||
|
padding: 10px 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
:currentPage="pagination.currentPage"
|
:currentPage="pagination.currentPage"
|
||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn label="ID" prop="id" width="80" />
|
<ElTableColumn label="ID" prop="id" width="80" />
|
||||||
@@ -236,7 +238,15 @@
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
// 系统类型相关辅助函数
|
// 系统类型相关辅助函数
|
||||||
const getTypeText = (type: string) => {
|
const getTypeText = (type: string) => {
|
||||||
const typeMap: { [key: string]: string } = {
|
const typeMap: { [key: string]: string } = {
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<el-button type="primary" @click="readfnpl" :disabled="selectedRows.length === 0">
|
||||||
|
<i class="iconfont-sys"></i> 批量已读
|
||||||
|
</el-button>
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="id" label="编号" align="center" width="240px">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{ scope.row.message.msg_no }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="id" label="通知标题" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{ scope.row.message.title }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="id" label="通知内容" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{ scope.row.message.content }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="id" label="发送人" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{ scope.row.message.sender_name }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-text class="mx-1" :type="scope.row.status === 0 ? 'info' : 'success'">
|
||||||
|
{{ scope.row.status === 1 ? '已读' : '未读' }}
|
||||||
|
</el-text>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="id" label="创建时间" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{ scope.row.message.created_at }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElRow>
|
||||||
|
<el-button link @click="showfn(scope.row)"> 详情 </el-button>
|
||||||
|
<!-- <el-button type="success" link @click="readfn(scope.row)"> 已读 </el-button> -->
|
||||||
|
<el-button type="danger" link @click="deleteRole(scope.row)"> 删除 </el-button>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
<ElDialog v-model="dialogVisible" title="通知详情" align-center>
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>发送人:{{ noticeDetail.sender_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>通知标题:{{ noticeDetail.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
通知内容:
|
||||||
|
<div class="message-text" v-html="noticeDetail.content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { commonService } from '@/api/commonApi'
|
||||||
|
// import { useAuth } from '@/composables/useAuth'
|
||||||
|
// const { hasAuth } = useAuth()
|
||||||
|
defineOptions({ name: 'Type ' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
|
||||||
|
// 表格实例引用
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
watch([keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
// 获取列表数据
|
||||||
|
const getWorkList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
|
||||||
|
const { records, current, size, total } = await commonService.getNoticeList({
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
})
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noticeDetail = ref<any>({})
|
||||||
|
const showfn = async (row: any) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
await commonService.getNoticeDetail({ id: row.id }).then((res: any) => {
|
||||||
|
noticeDetail.value = res.message
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const deleteRole = (row?: any) => {
|
||||||
|
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await commonService.delNotice({
|
||||||
|
id: row.id
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const readfnpl = async () => {
|
||||||
|
ElMessageBox.confirm('确定已读吗?', '确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await commonService.readNotice({
|
||||||
|
msg_ids: selectedRows.value
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.detail-info-table {
|
||||||
|
padding-left: 15px;
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.message-text {
|
||||||
|
padding: 10px 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -46,8 +46,8 @@
|
|||||||
<el-radio label="2">女</el-radio>
|
<el-radio label="2">女</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联系方式" prop="phone">
|
<el-form-item label="电话" prop="phone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入联系方式" />
|
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Email" prop="email">
|
<el-form-item label="Email" prop="email">
|
||||||
<el-input v-model="form.email" placeholder="请输入Email" />
|
<el-input v-model="form.email" placeholder="请输入Email" />
|
||||||
|
|||||||
@@ -46,8 +46,8 @@
|
|||||||
<el-radio :label="2">女</el-radio>
|
<el-radio :label="2">女</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联系方式" prop="phone">
|
<el-form-item label="电话" prop="phone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入联系方式" />
|
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Email" prop="email">
|
<el-form-item label="Email" prop="email">
|
||||||
<el-input v-model="form.email" placeholder="请输入Email" />
|
<el-input v-model="form.email" placeholder="请输入Email" />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<span>渠道:{{ customerInfo.channel_name }}</span>
|
<span>渠道:{{ customerInfo.channel_name }}</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="24" :md="6" :lg="5">
|
<el-col :sm="24" :md="6" :lg="5">
|
||||||
<span>联系方式:{{ customerInfo.phone }}</span>
|
<span>电话:{{ customerInfo.phone }}</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="24" :md="6" :lg="4">
|
<el-col :sm="24" :md="6" :lg="4">
|
||||||
<span>客户偏好:{{ customerInfo.car_type_name }}</span>
|
<span>客户偏好:{{ customerInfo.car_type_name }}</span>
|
||||||
|
|||||||
@@ -188,13 +188,27 @@
|
|||||||
>
|
>
|
||||||
<i class="iconfont-sys"></i> 跟进
|
<i class="iconfont-sys"></i> 跟进
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link type="danger" underline="never" @click="reportfn(scope.row)">
|
<el-link
|
||||||
|
type="warning"
|
||||||
|
underline="never"
|
||||||
|
@click="reportfn(scope.row)"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
>
|
||||||
<i class="iconfont-sys"></i> 标记无效
|
<i class="iconfont-sys"></i> 标记无效
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
type="danger"
|
type="danger"
|
||||||
underline="never"
|
underline="never"
|
||||||
@click="delectfn(scope.row)"
|
@click="delectfn(scope.row)"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
v-if="hasAuth('crm_customer@remove')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i> 移除
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="danger"
|
||||||
|
underline="never"
|
||||||
|
@click="crmdelectfn(scope.row)"
|
||||||
v-if="hasAuth('crm_customer@delete')"
|
v-if="hasAuth('crm_customer@delete')"
|
||||||
>
|
>
|
||||||
<i class="iconfont-sys"></i> 删除
|
<i class="iconfont-sys"></i> 删除
|
||||||
@@ -453,7 +467,7 @@
|
|||||||
}
|
}
|
||||||
//删除
|
//删除
|
||||||
const delectfn = (row: any) => {
|
const delectfn = (row: any) => {
|
||||||
ElMessageBox.confirm('确定要删除此用户吗?', '提示', {
|
ElMessageBox.confirm('确定要移除此客户到公海吗?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
@@ -468,6 +482,25 @@
|
|||||||
// 用户点击了取消
|
// 用户点击了取消
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const crmdelectfn = (row: any) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要删除此客户吗?删除后无法恢复,若存在疑问可标记为无效用户或操作移除,将客户移除到公海!',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await CrmService.crmCustomerDelete({ id: row.id, scene: 'force' })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
// 标记无效
|
// 标记无效
|
||||||
const reportfn = (row: any) => {
|
const reportfn = (row: any) => {
|
||||||
reportForm.id = row.id
|
reportForm.id = row.id
|
||||||
|
|||||||
Reference in New Issue
Block a user