This commit is contained in:
yuhawu 2025-08-06 10:17:57 +08:00
parent 082abbb60a
commit fe89d930f1
19 changed files with 1867 additions and 791 deletions

63
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.9.0",
"echarts": "^5.6.0",
"element-plus": "^2.9.11",
"escpos": "^3.0.0-alpha.6",
"escpos-usb": "^3.0.0-alpha.4",
@ -668,6 +669,22 @@
"safer-buffer": "^2.1.0"
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/element-plus": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.11.tgz",
@ -2186,6 +2203,21 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
}
},
"dependencies": {
@ -2635,6 +2667,22 @@
"safer-buffer": "^2.1.0"
}
},
"echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"requires": {
"tslib": "2.3.0",
"zrender": "5.6.1"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
},
"element-plus": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.11.tgz",
@ -3646,6 +3694,21 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true
},
"zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"requires": {
"tslib": "2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
}
}
}

View File

@ -8,6 +8,7 @@
},
"dependencies": {
"axios": "^1.9.0",
"echarts": "^5.6.0",
"element-plus": "^2.9.11",
"escpos": "^3.0.0-alpha.6",
"escpos-usb": "^3.0.0-alpha.4",

View File

@ -10,4 +10,5 @@ setupResponseInterceptors(instance)
export { adminApi } from './modules/admin'
export { invitationApi } from './modules/invitation'
export { depotApi } from './modules/depot'
export { userApi } from './modules/user'
export { userApi } from './modules/user'
export { userLoginApi } from './modules/userLogin'

View File

@ -54,11 +54,11 @@ export function setupResponseInterceptors(instance) {
return Promise.reject(error);
}
// 调用刷新接口
// 调用用户刷新接口
const formData = new FormData();
formData.append('refreshToken', refreshToken);
const response = await axiosInstance.post('/admin/refreshToken', formData);
const response = await axiosInstance.post('/userLogin/refreshToken', formData);
console.log('刷新token响应:', response);
// 后端返回格式: { code: 200, data: { accessToken: "xxx", refreshToken: "xxx" } }

View File

@ -2,8 +2,8 @@ import instance from '../../utils/axios.js'
// 用户相关API
const userApi = {
// 获取用户列表
getUserList: () => instance.get('/user/list'),
// 获取用户列表(支持分页和搜索)
getUserList: (params) => instance.get('/user/list', { params }),
// 获取单个用户信息
getUserById: (id) => instance.get(`/user/get/${id}`),

View File

@ -1,7 +1,37 @@
import axios from '@/utils/axios'
/**
* 获取权限树
* 获取当前用户权限树
*/
export function getUserPermissionTree() {
return axios({
url: '/admin/permission/user/tree',
method: 'get'
})
}
/**
* 获取当前用户菜单树用于侧边栏显示
*/
export function getUserMenuTree() {
return axios({
url: '/admin/permission/user/tree',
method: 'get'
})
}
/**
* 获取当前用户权限编码列表
*/
export function getUserPermissionCodes() {
return axios({
url: '/admin/permission/user/codes',
method: 'get'
})
}
/**
* 获取权限树管理用
*/
export function getPermissionTree() {
return axios({
@ -40,4 +70,4 @@ export function deletePermission(id) {
url: `/admin/permission/delete/${id}`,
method: 'delete'
})
}
}

View File

@ -4,7 +4,7 @@
<el-header height="60px"><navbar /></el-header>
<el-container>
<!-- 侧边栏 -->
<el-aside width="220px"><sidebar /></el-aside>
<el-aside width="220px"><Sidebar /></el-aside>
<!-- 标签页 -->
<el-main style="padding: 5px;">
<router-view />

View File

@ -1,7 +1,8 @@
<template>
<el-menu router :default-active="$route.path" :collapse="false" unique-opened background-color="#304156" text-color="#bfcbd9" active-text-color="#409EFF">
<el-menu router :default-active="$route.path" :collapse="false" unique-opened background-color="#304156"
text-color="#bfcbd9" active-text-color="#409EFF">
<!-- 一级菜单 -->
<el-sub-menu v-for="item in menuData" :key="item.path" :index="item.path">
<el-sub-menu v-for="item in filteredMenuData" :key="item.path" :index="item.path">
<template #title>
<el-icon>
<component :is="item.icon" />
@ -10,229 +11,242 @@
</template>
<!-- 二级菜单 -->
<el-sub-menu v-for="child in item.children" :key="child.path" :index="child.path">
<template #title>{{ child.title }}</template>
<!-- 三级菜单 -->
<el-menu-item v-for="sub in child.children" :key="sub.path" :index="sub.path">
{{ sub.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-for="child in item.children" :key="child.path" :index="child.path">
{{ child.title }}
</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import {Document as DocIcon,Setting,User,Message,ShoppingCart,Shop,Connection,Notebook,Box,TrendCharts } from '@element-plus/icons-vue'
import { shallowRef, computed, onMounted, watch } from 'vue'
import { Document as DocIcon, Setting, User, Message, ShoppingCart, Shop, Connection, Notebook, Box, TrendCharts, HomeFilled, Monitor } from '@element-plus/icons-vue'
import { hasPermission, getUserPermissions } from '@/utils/permission'
const menuData = shallowRef([{
title: '系统管理',
path: '/system',
icon: Setting,
children: [{
title: '入驻配置',
path: '/settledConfig',
children: [{
title: '配置列表',
path: '/settledConfig/list'
},
{
title: '会员开通记录',
path: '/settledConfig/memberRecord'
},
]
},
{
title: '用户管理',
path: '/user',
children: [{
title: '用户列表',
path: '/user/list'
},
{
title: '角色管理',
path: '/user/role'
},
{
title: '权限管理',
path: '/user/permission'
}
]
},
{
title: '邀请管理',
path: '/invitation',
children: [{
title: '邀请列表',
path: '/invitation/list'
}
]
},
{
title: '日志管理',
path: '/log',
children: [{
title: '操作日志',
path: '/log/operate'
},
{
title: '登录日志',
path: '/log/login'
}
]
}
]
},
{
title: '店铺管理',
path: '/shop',
icon: Shop,
children: [{
const menuData = shallowRef([
{
title: '系统管理',
path: '/system',
icon: Setting,
children: [
{
title: '配置列表',
path: '/SettledConfig/list',
permission: 'settled:config:list'
},
{
title: '会员开通记录',
path: '/SettledConfig/memberRecord',
permission: 'settled:member:record'
},
{
title: '用户列表',
path: '/user/list',
permission: 'user:list:view'
},
{
title: '角色管理',
path: '/user/role',
permission: 'user:role:manage'
},
{
title: '权限管理',
path: '/user/permission',
permission: 'user:permission:manage'
},
{
title: '邀请列表',
path: '/invitation/list',
permission: 'invitation:list:view'
},
{
title: '运行日志',
path: '/log/runningLog/list',
permission: 'log:running:view'
}
]
},
{
title: '店铺管理',
path: '/shop',
icon: Shop,
children: [
{
title: '店铺列表',
path: '/shop/list',
children: [{
title: '店铺列表',
path: '/shop/list'
}]
}]
},
{
title: '书品管理',
path: '/book',
icon: Notebook,
children: [{
permission: 'shop:list:view'
}
]
},
{
title: '书品管理',
path: '/book',
icon: Notebook,
children: [
{
title: '选品中心',
path: '/book/selection',
children: [{
title: '选品中心',
path: '/book/selection/center'
}]
}]
},
{
title: '仓储管理',
path: '/warehouse',
icon: Box,
children: [{
path: '/book/selection/center',
permission: 'book:selection:view'
}
]
},
{
title: '仓储管理',
path: '/warehouse',
icon: Box,
children: [
{
title: '货区管理',
path: '/warehouse/depot',
children: [{
title: '货区列表',
path: '/warehouse/depot/list'
}]
}]
},
{
title: '工具管理',
path: '/tools',
icon: DocIcon,
children: [{
title: '卡密管理',
path: '/tools/cards',
children: [{
title: '卡密列表',
path: '/tools/cards/list'
},{
title: '活跃卡密列表',
path: '/tools/cards/activeCardsList'
}]
}]
},
{
title: '审核管理',
path: '/examine',
children:[{
title: '违规审核',
path: '/examine/violation',
children:[{
title: '违规列表',
path: '/examine/violation/list'
}]
}]
},
{
title: '日志管理',
path: '/log',
children:[{
title: '运行日志',
path: '/log/runningLog',
children:[{
title: '日志列表',
path: '/log/runningLog/list'
}]
}]
},
{
title: '任务管理',
path: '/task',
icon: TrendCharts,
children:[{
path: '/warehouse/depot/list',
permission: 'warehouse:depot:view'
},
{
title: '物流模板',
path: '/warehouse/logistics'
}
]
},
{
title: '工具管理',
path: '/tools',
icon: DocIcon,
children: [
{
title: '卡密列表',
path: '/tools/cards/list',
permission: 'cards:list:view'
},
{
title: '活跃卡密列表',
path: '/tools/cards/activeCardsList',
permission: 'cards:active:view'
}
]
},
{
title: '审核管理',
path: '/examine',
children: [
{
title: '违规列表',
path: '/examine/violation/list'
}
]
},
{
title: '任务管理',
path: '/task',
icon: TrendCharts,
children: [
{
title: '任务列表',
path: '/task/list',
children:[{
title: '任务列表',
path: '/task/list'
}]
}]
},
{
title: '功能模块',
path: '/useModule',
children:[{
title: '订阅服务',
path: '/useModule/vas',
children:[{
title: '服务列表',
path: '/useModule/vas/list'
}]
}]
permission: 'task:list:view'
}
]
},
{
title: '功能模块',
path: '/useModule',
children: [
{
title: '服务列表',
path: '/useModule/vas/list',
permission: 'vas:list:view'
}
]
},
{
title: '监控中心',
path: '/monitor',
icon: Monitor,
children: [
{
title: '监控大屏',
path: '/monitor/dashboard',
permission: 'monitor:dashboard:view'
}
]
}
])
//
const filteredMenuData = computed(() => {
const permissions = getUserPermissions()
console.log('当前用户权限:', permissions)
return menuData.value.map(menu => {
//
const filteredChildren = menu.children.filter(child => {
//
if (child.permission) {
const hasAuth = hasPermission(child.permission)
console.log(`菜单权限检查: ${child.title} (${child.permission}) = ${hasAuth}`)
return hasAuth
}
//
return true
})
//
return {
...menu,
children: filteredChildren
}
// ...
])
}).filter(menu => {
//
return menu.children.length > 0
})
})
//
watch(() => getUserPermissions(), (newPermissions) => {
console.log('权限数据更新:', newPermissions)
}, { deep: true })
</script>
<style scoped>
.el-menu {
height: 100%;
border-right: none;
}
.el-menu {
height: 100%;
border-right: none;
}
.el-menu-item.is-active {
background-color: #263445 !important;
}
.el-menu-item.is-active {
background-color: #263445 !important;
}
:deep(.el-menu) {
border-right: none;
}
:deep(.el-menu) {
border-right: none;
}
:deep(.el-sub-menu.is-opened) {
> .el-sub-menu__title,
.el-menu-item {
:deep(.el-sub-menu.is-opened) {
>.el-sub-menu__title,
.el-menu-item {
background-color: #1a1a1a !important;
}
}
:deep(.el-menu-item):hover,
:deep(.el-sub-menu__title):hover {
background-color: #1a1a1a !important;
}
:deep(.el-menu-item.is-active) {
background-color: #1a1a1a !important;
color: var(--el-menu-active-color);
}
:deep(.el-menu-item.is-active + .el-sub-menu .el-sub-menu__title),
:deep(.el-menu-item.is-active)~.el-sub-menu .el-sub-menu__title {
background-color: #1a1a1a !important;
}
:deep(.el-sub-menu) {
&.is-active {
>.el-sub-menu__title {
background-color: #1a1a1a !important;
}
}
:deep(.el-menu-item):hover,
:deep(.el-sub-menu__title):hover {
background-color: #1a1a1a !important;
}
:deep(.el-menu-item.is-active) {
background-color: #1a1a1a !important;
color: var(--el-menu-active-color);
}
:deep(.el-menu-item.is-active + .el-sub-menu .el-sub-menu__title),
:deep(.el-menu-item.is-active) ~ .el-sub-menu .el-sub-menu__title {
background-color: #1a1a1a !important;
}
:deep(.el-sub-menu) {
&.is-active {
> .el-sub-menu__title {
background-color: #1a1a1a !important;
}
}
}
</style>
}
</style>

View File

@ -2,17 +2,32 @@ import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from './utils/axios'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './styles/global.css'
import { globalConfig } from './config/global'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import globalComponents from './components'
import { permission, permissionAll } from '@/directives/permission'
import { initUserPermissions } from '@/utils/permission'
const app = createApp(App)
app.config.globalProperties.$axios = axios
app.config.globalProperties.$global = globalConfig
// 过滤 Element Plus 的 slot 警告
app.config.warnHandler = (msg, instance, trace) => {
// 忽略 Element Plus 组件的 slot 警告
if (msg.includes('Slot "default" invoked outside of the render function')) {
return
}
// 其他警告正常显示
console.warn(`[Vue warn]: ${msg}`, instance, trace)
}
app.use(store).use(router).use(ElementPlus, { locale: zhCn }).use(globalComponents).mount('#app')
// 注册权限指令
app.directive('permission', permission)
app.directive('permission-all', permissionAll)
app.use(store)
app.use(router)
app.use(ElementPlus)
// 初始化用户权限
initUserPermissions().then(() => {
app.mount('#app')
})

View File

@ -1,5 +1,5 @@
import { createRouter,createWebHistory } from 'vue-router'
import store from '@/store'
import { createRouter, createWebHistory } from 'vue-router'
import { getUserPermissions } from '@/utils/permission'
const routes = [{
path: '/',
@ -8,91 +8,113 @@ const routes = [{
path: '',
component: () => import('@/components/TabsView.vue'),
meta: {noLayout: true},
children: [{
children: [
{
path: '/welcome',
component: () => import('@/views/Welcome/Index.vue'),
meta: { title: '欢迎' }
},
// 入驻配置
{
path: '/SettledConfig/list',
component: () => import('@/views/SettledConfig/List.vue'),
meta: { title: '配置列表' }
meta: { title: '配置列表', permission: 'settled:config:list' }
},
{
path: '/SettledConfig/memberRecord',
component: () => import('@/views/SettledConfig/MemberRecord.vue'),
meta: { title: '会员开通记录' }
meta: { title: '会员开通记录', permission: 'settled:member:record' }
},
// 用户管理
{
path: '/user/list',
component: () => import('@/views/User/List.vue'),
meta: { title: '用户列表' }
meta: { title: '用户列表', permission: 'user:list:view' }
},
{
path: '/user/role',
component: () => import('@/views/User/Role.vue'),
meta: { title: '角色管理' }
meta: { title: '角色管理', permission: 'user:role:manage' }
},
{
path: '/user/permission',
component: () => import('@/views/User/Permission.vue'),
meta: { title: '权限管理' }
meta: { title: '权限管理', permission: 'user:permission:manage' }
},
// 邀请管理
{
path: '/invitation/list',
component: () => import('@/views/Invitation/List/index.vue'),
meta: { title: '邀请列表', permission: 'invitation:list:view' }
},
// 店铺管理
{
path: '/shop/list',
component: () => import('@/views/Shop/index.vue'),
meta: { title: '店铺列表' }
meta: { title: '店铺列表', permission: 'shop:list:view' }
},
// 选品管理
{
path: '/book/selection/center',
component: () => import('@/views/baseInfo/index.vue'),
meta: { title: '选品中心', permission: 'book:selection:view' }
},
// 仓库管理
{
path: '/warehouse/depot/list',
component: () => import('@/views/Warehouse/Depot/List.vue'),
meta: { title: '货区管理', permission: 'warehouse:depot:view' }
},
// 工具管理
{
path: '/tools/cards/list',
component: () => import('@/views/Tools/Cards/List.vue'),
meta: { title: '卡密列表' }
meta: { title: '卡密列表', permission: 'cards:list:view' }
},
{
path: '/tools/cards/activeCardsList',
component: () => import('@/views/Tools/Cards/ActiveCardsList.vue'),
meta: { title: '活跃卡密列表' }
meta: { title: '活跃卡密列表', permission: 'cards:active:view' }
},
// 审核管理
{
path: '/examine/violation/list',
component: () => import('@/views/Examine/Violation/List.vue'),
meta: { title: '违规列表' }
},
// 日志管理
{
path: '/log/runningLog/list',
component: () => import('@/views/log/RunningLog/List.vue'),
meta: { title: '日志列表' }
},
{
path: '/useModule/vas/list',
component: () => import('@/views/UseModule/Vas/List.vue'),
meta: { title: '订阅服务' }
},
{
path: '/invitation/list',
component: () => import('@/views/Invitation/List/index.vue'),
meta: { title: '邀请列表' }
},
{
path: '/order/wechat/list',
component: () => import('@/views/order/wechat/list.vue'),
meta: { title: '订单列表' }
},
{
path: '/book/selection/center',
component: () => import('@/views/baseInfo/index.vue'),
meta: { title: '选品中心' }
},
{
path: '/warehouse/depot/list',
component: () => import('@/views/Warehouse/Depot/List.vue'),
meta: { title: '货区管理' }
component: () => import('@/views/Log/RunningLog/List.vue'),
meta: { title: '运行日志', permission: 'log:running:view' }
},
// 任务管理
{
path: '/task/list',
component: () => import('@/views/Task/List.vue'),
meta: { title: '任务列表' }
meta: { title: '任务列表', permission: 'task:list:view' }
},
// 功能模块
{
path: '/useModule/vas/list',
component: () => import('@/views/UseModule/Vas/List.vue'),
meta: { title: '服务列表', permission: 'vas:list:view' }
},
// 监控中心
{
path: '/monitor/dashboard',
component: () => import('@/views/Monitor/Dashboard.vue'),
meta: { title: '监控大屏', permission: 'monitor:dashboard:view' }
},
// 物流模板
{
path: '/warehouse/logistics',
component: () => import('@/views/logistics/index.vue'),
// meta: { title: '物流模板', permission: 'logistics:view' }
meta: { title: '物流模板' }
}
]
}]
@ -102,28 +124,32 @@ const routes = [{
component: () => import('@/views/Login/Index.vue'),
meta: { noLayout: true,public: true }
},
{
path: '/redirectUrl',
component: () => import('@/views/redirectUrl/index.vue'),
meta: { noLayout: true, title: '用户注册', public: true }
},
]
// 定义 路由
const router = createRouter({history: createWebHistory(),routes})
// 路由守卫
// 路由权限守卫
router.beforeEach((to, from, next) => {
// 定义 是否为公开
const isPublic = to.meta.public
// 定义 是否已认证
const isAuthenticated = store.getters.isAuthenticated
// 如果访问根路径, 重定向到welcome页
if (to.path === '/' && isAuthenticated) return next('/welcome')
// 已认证用户访问登录页自动跳转首页
if (to.path === '/login' && isAuthenticated) return next('/welcome')
// 需要认证且未登录
if (!isPublic && !isAuthenticated) {
// 跳转到登录 并且带上属性
return next({ path: '/login',query: { redirect: to.fullPath } })
// 检查路由是否需要权限
if (to.meta && to.meta.permission) {
const userPermissions = getUserPermissions()
const requiredPermission = to.meta.permission
if (userPermissions.includes(requiredPermission)) {
next()
} else {
// 没有权限跳转到403页面或首页
next('/403')
}
} else {
next()
}
// 正常跳转
next()
})
// 返回 模块
export default router
export default router

View File

@ -1,6 +1,7 @@
import { createStore } from 'vuex'
import { adminApi } from '../api/index.js'
import { userLoginApi } from '../api/index.js'
import webSocketService from '../utils/webSocket.js'
import { initUserPermissions } from '../utils/permission.js'
export default createStore({
state: {
@ -32,7 +33,7 @@ export default createStore({
localStorage.removeItem('refreshToken')
// 清除本地缓存
localStorage.removeItem('userInfo')
// 断开WebSocket连接
webSocketService.disconnect()
},
@ -44,21 +45,47 @@ export default createStore({
}
},
actions: {
async login({commit}, data) {
async login({ commit }, data) {
try {
// 调用登录接口
const response = await adminApi.login(data)
console.log("响应",response)
// 转换字段名username -> phonenumber因为userLogin接口使用phonenumber
let loginData = data;
if (data instanceof FormData) {
const newFormData = new FormData();
for (let [key, value] of data.entries()) {
if (key === 'username') {
newFormData.append('phonenumber', value);
} else if (key === 'captcha') {
newFormData.append('code', value);
} else {
newFormData.append(key, value);
}
}
loginData = newFormData;
// 调试日志:查看转换后的数据
console.log('转换后的FormData内容:');
for (let [key, value] of loginData.entries()) {
console.log(`${key}: ${value}`);
}
}
// 调用用户登录接口
const response = await userLoginApi.userLogin(loginData)
console.log("用户登录响应", response)
// 检查响应状态
if (response.code !== 200) {
throw new Error(response.message || '登录失败')
}
// 设置 状态
commit('SET_TOKEN', response.data)
// 设置 token 状态
commit('SET_TOKEN', {
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
})
// 添加更多日志确保token存在
console.log("登录成功准备连接WebSocket")
console.log("用户登录成功准备连接WebSocket")
console.log("accessToken值:", response.data.accessToken)
// 登录成功后连接WebSocket
@ -67,26 +94,29 @@ export default createStore({
} else {
console.error("无法连接WebSocket: accessToken不存在")
}
try {
// 调用查询接口获取用户信息
const admin = await adminApi.getAdmin()
// 检查响应状态
if (admin.code !== 200) {
console.error('获取用户信息失败:', admin.message)
// 不清空认证信息,仅返回警告
return Promise.resolve('登录成功,但获取用户信息失败')
}
// 设置 管理员信息
commit('SET_USER_INFO', admin.data)
} catch (adminError) {
// 记录错误但不清空认证信息
console.error('获取用户信息出错:', adminError)
return Promise.resolve('登录成功,但获取用户信息失败')
// 设置用户信息userLogin接口直接返回用户信息
const userInfo = {
userId: response.data.userId,
username: response.data.userName,
nickName: response.data.nickName,
phonenumber: response.data.phonenumber,
email: response.data.email,
sex: response.data.sex,
userType: response.data.userType,
status: response.data.status
}
commit('SET_USER_INFO', userInfo)
// 初始化用户权限
try {
await initUserPermissions()
console.log("用户权限初始化成功")
} catch (permissionError) {
console.error("用户权限初始化失败:", permissionError)
// 权限初始化失败不影响登录,只记录错误
}
// 抛出 消息
return Promise.resolve('登录成功')
} catch (error) {
@ -96,7 +126,7 @@ export default createStore({
return Promise.reject(error)
}
},
logout({commit}) {
logout({ commit }) {
// 清空 状态
commit('CLEAR_AUTH')
}

View File

@ -3,11 +3,27 @@ import axios from 'axios'
const instance = axios.create({
baseURL: '/api',
timeout: 30000, // 增加超时时间到30秒
// headers: {
// 'Content-Type': 'multipart/form-data'
// }
})
// 添加请求拦截器来处理Content-Type和Authorization
instance.interceptors.request.use(config => {
// 添加Authorization头
const token = localStorage.getItem('accessToken')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 如果是FormData让浏览器自动设置Content-Type
if (config.data instanceof FormData) {
// 删除Content-Type让浏览器自动设置包括boundary
delete config.headers['Content-Type']
} else if (config.data && typeof config.data === 'object') {
// 对于普通对象设置为JSON格式
config.headers['Content-Type'] = 'application/json'
}
return config
}, error => {
return Promise.reject(error)
})
export default instance

View File

@ -24,8 +24,8 @@
<el-option label="审核失败" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="min-width: 150px;">
<el-option label="正常" :value="0" />
@ -39,37 +39,29 @@
</el-form>
</div>
<div class="search-area">
<!-- 操作按钮 -->
<ActionBar @refresh="refreshData">
<template #left>
<el-button type="success" :disabled="multiple" @click="btnSuccess()">通过</el-button>
<el-button type="danger" :disabled="multiple" @click="btnError()" >驳回</el-button>
<el-button type="danger" :disabled="multiple" @click="btnRemove()" >删除</el-button>
<el-button type="danger" :disabled="multiple" @click="btnError()">驳回</el-button>
<el-button type="danger" :disabled="multiple" @click="btnRemove()">删除</el-button>
<el-button type="warning" :disabled="single" @click="btnEdit()">修改</el-button>
</template>
</ActionBar>
<!-- 数据表格 -->
<el-table
ref="tableRef"
:data="tableData"
border
stripe
style="width: 100%;"
v-loading="loading"
@selection-change="handleSelectionChange"
:row-class-name="setReviewCellStyle"
>
<el-table ref="tableRef" :data="tableData" border stripe style="width: 100%;" v-loading="loading"
@selection-change="handleSelectionChange" :row-class-name="setReviewCellStyle">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键" align="center" prop="id" v-if="true" />
<el-table-column label="违规平台" align="center" prop="sort" width="250px" >
<el-table-column label="违规平台" align="center" prop="sort" width="250px">
<template #default="scope">
<span v-for="fruit in scope.row.sort.split(',')" :key="fruit" >
{{getSort(fruit)}} &nbsp;
<span v-for="fruit in scope.row.sort.split(',')" :key="fruit">
{{ getSort(fruit) }} &nbsp;
</span>
</template>
</el-table-column>
@ -80,7 +72,7 @@
</el-table-column>
<el-table-column label="违规内容" align="center" prop="name" />
<el-table-column label="违规原因" align="center" prop="content" />
<el-table-column label="审核状态" align="center" prop="review" >
<el-table-column label="审核状态" align="center" prop="review">
<template #default="{ row }">
{{ getReview(row.review) }}
</template>
@ -95,33 +87,16 @@
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
<!-- 获取卡密对话框 -->
<el-dialog
title="驳回原因"
v-model="violationVisible"
width="500px"
:close-on-click-modal="false"
>
<el-input
v-model="remark"
style="width: 100%"
:rows="10"
type="textarea"
placeholder="请输入驳回原因"
/>
<el-dialog title="驳回原因" v-model="violationVisible" width="500px" :close-on-click-modal="false">
<el-input v-model="remark" style="width: 100%" :rows="10" type="textarea" placeholder="请输入驳回原因" />
<template #footer>
<span class="dialog-footer">
@ -136,7 +111,7 @@
<el-form-item label="违规平台" prop="sort">
<el-checkbox-group v-model="sortArr">
<el-checkbox v-for="dict in getSortContent()" :key="dict.value" :label="dict.value">
{{ dict.label }}
{{ dict.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
@ -151,9 +126,9 @@
</el-dialog>
</div>
</template>
@ -172,10 +147,10 @@ const remark = ref('')
//
const searchForm = reactive({
type:'',
name:'',
review:'',
status:''
type: '',
name: '',
review: '',
status: ''
})
//
@ -194,12 +169,12 @@ const single = ref(true);
const multiple = ref(true);
/** 多选框选中数据 */
const handleSelectionChange = (selection) => {
ids.value = selection.map(item => item.id);
names.value = selection.map(item => item.name);
reviews.value = selection.map(item => item.review);
sorts.value = selection.map(item => item.sort);
single.value = selection.length != 1;
multiple.value = !selection.length;
ids.value = selection.map(item => item.id);
names.value = selection.map(item => item.name);
reviews.value = selection.map(item => item.review);
sorts.value = selection.map(item => item.sort);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
@ -215,23 +190,23 @@ onMounted(() => {
const fetchData = async () => {
loading.value = true
try {
//
const params = {
page: pagination.current,
size: pagination.size,
pageNum: pagination.current, // pageNum
pageSize: pagination.size, // pageSize
type: searchForm.type !== null ? searchForm.type : undefined,
name: searchForm.name || undefined,
status: searchForm.status || undefined,
review: searchForm.review !== null ? searchForm.review : undefined,
}
// 使API
const res = await violationApi.pageQuery(params)
// response.data使res
if (res.code === 200) {
tableData.value = res.data.list || []
// data.rows data.list
tableData.value = res.data.rows || []
pagination.total = res.data.total || 0
} else {
ElMessage.error(res.message || '获取数据失败')
@ -248,7 +223,7 @@ const fetchData = async () => {
} else {
ElMessage.error(`请求错误: ${error.message}`)
}
//
tableData.value = []
pagination.total = 0
@ -260,12 +235,12 @@ const fetchData = async () => {
//
const btnError = () => {
for(var i=0;i<reviews.value.length;i++){
if(reviews.value[i] != '1' ){
ElMessage.error('请选择待审核的数据!!!')
return;
}
}
for (var i = 0; i < reviews.value.length; i++) {
if (reviews.value[i] != '1') {
ElMessage.error('请选择待审核的数据!!!')
return;
}
}
//
violationVisible.value = true
}
@ -288,8 +263,8 @@ const btnEdit = () => {
//
const btnSuccess = async () => {
for(var i=0;i<reviews.value.length;i++){
if(reviews.value[i] != '1' ){
for (var i = 0; i < reviews.value.length; i++) {
if (reviews.value[i] != '1') {
ElMessage.error('请选择待审核的数据!!!')
return;
}
@ -297,18 +272,18 @@ const btnSuccess = async () => {
const res = await violationApi.successSubmit(ids.value);
if(res.code == '200'){
if (res.code == '200') {
ElMessage.success('审核成功');
fetchData()
}else{
} else {
ElMessage.error('系统异常,请联系管理员');
}
}
const setReviewCellStyle = ({ row, column }) => {
if (row.review == 1) {
return 'shenhe-row'; // class
}
if (row.review == 1) {
return 'shenhe-row'; // class
}
}
@ -370,31 +345,31 @@ const getReview = (review) => {
const getSort = (sort) => {
const map = {
0:'拼多多',
1:'孔夫子',
2:'淘宝',
3:'咸鱼'
0: '拼多多',
1: '孔夫子',
2: '淘宝',
3: '咸鱼'
}
return map[sort] || 'info'
}
const getSortContent = () =>{
return [
const getSortContent = () => {
return [
{
"label":"拼多多",
"value":"0"
"label": "拼多多",
"value": "0"
},
{
"label":"孔夫子",
"value":"1"
"label": "孔夫子",
"value": "1"
},
{
"label":"淘宝",
"value":"2"
"label": "淘宝",
"value": "2"
},
{
"label":"咸鱼",
"value":"3"
"label": "咸鱼",
"value": "3"
},
]
@ -418,35 +393,35 @@ const formatDateTime = (timestamp) => {
return date.toLocaleString()
}
//
const submit = async () =>{
if(!remark.value){
const submit = async () => {
if (!remark.value) {
ElMessage.error('请填写驳回原因!!!');
return
}
const res = await violationApi.submit(ids.value,remark.value);
const res = await violationApi.submit(ids.value, remark.value);
if(res.code == '200'){
if (res.code == '200') {
violationVisible.value = false;
ElMessage.success('驳回成功');
remark.value = '';
fetchData()
}else{
} else {
ElMessage.error('系统异常,请联系管理员');
}
}
//
const editSubmit = async() =>{
const editSubmit = async () => {
const res = await violationApi.editSubmit(ids.value,sortArr.value);
const res = await violationApi.editSubmit(ids.value, sortArr.value);
if(res.code == '200'){
if (res.code == '200') {
violtaionEditVisible.value = false;
ElMessage.success('修改成功');
sortArr.value = [];
fetchData()
}else{
} else {
ElMessage.error('系统异常,请联系管理员');
}
}
@ -504,7 +479,7 @@ const editSubmit = async() =>{
.card-secret-content {
text-align: left;
p {
margin-bottom: 15px;
font-size: 16px;
@ -513,7 +488,7 @@ const editSubmit = async() =>{
.card-params-content {
text-align: left;
p {
margin-bottom: 15px;
font-size: 16px;
@ -533,4 +508,4 @@ const editSubmit = async() =>{
.unit-label {
margin-left: 5px;
}
</style>
</style>

View File

@ -1,9 +1,811 @@
<template>
首页
<div class="monitoring-dashboard">
<!-- 页面标题 -->
<div class="dashboard-header">
<h1>实时服务器监控大屏</h1>
<div class="last-update">
<span id="lastUpdate">最后更新: --:--:--</span>
<el-button @click="refreshData" size="small" type="primary">手动刷新</el-button>
</div>
</div>
<!-- 汇总卡片区域 -->
<div class="summary-section">
<div class="summary-card">
<div class="card-icon server-icon">
<i class="el-icon-monitor"></i>
</div>
<div class="card-content">
<div class="card-title">服务器状态</div>
<div class="card-value" id="serverCount">0/0</div>
<div class="card-desc">在线/总数</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon cpu-icon">
<i class="el-icon-cpu"></i>
</div>
<div class="card-content">
<div class="card-title">平均CPU</div>
<div class="card-value" id="avgCpu">0%</div>
<div class="card-desc">处理器使用率</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon memory-icon">
<i class="el-icon-memory-card"></i>
</div>
<div class="card-content">
<div class="card-title">平均内存</div>
<div class="card-value" id="avgMemory">0%</div>
<div class="card-desc">内存使用率</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon alert-icon">
<i class="el-icon-warning"></i>
</div>
<div class="card-content">
<div class="card-title">活跃告警</div>
<div class="card-value" id="alertCount">0</div>
<div class="card-desc">需要关注</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<div class="chart-container">
<div class="chart-title">服务器状态分布</div>
<div id="serverStatusChart" class="chart"></div>
</div>
<div class="chart-container">
<div class="chart-title">平均资源使用率</div>
<div id="resourceChart" class="chart"></div>
</div>
</div>
<!-- 服务器列表区域 -->
<div class="server-list-section">
<div class="section-title">
<h3>服务器详细信息</h3>
<div class="loading-indicator" id="serversLoading" v-show="loading">
<i class="el-icon-loading"></i> 加载中...
</div>
</div>
<div class="error-message" id="serversError" v-show="errorMessage">
{{ errorMessage }}
</div>
<div class="server-table-container">
<table class="server-table" id="serversTable">
<thead>
<tr>
<th>服务器名称</th>
<th>状态</th>
<th>CPU使用率</th>
<th>内存使用率</th>
<th>磁盘使用率</th>
<th>服务状态</th>
<th>最后更新</th>
</tr>
</thead>
<tbody id="serversTableBody">
<!-- 动态生成的服务器行 -->
</tbody>
</table>
</div>
</div>
</div>
</template>
<script></script>
<script>
import * as echarts from 'echarts'
export default {
name: 'MonitoringDashboard',
data() {
return {
loading: false,
errorMessage: '',
serverStatusChart: null,
resourceChart: null,
refreshTimer: null,
API_BASE_URL: '/api/monitor'
}
},
<style>
mounted() {
this.initCharts()
this.loadDashboardData()
this.startAutoRefresh()
//
window.addEventListener('resize', this.handleResize)
},
beforeUnmount() {
this.stopAutoRefresh()
window.removeEventListener('resize', this.handleResize)
//
if (this.serverStatusChart) {
this.serverStatusChart.dispose()
}
if (this.resourceChart) {
this.resourceChart.dispose()
}
},
methods: {
//
initCharts() {
this.$nextTick(() => {
const serverStatusEl = document.getElementById('serverStatusChart')
const resourceEl = document.getElementById('resourceChart')
if (serverStatusEl) {
this.serverStatusChart = echarts.init(serverStatusEl)
}
if (resourceEl) {
this.resourceChart = echarts.init(resourceEl)
}
})
},
//
startAutoRefresh() {
this.refreshTimer = setInterval(() => {
this.loadDashboardData()
}, 30000) // 30
},
//
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
}
},
//
async loadDashboardData() {
this.loading = true
this.errorMessage = ''
try {
const [summaryResponse, serversResponse, serverStatsResponse, resourceStatsResponse] = await Promise.all([
fetch(`${this.API_BASE_URL}/dashboard/summary`),
fetch(`${this.API_BASE_URL}/servers`),
fetch(`${this.API_BASE_URL}/stats/servers`),
fetch(`${this.API_BASE_URL}/stats/resources`)
])
if (summaryResponse.ok && serversResponse.ok && serverStatsResponse.ok && resourceStatsResponse.ok) {
const summaryData = await summaryResponse.json()
const serversData = await serversResponse.json()
const serverStatsData = await serverStatsResponse.json()
const resourceStatsData = await resourceStatsResponse.json()
// JsonResult
if (summaryData.code === 200 && serversData.code === 200 &&
serverStatsData.code === 200 && resourceStatsData.code === 200) {
this.updateSummaryCards(summaryData.data, serverStatsData.data)
this.updateCharts(serverStatsData.data, resourceStatsData.data)
this.updateServersTable(serversData.data)
this.updateLastUpdateTime()
} else {
this.errorMessage = '数据加载失败'
}
} else {
this.errorMessage = 'API请求失败'
}
} catch (error) {
console.error('加载数据失败:', error)
this.errorMessage = '网络连接失败'
} finally {
this.loading = false
}
},
//
updateSummaryCards(summaryData, serverStatsData) {
const serverCountEl = document.getElementById('serverCount')
const avgCpuEl = document.getElementById('avgCpu')
const avgMemoryEl = document.getElementById('avgMemory')
const alertCountEl = document.getElementById('alertCount')
if (serverCountEl) {
serverCountEl.textContent = `${serverStatsData.online}/${serverStatsData.total}`
}
if (avgCpuEl) {
avgCpuEl.textContent = `${summaryData.metrics.avgCpu}%`
}
if (avgMemoryEl) {
avgMemoryEl.textContent = `${summaryData.metrics.avgMemory}%`
}
if (alertCountEl) {
alertCountEl.textContent = summaryData.alerts.active
}
},
//
updateCharts(serverStatsData, resourceStatsData) {
//
if (this.serverStatusChart) {
const serverStatusOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
textStyle: {
color: '#333'
}
},
series: [{
name: '服务器状态',
type: 'pie',
radius: '60%',
center: ['60%', '50%'],
data: [
{ value: serverStatsData.online, name: '在线', itemStyle: { color: '#27ae60' } },
{ value: serverStatsData.warning, name: '警告', itemStyle: { color: '#f39c12' } },
{ value: serverStatsData.offline, name: '离线', itemStyle: { color: '#e74c3c' } }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
}
this.serverStatusChart.setOption(serverStatusOption)
}
// 使
if (this.resourceChart) {
const resourceOption = {
tooltip: {
trigger: 'axis',
formatter: '{b}: {c}%'
},
xAxis: {
type: 'category',
data: ['CPU', '内存', '磁盘'],
axisLabel: {
color: '#333'
}
},
yAxis: {
type: 'value',
max: 100,
axisLabel: {
formatter: '{value}%',
color: '#333'
}
},
series: [{
type: 'bar',
data: [
{ value: resourceStatsData.cpu, itemStyle: { color: '#e74c3c' } },
{ value: resourceStatsData.memory, itemStyle: { color: '#f39c12' } },
{ value: resourceStatsData.disk, itemStyle: { color: '#3498db' } }
],
barWidth: '60%'
}]
}
this.resourceChart.setOption(resourceOption)
}
},
//
updateServersTable(servers) {
const tbody = document.getElementById('serversTableBody')
if (!tbody) return
tbody.innerHTML = ''
servers.forEach(server => {
const row = document.createElement('tr')
row.innerHTML = `
<td class="server-name">${this.getFriendlyServerName(server.hostname)}</td>
<td><span class="status-badge status-${server.status}">${this.getStatusText(server.status)}</span></td>
<td>${this.createProgressBar(server.cpuPercent || 0)}</td>
<td>${this.createProgressBar(server.memPercent || 0)}</td>
<td>${this.createProgressBar(server.diskUsage || 0)}</td>
<td class="service-status">
<span class="service-indicator ${server.mysqlAlive ? 'online' : 'offline'}" title="MySQL">M</span>
<span class="service-indicator ${server.redisAlive ? 'online' : 'offline'}" title="Redis">R</span>
<span class="service-indicator ${server.kafkaAlive ? 'online' : 'offline'}" title="Kafka">K</span>
</td>
<td>${this.formatTimestamp(server.lastUpdate)}</td>
`
tbody.appendChild(row)
})
},
//
getFriendlyServerName(hostname) {
const nameMap = {
'VM-0-3-opencloudos': '服务层服务器',
'VM-0-16-opencloudos': '入口层服务器',
'VM-0-15-opencloudos': '负载A服务器',
'VM-0-9-opencloudos': '负载B层服务器',
'VM-0-7-opencloudos': '核价专用服务器',
'VM-0-6-opencloudos': '临-主服务服务器',
'VM-28-17-opencloudos': '临-测试服务器'
}
return nameMap[hostname] || hostname
},
//
createProgressBar(value) {
const percentage = Math.round(value || 0)
let colorClass = 'progress-low'
if (percentage > 80) colorClass = 'progress-high'
else if (percentage > 60) colorClass = 'progress-medium'
return `
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill ${colorClass}" style="width: ${percentage}%"></div>
</div>
<span class="progress-text">${percentage}%</span>
</div>
`
},
//
getStatusText(status) {
const statusMap = {
'online': '在线',
'warning': '警告',
'offline': '离线'
}
return statusMap[status] || status
},
//
formatTimestamp(timestamp) {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
return date.toLocaleString('zh-CN')
},
//
updateLastUpdateTime() {
const now = new Date()
const lastUpdateEl = document.getElementById('lastUpdate')
if (lastUpdateEl) {
lastUpdateEl.textContent = `最后更新: ${now.toLocaleTimeString()}`
}
},
//
refreshData() {
this.loadDashboardData()
},
//
handleResize() {
if (this.serverStatusChart) {
this.serverStatusChart.resize()
}
if (this.resourceChart) {
this.resourceChart.resize()
}
}
}
}
</script>
<style scoped>
.monitoring-dashboard {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #fff;
}
/* 页面标题区域 */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
backdrop-filter: blur(10px);
}
.dashboard-header h1 {
font-size: 2.5rem;
font-weight: bold;
margin: 0;
background: linear-gradient(45deg, #fff, #f0f0f0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.last-update {
display: flex;
align-items: center;
gap: 15px;
font-size: 1.1rem;
}
/* 汇总卡片区域 */
.summary-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
display: flex;
align-items: center;
padding: 25px;
background: rgba(255, 255, 255, 0.15);
border-radius: 15px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.summary-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.card-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
font-size: 24px;
}
.server-icon {
background: linear-gradient(45deg, #4CAF50, #45a049);
}
.cpu-icon {
background: linear-gradient(45deg, #2196F3, #1976D2);
}
.memory-icon {
background: linear-gradient(45deg, #FF9800, #F57C00);
}
.alert-icon {
background: linear-gradient(45deg, #f44336, #d32f2f);
}
.card-content {
flex: 1;
}
.card-title {
font-size: 1rem;
opacity: 0.9;
margin-bottom: 5px;
}
.card-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 5px;
}
.card-desc {
font-size: 0.9rem;
opacity: 0.7;
}
/* 图表区域 */
.charts-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.chart-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 15px;
text-align: center;
}
.chart {
height: 300px;
width: 100%;
}
/* 服务器列表区域 */
.server-list-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-title h3 {
font-size: 1.5rem;
margin: 0;
}
.loading-indicator {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
opacity: 0.8;
}
.error-message {
background: rgba(244, 67, 54, 0.2);
color: #ffcdd2;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #f44336;
}
.server-table-container {
overflow-x: auto;
}
.server-table {
width: 100%;
border-collapse: collapse;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
overflow: hidden;
}
.server-table th {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
text-align: left;
font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.server-table td {
padding: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.server-table tr:hover {
background: rgba(255, 255, 255, 0.05);
}
.server-name {
font-weight: bold;
color: #fff;
}
.status-badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: bold;
}
.status-online {
background: rgba(76, 175, 80, 0.2);
color: #4CAF50;
border: 1px solid #4CAF50;
}
.status-warning {
background: rgba(255, 152, 0, 0.2);
color: #FF9800;
border: 1px solid #FF9800;
}
.status-offline {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
border: 1px solid #f44336;
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
}
.progress-bar {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-low {
background: linear-gradient(90deg, #4CAF50, #8BC34A);
}
.progress-medium {
background: linear-gradient(90deg, #FF9800, #FFC107);
}
.progress-high {
background: linear-gradient(90deg, #f44336, #FF5722);
}
.progress-text {
font-size: 0.9rem;
font-weight: bold;
min-width: 40px;
}
.service-status {
display: flex;
gap: 8px;
}
.service-indicator {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
cursor: help;
}
.service-indicator.online {
background: #4CAF50;
color: white;
}
.service-indicator.offline {
background: #f44336;
color: white;
}
/* 响应式设计 */
@media (max-width: 768px) {
.dashboard-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.dashboard-header h1 {
font-size: 2rem;
}
.summary-section {
grid-template-columns: 1fr;
}
.charts-section {
grid-template-columns: 1fr;
}
.chart {
height: 250px;
}
.server-table-container {
font-size: 0.9rem;
}
.server-table th,
.server-table td {
padding: 10px 8px;
}
}
@media (max-width: 480px) {
.monitoring-dashboard {
padding: 10px;
}
.dashboard-header h1 {
font-size: 1.5rem;
}
.summary-card {
padding: 15px;
}
.card-icon {
width: 50px;
height: 50px;
font-size: 20px;
margin-right: 15px;
}
.card-value {
font-size: 1.5rem;
}
.chart {
height: 200px;
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.summary-card,
.chart-container,
.server-list-section {
animation: fadeIn 0.6s ease-out;
}
/* 滚动条样式 */
.server-table-container::-webkit-scrollbar {
height: 8px;
}
.server-table-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.server-table-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.server-table-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>

View File

@ -7,7 +7,7 @@
<el-divider/>
</div>
<el-form-item prop="username" class="form-item">
<el-input v-model="form.username" placeholder="请输入用户名" prefix-icon="User" class="input-item"/>
<el-input v-model="form.username" placeholder="请输入手机号" prefix-icon="User" class="input-item"/>
</el-form-item>
<el-form-item prop="password" class="form-item">
<el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password
@ -38,14 +38,14 @@
//
const form = reactive({
username: '',
username: '', // 使
password: '',
captcha: ''
})
//
const rules = reactive({
username: [{required: true, message: '用户名不能为空', trigger: 'blur'}],
username: [{required: true, message: '手机号不能为空', trigger: 'blur'}],
password: [{required: true, message: '密码不能为空', trigger: 'blur'}],
captcha: [{required: true, message: '验证码不能为空', trigger: 'blur'}]
})
@ -64,7 +64,7 @@
//
const refreshCaptcha = () => {
captchaUrl.value = `https://newadmin.buzhiyushu.cn/admin/generateCaptcha?t=${Date.now()}`
captchaUrl.value = `/api/userLogin/generateCaptcha?t=${Date.now()}`
}
//

View File

@ -166,6 +166,12 @@ const logsList = ref([])
const logsMessage = ref('')
const currentTaskId = ref('')
//
const detailLogsVisible = ref(false)
const detailLogsLoading = ref(false)
const detailLogsList = ref([])
const currentDetailShopLog = ref(null)
//
const fetchTaskList = async () => {
loading.value = true
@ -394,18 +400,46 @@ const refreshLogs = () => {
//
const handleViewDetail = async (row) => {
try {
//
currentDetailShopLog.value = row
//
detailLogsLoading.value = true
detailLogsVisible.value = true
// API
const res = await taskApi.getLogsDetailList(row.taskId, row.shopId)
console.log('详细日志响应:', res)
if (res.code === 200) {
//
ElMessage.success('查看详细日志功能待实现')
//
if (res.data && typeof res.data === 'object') {
//
if (Array.isArray(res.data.data)) {
detailLogsList.value = res.data.data
} else if (Array.isArray(res.data.list)) {
detailLogsList.value = res.data.list
} else if (Array.isArray(res.data.records)) {
detailLogsList.value = res.data.records
} else if (Array.isArray(res.data)) {
detailLogsList.value = res.data
} else {
detailLogsList.value = []
console.warn('未能识别的详细日志数据格式:', res.data)
}
} else {
detailLogsList.value = []
}
} else {
detailLogsList.value = []
ElMessage.error('获取详细日志失败: ' + (res.message || '未知错误'))
}
} catch (error) {
console.error('获取详细日志出错:', error)
ElMessage.error('获取详细日志失败: ' + (error.message || '未知错误'))
detailLogsList.value = []
} finally {
detailLogsLoading.value = false
}
}
@ -449,6 +483,41 @@ const formatLogMessage = (message) => {
return lines
}
// API
const handleApiError = (error, operation, defaultMessage = '操作失败') => {
console.error(`${operation}出错:`, error)
//
let errorMessage = defaultMessage
if (error.response) {
//
const status = error.response.status
if (status === 404) {
errorMessage = `${operation}: 资源不存在 (404)`
} else if (status === 403) {
errorMessage = `${operation}: 权限不足 (403)`
} else if (status === 401) {
errorMessage = `${operation}: 未授权,请重新登录 (401)`
} else if (status >= 500) {
errorMessage = `${operation}: 服务器错误 (${status})`
} else {
errorMessage = `${operation}: ${error.response.data?.message || error.message || '未知错误'}`
}
} else if (error.request) {
//
errorMessage = `${operation}: 网络错误,请检查网络连接`
} else {
//
errorMessage = `${operation}: ${error.message || '未知错误'}`
}
//
ElMessage.error(errorMessage)
return errorMessage
}
//
onMounted(() => {
fetchTaskList()

View File

@ -3,7 +3,7 @@
<div class="header-actions">
<el-input
v-model="searchKeyword"
placeholder="请输入用户名搜索"
placeholder="请输入用户名或手机号搜索"
clearable
class="search-input"
@clear="loadUserList"
@ -26,14 +26,28 @@
:data="userList"
border
style="width: 100%"
row-key="id"
row-key="userId"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="nickname" label="昵称" />
<el-table-column prop="phone" label="手机号" />
<!-- <el-table-column prop="userId" label="用户ID" width="120" /> -->
<el-table-column prop="userName" label="用户名" />
<el-table-column prop="nickName" label="昵称" />
<el-table-column prop="phonenumber" label="手机号" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="createTime" label="创建时间" :formatter="formatDate" />
<el-table-column prop="userType" label="用户类型" width="100">
<template #default="{ row }">
<el-tag :type="getUserTypeTag(row.userType)">
{{ getUserTypeText(row.userType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdTime" label="创建时间" :formatter="formatDate" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="openUserDialog(row)">
@ -72,8 +86,8 @@
label-width="100px"
class="user-form"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
<el-form-item label="用户名" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="!isEdit">
@ -84,12 +98,12 @@
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码" show-password />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname" placeholder="请输入昵称" />
<el-form-item label="昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
<el-form-item label="手机号" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
@ -141,13 +155,15 @@ const submitLoading = ref(false)
const roleList = ref([])
//
const form = reactive({
id: null,
username: '',
userId: null,
userName: '',
password: '',
confirmPassword: '',
nickname: '',
phone: '',
nickName: '',
phonenumber: '',
email: '',
userType: '01', //
status: '0', //
roleIds: [] // ID
})
@ -174,7 +190,7 @@ const validateConfirmPass = (rule, value, callback) => {
}
const rules = reactive({
username: [
userName: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
@ -184,7 +200,7 @@ const rules = reactive({
confirmPassword: [
{ validator: validateConfirmPass, trigger: 'blur' }
],
phone: [
phonenumber: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
@ -202,19 +218,21 @@ const formRef = ref(null)
const loadUserList = async () => {
try {
loading.value = true
const res = await userApi.getUserList()
const params = {
pageNum: currentPage.value,
pageSize: pageSize.value
}
//
if (searchKeyword.value) {
params.userName = searchKeyword.value
}
const res = await userApi.getUserList(params)
if (res.code === 200) {
userList.value = res.data || []
total.value = res.data?.length || 0
//
if (searchKeyword.value) {
userList.value = userList.value.filter(user =>
user.username?.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
user.nickname?.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
userList.value = res.data?.list || []
total.value = res.data?.total || 0
} else {
ElMessage.error(res.message || '获取用户列表失败')
}
@ -349,14 +367,24 @@ const resetForm = () => {
//
Object.keys(form).forEach(key => {
form[key] = key === 'id' ? null : ''
if (key === 'userId') {
form[key] = null
} else if (key === 'roleIds') {
form[key] = []
} else if (key === 'userType') {
form[key] = '01'
} else if (key === 'status') {
form[key] = '0'
} else {
form[key] = ''
}
})
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除用户 "${row.username || row.nickname || row.id}" 吗?`,
`确定要删除用户 "${row.userName || row.nickName || row.userId}" 吗?`,
'删除确认',
{
confirmButtonText: '确定',
@ -365,7 +393,7 @@ const handleDelete = (row) => {
}
).then(async () => {
try {
const res = await userApi.deleteUser(row.id)
const res = await userApi.deleteUser(row.userId)
if (res.code === 200) {
ElMessage.success('删除成功')
loadUserList()
@ -393,12 +421,50 @@ const handleCurrentChange = (val) => {
loadUserList()
}
//
const getUserTypeTag = (userType) => {
const typeMap = {
'00': 'danger', // -
'01': 'success', // - 绿
'02': 'warning', // -
'03': 'info', // -
'sys_user': 'danger' //
}
return typeMap[userType] || 'info'
}
//
const getUserTypeText = (userType) => {
const typeMap = {
'00': '系统用户',
'01': '普通用户',
'02': '业务用户',
'03': '审核用户',
'sys_user': '系统用户' //
}
return typeMap[userType] || '未知类型'
}
//
const formatDate = (row, column) => {
const dateValue = row[column.property]
if (!dateValue) return '-'
try {
// 20250802132522
if (typeof dateValue === 'number' || (typeof dateValue === 'string' && /^\d{14}$/.test(dateValue))) {
const timeStr = dateValue.toString()
const year = timeStr.substring(0, 4)
const month = timeStr.substring(4, 6)
const day = timeStr.substring(6, 8)
const hour = timeStr.substring(8, 10)
const minute = timeStr.substring(10, 12)
const second = timeStr.substring(12, 14)
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
//
const date = new Date(dateValue)
return date.toLocaleString('zh-CN', {
year: 'numeric',

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,14 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/api/,''),
// 如需处理WebSocket
ws: true
},
'/auth': {
target: 'https://api.buzhiyushu.cn',
// target: 'http://localhost:8080',
changeOrigin: true,
secure: true,
// 重写路径,保持 /auth 前缀
rewrite: (path) => path
}
}
}