newadmin
This commit is contained in:
parent
082abbb60a
commit
fe89d930f1
63
package-lock.json
generated
63
package-lock.json
generated
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -11,3 +11,4 @@ export { adminApi } from './modules/admin'
|
||||
export { invitationApi } from './modules/invitation'
|
||||
export { depotApi } from './modules/depot'
|
||||
export { userApi } from './modules/user'
|
||||
export { userLoginApi } from './modules/userLogin'
|
||||
@ -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" } }
|
||||
|
||||
@ -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}`),
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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,77 +11,58 @@
|
||||
</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 v-for="child in item.children" :key="child.path" :index="child.path">
|
||||
{{ child.title }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</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([{
|
||||
const menuData = shallowRef([
|
||||
{
|
||||
title: '系统管理',
|
||||
path: '/system',
|
||||
icon: Setting,
|
||||
children: [{
|
||||
title: '入驻配置',
|
||||
path: '/settledConfig',
|
||||
children: [{
|
||||
children: [
|
||||
{
|
||||
title: '配置列表',
|
||||
path: '/settledConfig/list'
|
||||
path: '/SettledConfig/list',
|
||||
permission: 'settled:config:list'
|
||||
},
|
||||
{
|
||||
title: '会员开通记录',
|
||||
path: '/settledConfig/memberRecord'
|
||||
},
|
||||
]
|
||||
path: '/SettledConfig/memberRecord',
|
||||
permission: 'settled:member:record'
|
||||
},
|
||||
{
|
||||
title: '用户管理',
|
||||
path: '/user',
|
||||
children: [{
|
||||
title: '用户列表',
|
||||
path: '/user/list'
|
||||
path: '/user/list',
|
||||
permission: 'user:list:view'
|
||||
},
|
||||
{
|
||||
title: '角色管理',
|
||||
path: '/user/role'
|
||||
path: '/user/role',
|
||||
permission: 'user:role:manage'
|
||||
},
|
||||
{
|
||||
title: '权限管理',
|
||||
path: '/user/permission'
|
||||
}
|
||||
]
|
||||
path: '/user/permission',
|
||||
permission: 'user:permission:manage'
|
||||
},
|
||||
{
|
||||
title: '邀请管理',
|
||||
path: '/invitation',
|
||||
children: [{
|
||||
title: '邀请列表',
|
||||
path: '/invitation/list'
|
||||
}
|
||||
]
|
||||
path: '/invitation/list',
|
||||
permission: 'invitation:list:view'
|
||||
},
|
||||
{
|
||||
title: '日志管理',
|
||||
path: '/log',
|
||||
children: [{
|
||||
title: '操作日志',
|
||||
path: '/log/operate'
|
||||
},
|
||||
{
|
||||
title: '登录日志',
|
||||
path: '/log/login'
|
||||
}
|
||||
]
|
||||
title: '运行日志',
|
||||
path: '/log/runningLog/list',
|
||||
permission: 'log:running:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -88,151 +70,183 @@
|
||||
title: '店铺管理',
|
||||
path: '/shop',
|
||||
icon: Shop,
|
||||
children: [{
|
||||
children: [
|
||||
{
|
||||
title: '店铺列表',
|
||||
path: '/shop/list',
|
||||
children: [{
|
||||
title: '店铺列表',
|
||||
path: '/shop/list'
|
||||
}]
|
||||
}]
|
||||
permission: 'shop:list:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '书品管理',
|
||||
path: '/book',
|
||||
icon: Notebook,
|
||||
children: [{
|
||||
children: [
|
||||
{
|
||||
title: '选品中心',
|
||||
path: '/book/selection',
|
||||
children: [{
|
||||
title: '选品中心',
|
||||
path: '/book/selection/center'
|
||||
}]
|
||||
}]
|
||||
path: '/book/selection/center',
|
||||
permission: 'book:selection:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '仓储管理',
|
||||
path: '/warehouse',
|
||||
icon: Box,
|
||||
children: [{
|
||||
children: [
|
||||
{
|
||||
title: '货区管理',
|
||||
path: '/warehouse/depot',
|
||||
children: [{
|
||||
title: '货区列表',
|
||||
path: '/warehouse/depot/list'
|
||||
}]
|
||||
}]
|
||||
path: '/warehouse/depot/list',
|
||||
permission: 'warehouse:depot:view'
|
||||
},
|
||||
{
|
||||
title: '物流模板',
|
||||
path: '/warehouse/logistics'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '工具管理',
|
||||
path: '/tools',
|
||||
icon: DocIcon,
|
||||
children: [{
|
||||
title: '卡密管理',
|
||||
path: '/tools/cards',
|
||||
children: [{
|
||||
children: [
|
||||
{
|
||||
title: '卡密列表',
|
||||
path: '/tools/cards/list'
|
||||
},{
|
||||
path: '/tools/cards/list',
|
||||
permission: 'cards:list:view'
|
||||
},
|
||||
{
|
||||
title: '活跃卡密列表',
|
||||
path: '/tools/cards/activeCardsList'
|
||||
}]
|
||||
}]
|
||||
path: '/tools/cards/activeCardsList',
|
||||
permission: 'cards:active:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '审核管理',
|
||||
path: '/examine',
|
||||
children:[{
|
||||
title: '违规审核',
|
||||
path: '/examine/violation',
|
||||
children:[{
|
||||
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:[{
|
||||
children: [
|
||||
{
|
||||
title: '任务列表',
|
||||
path: '/task/list',
|
||||
children:[{
|
||||
title: '任务列表',
|
||||
path: '/task/list'
|
||||
}]
|
||||
}]
|
||||
permission: 'task:list:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '功能模块',
|
||||
path: '/useModule',
|
||||
children:[{
|
||||
title: '订阅服务',
|
||||
path: '/useModule/vas',
|
||||
children:[{
|
||||
children: [
|
||||
{
|
||||
title: '服务列表',
|
||||
path: '/useModule/vas/list'
|
||||
}]
|
||||
}]
|
||||
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 {
|
||||
.el-menu {
|
||||
height: 100%;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
.el-menu-item.is-active {
|
||||
background-color: #263445 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu) {
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-opened) {
|
||||
> .el-sub-menu__title,
|
||||
: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 {
|
||||
:deep(.el-menu-item):hover,
|
||||
:deep(.el-sub-menu__title):hover {
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
: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 {
|
||||
: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) {
|
||||
:deep(.el-sub-menu) {
|
||||
&.is-active {
|
||||
> .el-sub-menu__title {
|
||||
>.el-sub-menu__title {
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
src/main.js
31
src/main.js
@ -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')
|
||||
})
|
||||
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
// 返回 模块
|
||||
export default router
|
||||
@ -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: {
|
||||
@ -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
|
||||
@ -68,23 +95,26 @@ export default createStore({
|
||||
console.error("无法连接WebSocket: accessToken不存在")
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用查询接口获取用户信息
|
||||
const admin = await adminApi.getAdmin()
|
||||
|
||||
// 检查响应状态
|
||||
if (admin.code !== 200) {
|
||||
console.error('获取用户信息失败:', admin.message)
|
||||
// 不清空认证信息,仅返回警告
|
||||
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)
|
||||
|
||||
// 设置 管理员信息
|
||||
commit('SET_USER_INFO', admin.data)
|
||||
} catch (adminError) {
|
||||
// 记录错误但不清空认证信息
|
||||
console.error('获取用户信息出错:', adminError)
|
||||
return Promise.resolve('登录成功,但获取用户信息失败')
|
||||
// 初始化用户权限
|
||||
try {
|
||||
await initUserPermissions()
|
||||
console.log("用户权限初始化成功")
|
||||
} catch (permissionError) {
|
||||
console.error("用户权限初始化失败:", permissionError)
|
||||
// 权限初始化失败不影响登录,只记录错误
|
||||
}
|
||||
|
||||
// 抛出 消息
|
||||
@ -96,7 +126,7 @@ export default createStore({
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
logout({commit}) {
|
||||
logout({ commit }) {
|
||||
// 清空 状态
|
||||
commit('CLEAR_AUTH')
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -46,30 +46,22 @@
|
||||
<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)}}
|
||||
<span v-for="fruit in scope.row.sort.split(',')" :key="fruit">
|
||||
{{ getSort(fruit) }}
|
||||
</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">
|
||||
@ -172,10 +147,10 @@ const remark = ref('')
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
type:'',
|
||||
name:'',
|
||||
review:'',
|
||||
status:''
|
||||
type: '',
|
||||
name: '',
|
||||
review: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
@ -218,8 +193,8 @@ const fetchData = async () => {
|
||||
|
||||
// 构建查询参数
|
||||
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,
|
||||
@ -230,8 +205,8 @@ const fetchData = async () => {
|
||||
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 || '获取数据失败')
|
||||
@ -260,8 +235,8 @@ const fetchData = async () => {
|
||||
|
||||
//驳回
|
||||
const btnError = () => {
|
||||
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;
|
||||
}
|
||||
@ -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,10 +272,10 @@ 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('系统异常,请联系管理员');
|
||||
}
|
||||
}
|
||||
@ -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 = () =>{
|
||||
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('系统异常,请联系管理员');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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()}`
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -22,6 +22,20 @@
|
||||
<span class="search-label">出版社</span>
|
||||
<el-input v-model="searchForm.publisher" placeholder="请输入出版社" clearable />
|
||||
</div>
|
||||
|
||||
<div class="search-item">
|
||||
<span class="search-label">分类</span>
|
||||
<el-select v-model="searchForm.category" placeholder="请选择或输入分类" clearable filterable allow-create
|
||||
style="width: 220px">
|
||||
<el-option label="大学教材" value="图书/教材教辅考试/大学教材" />
|
||||
</el-select>
|
||||
</div>
|
||||
<!-- <div class="search-item">
|
||||
<span class="search-label">分类</span>
|
||||
<el-select v-model="queryParams.category" placeholder="请选择或输入分类" clearable filterable allow-create @keyup.enter="handleQuery" style="width: 200px">
|
||||
<el-option label="大学教材" value="图书/教材教辅考试/大学教材" />
|
||||
</el-select>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="search-row">
|
||||
@ -36,28 +50,15 @@
|
||||
|
||||
<div class="search-item date-range-item">
|
||||
<span class="search-label">出版时间范围</span>
|
||||
<el-date-picker
|
||||
v-model="searchForm.timeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:shortcuts="dateShortcuts"
|
||||
/>
|
||||
<el-date-picker v-model="searchForm.timeRange" type="daterange" range-separator="至"
|
||||
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
|
||||
:shortcuts="dateShortcuts" />
|
||||
</div>
|
||||
|
||||
<div class="search-item">
|
||||
<span class="search-label">违规信息筛选</span>
|
||||
<el-select
|
||||
v-model="searchForm.violationTypes"
|
||||
placeholder="请选择违规条件"
|
||||
clearable
|
||||
multiple
|
||||
style="width: 240px"
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
>
|
||||
<el-select v-model="searchForm.violationTypes" placeholder="请选择违规条件" clearable multiple
|
||||
style="width: 240px" collapse-tags collapse-tags-tooltip>
|
||||
<el-option label="违规书号" :value="1" />
|
||||
<el-option label="套装书" :value="2" />
|
||||
<el-option label="一号多书" :value="3" />
|
||||
@ -68,7 +69,9 @@
|
||||
|
||||
<div class="search-item btn-item">
|
||||
<el-button type="primary" @click="handleNormalSearch" class="search-btn">
|
||||
<el-icon><Search /></el-icon>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
@ -77,7 +80,8 @@
|
||||
<div class="search-row" :class="{ 'advanced-search-hidden': !showAdvancedSearch }">
|
||||
<div class="search-item">
|
||||
<span class="search-label">销量</span>
|
||||
<el-select v-model="searchForm.inventoryType" placeholder="请选择销量类型" clearable @change="handleSaleTypeChange">
|
||||
<el-select v-model="searchForm.inventoryType" placeholder="请选择销量类型" clearable
|
||||
@change="handleSaleTypeChange">
|
||||
<el-option label="请选择销量类型" :value="null" />
|
||||
<el-option label="7天销量" :value="7" />
|
||||
<el-option label="15天销量" :value="15" />
|
||||
@ -92,31 +96,40 @@
|
||||
|
||||
<div class="search-item number-range-item">
|
||||
<span class="search-label">已售</span>
|
||||
<el-input-number v-model="searchForm.minInventory" :min="0" placeholder="最小值" controls-position="right" />
|
||||
<el-input-number v-model="searchForm.minInventory" :min="0" placeholder="最小值"
|
||||
controls-position="right" />
|
||||
<span class="range-separator">至</span>
|
||||
<el-input-number v-model="searchForm.maxInventory" :min="0" placeholder="最大值" controls-position="right" />
|
||||
<el-input-number v-model="searchForm.maxInventory" :min="0" placeholder="最大值"
|
||||
controls-position="right" />
|
||||
</div>
|
||||
|
||||
<div class="search-item number-range-item">
|
||||
<span class="search-label">在售</span>
|
||||
<el-input-number v-model="searchForm.minSelling" :min="0" placeholder="最小值" controls-position="right" />
|
||||
<el-input-number v-model="searchForm.minSelling" :min="0" placeholder="最小值"
|
||||
controls-position="right" />
|
||||
<span class="range-separator">至</span>
|
||||
<el-input-number v-model="searchForm.maxSelling" :min="0" placeholder="最大值" controls-position="right" />
|
||||
<el-input-number v-model="searchForm.maxSelling" :min="0" placeholder="最大值"
|
||||
controls-position="right" />
|
||||
</div>
|
||||
|
||||
<div class="search-item btn-item">
|
||||
<el-button @click="resetSearch" class="reset-btn">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdvancedSearch" plain class="adv-search-btn">高级搜索</el-button>
|
||||
<el-button type="primary" @click="handleAdvancedSearch" plain
|
||||
class="adv-search-btn">高级搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-advanced-search">
|
||||
<el-button type="text" @click="toggleAdvancedSearch">
|
||||
{{ showAdvancedSearch ? '收起高级搜索' : '展开高级搜索' }}
|
||||
<el-icon :class="{ 'rotate-icon': showAdvancedSearch }"><ArrowDown /></el-icon>
|
||||
<el-icon :class="{ 'rotate-icon': showAdvancedSearch }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,28 +149,16 @@
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="tableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
:header-cell-style="{ backgroundColor: '#f5f7fa', color: '#606266', textAlign: 'center' }"
|
||||
height="500"
|
||||
max-height="500"
|
||||
>
|
||||
<el-table ref="tableRef" :data="tableData" border style="width: 100%" @selection-change="handleSelectionChange"
|
||||
row-key="id" v-loading="loading"
|
||||
:header-cell-style="{ backgroundColor: '#f5f7fa', color: '#606266', textAlign: 'center' }" height="500"
|
||||
max-height="500">
|
||||
<el-table-column type="selection" align="center" width="50" />
|
||||
<el-table-column prop="bookName" align="center" label="书名" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div ref="textRef" class="ellipsis-text">
|
||||
<el-tooltip
|
||||
:content="row.bookName || '-'"
|
||||
placement="top"
|
||||
:disabled="!isTextEllipsis($event)"
|
||||
:enterable="false"
|
||||
>
|
||||
<el-tooltip :content="row.bookName || '-'" placement="top" :disabled="!isTextEllipsis($event)"
|
||||
:enterable="false">
|
||||
<span class="ellipsis-text">{{ row.bookName || '-' }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@ -165,28 +166,18 @@
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="书图片" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.bookPic && row.bookPic !== '0'"
|
||||
:src="getBookImageUrl(row)"
|
||||
:preview-src-list="previewList"
|
||||
:initial-index="getPreviewIndex(row)"
|
||||
style="width: 40px; height: 60px; object-fit: cover;"
|
||||
@click="handlePreview(row)"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
/>
|
||||
<el-image v-if="row.bookPic && row.bookPic !== '0'" :src="getBookImageUrl(row)"
|
||||
:preview-src-list="previewList" :initial-index="getPreviewIndex(row)"
|
||||
style="width: 40px; height: 60px; object-fit: cover;" @click="handlePreview(row)" fit="cover"
|
||||
preview-teleported />
|
||||
<span v-else class="no-image">无图</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isbn" align="center" label="isbn" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div ref="textRef" class="ellipsis-text">
|
||||
<el-tooltip
|
||||
:content="row.isbn || '-'"
|
||||
placement="top"
|
||||
:disabled="!isTextEllipsis($event)"
|
||||
:enterable="false"
|
||||
>
|
||||
<el-tooltip :content="row.isbn || '-'" placement="top" :disabled="!isTextEllipsis($event)"
|
||||
:enterable="false">
|
||||
<span class="ellipsis-text">{{ row.isbn || '-' }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@ -195,12 +186,8 @@
|
||||
<el-table-column prop="author" align="center" label="作者" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div ref="textRef" class="ellipsis-text">
|
||||
<el-tooltip
|
||||
:content="row.author || '-'"
|
||||
placement="top"
|
||||
:disabled="!isTextEllipsis($event)"
|
||||
:enterable="false"
|
||||
>
|
||||
<el-tooltip :content="row.author || '-'" placement="top" :disabled="!isTextEllipsis($event)"
|
||||
:enterable="false">
|
||||
<span class="ellipsis-text">{{ row.author || '-' }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@ -209,12 +196,8 @@
|
||||
<el-table-column prop="publisher" align="center" label="出版社" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div ref="textRef" class="ellipsis-text">
|
||||
<el-tooltip
|
||||
:content="row.publisher || '-'"
|
||||
placement="top"
|
||||
:disabled="!isTextEllipsis($event)"
|
||||
:enterable="false"
|
||||
>
|
||||
<el-tooltip :content="row.publisher || '-'" placement="top" :disabled="!isTextEllipsis($event)"
|
||||
:enterable="false">
|
||||
<span class="ellipsis-text">{{ row.publisher || '-' }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@ -242,30 +225,37 @@
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销量详情" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-popover
|
||||
placement="right"
|
||||
trigger="hover"
|
||||
:width="220"
|
||||
popper-class="sales-popover"
|
||||
>
|
||||
<el-popover placement="right" trigger="hover" :width="220" popper-class="sales-popover">
|
||||
<template #default>
|
||||
<div class="sales-detail">
|
||||
<div class="sales-item"><span class="sales-label">7天销量:</span> {{ row.daySale7 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">15天销量:</span> {{ row.daySale15 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">30天销量:</span> {{ row.daySale30 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">60天销量:</span> {{ row.daySale60 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">90天销量:</span> {{ row.daySale90 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">180天销量:</span> {{ row.daySale180 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">365天销量:</span> {{ row.daySale365 || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">今年销量:</span> {{ row.thisYearSale || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">去年销量:</span> {{ row.lastYearSale || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">总销量:</span> {{ row.totalSale || 0 }}</div>
|
||||
<div class="sales-item"><span class="sales-label">7天销量:</span> {{ row.daySale7 || 0 }}
|
||||
</div>
|
||||
<div class="sales-item"><span class="sales-label">15天销量:</span> {{ row.daySale15 || 0 }}
|
||||
</div>
|
||||
<div class="sales-item"><span class="sales-label">30天销量:</span> {{ row.daySale30 || 0 }}
|
||||
</div>
|
||||
<div class="sales-item"><span class="sales-label">60天销量:</span> {{ row.daySale60 || 0 }}
|
||||
</div>
|
||||
<div class="sales-item"><span class="sales-label">90天销量:</span> {{ row.daySale90 || 0 }}
|
||||
</div>
|
||||
<div class="sales-item"><span class="sales-label">180天销量:</span> {{ row.daySale180 || 0
|
||||
}}</div>
|
||||
<div class="sales-item"><span class="sales-label">365天销量:</span> {{ row.daySale365 || 0
|
||||
}}</div>
|
||||
<div class="sales-item"><span class="sales-label">今年销量:</span> {{ row.thisYearSale || 0
|
||||
}}</div>
|
||||
<div class="sales-item"><span class="sales-label">去年销量:</span> {{ row.lastYearSale || 0
|
||||
}}</div>
|
||||
<div class="sales-item"><span class="sales-label">总销量:</span> {{ row.totalSale || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #reference>
|
||||
<div class="sales-trigger">
|
||||
<span>{{ row.sale7Days || 0 }}</span>
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
<el-icon>
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
@ -298,31 +288,15 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
:current-page="pagination.current"
|
||||
: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 :current-page="pagination.current" :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>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
:title="dialogType === 'add' ? '新增图书信息' : '编辑图书信息'"
|
||||
v-model="dialogVisible"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
label-position="right"
|
||||
>
|
||||
<el-dialog :title="dialogType === 'add' ? '新增图书信息' : '编辑图书信息'" v-model="dialogVisible" width="800px"
|
||||
:close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" label-position="right">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="书名" prop="bookName">
|
||||
@ -331,7 +305,8 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="ISBN" prop="isbn">
|
||||
<el-input v-model="formData.isbn" placeholder="请输入ISBN" maxlength="30" :disabled="dialogType === 'edit'" />
|
||||
<el-input v-model="formData.isbn" placeholder="请输入ISBN" maxlength="30"
|
||||
:disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -339,12 +314,14 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="作者" prop="author">
|
||||
<el-input v-model="formData.author" placeholder="请输入作者" maxlength="400" :disabled="dialogType === 'edit'" />
|
||||
<el-input v-model="formData.author" placeholder="请输入作者" maxlength="400"
|
||||
:disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="出版社" prop="publisher">
|
||||
<el-input v-model="formData.publisher" placeholder="请输入出版社" maxlength="200" :disabled="dialogType === 'edit'" />
|
||||
<el-input v-model="formData.publisher" placeholder="请输入出版社" maxlength="200"
|
||||
:disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -352,18 +329,14 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="出版时间" prop="publicationTime">
|
||||
<el-date-picker
|
||||
v-model="formData.publicationTime"
|
||||
type="date"
|
||||
placeholder="选择出版时间"
|
||||
value-format="YYYY-MM"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
<el-date-picker v-model="formData.publicationTime" type="date" placeholder="选择出版时间"
|
||||
value-format="YYYY-MM" :disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="装帧" prop="bindingLayout">
|
||||
<el-input v-model="formData.bindingLayout" placeholder="请输入装帧" maxlength="30" :disabled="dialogType === 'edit'" />
|
||||
<el-input v-model="formData.bindingLayout" placeholder="请输入装帧" maxlength="30"
|
||||
:disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -371,12 +344,14 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="定价" prop="fixPrice">
|
||||
<el-input-number v-model="formData.fixPrice" :min="0" :precision="2" :step="1" placeholder="请输入定价" :disabled="dialogType === 'edit'" />
|
||||
<el-input-number v-model="formData.fixPrice" :min="0" :precision="2" :step="1"
|
||||
placeholder="请输入定价" :disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="页数" prop="pages">
|
||||
<el-input-number v-model="formData.pages" :min="0" :precision="0" :step="1" placeholder="请输入页数" :disabled="dialogType === 'edit'" />
|
||||
<el-input-number v-model="formData.pages" :min="0" :precision="0" :step="1"
|
||||
placeholder="请输入页数" :disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -384,53 +359,40 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="编辑" prop="editor">
|
||||
<el-input v-model="formData.editor" placeholder="请输入编辑" maxlength="30" :disabled="dialogType === 'edit'" />
|
||||
<el-input v-model="formData.editor" placeholder="请输入编辑" maxlength="30"
|
||||
:disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="版次" prop="edition">
|
||||
<el-input v-model="formData.edition" placeholder="请输入版次" maxlength="50" :disabled="dialogType === 'edit'" />
|
||||
<el-input v-model="formData.edition" placeholder="请输入版次" maxlength="50"
|
||||
:disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="书图片" prop="bookPic">
|
||||
<el-upload
|
||||
class="book-pic-uploader"
|
||||
:action="uploadUrl"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="beforeBookPicUpload"
|
||||
:disabled="dialogType === 'edit'"
|
||||
>
|
||||
<img v-if="formData.bookPic && formData.bookPic !== '0'" :src="formData.bookPic" class="book-pic" />
|
||||
<el-icon v-else class="book-pic-uploader-icon" :class="{'disabled-uploader': dialogType === 'edit'}"><Plus /></el-icon>
|
||||
<el-upload class="book-pic-uploader" :action="uploadUrl" :show-file-list="false"
|
||||
:on-success="handleUploadSuccess" :before-upload="beforeBookPicUpload"
|
||||
:disabled="dialogType === 'edit'">
|
||||
<img v-if="formData.bookPic && formData.bookPic !== '0'" :src="formData.bookPic"
|
||||
class="book-pic" />
|
||||
<el-icon v-else class="book-pic-uploader-icon"
|
||||
:class="{ 'disabled-uploader': dialogType === 'edit' }">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
<div v-if="dialogType === 'edit'" class="edit-disabled-tip">编辑模式下不允许更改图片</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内容简介" prop="content">
|
||||
<el-input
|
||||
v-model="formData.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入内容简介"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
<el-input v-model="formData.content" type="textarea" :rows="4" placeholder="请输入内容简介" maxlength="500"
|
||||
show-word-limit :disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" maxlength="500"
|
||||
show-word-limit :disabled="dialogType === 'edit'" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@ -442,13 +404,8 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 违规设置对话框 -->
|
||||
<el-dialog
|
||||
title="设置违规类型"
|
||||
v-model="configDialogVisible"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
class="violation-dialog"
|
||||
>
|
||||
<el-dialog title="设置违规类型" v-model="configDialogVisible" width="500px" :close-on-click-modal="false"
|
||||
class="violation-dialog">
|
||||
<div class="violation-config">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
@ -495,7 +452,8 @@
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="configDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleViolationSubmit" :loading="configSubmitLoading">确定</el-button>
|
||||
<el-button type="primary" @click="handleViolationSubmit"
|
||||
:loading="configSubmitLoading">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -531,7 +489,8 @@ const searchForm = reactive({
|
||||
maxInventory: 999999,
|
||||
minSelling: 0,
|
||||
maxSelling: 999999,
|
||||
hasImage: null
|
||||
hasImage: null,
|
||||
category: undefined
|
||||
})
|
||||
const currentPreviewImg = ref('') // 仅存储当前点击的图片
|
||||
|
||||
@ -721,7 +680,8 @@ const fetchData = async (isAdvancedSearch = false) => {
|
||||
bookSet: 0,
|
||||
onenumMbooks: 0,
|
||||
illPublisher: 0,
|
||||
illAuthor: 0
|
||||
illAuthor: 0,
|
||||
category: searchForm.category || undefined
|
||||
}
|
||||
|
||||
// 添加时间范围参数
|
||||
@ -883,7 +843,7 @@ const handleBatchDelete = () => {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
// 删除
|
||||
@ -901,7 +861,7 @@ const handleDelete = (row) => {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
// 配置违规
|
||||
@ -1078,7 +1038,7 @@ const getBookImageUrl = (row) => {
|
||||
|
||||
// 直接返回拼接的图片URL,不再做存在性检查
|
||||
if (row.bookPic) {
|
||||
const image = imageUrlOne + row.bookPic;
|
||||
const image = row.bookPic;
|
||||
return image;
|
||||
} else if (row.bookName && row.isbn) {
|
||||
// 使用书名md5和isbn拼接备用URL
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user