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", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
"echarts": "^5.6.0",
"element-plus": "^2.9.11", "element-plus": "^2.9.11",
"escpos": "^3.0.0-alpha.6", "escpos": "^3.0.0-alpha.6",
"escpos-usb": "^3.0.0-alpha.4", "escpos-usb": "^3.0.0-alpha.4",
@ -668,6 +669,22 @@
"safer-buffer": "^2.1.0" "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": { "node_modules/element-plus": {
"version": "2.9.11", "version": "2.9.11",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.11.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.11.tgz",
@ -2186,6 +2203,21 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true, "dev": true,
"license": "MIT" "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": { "dependencies": {
@ -2635,6 +2667,22 @@
"safer-buffer": "^2.1.0" "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": { "element-plus": {
"version": "2.9.11", "version": "2.9.11",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.11.tgz", "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", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true "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": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
"echarts": "^5.6.0",
"element-plus": "^2.9.11", "element-plus": "^2.9.11",
"escpos": "^3.0.0-alpha.6", "escpos": "^3.0.0-alpha.6",
"escpos-usb": "^3.0.0-alpha.4", "escpos-usb": "^3.0.0-alpha.4",

View File

@ -11,3 +11,4 @@ export { adminApi } from './modules/admin'
export { invitationApi } from './modules/invitation' export { invitationApi } from './modules/invitation'
export { depotApi } from './modules/depot' 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); return Promise.reject(error);
} }
// 调用刷新接口 // 调用用户刷新接口
const formData = new FormData(); const formData = new FormData();
formData.append('refreshToken', refreshToken); formData.append('refreshToken', refreshToken);
const response = await axiosInstance.post('/admin/refreshToken', formData); const response = await axiosInstance.post('/userLogin/refreshToken', formData);
console.log('刷新token响应:', response); console.log('刷新token响应:', response);
// 后端返回格式: { code: 200, data: { accessToken: "xxx", refreshToken: "xxx" } } // 后端返回格式: { code: 200, data: { accessToken: "xxx", refreshToken: "xxx" } }

View File

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

View File

@ -1,7 +1,37 @@
import axios from '@/utils/axios' 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() { export function getPermissionTree() {
return axios({ return axios({

View File

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

View File

@ -1,7 +1,8 @@
<template> <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> <template #title>
<el-icon> <el-icon>
<component :is="item.icon" /> <component :is="item.icon" />
@ -10,77 +11,58 @@
</template> </template>
<!-- 二级菜单 --> <!-- 二级菜单 -->
<el-sub-menu v-for="child in item.children" :key="child.path" :index="child.path"> <el-menu-item v-for="child in item.children" :key="child.path" :index="child.path">
<template #title>{{ child.title }}</template> {{ child.title }}
<!-- 三级菜单 -->
<el-menu-item v-for="sub in child.children" :key="sub.path" :index="sub.path">
{{ sub.title }}
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
</el-sub-menu>
</el-menu> </el-menu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { shallowRef } from 'vue' import { shallowRef, computed, onMounted, watch } from 'vue'
import {Document as DocIcon,Setting,User,Message,ShoppingCart,Shop,Connection,Notebook,Box,TrendCharts } from '@element-plus/icons-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: '系统管理', title: '系统管理',
path: '/system', path: '/system',
icon: Setting, icon: Setting,
children: [{ children: [
title: '入驻配置', {
path: '/settledConfig',
children: [{
title: '配置列表', title: '配置列表',
path: '/settledConfig/list' path: '/SettledConfig/list',
permission: 'settled:config:list'
}, },
{ {
title: '会员开通记录', title: '会员开通记录',
path: '/settledConfig/memberRecord' path: '/SettledConfig/memberRecord',
}, permission: 'settled:member:record'
]
}, },
{ {
title: '用户管理',
path: '/user',
children: [{
title: '用户列表', title: '用户列表',
path: '/user/list' path: '/user/list',
permission: 'user:list:view'
}, },
{ {
title: '角色管理', title: '角色管理',
path: '/user/role' path: '/user/role',
permission: 'user:role:manage'
}, },
{ {
title: '权限管理', title: '权限管理',
path: '/user/permission' path: '/user/permission',
} permission: 'user:permission:manage'
]
}, },
{ {
title: '邀请管理',
path: '/invitation',
children: [{
title: '邀请列表', title: '邀请列表',
path: '/invitation/list' path: '/invitation/list',
} permission: 'invitation:list:view'
]
}, },
{ {
title: '日志管理', title: '运行日志',
path: '/log', path: '/log/runningLog/list',
children: [{ permission: 'log:running:view'
title: '操作日志',
path: '/log/operate'
},
{
title: '登录日志',
path: '/log/login'
}
]
} }
] ]
}, },
@ -88,108 +70,139 @@
title: '店铺管理', title: '店铺管理',
path: '/shop', path: '/shop',
icon: Shop, icon: Shop,
children: [{ children: [
{
title: '店铺列表', title: '店铺列表',
path: '/shop/list', path: '/shop/list',
children: [{ permission: 'shop:list:view'
title: '店铺列表', }
path: '/shop/list' ]
}]
}]
}, },
{ {
title: '书品管理', title: '书品管理',
path: '/book', path: '/book',
icon: Notebook, icon: Notebook,
children: [{ children: [
{
title: '选品中心', title: '选品中心',
path: '/book/selection', path: '/book/selection/center',
children: [{ permission: 'book:selection:view'
title: '选品中心', }
path: '/book/selection/center' ]
}]
}]
}, },
{ {
title: '仓储管理', title: '仓储管理',
path: '/warehouse', path: '/warehouse',
icon: Box, icon: Box,
children: [{ children: [
{
title: '货区管理', title: '货区管理',
path: '/warehouse/depot', path: '/warehouse/depot/list',
children: [{ permission: 'warehouse:depot:view'
title: '货区列表', },
path: '/warehouse/depot/list' {
}] title: '物流模板',
}] path: '/warehouse/logistics'
}
]
}, },
{ {
title: '工具管理', title: '工具管理',
path: '/tools', path: '/tools',
icon: DocIcon, icon: DocIcon,
children: [{ children: [
title: '卡密管理', {
path: '/tools/cards',
children: [{
title: '卡密列表', title: '卡密列表',
path: '/tools/cards/list' path: '/tools/cards/list',
},{ permission: 'cards:list:view'
},
{
title: '活跃卡密列表', title: '活跃卡密列表',
path: '/tools/cards/activeCardsList' path: '/tools/cards/activeCardsList',
}] permission: 'cards:active:view'
}] }
]
}, },
{ {
title: '审核管理', title: '审核管理',
path: '/examine', path: '/examine',
children:[{ children: [
title: '违规审核', {
path: '/examine/violation',
children:[{
title: '违规列表', title: '违规列表',
path: '/examine/violation/list' path: '/examine/violation/list'
}] }
}] ]
},
{
title: '日志管理',
path: '/log',
children:[{
title: '运行日志',
path: '/log/runningLog',
children:[{
title: '日志列表',
path: '/log/runningLog/list'
}]
}]
}, },
{ {
title: '任务管理', title: '任务管理',
path: '/task', path: '/task',
icon: TrendCharts, icon: TrendCharts,
children:[{ children: [
{
title: '任务列表', title: '任务列表',
path: '/task/list', path: '/task/list',
children:[{ permission: 'task:list:view'
title: '任务列表', }
path: '/task/list' ]
}]
}]
}, },
{ {
title: '功能模块', title: '功能模块',
path: '/useModule', path: '/useModule',
children:[{ children: [
title: '订阅服务', {
path: '/useModule/vas',
children:[{
title: '服务列表', 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> </script>
<style scoped> <style scoped>
@ -207,6 +220,7 @@
} }
:deep(.el-sub-menu.is-opened) { :deep(.el-sub-menu.is-opened) {
>.el-sub-menu__title, >.el-sub-menu__title,
.el-menu-item { .el-menu-item {
background-color: #1a1a1a !important; background-color: #1a1a1a !important;

View File

@ -2,17 +2,32 @@ import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import store from './store' import store from './store'
import axios from './utils/axios'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import './styles/global.css' import { permission, permissionAll } from '@/directives/permission'
import { globalConfig } from './config/global' import { initUserPermissions } from '@/utils/permission'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import globalComponents from './components'
const app = createApp(App) 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 { createRouter, createWebHistory } from 'vue-router'
import store from '@/store' import { getUserPermissions } from '@/utils/permission'
const routes = [{ const routes = [{
path: '/', path: '/',
@ -8,91 +8,113 @@ const routes = [{
path: '', path: '',
component: () => import('@/components/TabsView.vue'), component: () => import('@/components/TabsView.vue'),
meta: {noLayout: true}, meta: {noLayout: true},
children: [{ children: [
{
path: '/welcome', path: '/welcome',
component: () => import('@/views/Welcome/Index.vue'), component: () => import('@/views/Welcome/Index.vue'),
meta: { title: '欢迎' } meta: { title: '欢迎' }
}, },
// 入驻配置
{ {
path: '/SettledConfig/list', path: '/SettledConfig/list',
component: () => import('@/views/SettledConfig/List.vue'), component: () => import('@/views/SettledConfig/List.vue'),
meta: { title: '配置列表' } meta: { title: '配置列表', permission: 'settled:config:list' }
}, },
{ {
path: '/SettledConfig/memberRecord', path: '/SettledConfig/memberRecord',
component: () => import('@/views/SettledConfig/MemberRecord.vue'), component: () => import('@/views/SettledConfig/MemberRecord.vue'),
meta: { title: '会员开通记录' } meta: { title: '会员开通记录', permission: 'settled:member:record' }
}, },
// 用户管理
{ {
path: '/user/list', path: '/user/list',
component: () => import('@/views/User/List.vue'), component: () => import('@/views/User/List.vue'),
meta: { title: '用户列表' } meta: { title: '用户列表', permission: 'user:list:view' }
}, },
{ {
path: '/user/role', path: '/user/role',
component: () => import('@/views/User/Role.vue'), component: () => import('@/views/User/Role.vue'),
meta: { title: '角色管理' } meta: { title: '角色管理', permission: 'user:role:manage' }
}, },
{ {
path: '/user/permission', path: '/user/permission',
component: () => import('@/views/User/Permission.vue'), 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', path: '/shop/list',
component: () => import('@/views/Shop/index.vue'), 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', path: '/tools/cards/list',
component: () => import('@/views/Tools/Cards/List.vue'), component: () => import('@/views/Tools/Cards/List.vue'),
meta: { title: '卡密列表' } meta: { title: '卡密列表', permission: 'cards:list:view' }
}, },
{ {
path: '/tools/cards/activeCardsList', path: '/tools/cards/activeCardsList',
component: () => import('@/views/Tools/Cards/ActiveCardsList.vue'), component: () => import('@/views/Tools/Cards/ActiveCardsList.vue'),
meta: { title: '活跃卡密列表' } meta: { title: '活跃卡密列表', permission: 'cards:active:view' }
}, },
// 审核管理
{ {
path: '/examine/violation/list', path: '/examine/violation/list',
component: () => import('@/views/Examine/Violation/List.vue'), component: () => import('@/views/Examine/Violation/List.vue'),
meta: { title: '违规列表' } meta: { title: '违规列表' }
}, },
// 日志管理
{ {
path: '/log/runningLog/list', path: '/log/runningLog/list',
component: () => import('@/views/log/RunningLog/List.vue'), component: () => import('@/views/Log/RunningLog/List.vue'),
meta: { title: '日志列表' } meta: { title: '运行日志', permission: 'log:running:view' }
},
{
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: '货区管理' }
}, },
// 任务管理
{ {
path: '/task/list', path: '/task/list',
component: () => import('@/views/Task/List.vue'), 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'), component: () => import('@/views/Login/Index.vue'),
meta: { noLayout: true,public: true } 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}) const router = createRouter({history: createWebHistory(),routes})
// 路由守卫 // 路由权限守卫
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// 定义 是否为公开 // 检查路由是否需要权限
const isPublic = to.meta.public if (to.meta && to.meta.permission) {
// 定义 是否已认证 const userPermissions = getUserPermissions()
const isAuthenticated = store.getters.isAuthenticated const requiredPermission = to.meta.permission
// 如果访问根路径, 重定向到welcome页
if (to.path === '/' && isAuthenticated) return next('/welcome') if (userPermissions.includes(requiredPermission)) {
// 已认证用户访问登录页自动跳转首页
if (to.path === '/login' && isAuthenticated) return next('/welcome')
// 需要认证且未登录
if (!isPublic && !isAuthenticated) {
// 跳转到登录 并且带上属性
return next({ path: '/login',query: { redirect: to.fullPath } })
}
// 正常跳转
next() next()
} else {
// 没有权限跳转到403页面或首页
next('/403')
}
} else {
next()
}
}) })
// 返回 模块 // 返回 模块
export default router export default router

View File

@ -1,6 +1,7 @@
import { createStore } from 'vuex' import { createStore } from 'vuex'
import { adminApi } from '../api/index.js' import { userLoginApi } from '../api/index.js'
import webSocketService from '../utils/webSocket.js' import webSocketService from '../utils/webSocket.js'
import { initUserPermissions } from '../utils/permission.js'
export default createStore({ export default createStore({
state: { state: {
@ -46,19 +47,45 @@ export default createStore({
actions: { actions: {
async login({ commit }, data) { async login({ commit }, data) {
try { try {
// 调用登录接口 // 转换字段名username -> phonenumber因为userLogin接口使用phonenumber
const response = await adminApi.login(data) let loginData = data;
console.log("响应",response) 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) { if (response.code !== 200) {
throw new Error(response.message || '登录失败') throw new Error(response.message || '登录失败')
} }
// 设置 状态 // 设置 token 状态
commit('SET_TOKEN', response.data) commit('SET_TOKEN', {
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
})
// 添加更多日志确保token存在 // 添加更多日志确保token存在
console.log("登录成功准备连接WebSocket") console.log("用户登录成功准备连接WebSocket")
console.log("accessToken值:", response.data.accessToken) console.log("accessToken值:", response.data.accessToken)
// 登录成功后连接WebSocket // 登录成功后连接WebSocket
@ -68,23 +95,26 @@ export default createStore({
console.error("无法连接WebSocket: accessToken不存在") console.error("无法连接WebSocket: accessToken不存在")
} }
try { // 设置用户信息userLogin接口直接返回用户信息
// 调用查询接口获取用户信息 const userInfo = {
const admin = await adminApi.getAdmin() userId: response.data.userId,
username: response.data.userName,
// 检查响应状态 nickName: response.data.nickName,
if (admin.code !== 200) { phonenumber: response.data.phonenumber,
console.error('获取用户信息失败:', admin.message) email: response.data.email,
// 不清空认证信息,仅返回警告 sex: response.data.sex,
return Promise.resolve('登录成功,但获取用户信息失败') userType: response.data.userType,
status: response.data.status
} }
commit('SET_USER_INFO', userInfo)
// 设置 管理员信息 // 初始化用户权限
commit('SET_USER_INFO', admin.data) try {
} catch (adminError) { await initUserPermissions()
// 记录错误但不清空认证信息 console.log("用户权限初始化成功")
console.error('获取用户信息出错:', adminError) } catch (permissionError) {
return Promise.resolve('登录成功,但获取用户信息失败') console.error("用户权限初始化失败:", permissionError)
// 权限初始化失败不影响登录,只记录错误
} }
// 抛出 消息 // 抛出 消息

View File

@ -3,11 +3,27 @@ import axios from 'axios'
const instance = axios.create({ const instance = axios.create({
baseURL: '/api', baseURL: '/api',
timeout: 30000, // 增加超时时间到30秒 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 export default instance

View File

@ -52,16 +52,8 @@
</template> </template>
</ActionBar> </ActionBar>
<!-- 数据表格 --> <!-- 数据表格 -->
<el-table <el-table ref="tableRef" :data="tableData" border stripe style="width: 100%;" v-loading="loading"
ref="tableRef" @selection-change="handleSelectionChange" :row-class-name="setReviewCellStyle">
: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 type="selection" width="55" align="center" />
<el-table-column label="主键" align="center" prop="id" v-if="true" /> <el-table-column label="主键" align="center" prop="id" v-if="true" />
@ -95,33 +87,16 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size"
v-model:current-page="pagination.current" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
v-model:page-size="pagination.size" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div> </div>
</div> </div>
<!-- 获取卡密对话框 --> <!-- 获取卡密对话框 -->
<el-dialog <el-dialog title="驳回原因" v-model="violationVisible" width="500px" :close-on-click-modal="false">
title="驳回原因" <el-input v-model="remark" style="width: 100%" :rows="10" type="textarea" placeholder="请输入驳回原因" />
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> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@ -218,8 +193,8 @@ const fetchData = async () => {
// //
const params = { const params = {
page: pagination.current, pageNum: pagination.current, // pageNum
size: pagination.size, pageSize: pagination.size, // pageSize
type: searchForm.type !== null ? searchForm.type : undefined, type: searchForm.type !== null ? searchForm.type : undefined,
name: searchForm.name || undefined, name: searchForm.name || undefined,
status: searchForm.status || undefined, status: searchForm.status || undefined,
@ -230,8 +205,8 @@ const fetchData = async () => {
const res = await violationApi.pageQuery(params) const res = await violationApi.pageQuery(params)
// response.data使res // response.data使res
if (res.code === 200) { if (res.code === 200) {
// data.rows data.list
tableData.value = res.data.list || [] tableData.value = res.data.rows || []
pagination.total = res.data.total || 0 pagination.total = res.data.total || 0
} else { } else {
ElMessage.error(res.message || '获取数据失败') ElMessage.error(res.message || '获取数据失败')

View File

@ -1,9 +1,811 @@
<template> <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> </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> </style>

View File

@ -7,7 +7,7 @@
<el-divider/> <el-divider/>
</div> </div>
<el-form-item prop="username" class="form-item"> <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>
<el-form-item prop="password" class="form-item"> <el-form-item prop="password" class="form-item">
<el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password <el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password
@ -38,14 +38,14 @@
// //
const form = reactive({ const form = reactive({
username: '', username: '', // 使
password: '', password: '',
captcha: '' captcha: ''
}) })
// //
const rules = reactive({ const rules = reactive({
username: [{required: true, message: '用户名不能为空', trigger: 'blur'}], username: [{required: true, message: '手机号不能为空', trigger: 'blur'}],
password: [{required: true, message: '密码不能为空', trigger: 'blur'}], password: [{required: true, message: '密码不能为空', trigger: 'blur'}],
captcha: [{required: true, message: '验证码不能为空', trigger: 'blur'}] captcha: [{required: true, message: '验证码不能为空', trigger: 'blur'}]
}) })
@ -64,7 +64,7 @@
// //
const refreshCaptcha = () => { 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 logsMessage = ref('')
const currentTaskId = ref('') const currentTaskId = ref('')
//
const detailLogsVisible = ref(false)
const detailLogsLoading = ref(false)
const detailLogsList = ref([])
const currentDetailShopLog = ref(null)
// //
const fetchTaskList = async () => { const fetchTaskList = async () => {
loading.value = true loading.value = true
@ -394,18 +400,46 @@ const refreshLogs = () => {
// //
const handleViewDetail = async (row) => { const handleViewDetail = async (row) => {
try { try {
//
currentDetailShopLog.value = row
//
detailLogsLoading.value = true
detailLogsVisible.value = true
// API
const res = await taskApi.getLogsDetailList(row.taskId, row.shopId) const res = await taskApi.getLogsDetailList(row.taskId, row.shopId)
console.log('详细日志响应:', res) console.log('详细日志响应:', res)
if (res.code === 200) { 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 { } else {
detailLogsList.value = []
console.warn('未能识别的详细日志数据格式:', res.data)
}
} else {
detailLogsList.value = []
}
} else {
detailLogsList.value = []
ElMessage.error('获取详细日志失败: ' + (res.message || '未知错误')) ElMessage.error('获取详细日志失败: ' + (res.message || '未知错误'))
} }
} catch (error) { } catch (error) {
console.error('获取详细日志出错:', error) console.error('获取详细日志出错:', error)
ElMessage.error('获取详细日志失败: ' + (error.message || '未知错误')) ElMessage.error('获取详细日志失败: ' + (error.message || '未知错误'))
detailLogsList.value = []
} finally {
detailLogsLoading.value = false
} }
} }
@ -449,6 +483,41 @@ const formatLogMessage = (message) => {
return lines 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(() => { onMounted(() => {
fetchTaskList() fetchTaskList()

View File

@ -3,7 +3,7 @@
<div class="header-actions"> <div class="header-actions">
<el-input <el-input
v-model="searchKeyword" v-model="searchKeyword"
placeholder="请输入用户名搜索" placeholder="请输入用户名或手机号搜索"
clearable clearable
class="search-input" class="search-input"
@clear="loadUserList" @clear="loadUserList"
@ -26,14 +26,28 @@
:data="userList" :data="userList"
border border
style="width: 100%" style="width: 100%"
row-key="id" row-key="userId"
> >
<el-table-column prop="id" label="ID" width="80" /> <!-- <el-table-column prop="userId" label="用户ID" width="120" /> -->
<el-table-column prop="username" label="用户名" /> <el-table-column prop="userName" label="用户名" />
<el-table-column prop="nickname" label="昵称" /> <el-table-column prop="nickName" label="昵称" />
<el-table-column prop="phone" label="手机号" /> <el-table-column prop="phonenumber" label="手机号" />
<el-table-column prop="email" 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"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="openUserDialog(row)"> <el-button type="primary" link @click="openUserDialog(row)">
@ -72,8 +86,8 @@
label-width="100px" label-width="100px"
class="user-form" class="user-form"
> >
<el-form-item label="用户名" prop="username"> <el-form-item label="用户名" prop="userName">
<el-input v-model="form.username" placeholder="请输入用户名" /> <el-input v-model="form.userName" placeholder="请输入用户名" />
</el-form-item> </el-form-item>
<el-form-item label="密码" prop="password" v-if="!isEdit"> <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-input v-model="form.confirmPassword" type="password" placeholder="请确认密码" show-password />
</el-form-item> </el-form-item>
<el-form-item label="昵称" prop="nickname"> <el-form-item label="昵称" prop="nickName">
<el-input v-model="form.nickname" placeholder="请输入昵称" /> <el-input v-model="form.nickName" placeholder="请输入昵称" />
</el-form-item> </el-form-item>
<el-form-item label="手机号" prop="phone"> <el-form-item label="手机号" prop="phonenumber">
<el-input v-model="form.phone" placeholder="请输入手机号" /> <el-input v-model="form.phonenumber" placeholder="请输入手机号" />
</el-form-item> </el-form-item>
<el-form-item label="邮箱" prop="email"> <el-form-item label="邮箱" prop="email">
@ -141,13 +155,15 @@ const submitLoading = ref(false)
const roleList = ref([]) const roleList = ref([])
// //
const form = reactive({ const form = reactive({
id: null, userId: null,
username: '', userName: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
nickname: '', nickName: '',
phone: '', phonenumber: '',
email: '', email: '',
userType: '01', //
status: '0', //
roleIds: [] // ID roleIds: [] // ID
}) })
@ -174,7 +190,7 @@ const validateConfirmPass = (rule, value, callback) => {
} }
const rules = reactive({ const rules = reactive({
username: [ userName: [
{ required: true, message: '请输入用户名', trigger: 'blur' }, { required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' } { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
], ],
@ -184,7 +200,7 @@ const rules = reactive({
confirmPassword: [ confirmPassword: [
{ validator: validateConfirmPass, trigger: 'blur' } { validator: validateConfirmPass, trigger: 'blur' }
], ],
phone: [ phonenumber: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' } { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
], ],
email: [ email: [
@ -202,19 +218,21 @@ const formRef = ref(null)
const loadUserList = async () => { const loadUserList = async () => {
try { try {
loading.value = true 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) { if (res.code === 200) {
userList.value = res.data || [] userList.value = res.data?.list || []
total.value = res.data?.length || 0 total.value = res.data?.total || 0
//
if (searchKeyword.value) {
userList.value = userList.value.filter(user =>
user.username?.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
user.nickname?.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
} else { } else {
ElMessage.error(res.message || '获取用户列表失败') ElMessage.error(res.message || '获取用户列表失败')
} }
@ -349,14 +367,24 @@ const resetForm = () => {
// //
Object.keys(form).forEach(key => { 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) => { const handleDelete = (row) => {
ElMessageBox.confirm( ElMessageBox.confirm(
`确定要删除用户 "${row.username || row.nickname || row.id}" 吗?`, `确定要删除用户 "${row.userName || row.nickName || row.userId}" 吗?`,
'删除确认', '删除确认',
{ {
confirmButtonText: '确定', confirmButtonText: '确定',
@ -365,7 +393,7 @@ const handleDelete = (row) => {
} }
).then(async () => { ).then(async () => {
try { try {
const res = await userApi.deleteUser(row.id) const res = await userApi.deleteUser(row.userId)
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('删除成功') ElMessage.success('删除成功')
loadUserList() loadUserList()
@ -393,12 +421,50 @@ const handleCurrentChange = (val) => {
loadUserList() 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 formatDate = (row, column) => {
const dateValue = row[column.property] const dateValue = row[column.property]
if (!dateValue) return '-' if (!dateValue) return '-'
try { 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) const date = new Date(dateValue)
return date.toLocaleString('zh-CN', { return date.toLocaleString('zh-CN', {
year: 'numeric', year: 'numeric',

View File

@ -22,6 +22,20 @@
<span class="search-label">出版社</span> <span class="search-label">出版社</span>
<el-input v-model="searchForm.publisher" placeholder="请输入出版社" clearable /> <el-input v-model="searchForm.publisher" placeholder="请输入出版社" clearable />
</div> </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>
<div class="search-row"> <div class="search-row">
@ -36,28 +50,15 @@
<div class="search-item date-range-item"> <div class="search-item date-range-item">
<span class="search-label">出版时间范围</span> <span class="search-label">出版时间范围</span>
<el-date-picker <el-date-picker v-model="searchForm.timeRange" type="daterange" range-separator=""
v-model="searchForm.timeRange" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
type="daterange" :shortcuts="dateShortcuts" />
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
/>
</div> </div>
<div class="search-item"> <div class="search-item">
<span class="search-label">违规信息筛选</span> <span class="search-label">违规信息筛选</span>
<el-select <el-select v-model="searchForm.violationTypes" placeholder="请选择违规条件" clearable multiple
v-model="searchForm.violationTypes" style="width: 240px" collapse-tags collapse-tags-tooltip>
placeholder="请选择违规条件"
clearable
multiple
style="width: 240px"
collapse-tags
collapse-tags-tooltip
>
<el-option label="违规书号" :value="1" /> <el-option label="违规书号" :value="1" />
<el-option label="套装书" :value="2" /> <el-option label="套装书" :value="2" />
<el-option label="一号多书" :value="3" /> <el-option label="一号多书" :value="3" />
@ -68,7 +69,9 @@
<div class="search-item btn-item"> <div class="search-item btn-item">
<el-button type="primary" @click="handleNormalSearch" class="search-btn"> <el-button type="primary" @click="handleNormalSearch" class="search-btn">
<el-icon><Search /></el-icon> <el-icon>
<Search />
</el-icon>
搜索 搜索
</el-button> </el-button>
</div> </div>
@ -77,7 +80,8 @@
<div class="search-row" :class="{ 'advanced-search-hidden': !showAdvancedSearch }"> <div class="search-row" :class="{ 'advanced-search-hidden': !showAdvancedSearch }">
<div class="search-item"> <div class="search-item">
<span class="search-label">销量</span> <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="请选择销量类型" :value="null" />
<el-option label="7天销量" :value="7" /> <el-option label="7天销量" :value="7" />
<el-option label="15天销量" :value="15" /> <el-option label="15天销量" :value="15" />
@ -92,31 +96,40 @@
<div class="search-item number-range-item"> <div class="search-item number-range-item">
<span class="search-label">已售</span> <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> <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>
<div class="search-item number-range-item"> <div class="search-item number-range-item">
<span class="search-label">在售</span> <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> <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>
<div class="search-item btn-item"> <div class="search-item btn-item">
<el-button @click="resetSearch" class="reset-btn"> <el-button @click="resetSearch" class="reset-btn">
<el-icon><Refresh /></el-icon> <el-icon>
<Refresh />
</el-icon>
重置 重置
</el-button> </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> </div>
<div class="toggle-advanced-search"> <div class="toggle-advanced-search">
<el-button type="text" @click="toggleAdvancedSearch"> <el-button type="text" @click="toggleAdvancedSearch">
{{ showAdvancedSearch ? '收起高级搜索' : '展开高级搜索' }} {{ showAdvancedSearch ? '收起高级搜索' : '展开高级搜索' }}
<el-icon :class="{ 'rotate-icon': showAdvancedSearch }"><ArrowDown /></el-icon> <el-icon :class="{ 'rotate-icon': showAdvancedSearch }">
<ArrowDown />
</el-icon>
</el-button> </el-button>
</div> </div>
</div> </div>
@ -136,28 +149,16 @@
</div> </div>
<!-- 数据表格 --> <!-- 数据表格 -->
<el-table <el-table ref="tableRef" :data="tableData" border style="width: 100%" @selection-change="handleSelectionChange"
ref="tableRef" row-key="id" v-loading="loading"
:data="tableData" :header-cell-style="{ backgroundColor: '#f5f7fa', color: '#606266', textAlign: 'center' }" height="500"
border max-height="500">
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 type="selection" align="center" width="50" />
<el-table-column prop="bookName" align="center" label="书名" show-overflow-tooltip> <el-table-column prop="bookName" align="center" label="书名" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<div ref="textRef" class="ellipsis-text"> <div ref="textRef" class="ellipsis-text">
<el-tooltip <el-tooltip :content="row.bookName || '-'" placement="top" :disabled="!isTextEllipsis($event)"
:content="row.bookName || '-'" :enterable="false">
placement="top"
:disabled="!isTextEllipsis($event)"
:enterable="false"
>
<span class="ellipsis-text">{{ row.bookName || '-' }}</span> <span class="ellipsis-text">{{ row.bookName || '-' }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
@ -165,28 +166,18 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="书图片" width="80"> <el-table-column align="center" label="书图片" width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-image <el-image v-if="row.bookPic && row.bookPic !== '0'" :src="getBookImageUrl(row)"
v-if="row.bookPic && row.bookPic !== '0'" :preview-src-list="previewList" :initial-index="getPreviewIndex(row)"
:src="getBookImageUrl(row)" style="width: 40px; height: 60px; object-fit: cover;" @click="handlePreview(row)" fit="cover"
:preview-src-list="previewList" preview-teleported />
: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> <span v-else class="no-image">无图</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="isbn" align="center" label="isbn" width="120" show-overflow-tooltip> <el-table-column prop="isbn" align="center" label="isbn" width="120" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<div ref="textRef" class="ellipsis-text"> <div ref="textRef" class="ellipsis-text">
<el-tooltip <el-tooltip :content="row.isbn || '-'" placement="top" :disabled="!isTextEllipsis($event)"
:content="row.isbn || '-'" :enterable="false">
placement="top"
:disabled="!isTextEllipsis($event)"
:enterable="false"
>
<span class="ellipsis-text">{{ row.isbn || '-' }}</span> <span class="ellipsis-text">{{ row.isbn || '-' }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
@ -195,12 +186,8 @@
<el-table-column prop="author" align="center" label="作者" show-overflow-tooltip> <el-table-column prop="author" align="center" label="作者" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<div ref="textRef" class="ellipsis-text"> <div ref="textRef" class="ellipsis-text">
<el-tooltip <el-tooltip :content="row.author || '-'" placement="top" :disabled="!isTextEllipsis($event)"
:content="row.author || '-'" :enterable="false">
placement="top"
:disabled="!isTextEllipsis($event)"
:enterable="false"
>
<span class="ellipsis-text">{{ row.author || '-' }}</span> <span class="ellipsis-text">{{ row.author || '-' }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
@ -209,12 +196,8 @@
<el-table-column prop="publisher" align="center" label="出版社" show-overflow-tooltip> <el-table-column prop="publisher" align="center" label="出版社" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<div ref="textRef" class="ellipsis-text"> <div ref="textRef" class="ellipsis-text">
<el-tooltip <el-tooltip :content="row.publisher || '-'" placement="top" :disabled="!isTextEllipsis($event)"
:content="row.publisher || '-'" :enterable="false">
placement="top"
:disabled="!isTextEllipsis($event)"
:enterable="false"
>
<span class="ellipsis-text">{{ row.publisher || '-' }}</span> <span class="ellipsis-text">{{ row.publisher || '-' }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
@ -242,30 +225,37 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="销量详情" width="100"> <el-table-column align="center" label="销量详情" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-popover <el-popover placement="right" trigger="hover" :width="220" popper-class="sales-popover">
placement="right"
trigger="hover"
:width="220"
popper-class="sales-popover"
>
<template #default> <template #default>
<div class="sales-detail"> <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">7天销量:</span> {{ row.daySale7 || 0 }}
<div class="sales-item"><span class="sales-label">15天销量:</span> {{ row.daySale15 || 0 }}</div> </div>
<div class="sales-item"><span class="sales-label">30天销量:</span> {{ row.daySale30 || 0 }}</div> <div class="sales-item"><span class="sales-label">15天销量:</span> {{ row.daySale15 || 0 }}
<div class="sales-item"><span class="sales-label">60天销量:</span> {{ row.daySale60 || 0 }}</div> </div>
<div class="sales-item"><span class="sales-label">90天销量:</span> {{ row.daySale90 || 0 }}</div> <div class="sales-item"><span class="sales-label">30天销量:</span> {{ row.daySale30 || 0 }}
<div class="sales-item"><span class="sales-label">180天销量:</span> {{ row.daySale180 || 0 }}</div> </div>
<div class="sales-item"><span class="sales-label">365天销量:</span> {{ row.daySale365 || 0 }}</div> <div class="sales-item"><span class="sales-label">60天销量:</span> {{ row.daySale60 || 0 }}
<div class="sales-item"><span class="sales-label">今年销量:</span> {{ row.thisYearSale || 0 }}</div> </div>
<div class="sales-item"><span class="sales-label">去年销量:</span> {{ row.lastYearSale || 0 }}</div> <div class="sales-item"><span class="sales-label">90天销量:</span> {{ row.daySale90 || 0 }}
<div class="sales-item"><span class="sales-label">总销量:</span> {{ row.totalSale || 0 }}</div> </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> </div>
</template> </template>
<template #reference> <template #reference>
<div class="sales-trigger"> <div class="sales-trigger">
<span>{{ row.sale7Days || 0 }}</span> <span>{{ row.sale7Days || 0 }}</span>
<el-icon><ArrowRight /></el-icon> <el-icon>
<ArrowRight />
</el-icon>
</div> </div>
</template> </template>
</el-popover> </el-popover>
@ -298,31 +288,15 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination :current-page="pagination.current" :page-size="pagination.size"
:current-page="pagination.current" :page-sizes="[10, 20, 50, 100]" :layout="'total, sizes, prev, pager, next, jumper'"
:page-size="pagination.size" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
: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 <el-dialog :title="dialogType === 'add' ? '新增图书信息' : '编辑图书信息'" v-model="dialogVisible" width="800px"
:title="dialogType === 'add' ? '新增图书信息' : '编辑图书信息'" :close-on-click-modal="false">
v-model="dialogVisible" <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" label-position="right">
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-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="书名" prop="bookName"> <el-form-item label="书名" prop="bookName">
@ -331,7 +305,8 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="ISBN" prop="isbn"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -339,12 +314,14 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="作者" prop="author"> <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-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="出版社" prop="publisher"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -352,18 +329,14 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="出版时间" prop="publicationTime"> <el-form-item label="出版时间" prop="publicationTime">
<el-date-picker <el-date-picker v-model="formData.publicationTime" type="date" placeholder="选择出版时间"
v-model="formData.publicationTime" value-format="YYYY-MM" :disabled="dialogType === 'edit'" />
type="date"
placeholder="选择出版时间"
value-format="YYYY-MM"
:disabled="dialogType === 'edit'"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="装帧" prop="bindingLayout"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -371,12 +344,14 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="定价" prop="fixPrice"> <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-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="页数" prop="pages"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -384,53 +359,40 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="编辑" prop="editor"> <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-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="版次" prop="edition"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-form-item label="书图片" prop="bookPic"> <el-form-item label="书图片" prop="bookPic">
<el-upload <el-upload class="book-pic-uploader" :action="uploadUrl" :show-file-list="false"
class="book-pic-uploader" :on-success="handleUploadSuccess" :before-upload="beforeBookPicUpload"
:action="uploadUrl" :disabled="dialogType === 'edit'">
:show-file-list="false" <img v-if="formData.bookPic && formData.bookPic !== '0'" :src="formData.bookPic"
:on-success="handleUploadSuccess" class="book-pic" />
:before-upload="beforeBookPicUpload" <el-icon v-else class="book-pic-uploader-icon"
:disabled="dialogType === 'edit'" :class="{ 'disabled-uploader': dialogType === 'edit' }">
> <Plus />
<img v-if="formData.bookPic && formData.bookPic !== '0'" :src="formData.bookPic" class="book-pic" /> </el-icon>
<el-icon v-else class="book-pic-uploader-icon" :class="{'disabled-uploader': dialogType === 'edit'}"><Plus /></el-icon>
</el-upload> </el-upload>
<div v-if="dialogType === 'edit'" class="edit-disabled-tip">编辑模式下不允许更改图片</div> <div v-if="dialogType === 'edit'" class="edit-disabled-tip">编辑模式下不允许更改图片</div>
</el-form-item> </el-form-item>
<el-form-item label="内容简介" prop="content"> <el-form-item label="内容简介" prop="content">
<el-input <el-input v-model="formData.content" type="textarea" :rows="4" placeholder="请输入内容简介" maxlength="500"
v-model="formData.content" show-word-limit :disabled="dialogType === 'edit'" />
type="textarea"
:rows="4"
placeholder="请输入内容简介"
maxlength="500"
show-word-limit
:disabled="dialogType === 'edit'"
/>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" maxlength="500"
v-model="formData.remark" show-word-limit :disabled="dialogType === 'edit'" />
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="500"
show-word-limit
:disabled="dialogType === 'edit'"
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -442,13 +404,8 @@
</el-dialog> </el-dialog>
<!-- 违规设置对话框 --> <!-- 违规设置对话框 -->
<el-dialog <el-dialog title="设置违规类型" v-model="configDialogVisible" width="500px" :close-on-click-modal="false"
title="设置违规类型" class="violation-dialog">
v-model="configDialogVisible"
width="500px"
:close-on-click-modal="false"
class="violation-dialog"
>
<div class="violation-config"> <div class="violation-config">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
@ -495,7 +452,8 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="configDialogVisible = false">取消</el-button> <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> </span>
</template> </template>
</el-dialog> </el-dialog>
@ -531,7 +489,8 @@ const searchForm = reactive({
maxInventory: 999999, maxInventory: 999999,
minSelling: 0, minSelling: 0,
maxSelling: 999999, maxSelling: 999999,
hasImage: null hasImage: null,
category: undefined
}) })
const currentPreviewImg = ref('') // const currentPreviewImg = ref('') //
@ -721,7 +680,8 @@ const fetchData = async (isAdvancedSearch = false) => {
bookSet: 0, bookSet: 0,
onenumMbooks: 0, onenumMbooks: 0,
illPublisher: 0, illPublisher: 0,
illAuthor: 0 illAuthor: 0,
category: searchForm.category || undefined
} }
// //
@ -1078,7 +1038,7 @@ const getBookImageUrl = (row) => {
// URL // URL
if (row.bookPic) { if (row.bookPic) {
const image = imageUrlOne + row.bookPic; const image = row.bookPic;
return image; return image;
} else if (row.bookName && row.isbn) { } else if (row.bookName && row.isbn) {
// 使md5isbnURL // 使md5isbnURL

View File

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