no commit message

This commit is contained in:
annnj-company
2026-04-24 11:28:23 +08:00
parent 49476bc357
commit 522c222ae3
11 changed files with 303 additions and 47 deletions

View File

@@ -140,3 +140,9 @@ export async function checkBusinessNoIsValid (params) {
const res = await post('admin/Bussiness/checkBusinessNoIsValid', params) const res = await post('admin/Bussiness/checkBusinessNoIsValid', params)
return res return res
} }
// 获取未回价数量return_price_status=2
export async function getUnreturnedPriceCount (params) {
const res = await post('admin/Pending/getUnreturnedPriceCount', params)
return res
}

View File

@@ -1,9 +1,8 @@
<template> <template>
<div style="height: 100%;"> <div style="height: 100%;">
<el-menu <el-menu
:default-active="'0'" :default-active="'0'"
@select="handleSelect" @select="handleSelect"
@open="handleOpen" @open="handleOpen"
:collapse="collapse" :collapse="collapse"
@@ -11,22 +10,25 @@
> >
<template v-for="(menuItem, i_index) in menuData"> <template v-for="(menuItem, i_index) in menuData">
<el-menu-item :index="String(i_index)" :key="i_index"> <el-menu-item :index="String(i_index)" :key="i_index">
<template > <template >
<!-- <i style="margin-top: -18px;color: #444444;" :class=" replacePhrases( menuItem.title, iconsRule) "></i>
<div style="margin-top:-48px;" class="menu-custom">{{ replacePhrases( menuItem.title, replaceRule1)}}</div> -->
<div style="display: flex; align-items: center;" class="menu-custom"> <div style="display: flex; align-items: center;" class="menu-custom">
<i :class="replacePhrases(menuItem.title, iconsRule)"></i> <i :class="replacePhrases(menuItem.title, iconsRule)"></i>
<div style="margin-left: 8px;">{{ replacePhrases(menuItem.title, replaceRule1) }}</div> <div style="display: flex; align-items: center; margin-left: 8px;">
{{ replacePhrases(menuItem.title, replaceRule1) }}
</div>
<div v-if="isHuiJiaMenu(menuItem) && unreturnedCount >= 1" class="push-value-badge">{{ unreturnedCount }}</div>
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
</template> </template>
</el-menu> </el-menu>
</div> </div>
</template> </template>
<script> <script>
import "@/styles/globalSetting.less"; import "@/styles/globalSetting.less";
import { EventBus } from '@/libs/eventBus'
import socket from '@/libs/socket'
export default { export default {
name: 'NxIconMenu', name: 'NxIconMenu',
props: { props: {
@@ -42,16 +44,17 @@
data() { data() {
return { return {
activeMenu: '1-4-1', activeMenu: '1-4-1',
unreturnedCount: 0,
replaceRule1:[ replaceRule1:[
['询价项目','询价'], ['询价项目','询价'],
['回价项目','回价'], ['回价项目','回价'],
['预估单','预估'], ['预估单','预估'],
['查勘项目','查勘'], ['查勘项目','查勘'],
['报告项目','报告'], ['报告项目','报告'],
['分部管理','分部'], ['分部管理','分部'],
['财务项目','财务'], ['财务项目','财务'],
['基础设置','设置'], ['基础设置','设置'],
['OA办公','OA'],// 隐藏 ['OA办公','OA'],
['银行对接','银行'], ['银行对接','银行'],
], ],
iconsRule:[ iconsRule:[
@@ -63,36 +66,47 @@
['报告项目','el-icon-tickets'], ['报告项目','el-icon-tickets'],
['财务项目','el-icon-money'], ['财务项目','el-icon-money'],
['基础设置','el-icon-setting'], ['基础设置','el-icon-setting'],
['OA办公' , 'el-icon-bell'], // 隐藏 ['OA办公' , 'el-icon-bell'],
['银行对接','el-icon-bank-card'], ['银行对接','el-icon-bank-card'],
] ]
}; };
}, },
mounted() {
// 监听EventBus事件
this.eventBusCallback = (count) => {
this.$set(this, 'unreturnedCount', count)
}
this.socketCallback = (data) => {
if (data && data.count !== undefined) {
this.$set(this, 'unreturnedCount', data.count)
}
}
EventBus.$on('updateUnreturnedCount', this.eventBusCallback)
socket.on('updateUnreturnedCount', this.socketCallback)
},
beforeDestroy() {
EventBus.$off('updateUnreturnedCount', this.eventBusCallback)
socket.off('updateUnreturnedCount', this.socketCallback)
},
methods: { methods: {
isHuiJiaMenu(menuItem) {
return menuItem.title === '回价项目' || menuItem.routerName === 'priceReturn'
},
handleSelect(key, item , externalInfo ) { handleSelect(key, item , externalInfo ) {
console.log('menu-select', key, item,externalInfo); this.$emit('menu-select', key, item.routerName, externalInfo);
this.$emit('menu-select', key, item.routerName, externalInfo);
console.log('externalInfo menu-select', externalInfo ,key,item );
}, },
handleOpen(key, keyPath) { handleOpen(key, keyPath) {
console.log('menu-open', key, keyPath, externalInfo ); this.$emit('menu-open', key, keyPath);
this.$emit('menu-open', key, keyPath, externalInfo);
console.log('externalInfo menu-open', externalInfo ,key,item );
}, },
handleClose(key, keyPath, externalInfo ) { handleClose(key, keyPath, externalInfo ) {
console.log('menu-close', key, keyPath, externalInfo);
this.$emit('menu-close', key, keyPath, externalInfo); this.$emit('menu-close', key, keyPath, externalInfo);
}, },
toggleCollapse() { toggleCollapse() {
// 触发事件通知父组件改变collapse状态
this.$emit('toggle-collapse', !this.collapse); this.$emit('toggle-collapse', !this.collapse);
}, },
replacePhrases(text, replacementRules) { replacePhrases(text, replacementRules) {
// 遍历替换规则数组
for (let i=0; i< replacementRules.length; i++) { for (let i=0; i< replacementRules.length; i++) {
// 使用正则表达式查找匹配的短语
if (text == replacementRules[i][0]) { if (text == replacementRules[i][0]) {
return replacementRules[i][1]; return replacementRules[i][1];
} }
@@ -102,7 +116,7 @@
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.collapse-btn { .collapse-btn {
height: 40px; height: 40px;
@@ -144,6 +158,8 @@
} }
.menu-custom { .menu-custom {
color: var(--menu-font-color); color: var(--menu-font-color);
display: flex;
align-items: center;
} }
:deep .el-menu-item.is-active { :deep .el-menu-item.is-active {
background-color: var(--bg-color); background-color: var(--bg-color);
@@ -151,4 +167,20 @@
:deep .el-menu-item.is-active .menu-custom { :deep .el-menu-item.is-active .menu-custom {
color: var(--main-color); color: var(--main-color);
} }
</style> .push-value-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
line-height: 18px;
padding: 0 4px;
font-size: 11px;
color: #fff;
background-color: #f56c6c;
border-radius: 9px;
margin-left: 4px;
text-align: center;
flex-shrink: 0;
}
</style>

View File

@@ -3,27 +3,85 @@
<div style="height: 100%;"> <div style="height: 100%;">
<!-- 如果有子菜单则渲染子菜单 --> <!-- 如果有子菜单则渲染子菜单 -->
<el-submenu style="height: 100%;" v-if="item.children && item.children.length > 0" :index="item.routerName" > <el-submenu style="height: 100%;" v-if="item.children && item.children.length > 0" :index="item.routerName" >
<template slot="title"> <template slot="title">
{{ item.title }} {{ item.title }}
<!-- 一级菜单回价显示无数量红点 -->
<span v-if="isHuiJiaMenu && unreturnedCount >= 1" class="push-dot-badge"></span>
<!-- 二级菜单未回价显示带数量红点 -->
<span v-else-if="isUnreturnedPriceMenu && unreturnedCount > 0" class="push-value-badge">{{ unreturnedCount }}</span>
</template> </template>
<!-- 递归调用MenuItem组件 --> <!-- 递归调用MenuItem组件 -->
<menu-item v-for="(child, index) in item.children" :key="index" :item="child" class="menu-custom"></menu-item> <menu-item v-for="(child, index) in item.children" :key="index" :item="child" class="menu-custom"></menu-item>
</el-submenu> </el-submenu>
<!-- 如果没有子菜单则渲染菜单项 --> <!-- 如果没有子菜单则渲染菜单项 -->
<el-menu-item v-else :index="item.routerName" class="menu-custom"> <el-menu-item v-else :index="item.routerName" class="menu-custom">
<!-- <i class="el-icon-document"></i> --> <!-- <i class="el-icon-document"></i> -->
{{ item.title }} {{ item.title }}
<!-- 三级菜单未回价列表显示带数量红点 -->
<span v-if="isUnreturnedPriceListMenu && unreturnedCount > 0" class="push-value-badge">{{ unreturnedCount }}</span>
</el-menu-item> </el-menu-item>
</div> </div>
</template> </template>
<script> <script>
import "@/styles/globalSetting.less"; import "@/styles/globalSetting.less";
import { EventBus } from '@/libs/eventBus'
import socket from '@/libs/socket'
export default { export default {
name: 'MenuItem', name: 'MenuItem',
props: { props: {
item: Object item: Object
},
data() {
return {
unreturnedCount: 0
}
},
computed: {
// 判断是否是一级菜单【回价项目】
isHuiJiaMenu() {
const isMatch = this.item.title === '回价项目' || this.item.routerName === 'priceReturn'
return isMatch
},
// 判断是否是二级菜单【未回价】
isUnreturnedPriceMenu() {
const isMatch = this.item.path === '/priceReturn/unreturnedPrice' || this.item.title === '未回价' || this.item.routerName === 'unreturnedPrice'
return isMatch
},
// 判断是否是三级菜单【未回价列表】
isUnreturnedPriceListMenu() {
const isMatch = this.item.title === '未回价列表'
return isMatch
}
},
mounted() {
console.log('菜单组件mounted - item:', this.item)
// 保存回调引用用于销毁时移除
this.eventBusCallback = (count) => {
this.unreturnedCount = count
console.log('EventBus收到未回价数量更新:', count, '当前item:', this.item.name || this.item.title)
}
this.socketCallback = (data) => {
if (data && data.count !== undefined) {
// 使用Vue.set确保响应式更新
this.$set(this, 'unreturnedCount', data.count)
console.log('WebSocket收到未回价数量更新:', data.count, '当前item:', this.item.name || this.item.title)
}
}
// 监听EventBus事件页面初始化时
EventBus.$on('updateUnreturnedCount', this.eventBusCallback)
// 监听WebSocket推送的未回价数量更新
socket.on('updateUnreturnedCount', this.socketCallback)
},
beforeDestroy() {
// 移除监听
EventBus.$off('updateUnreturnedCount', this.eventBusCallback)
socket.off('updateUnreturnedCount', this.socketCallback)
} }
} }
</script> </script>
@@ -37,4 +95,26 @@
:deep .el-menu-item.is-active.menu-custom { :deep .el-menu-item.is-active.menu-custom {
color: var(--main-color); color: var(--main-color);
} }
</style> .push-value-badge {
display: inline-block;
min-width: 20px;
height: 20px;
line-height: 20px;
padding: 0 6px;
font-size: 12px;
color: #fff;
background-color: #f56c6c;
border-radius: 10px;
margin-left: 8px;
text-align: center;
}
.push-dot-badge {
display: inline-block;
width: 8px;
height: 8px;
background-color: #f56c6c;
border-radius: 50%;
margin-left: 8px;
margin-top: 4px;
}
</style>

View File

@@ -1,9 +1,65 @@
import io from 'socket.io-client' import io from 'socket.io-client'
// 使用 polling 模式以兼容 PHPSocketIO 1.1
const socket = io(process.env.VUE_APP_SOCKET_URL, { const socket = io(process.env.VUE_APP_SOCKET_URL, {
transports: ['websocket'], transports: ['polling'],
reconnection: true, // 是否自动重新建立连接默认为true reconnection: true,
reconnectionAttempts: 1 // 尝试重连的次数,默认为无限次 reconnectionAttempts: 5,
autoConnect: false,
forceNew: true,
timeout: 10000,
jsonp: false,
query: {},
upgrade: false,
allowUpgrades: false,
rejectUnauthorized: false,
timestampRequests: true
}) })
// 添加连接状态监听
socket.on('connect', () => {
console.log('WebSocket已成功连接到服务器:', process.env.VUE_APP_SOCKET_URL)
})
socket.on('disconnect', (reason) => {
console.log('WebSocket连接断开原因:', reason)
})
socket.on('connect_error', (error) => {
console.error('WebSocket连接失败:', error)
})
socket.on('connect_timeout', () => {
console.error('WebSocket连接超时')
})
// 连接方法
export const connectSocket = () => {
console.log('尝试启动WebSocket连接...')
console.log('当前连接状态:', socket.connected)
console.log('连接状态详细:', socket.io && socket.io.engine ? socket.io.engine.transport.name : '未知')
console.log('目标地址:', process.env.VUE_APP_SOCKET_URL)
// 如果未连接或者正在断开连接,尝试连接
if (!socket.connected || socket.disconnected) {
socket.connect()
console.log('WebSocket连接请求已发送')
} else {
console.log('WebSocket已经处于连接状态')
}
}
// 断开连接方法
export const disconnectSocket = () => {
console.log('尝试断开WebSocket连接...')
console.log('当前连接状态:', socket.connected)
if (socket.connected) {
socket.disconnect()
console.log('WebSocket连接已断开')
} else {
console.log('WebSocket未连接无需断开')
}
}
export default socket export default socket

View File

@@ -0,0 +1,26 @@
import { EventBus } from './eventBus'
import { getUnreturnedPriceCount } from '@/api/businessManage/inquiry'
/**
* 获取并更新未回价数量
* 调用API获取最新数量并通过EventBus广播给所有监听者
*/
export const fetchAndUpdateUnreturnedPriceCount = async () => {
console.log('fetchAndUpdateUnreturnedPriceCount: 开始获取未回价数量...')
try {
const response = await getUnreturnedPriceCount()
if (response && response.code === 1) {
const count = response.data.count || 0
console.log('fetchAndUpdateUnreturnedPriceCount: 获取到未回价数量:', count)
// 通过事件总线传递数量给菜单组件
EventBus.$emit('updateUnreturnedCount', count)
return count
} else {
console.warn('fetchAndUpdateUnreturnedPriceCount: 接口返回异常:', response)
return 0
}
} catch (error) {
console.error('fetchAndUpdateUnreturnedPriceCount: 获取未回价数量失败:', error)
return 0
}
}

View File

@@ -5,6 +5,8 @@ import iView from 'view-design'
import { getToken, canTurnTo, initRouter, isRefreshToken } from '@/libs/util' import { getToken, canTurnTo, initRouter, isRefreshToken } from '@/libs/util'
import * as localStore from 'store' import * as localStore from 'store'
import store from '@/store' import store from '@/store'
import { connectSocket } from '@/libs/socket'
import { fetchAndUpdateUnreturnedPriceCount } from '@/libs/unreturnedPrice'
Vue.use(Router) Vue.use(Router)
let routers = routes let routers = routes
@@ -30,6 +32,14 @@ const HOME_PAGE_NAME = 'home' // 平台首页
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
iView.LoadingBar.start() iView.LoadingBar.start()
const token = getToken() const token = getToken()
// 如果用户已登录确保WebSocket连接已建立并获取最新未回价数量
if (token && to.name !== LOGIN_PAGE_NAME) {
connectSocket()
// 每次刷新页面都获取最新的未回价数量
fetchAndUpdateUnreturnedPriceCount()
}
if (!token && to.name !== LOGIN_PAGE_NAME) { if (!token && to.name !== LOGIN_PAGE_NAME) {
localStore.clearAll() localStore.clearAll()
// 未登录且要跳转的页面不是登录页 // 未登录且要跳转的页面不是登录页

View File

@@ -3,6 +3,7 @@ import { login, checkLogin } from '@/api/user'
import store from 'store' import store from 'store'
import config from '@/config' import config from '@/config'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { connectSocket } from '@/libs/socket'
export default { export default {
state: { state: {
@@ -33,8 +34,8 @@ export default {
login (state, data) { login (state, data) {
console.log("user.js::login::userinfo",data.userinfo) console.log("user.js::login::userinfo",data.userinfo)
// store.set('indexMenuList', JSON.stringify(data.menu['index'])) // store.set('indexMenuList', JSON.stringify(data.menu['index']))
// store.set('busMenuList', JSON.stringify(data.menu)) // store.set('busMenuList', JSON.stringify(data.menu))
store.set('busMenuList', JSON.stringify(data.admmenu)) store.set('busMenuList', JSON.stringify(data.admmenu))
store.set('userinfo', data.userinfo) store.set('userinfo', data.userinfo)
store.set('isAdmin',data.userinfo.roleCode.includes("ROLE_ADMIN")) store.set('isAdmin',data.userinfo.roleCode.includes("ROLE_ADMIN"))
store.set('access', data.perFlags) store.set('access', data.perFlags)
@@ -53,7 +54,7 @@ export default {
}, },
actions: { actions: {
handleLogin ({ commit }, { username, password }) { handleLogin ({ commit }, { username, password }) {
username = username.trim() username = username.trim()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
login({ username, password, version: config.version }).then(res => { login({ username, password, version: config.version }).then(res => {
@@ -61,6 +62,8 @@ export default {
store.clearAll() store.clearAll()
commit('setToken', res.data.apiAuth) commit('setToken', res.data.apiAuth)
commit('login', res.data) commit('login', res.data)
// 登录成功后启动WebSocket连接
connectSocket()
} }
resolve(res) resolve(res)
}).catch(err => { }).catch(err => {

View File

@@ -196,13 +196,10 @@ export default {
WorkflowProgressCount: 0 // 工作流进度-未读消息数量 WorkflowProgressCount: 0 // 工作流进度-未读消息数量
}, },
label1: (h) => { label1: (h) => {
return h('div', [ const count = this.formItem.inquiryPendingCount || 0
return h('div', { style: { display: 'flex', alignItems: 'center' } }, [
h('span', '询价待处理'), h('span', '询价待处理'),
h('Badge', { count > 0 ? h('span', { class: 'message-badge' }, count) : null
props: {
count: this.formItem.inquiryPendingCount
}
})
]) ])
}, },
/* label2: (h) => { /* label2: (h) => {
@@ -321,6 +318,8 @@ export default {
getMessageList().then(res => { getMessageList().then(res => {
if (res.code === 1) { if (res.code === 1) {
this.formItem = res.data this.formItem = res.data
console.log('message.vue 获取消息列表成功:', this.formItem)
console.log('询价待处理数量:', this.formItem.inquiryPendingCount)
} else { } else {
this.$Message.error(res.msg) this.$Message.error(res.msg)
} }
@@ -332,7 +331,7 @@ export default {
if (res.code === 1) { if (res.code === 1) {
if (row.message_type == 5 || row.message_type == 8 || row.message_type == 11 || row.message_type == 14 || row.message_type == 29 || row.message_type == 31){ if (row.message_type == 5 || row.message_type == 8 || row.message_type == 11 || row.message_type == 14 || row.message_type == 29 || row.message_type == 31){
this.getMessageList() this.getMessageList()
} }
} else { } else {
this.$Message.error(res.msg) this.$Message.error(res.msg)
} }
@@ -372,4 +371,17 @@ export default {
.issee { .issee {
color: #ccc; color: #ccc;
} }
.message-badge {
display: inline-block;
min-width: 18px;
height: 18px;
line-height: 18px;
padding: 0 4px;
font-size: 11px;
color: #fff;
background-color: #f56c6c;
border-radius: 9px;
margin-left: 6px;
text-align: center;
}
</style> </style>

View File

@@ -24,8 +24,33 @@
</template> </template>
<script> <script>
import homePage from './components/message' import homePage from './components/message'
import { connectSocket } from '@/libs/socket'
import { EventBus } from '@/libs/eventBus'
import { getUnreturnedPriceCount } from '@/api/businessManage/inquiry'
export default { export default {
components: { homePage } components: { homePage },
mounted() {
// 进入首页时启动websocket连接
connectSocket()
// 获取未回价数量
this.getUnreturnedPriceCountData()
},
methods: {
// 获取未回价数量
async getUnreturnedPriceCountData() {
try {
const response = await getUnreturnedPriceCount()
if (response && response.code === 1) {
const count = response.data.count || 0
console.log('未回价数量:', count)
// 通过事件总线传递数量给菜单组件
EventBus.$emit('updateUnreturnedCount', count)
}
} catch (error) {
console.error('获取未回价数量失败:', error)
}
}
}
} }
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="user-avator-dropdown"> <div class="user-avator-dropdown">
<Dropdown @on-click="handleClick"> <Dropdown @on-click="handleClick">
<div style="display: inline-block; vertical-align: middle;"> <div style="display: inline-block; vertical-align: middle;">
<Avatar :src="userAvator || require('@/assets/images/avatar.jpg')" size="small" style="margin-right: 10px" /> <Avatar :src="userAvator || require('@/assets/images/avatar.jpg')" size="small" style="margin-right: 10px" />
<!-- <span style="color: #444;">{{username}} - [退出]<i color="#444" class="el-icon-setting"></i></span> --> <!-- <span style="color: #444;">{{username}} - [退出]<i color="#444" class="el-icon-setting"></i></span> -->
<span style="color: #444;">{{username}}</span> <span style="color: #444;">{{username}}</span>
</div> </div>
<DropdownMenu slot="list" align="center"> <DropdownMenu slot="list" align="center">
<!-- <DropdownItem name="usercenter">个人信息</DropdownItem> --> <!-- <DropdownItem name="usercenter">个人信息</DropdownItem> -->
@@ -27,6 +27,7 @@ import './avatar.less'
import { logout } from '@/api/user' import { logout } from '@/api/user'
import store from 'store' import store from 'store'
import changePassword from './changePassword' import changePassword from './changePassword'
import { disconnectSocket } from '@/libs/socket'
export default { export default {
name: 'avatar', name: 'avatar',
props: { props: {
@@ -123,6 +124,8 @@ export default {
switch (name) { switch (name) {
case 'logout': case 'logout':
logout({}).then(() => { logout({}).then(() => {
// 断开WebSocket连接
disconnectSocket()
this.$store.commit('logout') this.$store.commit('logout')
location.reload() location.reload()
}) })

View File

@@ -4,7 +4,7 @@
<!-- <Avatar :src="userAvator"/> --> <!-- <Avatar :src="userAvator"/> -->
<span style="color: #444; ">{{username}}-[退出]</span> <span style="color: #444; ">{{username}}-[退出]</span>
<i class="el-icon-setting"></i> <i class="el-icon-setting"></i>
<DropdownMenu slot="list"> <DropdownMenu slot="list">
<!-- <DropdownItem name="usercenter">个人信息</DropdownItem> <!-- <DropdownItem name="usercenter">个人信息</DropdownItem>
<DropdownItem name="changePwd">修改密码</DropdownItem> --> <DropdownItem name="changePwd">修改密码</DropdownItem> -->
@@ -18,6 +18,7 @@
import './user.less' import './user.less'
import { logout } from '@/api/user' import { logout } from '@/api/user'
import store from 'store' import store from 'store'
import { disconnectSocket } from '@/libs/socket'
export default { export default {
name: 'user', name: 'user',
props: { props: {
@@ -39,6 +40,8 @@ export default {
switch (name) { switch (name) {
case 'logout': case 'logout':
logout({}).then(() => { logout({}).then(() => {
// 断开WebSocket连接
disconnectSocket()
this.$store.commit('logout') this.$store.commit('logout')
location.reload() location.reload()
}) })