You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

762 lines
21 KiB

5 months ago
5 months ago
2 months ago
2 months ago
2 months ago
5 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. <script setup>
  2. import {computed, onMounted, ref} from 'vue'
  3. import {useRoute, useRouter} from 'vue-router'
  4. import {ElMessage} from 'element-plus'
  5. import ChangePassword from '@/components/dialogs/changePassword.vue'
  6. import {useAdminStore} from '@/store'
  7. import {storeToRefs} from 'pinia'
  8. import {filterMenu, getRoutePath} from "@/utils/menuUtils.js";
  9. import API from '@/util/http.js'
  10. import bell from '@/assets/SvgIcons/bell.svg'
  11. import noMessage from '@/assets/images/no-message.svg'
  12. import goTop from '@/assets/SvgIcons/go-top.svg'
  13. import {getOrderPage} from '@/utils/goToCheck.js'
  14. import {groupMessages} from "@/utils/getMessage.js"
  15. import {useMessageStore} from '@/store/index.js'
  16. // ------------------ ICONS ------------------
  17. const icons = import.meta.glob('@/assets/SvgIcons/*.svg', {eager: true})
  18. const menuNameMap = {
  19. '工作台': 'workbench',
  20. '金币管理': 'gold-management',
  21. '现金管理': 'cash-management',
  22. '活动管理': 'activity-management',
  23. '频道管理': 'channel-management',
  24. '权限管理': 'permission-management',
  25. }
  26. const getIconPath = (menuName) => {
  27. const englishName = menuNameMap[menuName] || menuName;
  28. const possibleKeys = [
  29. `@/assets/SvgIcons/${englishName}.svg`,
  30. `./SvgIcons/${englishName}.svg`,
  31. `/src/assets/SvgIcons/${englishName}.svg`
  32. ]
  33. for (const key of possibleKeys) {
  34. if (icons[key]) {
  35. const iconModule = icons[key]
  36. return iconModule.default || iconModule
  37. }
  38. }
  39. }
  40. // ------------------ 刷新数据 ------------------
  41. const refreshData = async () => {
  42. try {
  43. ElMessage({message: '数据刷新中,请稍候...', type: 'info'});
  44. const response = await API({url: '/Mysql', method: 'POST', data: {}});
  45. if (response && response.code === 200) {
  46. const currentRoute = route.fullPath;
  47. router.replace('/blank');
  48. setTimeout(() => router.replace(currentRoute), 10);
  49. ElMessage.success('数据刷新成功');
  50. } else {
  51. ElMessage.error('数据刷新失败:' + (response?.msg || '未知错误'));
  52. }
  53. } catch (error) {
  54. console.error(error)
  55. ElMessage.error('数据刷新异常,请重试');
  56. }
  57. }
  58. // ------------------ 菜单逻辑 ------------------
  59. const route = useRoute()
  60. const router = useRouter()
  61. const adminStore = useAdminStore()
  62. const {adminData, menuTree, flag} = storeToRefs(adminStore)
  63. const menuList = ref(filterMenu(menuTree.value))
  64. function findBestMatch(menuList, path) {
  65. let bestMatch = ''
  66. function traverse(menus) {
  67. for (const item of menus) {
  68. const itemPath = getRoutePath(item)
  69. if (path.startsWith(itemPath) && itemPath.length > bestMatch.length) {
  70. bestMatch = itemPath
  71. }
  72. if (item.children?.length) traverse(item.children)
  73. }
  74. }
  75. traverse(menuList)
  76. return bestMatch || path
  77. }
  78. const activeMenu = computed(() => findBestMatch(menuList.value, route.path))
  79. // ------------------ 用户信息 / 密码修改 ------------------
  80. const messageVisible = ref(false)
  81. const openMessage = () => (messageVisible.value = true)
  82. const closeMessage = () => (messageVisible.value = false)
  83. const showPasswordDialog = ref(false)
  84. const pwdRef = ref()
  85. const openChangePassword = () => (showPasswordDialog.value = true)
  86. const onPwdDialogClosed = () => pwdRef.value?.resetFields()
  87. // ------------------ 退出登录 ------------------
  88. function logout() {
  89. const machineId = localStorage.getItem('machineId')
  90. localStorage.removeItem('token')
  91. adminStore.clearState()
  92. router.push('/login?machineId=' + machineId)
  93. ElMessage.success('退出成功')
  94. }
  95. // ------------------ 员工数据开关 ------------------
  96. const toggleFlag = () => {
  97. const newFlag = flag.value === 1 ? 0 : 1
  98. adminStore.setFlag(newFlag)
  99. ElMessage.success(newFlag === 1 ? '员工数据已隐藏' : '员工数据已显示')
  100. }
  101. // ------------------ 消息中心(完全修复版) ------------------
  102. const messageStore = useMessageStore()
  103. const {messages} = storeToRefs(messageStore)
  104. // 获取消息
  105. const getMessage = async () => {
  106. try {
  107. const res = await API({
  108. url: '/getMessage',
  109. method: 'POST',
  110. data: {}
  111. });
  112. if (res?.data) {
  113. const cleanList = res.data.filter(i => i.flag !== 1)
  114. messageStore.setMessages(cleanList)
  115. }
  116. } catch (e) {
  117. console.error("getMessage error:", e)
  118. }
  119. }
  120. // 点击铃铛 → 打开弹窗并刷新
  121. const showMessageDialog = ref(false)
  122. const openMessageDialog = async () => {
  123. showMessageDialog.value = true
  124. await getMessage() // 等待消息更新
  125. }
  126. // 关闭消息窗口
  127. const closeMessageDialog = () => showMessageDialog.value = false
  128. // 小红点(完全响应式)
  129. const messageDot = computed(() => messages.value.length > 0)
  130. // 消息数量(完全响应式)
  131. const messageNum = computed(() => messages.value.length)
  132. // 按日期分组(computed)
  133. const messageList = computed(() => groupMessages(messages.value))
  134. // 按日期生成最终结构(computed)
  135. const groupedMessages = computed(() => {
  136. const result = {}
  137. messageList.value.forEach(item => {
  138. if (!result[item.group]) result[item.group] = []
  139. result[item.group].push(item)
  140. })
  141. return result
  142. })
  143. // 显示全部 or 显示前两条
  144. const showAll = ref(false)
  145. const displayMessages = computed(() => {
  146. if (showAll.value) return groupedMessages.value
  147. let count = 0
  148. const limited = {}
  149. const groupOrder = ['今天', '昨天', '更早']
  150. for (const g of groupOrder) {
  151. const group = groupedMessages.value[g]
  152. if (!group) continue
  153. limited[g] = []
  154. for (const item of group) {
  155. if (count < 2) {
  156. limited[g].push(item)
  157. count++
  158. }
  159. }
  160. if (limited[g].length === 0) delete limited[g]
  161. if (count >= 2) break
  162. }
  163. return limited
  164. })
  165. const toggleShowAll = () => showAll.value = !showAll.value
  166. // 返回顶部
  167. const scrollContainer = ref(null)
  168. const scrollToTop = () => scrollContainer.value?.scrollTo({top: 0, behavior: 'smooth'})
  169. // 点击消息 → 已读 + 跳转
  170. const handleMessageClick = async (item) => {
  171. const res = await API({
  172. url: '/getMessage/update',
  173. method: 'POST',
  174. data: {id: item.id}
  175. });
  176. if (res.code === 200) {
  177. closeMessageDialog()
  178. await router.push(getOrderPage(item.status))
  179. await getMessage()
  180. ElMessage.success('跳转成功')
  181. } else {
  182. ElMessage.error('跳转失败')
  183. }
  184. }
  185. onMounted(() => getMessage())
  186. </script>
  187. <template>
  188. <div class="main-container">
  189. <!-- 背景毛玻璃层作为内容容器 -->
  190. <div class="background-glass">
  191. <!-- 侧边栏 -->
  192. <div class="sidebar-container">
  193. <el-aside class="sidebar-layout">
  194. <div class="logo">
  195. <img src="../assets/logo.png" alt="logo" style="width: 9vh; height: 9vh"/>
  196. </div>
  197. <div class="menu-scroll-container">
  198. <el-menu :router="true" :default-active="activeMenu" style="min-height: 80vh;border:none;">
  199. <!-- 递归渲染菜单层级 -->
  200. <template v-for="menu in menuList" :key="menu.id">
  201. <!-- 有子菜单的父级菜单menuType=2 且存在children -->
  202. <el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.id.toString()">
  203. <template #title>
  204. <img
  205. :src="getIconPath(menu.menuName)"
  206. :alt="`${menu.menuName}图标`"
  207. style="width: 4vh; height: 4vh; margin-right: 4px;"
  208. >
  209. <span>{{ menu.menuName }}</span>
  210. </template>
  211. <!-- 子菜单 -->
  212. <template v-for="child in menu.children" :key="child.id">
  213. <!-- 子菜单为叶子节点无children -->
  214. <el-menu-item v-if="!child.children || child.children.length === 0" :index="getRoutePath(child)">
  215. <el-icon style="margin-right: 4px;">
  216. <Folder/>
  217. </el-icon>
  218. <span>{{ child.menuName }}</span>
  219. </el-menu-item>
  220. <!-- 子菜单有下级 -->
  221. <el-sub-menu v-else :index="child.id.toString()">
  222. <template #title>
  223. <el-icon style="margin-right: 4px;">
  224. <Folder/>
  225. </el-icon>
  226. <span>{{ child.menuName }}</span>
  227. </template>
  228. <!-- 递归 下一级-->
  229. <template v-for="grandChild in child.children" :key="grandChild.id">
  230. <el-menu-item :index="getRoutePath(grandChild)">
  231. <el-icon style="margin-right: 4px;">
  232. <Folder/>
  233. </el-icon>
  234. <span>{{ grandChild.menuName }}</span>
  235. </el-menu-item>
  236. </template>
  237. </el-sub-menu>
  238. </template>
  239. </el-sub-menu>
  240. <!-- 无子菜单的一级菜单 -->
  241. <el-menu-item v-else :index="getRoutePath(menu)">
  242. <img
  243. :src="getIconPath(menu.menuName)"
  244. :alt="`${menu.menuName}图标`"
  245. style="width: 4vh; height: 4vh; margin-right: 4px;"
  246. >
  247. <span>{{ menu.menuName }}</span>
  248. </el-menu-item>
  249. </template>
  250. </el-menu>
  251. </div>
  252. <div style="display: flex">
  253. <!-- 底部固定的设置中心 -->
  254. <div class="settings-container">
  255. <el-dropdown placement="top-start">
  256. <span class="el-dropdown-link">
  257. <!-- 暂时使用静态路径确保设置图标正常显示 -->
  258. <img src="@/assets/SvgIcons/setting.svg" alt="设置" style="width: 4vh; height: 4vh"/>
  259. <span>设置中心</span>
  260. <el-icon class="arrow-icon">
  261. <ArrowUp/>
  262. </el-icon>
  263. </span>
  264. <template #dropdown>
  265. <el-dropdown-menu>
  266. <!-- <el-dropdown-item @click="refreshData()">数据刷新</el-dropdown-item>-->
  267. <!-- 员工数据开关 -->
  268. <el-dropdown-item @click="toggleFlag()">
  269. {{ flag === 1 ? '显示员工数据' : '隐藏员工数据' }}
  270. </el-dropdown-item>
  271. <el-dropdown-item @click="message()">查看个人信息</el-dropdown-item>
  272. <el-dropdown-item @click="openChangePassword">修改密码</el-dropdown-item>
  273. <el-dropdown-item @click="logout">退出登录</el-dropdown-item>
  274. </el-dropdown-menu>
  275. </template>
  276. </el-dropdown>
  277. </div>
  278. <!-- 消息提示 这里的 小红点不用el-badge 他在切换状态会抽一下 应该是dom问题 -->
  279. <div class="message-container">
  280. <div style="position: relative;">
  281. <el-image :src="bell" style="width: 28px; height: 28px;" @click="openMessageDialog"></el-image>
  282. <span v-show="messageDot" class="dot"></span>
  283. </div>
  284. </div>
  285. </div>
  286. </el-aside>
  287. </div>
  288. <!-- 右侧内容区域 -->
  289. <div class="content-container">
  290. <!-- 头部
  291. <el-header class="header">
  292. </el-header> -->
  293. <!-- 主内容区域 -->
  294. <div class="main-area">
  295. <el-main>
  296. <router-view></router-view>
  297. </el-main>
  298. </div>
  299. </div>
  300. </div>
  301. <!-- 查看个人信息 -->
  302. <el-dialog v-model="messageVisible" title="查看个人信息" width="500px">
  303. <el-form :model="adminData">
  304. <el-form-item label="用户姓名" label-width="100px" label-position="left">
  305. <span class="message-font">{{ adminData.adminName }}</span>
  306. </el-form-item>
  307. <el-form-item label="精网号" label-width="100px" label-position="left">
  308. <span class="message-font">{{ adminData.account }}</span>
  309. </el-form-item>
  310. <el-form-item label="地区" label-width="100px" label-position="left">
  311. <span class="message-font">{{ adminData.markets }}</span>
  312. </el-form-item>
  313. <el-form-item label="注册时间" label-width="100px" label-position="left">
  314. <span class="message-font">{{ adminData.createTime }}</span>
  315. </el-form-item>
  316. </el-form>
  317. <template #footer>
  318. <div>
  319. <el-button text @click="closeMessage()">关闭</el-button>
  320. </div>
  321. </template>
  322. </el-dialog>
  323. <!-- 自定义密码修改弹窗组件 -->
  324. <el-dialog v-model="showPasswordDialog" :center="true" width="470px" @closed="onPwdDialogClosed">
  325. <ChangePassword ref="pwdRef" @confirm="showPasswordDialog = false"/>
  326. </el-dialog>
  327. <!--消息推送的弹窗-->
  328. <el-dialog style="background: #F3FAFE" v-model="showMessageDialog" title="" width="500px">
  329. <div class="message-title">
  330. <el-divider
  331. class="divider"
  332. direction="vertical"
  333. ></el-divider>
  334. 消息中心 ({{ messageNum }})
  335. </div>
  336. <!-- todo 这是为了样式显示 一定要改逻辑-->
  337. <div v-if="messageNum === 0">
  338. <div class="no-message">
  339. <el-image :src="noMessage"></el-image>
  340. <p class="no-message-text">暂无未办消息快去处理工作吧</p>
  341. </div>
  342. </div>
  343. <div v-else
  344. ref="scrollContainer"
  345. style="max-height: 60vh; overflow-y: auto;">
  346. <!-- 按时间分组的消息列表 -->
  347. <div
  348. v-for="(group, time) in displayMessages"
  349. :key="time"
  350. style="margin-bottom: 16px;"
  351. >
  352. <div class="time-header">
  353. {{ time }} <span class="little-dot"></span>
  354. <el-divider
  355. style="height: 2px; align-self: stretch;flex: 1;background: #CEE5FE; border: none;"
  356. ></el-divider>
  357. </div>
  358. <div
  359. v-for="item in group"
  360. :key="item.id"
  361. class="message-item"
  362. >
  363. <div style="display: flex; margin-bottom: 10px">
  364. <span class="red-dot"></span>
  365. <span class="message-card-title">{{ item.title }}</span>
  366. <div
  367. class="message-time"
  368. :style="{ color: item.czTime.includes('分钟') ? 'red' : '' }"
  369. >
  370. {{ item.czTime }}
  371. </div>
  372. </div>
  373. <p class="message-desc">{{ item.desc }}</p>
  374. <el-button
  375. type="primary"
  376. style="margin: 0 auto; display: block;"
  377. @click="handleMessageClick(item)"
  378. >
  379. 前往查看
  380. </el-button>
  381. </div>
  382. <el-divider
  383. style="height: 1px; align-self: stretch;flex: 1;background: #CEE5FE; border: none;"
  384. ></el-divider>
  385. </div>
  386. <!-- 控制按钮 -->
  387. <div>
  388. <el-button
  389. type="text"
  390. v-if="messageNum > 2"
  391. class="view-all"
  392. @click="toggleShowAll"
  393. >
  394. {{ showAll ? '收起' : '查看全部' }}
  395. </el-button>
  396. <div v-if="showAll" @click="scrollToTop" class="go-top">
  397. <el-image
  398. :src="goTop"
  399. style="width: 20px; height: 20px;"
  400. fit="contain"
  401. />
  402. <span>返回顶部</span>
  403. </div>
  404. </div>
  405. </div>
  406. </el-dialog>
  407. </div>
  408. </template>
  409. <style scoped>
  410. /* 主容器,设置背景图并居中 */
  411. .main-container {
  412. position: fixed;
  413. top: 0;
  414. left: 0;
  415. right: 0;
  416. bottom: 0;
  417. background-image: url('@/assets/backgroundBlue.png');
  418. background-size: cover;
  419. background-position: center center;
  420. background-repeat: no-repeat;
  421. overflow: hidden;
  422. }
  423. /* 背景毛玻璃层(作为内容容器) */
  424. .background-glass {
  425. position: absolute;
  426. top: 1vh;
  427. left: 1vh;
  428. right: 1vh;
  429. bottom: 1vh;
  430. background-image: url('@/assets/blue-background.png');
  431. background-size: cover;
  432. z-index: 1;
  433. display: flex;
  434. flex-direction: row;
  435. padding: 10px;
  436. border-radius: 12px;
  437. }
  438. /* 侧边栏容器 */
  439. .sidebar-container {
  440. flex-shrink: 0;
  441. }
  442. .logo {
  443. display: flex;
  444. align-items: center;
  445. justify-content: center;
  446. height: 12vh;
  447. }
  448. /* 中间可滚动菜单容器 */
  449. .menu-scroll-container {
  450. flex: 1;
  451. overflow-y: auto;
  452. padding: 10px 0;
  453. }
  454. /* 底部设置中心样式 */
  455. .settings-container {
  456. padding: 10px 0 10px 20px; /* 上,右, 下,左 */
  457. display: flex;
  458. align-items: center; /* 垂直居中 */
  459. }
  460. /* 调整下拉菜单的样式,确保它向上弹出 */
  461. .el-dropdown-link:focus {
  462. /* 移除底部的异常效果 */
  463. outline: none;
  464. text-decoration: none;
  465. }
  466. .el-dropdown-link {
  467. display: flex;
  468. align-items: center;
  469. cursor: pointer;
  470. gap: 10px; /* 图标和文字左右间距 */
  471. }
  472. .sidebar-layout {
  473. width: 15vw;
  474. height: 100%;
  475. background: #E7F4FD; /* 浅蓝色背景 */
  476. /* backdrop-filter: blur(5px); 毛玻璃效果 --消耗性能 */
  477. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影增强层次感 */
  478. border-radius: 12px;
  479. display: flex;
  480. flex-direction: column;
  481. position: relative;
  482. transition: all 0.3s ease;
  483. }
  484. /* 内容区域容器 */
  485. .content-container {
  486. flex: 1;
  487. display: flex;
  488. flex-direction: column;
  489. margin-left: 5px;
  490. gap: 5px;
  491. height: 100%;
  492. overflow: hidden;
  493. }
  494. /* 主内容区域容器 */
  495. .main-area {
  496. flex: 1;
  497. background: #E7F4FD;
  498. /* 半透明浅色背景 */
  499. /* backdrop-filter: blur(5px); */
  500. /* 毛玻璃效果 */
  501. border-radius: 12px;
  502. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  503. /* 添加阴影增强层次感 */
  504. overflow: hidden;
  505. display: flex;
  506. flex-direction: column;
  507. }
  508. /* 主内容区域样式 */
  509. .el-main {
  510. height: 100%;
  511. padding: 1px 8px 10px 8px;
  512. background: transparent;
  513. overflow-y: auto;
  514. /* 应用自定义滚动条 */
  515. }
  516. /* 确保el-menu撑满容器 */
  517. .sidebar-layout .el-menu {
  518. width: 100%;
  519. }
  520. /* 侧边栏菜单样式 适配浅色背景 */
  521. .el-menu {
  522. background: transparent !important;
  523. }
  524. /* 工作台,金币管理,现金管理 */
  525. ::v-deep(.el-sub-menu__title:hover),
  526. ::v-deep(.el-menu-item:hover) {
  527. background: #E5EBFE;
  528. }
  529. /* 子菜单展开时和背景同色 */
  530. ::v-deep(.el-sub-menu__title),
  531. ::v-deep(.el-menu-item) {
  532. background: #E7F4FD;
  533. }
  534. .message-font {
  535. /* 个人信息字体样式 */
  536. font-size: 16px;
  537. font-weight: bold;
  538. }
  539. /* 确保全局el-container适应容器 */
  540. :deep(.el-container) {
  541. /* vue3的深度选择器,用于覆盖element-plus的默认样式 */
  542. min-height: 100%;
  543. width: 100%;
  544. background: transparent;
  545. }
  546. /* 为侧边栏和主内容区域添加滚动条样式 */
  547. .menu-scroll-container,
  548. .el-main {
  549. scrollbar-width: thin;
  550. /* Firefox */
  551. scrollbar-color: rgba(0, 0, 0, 0.3) rgba(255, 255, 255, 0.2);
  552. /* Firefox滑块和轨道颜色 */
  553. }
  554. /* 小红点 */
  555. .dot {
  556. position: absolute;
  557. top: -2px;
  558. right: -2px;
  559. width: 8px;
  560. height: 8px;
  561. background: #F23C39;
  562. border-radius: 50%;
  563. }
  564. /* 消息中心整体容器 */
  565. .message-container {
  566. padding: 10px 50px 10px 50px; /* 上,右, 下,左 */
  567. display: flex;
  568. align-items: center; /* 垂直居中 */
  569. }
  570. /* 消息中心标题 */
  571. .message-title {
  572. display: flex;
  573. font-size: 16px;
  574. color: black;
  575. .divider {
  576. align-items: flex-start;
  577. gap: 36px;
  578. align-self: stretch;
  579. height: 20px;
  580. border-left: 3px solid #266EFF;
  581. }
  582. }
  583. /* 无消息的样式 */
  584. .no-message {
  585. text-align: center;
  586. position: relative;
  587. .no-message-text {
  588. position: absolute;
  589. top: 60%;
  590. left: 50%;
  591. /* 水平垂直居中 */
  592. transform: translate(-50%, -50%);
  593. /* 文字样式 */
  594. font-weight: bold;
  595. margin: 0;
  596. /* 可添加更多样式(如字体大小、阴影等) */
  597. font-size: 14px;
  598. }
  599. }
  600. /* 有消息的样式 */
  601. .time-header {
  602. font-size: 14px;
  603. color: #666;
  604. display: flex;
  605. align-items: center;
  606. gap: 8px;
  607. }
  608. .message-item {
  609. height: 100px;
  610. padding: 10px 10px 10px 10px;
  611. border-radius: 4px;
  612. border: 1px solid #E5E5E5;
  613. background: #FCFEFF;
  614. margin-bottom: 10px;
  615. }
  616. .message-card-title {
  617. font-weight: bold;
  618. margin-right: 4px;
  619. }
  620. /* 圆点样式 */
  621. .red-dot {
  622. width: 6px;
  623. height: 6px;
  624. margin-right: 9px;
  625. border-radius: 50%;
  626. background-color: red;
  627. }
  628. .message-desc {
  629. font-size: 13px;
  630. color: #666;
  631. margin: 4px 0 15px 15px;
  632. }
  633. .message-time {
  634. margin-left: auto;
  635. font-size: 13px;
  636. color: #999;
  637. }
  638. .view-all {
  639. display: block;
  640. margin: 0 auto 16px;
  641. }
  642. .little-dot {
  643. display: inline-block;
  644. width: 8px; /* 圆点直径 */
  645. height: 8px;
  646. border-radius: 50%; /* 圆形 */
  647. background-color: #CEE5FE;;
  648. }
  649. /* 返回最上*/
  650. .go-top {
  651. display: flex;
  652. flex-direction: column;
  653. align-items: center;
  654. gap: 4px;
  655. cursor: pointer;
  656. padding: 8px;
  657. }
  658. </style>