动作参数取值范围+角色配置权限

This commit is contained in:
2026-04-30 15:16:48 +08:00
parent 0bc6dd7761
commit 05770f7e56
177 changed files with 13913 additions and 9863 deletions

1
rc_autoplc_front/.env Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://223.71.122.54:9090

View File

@@ -2,9 +2,9 @@
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/public/Laboratory.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>中核404内照射上位机中控系统</title>
</head>
<body>
<div id="app"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -14,11 +14,15 @@
"type-check": "vue-tsc --build"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"axios": "^1.13.2",
"echarts": "^6.0.0",
"element-plus": "^2.12.0",
"html2canvas": "^1.4.1",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.25",
"vue-echarts": "^8.0.1",
"vue-router": "^4.6.3"
},
"devDependencies": {
@@ -27,6 +31,7 @@
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"npm-run-all2": "^8.0.4",
"sass-embedded": "^1.97.3",
"typescript": "~5.9.0",
"vite": "^7.2.4",
"vite-plugin-vue-devtools": "^8.0.5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
export function dicdataadd(data: any) {
return request({
url: '/sysDicData/add',
method: 'post',
data,
})
}
export function dicdatadel(id: string | number) {
return request({
url: `/sysDicData/del/${id}`,
method: 'delete',
})
}
export function dicdataupd(data: any) {
return request({
url: '/sysDicData/update',
method: 'put',
data,
})
}
export function dicdatalist(data: any) {
return request({
url: '/sysDicData/listPage',
method: 'get',
params: data,
})
}
export function dicdatabyid(id: string | number) {
return request({
url: `/sysDicData/getById/${id}`,
method: 'get',
})
}
export function dicdataall() {
return request({
url: `/sysDicData/list`,
method: 'get',
})
}
export function dicdatabydicid(dicId : string | number) {
return request({
url: `/sysDicData/listByDicId/${dicId}`,
method: 'get',
})
}

View File

@@ -0,0 +1,46 @@
import request from '@/utils/request'
export function dictypeadd(data: any) {
return request({
url: '/sysDicType/add',
method: 'post',
data,
})
}
export function dictypedel(id: string | number) {
return request({
url: `/sysDicType/del/${id}`,
method: 'delete',
})
}
export function dictypeupd(data: any) {
return request({
url: '/sysDicType/update',
method: 'put',
data,
})
}
export function dictypelist(data: any) {
return request({
url: '/sysDicType/listPage',
method: 'get',
params: data,
})
}
export function dictypebyid(id: string | number) {
return request({
url: `/sysDicType/getById/${id}`,
method: 'get',
})
}
export function dictypeall() {
return request({
url: `/sysDicType/list`,
method: 'get',
})
}

View File

@@ -0,0 +1,47 @@
import request from '@/utils/request'
export function permissionadd(data: any) {
return request({
url: '/permission/add',
method: 'post',
data,
})
}
export function permissiondel(id: string | number) {
return request({
url: `/permission/del/${id}`,
method: 'delete',
})
}
export function permissionupd(data: any) {
return request({
url: '/permission/update',
method: 'put',
data,
})
}
export function permissionlist(data: any) {
return request({
url: '/permission/listPage',
method: 'get',
params: data,
})
}
export function permissionbyid(id: string | number) {
return request({
url: `/permission/getById/${id}`,
method: 'get',
})
}
export function permissionselect(data?: any) {
return request({
url: '/permission/list',
method: 'get',
params: data,
})
}

View File

@@ -0,0 +1,38 @@
import request from '@/utils/request'
// 给角色添加权限
export function rolePermissionAdd(data: any) {
return request({
url: '/rolePermission/add',
method: 'post',
data,
})
}
// 清空某个角色的所有权限
export function rolePermissionClearByRoleId(roleId: string | number) {
return request({
url: `/rolePermission/clearByRoleId/${roleId}`,
method: 'delete',
})
}
// 删除单条角色权限关联
export function rolePermissionDel(roleId: string | number, permissionId: string | number) {
return request({
url: `/rolePermission/del`,
method: 'delete',
params: {
roleId,
permissionId
}
})
}
// 根据角色ID查询已绑定的权限ID列表
export function rolePermissionListByRoleId(roleId: string | number) {
return request({
url: `/rolePermission/listByRoleId/${roleId}`,
method: 'get',
})
}

View File

@@ -44,4 +44,26 @@ export function userbyid(id: string | number) {
url: `/user/getById/${id}`,
method: 'get',
})
}
export function userlogin(username: string, password: string) {
return request({
url: '/user/login',
method: 'post',
params: {
username,
password,
},
})
}
export function userregister(username: string, password: string) {
return request({
url: '/user/register',
method: 'post',
params: {
username,
password,
},
})
}

View File

@@ -44,4 +44,28 @@ export function devparamselect(data: any) {
method: 'get',
params: data,
})
}
// 为参数增加设备的devId
export function devparamaddDevId(devId: number, paramId: number) {
return request({
url: '/devParam/addDevId',
method: 'post',
params: {
devId,
paramId,
},
})
}
// 为参数减少设备的devId
export function devparamdelDevId(devId: number, paramId: number) {
return request({
url: '/devParam/delDevId',
method: 'post',
params: {
devId,
paramId,
},
})
}

View File

@@ -0,0 +1,39 @@
import request from '@/utils/request'
export function flowInfoadd(data: any) {
return request({
url: '/flowInfo/add',
method: 'post',
data,
})
}
export function flowInfodel(id: string | number) {
return request({
url: `/flowInfo/del/${id}`,
method: 'delete',
})
}
export function flowInfoupd(data: any) {
return request({
url: '/flowInfo/update',
method: 'put',
data,
})
}
export function flowInfolist(data: any) {
return request({
url: '/flowInfo/listPage',
method: 'get',
params: data,
})
}
export function flowInfobyid(id: string | number) {
return request({
url: `/flowInfo/getById/${id}`,
method: 'get',
})
}

View File

@@ -0,0 +1,47 @@
import request from '@/utils/request'
export function goodsFlowStatusadd(data: any) {
return request({
url: '/goodsFlowStatus/add',
method: 'post',
data,
})
}
export function goodsFlowStatusdel(id: string | number) {
return request({
url: `/goodsFlowStatus/del/${id}`,
method: 'delete',
})
}
export function goodsFlowStatusupd(data: any) {
return request({
url: '/goodsFlowStatus/update',
method: 'put',
data,
})
}
export function goodsFlowStatuslist(data: any) {
return request({
url: '/goodsFlowStatus/listPage',
method: 'get',
params: data,
})
}
export function goodsFlowStatusselect(data: any) {
return request({
url: '/goodsFlowStatus/list',
method: 'get',
params: data,
})
}
export function goodsFlowStatusbyid(id: string | number) {
return request({
url: `/goodsFlowStatus/getById/${id}`,
method: 'get',
})
}

View File

@@ -0,0 +1,39 @@
import request from '@/utils/request'
export function goodsInfoadd(data: any) {
return request({
url: '/goodsInfo/add',
method: 'post',
data,
})
}
export function goodsInfodel(id: string | number) {
return request({
url: `/goodsInfo/del/${id}`,
method: 'delete',
})
}
export function goodsInfoupd(data: any) {
return request({
url: '/goodsInfo/update',
method: 'put',
data,
})
}
export function goodsInfolist(data: any) {
return request({
url: '/goodsInfo/listPage',
method: 'get',
params: data,
})
}
export function goodsInfobyid(id: string | number) {
return request({
url: `/goodsInfo/getById/${id}`,
method: 'get',
})
}

View File

@@ -0,0 +1,121 @@
import request from '@/utils/request'
// 连接PLC设备
export function plcConnect(data: any) {
return request({
url: '/plc/connect',
method: 'post',
params: data
})
}
// 调用PLC设备执行动作
export function plcDoAction(data: any) {
return request({
url: '/plc/doAction',
method: 'post',
params: data
})
}
// 获取全部PLC连接对象
export function plcGetAll() {
return request({
url: '/plc/getAll',
method: 'get'
})
}
// PLC设备获取模式值
export function plcGetModel(ipAddr: string) {
return request({
url: '/plc/getModel',
method: 'get',
params: { ipAddr }
})
}
// 根据IP地址查询PLC连接对象
export function plcGetPlcByIp(ipAddr: string) {
return request({
url: '/plc/getPlcByIp',
method: 'get',
params: { ipAddr }
})
}
// 暂停运行PLC
export function plcPause(ipAddr: string) {
return request({
url: '/plc/pause',
method: 'post',
params: { ipAddr }
})
}
// PLC设备获取状态值
export function plcPlcStatus(ipAddr: string) {
return request({
url: '/plc/plcStatus',
method: 'get',
params: { ipAddr }
})
}
// 获取主 PLC 状态对象Swagger: GET /plc/mastertPlcStatus
export function masterPlcStatus() {
return request({
url: '/plc/mastertPlcStatus',
method: 'get',
})
}
// 别名:更贴近接口语义
export function plcMasterPlcStatus() {
return masterPlcStatus()
}
// PLC设备复位故障
export function plcResetError(ipAddr: string) {
return request({
url: '/plc/resetError',
method: 'post',
params: { ipAddr }
})
}
// 运行PLC
export function plcRun(ipAddr: string) {
return request({
url: '/plc/run',
method: 'post',
params: { ipAddr }
})
}
// 为样品执行国标
export function plcRunFlow(data: any) {
return request({
url: '/plc/runFlow',
method: 'post',
params: data
})
}
// PLC设备设置模式
export function plcSetModel(data: any) {
return request({
url: '/plc/setModel',
method: 'post',
params: data
})
}
// 停止PLC
export function plcStop(ipAddr: string) {
return request({
url: '/plc/stop',
method: 'post',
params: { ipAddr }
})
}

View File

@@ -0,0 +1,63 @@
import request from '@/utils/request'
export function stepInfoadd(data: any) {
return request({
url: '/stepInfo/add',
method: 'post',
data,
})
}
export function stepInfoaddlist(data: any) {
return request({
url: '/stepInfo/batchAdd',
method: 'post',
data,
})
}
export function stepInfodel(id: string | number) {
return request({
url: `/stepInfo/del/${id}`,
method: 'delete',
})
}
export function stepInfodellist(data: any[]) {
return request({
url: '/stepInfo/batchDel',
method: 'delete',
data
})
}
export function stepInfoupd(data: any) {
return request({
url: '/stepInfo/update',
method: 'put',
data,
})
}
export function stepInfoupdlist(data: any) {
return request({
url: '/stepInfo/batchUpdate',
method: 'post',
data,
})
}
export function stepInfolist(data: any) {
return request({
url: '/stepInfo/listPage',
method: 'get',
params: data,
})
}
export function stepInfobyid(id: string | number) {
return request({
url: `/stepInfo/getById/${id}`,
method: 'get',
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@@ -1,12 +1,30 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue'),
meta: { requiresAuth: false },
},
{
path: '/',
component: () => import('../views/Layout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: '/home',
},
{
path: '/home',
name: 'home',
component: () => import('../views/Home.vue'),
},
{
path: '/user',
name: 'user',
@@ -22,6 +40,11 @@ const router = createRouter({
name: 'department',
component: () => import('../views/department/index.vue'),
},
{
path: '/permission',
name: 'permission',
component: () => import('../views/permission/index.vue'),
},
{
path: '/position',
name: 'position',
@@ -42,6 +65,11 @@ const router = createRouter({
name: 'island-info',
component: () => import('../views/islandInfo/index.vue'),
},
{
path: '/devparam',
name: 'devparam',
component: () => import('../views/devparam/index.vue'),
},
{
path: '/devinfo',
name: 'devinfo',
@@ -52,20 +80,69 @@ const router = createRouter({
name: 'plc-devinfo',
component: () => import('../views/devinfo/plc.vue'),
},
{
path: '/goods-info',
name: 'goods-info',
component: () => import('../views/goods/index.vue'),
},
{
path: '/flow-info',
name: 'flow-info',
component: () => import('../views/flowinfo/index.vue'),
},
{
path: '/step-info',
name: 'step-info',
component: () => import('../views/stepinfo/index.vue'),
},
{
path: '/plc-device-control',
name: 'plc-device-control',
component: () => import('../views/plcdevicecontrol/index.vue'),
},
{
path: '/sample-injection',
name: 'sample-injection',
component: () => import('../views/sampleinjection/index.vue'),
},
],
},
],
})
// 添加全局路由守卫
// router.beforeEach((to, from, next) => {
// const token = localStorage.getItem('token')
router.beforeEach((to, _from, next) => {
const authStore = useAuthStore()
const token = authStore.getToken()
// if (to.path !== '/login' && !token) {
// next('/login')
// } else {
// next()
// }
// })
// 如果访问登录页
if (to.path === '/login') {
// 已登录状态下访问登录页,自动跳转到首页
if (token) {
next('/home')
ElMessage.success('已登录,自动跳转')
} else {
// 未登录,正常进入登录页
next()
}
return
}
// 访问需要认证的页面但未登录无token
if (to.meta.requiresAuth && !token) {
ElMessage.warning('请先登录')
next('/login')
return
}
// 如果访问根路径,重定向到首页
if (to.path === '/') {
next('/home')
return
}
// 已登录且访问合法页面,正常放行
next()
})
export default router

View File

@@ -1,38 +0,0 @@
// import router from './index' // 导入你的路由实例与index.ts对应
// import { ElMessage } from 'element-plus'
// import { useAuthStore } from '@/stores/auth' // 你的Pinia auth store路径
// // 路由前置守卫:每次路由跳转前执行
// router.beforeEach((to, from, next) => {
// const authStore = useAuthStore() // 获取auth store实例
// const token = authStore.getToken() // 调用你的getToken()方法获取token
// // 1. 如果访问登录页
// if (to.path === '/login') {
// // 已登录状态下访问登录页,自动跳转到首页/dashboard
// if (token) {
// next('/dashboard')
// ElMessage.success('已登录,自动跳转')
// } else {
// // 未登录,正常进入登录页
// next()
// }
// return
// }
// // 2. 访问非登录页但未登录无token
// if (!token) {
// ElMessage.warning('请先登录')
// next('/login') // 强制跳转到登录页
// return
// }
// // 3. 访问根路径自动跳转到dashboard
// if (to.path === '/') {
// next('/dashboard')
// return
// }
// // 4. 已登录且访问合法页面,正常放行
// next()
// })

View File

@@ -30,6 +30,38 @@ const TokenManager = {
// 删除默认请求头
delete request.defaults.headers.common['Authorization']
},
// 初始化token应用启动时调用
initToken() {
const token = this.getToken()
if (token) {
request.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
},
}
// 应用启动时初始化token
TokenManager.initToken()
// 登录过期提示防重复标志
let isLoginExpiredShown = false
// 处理登录过期(防重复提示)
const handleLoginExpired = () => {
if (!isLoginExpiredShown) {
isLoginExpiredShown = true
// 清除token
TokenManager.removeToken()
// 显示提示消息
ElMessage.error('登录已过期,请重新登录')
// 跳转到登录页
router.replace('/login').finally(() => {
// 延迟重置标志位,确保跳转完成后再允许下次提示
setTimeout(() => {
isLoginExpiredShown = false
}, 1000)
})
}
}
// 请求拦截器
@@ -61,28 +93,65 @@ request.interceptors.response.use(
// 处理业务错误码
if (data.code === 302) {
// 清除token
TokenManager.removeToken()
// 跳转到登录页
router.replace('/login')
// 处理登录过期(防重复提示)
handleLoginExpired()
return Promise.reject(data)
}
// 其他错误码
if (data.code !== 0 && data.code !== undefined) {
ElMessage.error(data.message || data.msg || '请求失败')
// 检查是否是状态刷新相关的请求,如果是则静默处理某些错误
const url = response.config?.url || ''
const isStatusRefreshRequest = url.includes('/plc/plcStatus') || url.includes('/plc/getModel')
const errorMsg = data.message || data.msg || ''
// 如果是状态刷新请求,且错误消息包含特定关键词,则静默处理
if (isStatusRefreshRequest && (
errorMsg.includes('停止失败') ||
errorMsg.includes('未连接') ||
errorMsg.includes('连接失败') ||
errorMsg.includes('获取状态失败') ||
errorMsg.includes('获取模式失败')
)) {
// 静默处理,不弹出错误提示
console.debug('状态刷新请求错误(已静默处理):', errorMsg)
return Promise.reject(data)
}
ElMessage.error(errorMsg || '请求失败')
return Promise.reject(data)
}
return data
},
(error) => {
// 处理401未授权错误
if (error.response?.status === 401) {
// 处理登录过期(防重复提示)
handleLoginExpired()
return Promise.reject(error)
}
// 检查是否是状态刷新相关的请求
const url = error.config?.url || ''
const isStatusRefreshRequest = url.includes('/plc/plcStatus') || url.includes('/plc/getModel')
const errorMsg = error.response?.data?.message || error.response?.data?.msg || '请求失败'
// 如果是状态刷新请求,且错误消息包含特定关键词,则静默处理
if (isStatusRefreshRequest && (
errorMsg.includes('停止失败') ||
errorMsg.includes('未连接') ||
errorMsg.includes('连接失败') ||
errorMsg.includes('获取状态失败') ||
errorMsg.includes('获取模式失败')
)) {
// 静默处理,不弹出错误提示
console.debug('状态刷新请求错误(已静默处理):', errorMsg)
return Promise.reject(error)
}
// 网络错误或其他错误
ElMessage.error(
error.response?.data?.message ||
error.response?.data?.msg ||
'请求失败'
)
ElMessage.error(errorMsg)
return Promise.reject(error)
}
)

View File

@@ -0,0 +1,56 @@
<template>
<div class="home-container">
<div class="welcome-content">
<h1 class="welcome-title">欢迎使用中核404内照射上位机中控系统</h1>
</div>
</div>
</template>
<script setup lang="ts">
// 首页组件,未来将展示数字孪生监控大屏
</script>
<style scoped>
.home-container {
width: 100%;
height: calc(92vh - 25px);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.welcome-content {
text-align: center;
color: #080101;
padding: 40px;
max-width: 100%;
box-sizing: border-box;
}
.welcome-title {
font-size: 48px;
font-weight: 600;
margin: 0;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
letter-spacing: 2px;
line-height: 1.4;
}
.welcome-subtitle {
font-size: 24px;
margin: 0;
opacity: 0.9;
font-weight: 300;
letter-spacing: 1px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.welcome-title {
font-size: 32px;
letter-spacing: 1px;
}
}
</style>

View File

@@ -3,7 +3,7 @@
<!-- 顶部导航栏 -->
<el-header class="layout-header">
<div class="header-left">
<h1 class="project-title">北京融创智能仪器管理系统</h1>
<h1 class="project-title">中核404内照射上位机中控系统</h1>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
@@ -31,52 +31,74 @@
router
:collapse="false"
>
<el-menu-item index="/home">
<el-icon><component :is="getMenuIcon('首页')" /></el-icon>
<span>首页</span>
</el-menu-item>
<el-sub-menu index="system">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统管理</span>
<el-icon><component :is="getMenuIcon('系统管理')" /></el-icon>
<span>系统信息管理</span>
</template>
<el-menu-item index="/user">
<el-icon><User /></el-icon>
<el-icon><component :is="getMenuIcon('用户管理')" /></el-icon>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/role">
<el-icon><Avatar /></el-icon>
<el-icon><component :is="getMenuIcon('角色管理')" /></el-icon>
<span>角色管理</span>
</el-menu-item>
<el-menu-item index="/permission">
<el-icon><component :is="getMenuIcon('权限管理')" /></el-icon>
<span>权限管理</span>
</el-menu-item>
<el-menu-item index="/department">
<el-icon><OfficeBuilding /></el-icon>
<el-icon><component :is="getMenuIcon('部门管理')" /></el-icon>
<span>部门管理</span>
</el-menu-item>
<el-menu-item index="/position">
<el-icon><Briefcase /></el-icon>
<span>职位管理</span>
</el-menu-item>
<el-menu-item index="/manage-log">
<el-icon><Document /></el-icon>
<el-icon><component :is="getMenuIcon('操作日志管理')" /></el-icon>
<span>操作日志管理</span>
</el-menu-item>
<el-menu-item index="/user-role">
<el-icon><UserFilled /></el-icon>
<span>用户角色管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="island">
<template #title>
<el-icon><Grid /></el-icon>
<span>业务管理</span>
<el-icon><component :is="getMenuIcon('业务管理')" /></el-icon>
<span>基础数据管理</span>
</template>
<el-menu-item index="/island-info">
<el-icon><Setting /></el-icon>
<span>功能岛管理</span>
<el-menu-item index="/plc-devinfo">
<el-icon><component :is="getMenuIcon('PLC设备管理')" /></el-icon>
<span>PLC设备管理</span>
</el-menu-item>
<el-menu-item index="/devparam">
<el-icon><component :is="getMenuIcon('动作参数管理')" /></el-icon>
<span>动作参数管理</span>
</el-menu-item>
<el-menu-item index="/devinfo">
<el-icon><Monitor /></el-icon>
<span>设备管理</span>
<el-icon><component :is="getMenuIcon('动作管理')" /></el-icon>
<span>动作管理</span>
</el-menu-item>
<el-menu-item index="/plc-devinfo">
<el-icon><Connection /></el-icon>
<span>PLC设备管理</span>
<el-menu-item index="/island-info">
<el-icon><component :is="getMenuIcon('功能岛管理')" /></el-icon>
<span>功能岛管理</span>
</el-menu-item>
<el-menu-item index="/step-info">
<el-icon><component :is="getMenuIcon('流程创建')" /></el-icon>
<span>SOP管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="flow">
<template #title>
<el-icon><component :is="getMenuIcon('流程管理')" /></el-icon>
<span>业务流程控制</span>
</template>
<el-menu-item index="/sample-injection">
<el-icon><component :is="getMenuIcon('进样控制')" /></el-icon>
<span>进样控制</span>
</el-menu-item>
<el-menu-item index="/goods-info">
<el-icon><component :is="getMenuIcon('样品管理')" /></el-icon>
<span>样品记录</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
@@ -94,7 +116,27 @@
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { User, Setting, Avatar, OfficeBuilding, Briefcase, Document, CaretBottom, UserFilled, Grid, Monitor, Connection, Plus } from '@element-plus/icons-vue'
import {
User,
Setting,
Avatar,
OfficeBuilding,
Briefcase,
Document,
CaretBottom,
UserFilled,
Key,
Grid,
Monitor,
Connection,
List,
EditPen,
Files,
Box,
Operation,
Tools,
House,
} from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
@@ -111,6 +153,40 @@ const activeMenu = computed(() => {
return route.path
})
// 根据菜单名称获取图标
const getMenuIcon = (menuName: string) => {
const iconMap: Record<string, any> = {
// 一级
首页: House,
系统管理: Setting,
业务管理: Grid,
流程管理: List,
// 系统管理
用户管理: User,
角色管理: Avatar,
部门管理: OfficeBuilding,
权限管理: Key,
职位管理: Briefcase,
操作日志管理: Document,
用户角色管理: UserFilled,
// 业务管理
PLC设备管理: Connection,
动作参数管理: Tools,
动作管理: Monitor,
功能岛管理: Grid,
// 流程管理
PLC设备控制: Operation,
样品管理: Box,
'标准流程管理': Files, // 文件图标,适合标准流程管理
'流程创建': EditPen,
进样控制: Operation,
}
return iconMap[menuName] || EditPen
}
// 处理下拉菜单命令
const handleCommand = (command: string) => {
if (command === 'logout') {
@@ -122,8 +198,8 @@ const handleCommand = (command: string) => {
authStore.removeToken()
localStorage.removeItem('username')
ElMessage.success('退出成功')
// 跳转到登录页(如果有登录页)
// router.push('/login')
// 跳转到登录页
router.push('/login')
})
.catch(() => {})
}
@@ -132,7 +208,7 @@ const handleCommand = (command: string) => {
<style scoped>
.layout-container {
height: 100vh;
height: 98vh;
display: flex;
flex-direction: column;
}
@@ -207,7 +283,9 @@ const handleCommand = (command: string) => {
.layout-main {
padding: 20px;
background-color: #f5f5f5;
overflow-y: auto;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,317 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h2>中核404内照射上位机中控系统</h2>
<p>用户登录</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
label-width="0"
@keyup.enter="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名2-16位的字母、数字、下划线或减号"
size="large"
:prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
:prefix-icon="Lock"
show-password
clearable
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<div class="button-row">
<el-button
type="primary"
size="large"
class="login-button"
:loading="loginLoading"
@click="handleLogin"
>
登录
</el-button>
<el-button
size="large"
class="register-button"
@click="showRegisterDialog = true"
>
注册
</el-button>
</div>
</el-form-item>
</el-form>
</div>
<!-- 注册弹窗 -->
<el-dialog
v-model="showRegisterDialog"
title="用户注册"
width="500px"
:close-on-click-modal="false"
>
<el-form
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
label-width="100px"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="registerForm.username"
placeholder="2-16位的字母、数字、下划线或减号"
clearable
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="请输入密码"
show-password
clearable
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="请再次输入密码"
show-password
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showRegisterDialog = false">取消</el-button>
<el-button
type="primary"
:loading="registerLoading"
@click="handleRegister"
>
注册
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
import { userlogin, userregister } from '@/api/system/user'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
// 登录表单
const loginFormRef = ref<FormInstance>()
const loginForm = reactive({
username: '',
password: '',
})
const loginLoading = ref(false)
// 注册表单
const registerFormRef = ref<FormInstance>()
const registerForm = reactive({
username: '',
password: '',
confirmPassword: '',
})
const registerLoading = ref(false)
const showRegisterDialog = ref(false)
// 登录表单验证规则
const loginRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_-]{2,16}$/,
message: '用户名必须是2-16位的字母、数字、下划线或减号',
trigger: 'blur',
},
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
],
}
// 注册表单验证规则
const validateConfirmPassword = (rule: any, value: string, callback: Function) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const registerRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_-]{2,16}$/,
message: '用户名必须是2-16位的字母、数字、下划线或减号',
trigger: 'blur',
},
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' },
],
}
// 处理登录
const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loginLoading.value = true
try {
const response: any = await userlogin(loginForm.username, loginForm.password)
// 根据图片返回的data字段直接是token字符串
if (response.code === 0 && response.data) {
const token = response.data
// 存储token
authStore.setToken(token)
// 存储用户名
localStorage.setItem('username', loginForm.username)
ElMessage.success('登录成功')
// 跳转到首页
router.push('/home')
}
} catch (error: any) {
// 错误提示已在全局响应拦截中处理,这里不重复弹出
console.warn('login error:', error)
} finally {
loginLoading.value = false
}
}
})
}
// 处理注册
const handleRegister = async () => {
if (!registerFormRef.value) return
await registerFormRef.value.validate(async (valid) => {
if (valid) {
registerLoading.value = true
try {
const response: any = await userregister(registerForm.username, registerForm.password)
if (response.code === 0) {
ElMessage.success('注册成功,请登录')
// 关闭注册弹窗
showRegisterDialog.value = false
// 清空注册表单
if (registerFormRef.value) {
registerFormRef.value.resetFields()
}
// 自动填充登录表单的用户名
loginForm.username = registerForm.username
} else {
ElMessage.error(response.message || '注册失败')
}
} catch (error: any) {
ElMessage.error(error.message || '注册失败')
} finally {
registerLoading.value = false
}
}
})
}
</script>
<style scoped>
.login-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box {
width: 450px;
padding: 40px;
background: #fff;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
margin: 0 0 10px 0;
font-size: 24px;
color: #333;
font-weight: 500;
}
.login-header p {
margin: 0;
font-size: 14px;
color: #999;
}
.login-form {
margin-top: 30px;
}
.login-form :deep(.el-form-item) {
margin-bottom: 20px;
}
.button-row {
width: 100%;
display: flex;
gap: 12px;
}
.login-button,
.register-button {
flex: 1 1 0;
width: auto;
display: block;
}
.login-button {
margin: 0;
}
</style>

View File

@@ -1,5 +1,10 @@
<template>
<div class="dept-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统信息管理</el-breadcrumb-item>
<el-breadcrumb-item>部门管理</el-breadcrumb-item>
</el-breadcrumb>
<div class="dept-container">
<!-- 左侧树形控件 -->
<el-card class="tree-card" shadow="never">

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
<template>
<div class="plc-devinfo-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>基础数据管理</el-breadcrumb-item>
<el-breadcrumb-item>PLC设备管理</el-breadcrumb-item>
</el-breadcrumb>
<!-- 搜索栏 -->
<el-card class="search-card" shadow="never">
<div class="search-bar">
@@ -12,18 +17,6 @@
style="width: 200px"
/>
</el-form-item>
<el-form-item label="设备状态">
<el-select
v-model="queryForm.status"
placeholder="请选择设备状态"
clearable
style="width: 200px"
>
<el-option label="空闲" :value="0" />
<el-option label="运行" :value="1" />
<el-option label="故障" :value="4" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
@@ -73,18 +66,11 @@
{{ row.protocolType || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="company" label="公司名" min-width="120" show-overflow-tooltip>
<el-table-column prop="company" label="设备类型" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.company || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="devDesc" label="描述" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.devDesc || '暂无' }}
@@ -156,7 +142,7 @@
clearable
/>
</el-form-item>
<el-form-item label="设备型号">
<el-form-item label="设备型号" prop="devModel">
<el-input
v-model="formData.devModel"
disabled
@@ -186,12 +172,11 @@
clearable
/>
</el-form-item>
<el-form-item label="公司名" prop="company">
<el-input
v-model="formData.company"
placeholder="请输入公司名"
clearable
/>
<el-form-item label="设备类型" prop="company">
<el-radio-group v-model="formData.company">
<el-radio label="主站">主站</el-radio>
<el-radio label="从站">从站</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="描述" prop="devDesc">
<el-input
@@ -247,7 +232,6 @@ const total = ref(0)
// 查询表单
const queryForm = reactive({
devName: '',
status: undefined as number | undefined,
})
// 分页
@@ -268,19 +252,108 @@ const formData = reactive({
devName: '',
devModel: 'PLC',
ipAddr: '',
port: undefined as number | undefined,
protocolType: '',
company: '',
port: 502 as number | undefined,
protocolType: 'Modelbus',
company: '从站',
devDesc: '',
remark: '',
status: 0, // 状态自动设置为0
status: 0, // 状态默认为0
runModel: 0, // 运行模式默认为0
})
// IP地址校验函数
const validateIpAddr = (_rule: any, value: any, callback: any) => {
if (!value || String(value).trim() === '') {
callback(new Error('请输入IP地址'))
return
}
const ipStr = String(value).trim().toLowerCase()
// 禁止本机IP格式
if (ipStr === 'localhost' || ipStr === '127.0.0.1') {
callback(new Error('不允许使用本机IP地址localhost或127.0.0.1'))
return
}
// IPv4地址格式校验xxx.xxx.xxx.xxx每段0-255
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
const match = ipStr.match(ipv4Regex)
if (!match) {
callback(new Error('请输入有效的IP地址格式192.168.1.1'))
return
}
// 检查每段数字是否在0-255范围内
for (let i = 1; i <= 4; i++) {
const segment = match[i]
if (!segment) {
callback(new Error('IP地址格式错误'))
return
}
const num = parseInt(segment, 10)
if (num < 0 || num > 255) {
callback(new Error('IP地址每段数字应在0-255范围内'))
return
}
}
// 再次检查是否为127.0.0.1防止用户输入其他格式的127.0.0.1
if (match[1] === '127' && match[2] === '0' && match[3] === '0' && match[4] === '1') {
callback(new Error('不允许使用本机IP地址127.0.0.1'))
return
}
callback()
}
// 表单验证规则
const formRules = {
devName: [
{ required: true, message: '请输入设备名称', trigger: 'blur' },
],
devModel: [
{ required: true, message: '设备型号不能为空', trigger: 'blur' },
],
ipAddr: [
{ required: true, validator: validateIpAddr, trigger: ['blur', 'change'] },
],
port: [
{ required: true, message: '请输入端口', trigger: 'blur' },
{ type: 'number', min: 1, max: 65535, message: '端口范围应在1-65535之间', trigger: 'blur' },
],
protocolType: [
{ required: true, message: '请输入协议类型', trigger: 'blur' },
],
company: [
{
validator: (_rule: any, value: any, callback: any) => {
if (!value || String(value).trim() === '') {
callback(new Error('请输入设备类型'))
return
}
const normalized = String(value).trim()
if (normalized !== '主站' && normalized !== '从站') {
callback(new Error('设备类型只能为主站或从站'))
return
}
if (normalized === '主站') {
const conflict = deviceList.value.find((item: any) => {
const isMaster = item?.company === '主站'
const isSame = isEdit.value && formData.id && String(item.id) === String(formData.id)
return isMaster && !isSame
})
if (conflict) {
callback(new Error('主站PLC只能有一个请先修改或删除已有主站设备'))
return
}
}
callback()
},
trigger: ['blur', 'change'],
},
],
}
// 获取序号
@@ -288,21 +361,6 @@ const getIndex = (index: number) => {
return (pagination.pageNum - 1) * pagination.pageSize + index + 1
}
// 获取状态类型
const getStatusType = (status: number | null | undefined) => {
if (status === 0) return 'success' // 空闲 - 绿色
if (status === 1) return 'warning' // 运行 - 黄色
if (status === 4) return 'danger' // 故障 - 红色
return 'info'
}
// 获取状态文本
const getStatusText = (status: number | null | undefined) => {
if (status === 0) return '空闲'
if (status === 1) return '运行'
if (status === 4) return '故障'
return '未知'
}
// 获取设备列表只查询devModel为PLC的设备
const getDeviceList = async () => {
@@ -318,10 +376,6 @@ const getDeviceList = async () => {
if (queryForm.devName) {
params.devName = queryForm.devName
}
// 设备状态精确查询
if (queryForm.status !== undefined && queryForm.status !== null) {
params.status = queryForm.status
}
const res: any = await devInfolist(params)
@@ -366,7 +420,6 @@ const handleSearch = () => {
// 重置查询
const resetSearch = () => {
queryForm.devName = ''
queryForm.status = undefined
handleSearch()
}
@@ -396,12 +449,13 @@ const resetForm = () => {
formData.devName = ''
formData.devModel = 'PLC'
formData.ipAddr = ''
formData.port = undefined
formData.protocolType = ''
formData.company = ''
formData.port = 502
formData.protocolType = 'Modelbus'
formData.company = '从站'
formData.devDesc = ''
formData.remark = ''
formData.status = 0
formData.status = 0 // 状态默认为0
formData.runModel = 0 // 运行模式默认为0
formRef.value?.clearValidate()
}
@@ -417,12 +471,13 @@ const handleEdit = async (item: any) => {
formData.devName = data.devName ?? ''
formData.devModel = data.devModel ?? 'PLC'
formData.ipAddr = data.ipAddr ?? ''
formData.port = data.port ?? undefined
formData.protocolType = data.protocolType ?? ''
formData.company = data.company ?? ''
formData.port = data.port ?? 502
formData.protocolType = data.protocolType ?? 'Modelbus'
formData.company = data.company ?? '从站'
formData.devDesc = data.devDesc ?? ''
formData.remark = data.remark ?? ''
formData.status = data.status ?? 0
formData.runModel = data.runModel ?? 0
isEdit.value = true
drawerTitle.value = '编辑PLC设备'
@@ -475,25 +530,36 @@ const handleSubmit = async () => {
await formRef.value.validate()
submitting.value = true
// 构建提交数据,只包含非空字段(根据后端要求:非空则入库/更新)
// 构建提交数据
const submitData: any = {
devName: formData.devName,
devModel: 'PLC', // 设备型号固定为PLC
status: 0, // 状态自动设置为0
ipAddr: formData.ipAddr, // 必填项
port: formData.port, // 必填项
protocolType: formData.protocolType, // 必填项
status: isEdit.value ? formData.status : 0, // 新增时状态为0编辑时使用原值
runModel: isEdit.value ? formData.runModel : 0, // 新增时运行模式为0编辑时使用原值
}
// 添加其他字段(如果非空)
if (formData.ipAddr) {
submitData.ipAddr = formData.ipAddr
}
if (formData.port !== undefined && formData.port !== null) {
submitData.port = formData.port
}
if (formData.protocolType) {
submitData.protocolType = formData.protocolType
}
// 添加其他可选字段(如果非空)
if (formData.company) {
submitData.company = formData.company
const normalized = String(formData.company).trim()
if (normalized !== '主站' && normalized !== '从站') {
ElMessage.warning('设备类型只能为主站或从站')
return
}
if (normalized === '主站') {
const conflict = deviceList.value.find((item: any) => {
const isMaster = item?.company === '主站'
const isSame = isEdit.value && formData.id && String(item.id) === String(formData.id)
return isMaster && !isSame
})
if (conflict) {
ElMessage.warning('主站PLC只能有一个请先修改或删除已有主站设备')
return
}
}
submitData.company = normalized
}
if (formData.devDesc) {
submitData.devDesc = formData.devDesc

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
<template>
<div class="island-info-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>基础数据管理</el-breadcrumb-item>
<el-breadcrumb-item>功能岛管理</el-breadcrumb-item>
</el-breadcrumb>
<!-- 搜索栏 -->
<el-card class="search-card" shadow="never">
<div class="search-bar">
@@ -37,6 +42,7 @@
v-for="(item, index) in islandList"
:key="item.id"
class="island-card"
:style="{ animationDelay: `${index * 50}ms` }"
>
<!-- 左侧步骤编号和过滤器图标 -->
<div class="card-left">
@@ -87,8 +93,8 @@
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[9]"
layout="total, prev, pager, next, jumper"
:total="total"
background
@current-change="handleCurrentChange"
@@ -109,7 +115,7 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
label-width="120px"
label-position="left"
>
<el-form-item label="功能岛名称" prop="islandName">
@@ -127,6 +133,16 @@
clearable
/>
</el-form-item>
<el-form-item label="PLC映射地址" prop="plcAddr">
<el-input-number
v-model="formData.plcAddr"
placeholder="请输入PLC映射地址"
:min="0"
:step="1"
:precision="0"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="功能岛Logo">
<div class="logo-preview">
<el-icon class="preview-icon" :size="60">
@@ -144,41 +160,39 @@
clearable
/>
</el-form-item>
<el-form-item label="绑定设备">
<div class="bind-device-wrapper">
<el-button type="primary" @click="handleBindDevice">
<el-icon><Plus /></el-icon>
绑定设备
</el-button>
<div v-if="boundDevices.length > 0" class="bound-devices-table-wrapper">
<el-table
:data="boundDevices"
border
stripe
size="small"
style="width: 100%; margin-top: 12px"
>
<el-table-column type="index" label="序号" width="60" :index="getBoundDeviceIndex" />
<el-table-column prop="devName" label="设备名称" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.devName || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="devDesc" label="描述" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.devDesc || '暂无' }}
</template>
</el-table-column>
</el-table>
<el-form-item v-if="isEdit" label="动作信息">
<div class="device-list-wrapper">
<el-table
v-if="deviceList.length > 0"
v-loading="deviceLoading"
:data="deviceList"
border
stripe
size="small"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" :index="getDeviceIndex" />
<el-table-column prop="devName" label="动作名称" width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ row.devName || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="plcAddr" label="PLC映射地址" width="140" show-overflow-tooltip>
<template #default="{ row }">
{{ row.plcAddr ?? '暂无' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div v-if="deviceList.length === 0 && !deviceLoading" class="no-devices">
该功能岛暂无动作
</div>
<div v-else class="no-bound-devices">暂无绑定设备</div>
</div>
</el-form-item>
</el-form>
@@ -192,66 +206,6 @@
</template>
</el-drawer>
<!-- 绑定设备对话框 -->
<el-dialog
v-model="bindDeviceDialogVisible"
:title="isEdit ? '绑定/解绑设备' : '绑定设备'"
width="800px"
:before-close="handleBindDialogClose"
>
<el-table
ref="deviceTableRef"
v-loading="deviceLoading"
:data="deviceListForBind"
border
stripe
style="width: 100%"
@selection-change="handleDeviceSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="序号" width="60" :index="getDeviceIndex" />
<el-table-column prop="devName" label="设备名称" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.devName || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="devModel" label="设备型号" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.devModel || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="ipAddr" label="IP地址" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.ipAddr || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="port" label="端口" width="80">
<template #default="{ row }">
{{ row.port !== null && row.port !== undefined ? row.port : '暂无' }}
</template>
</el-table-column>
<el-table-column prop="protocolType" label="协议" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ row.protocolType || '暂无' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleBindDialogClose">取消</el-button>
<el-button type="primary" @click="handleSaveDeviceBind" :loading="savingDeviceBind">
保存
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -281,6 +235,23 @@ import {
Monitor,
Cpu,
DataAnalysis,
Upload,
Download,
MagicStick,
TakeawayBox,
CollectionTag,
Position,
Dish,
Bowl,
HomeFilled,
Odometer,
Sunrise,
ScaleToOriginal,
// 下面这些图标在项目其它页面已使用过,确保依赖里一定存在
Document,
Files,
List,
EditPen,
} from '@element-plus/icons-vue'
import {
islandInfoadd,
@@ -289,13 +260,12 @@ import {
islandInfolist,
islandInfobyid,
} from '@/api/tb/islandinfo'
import { devselect, devInfoupd } from '@/api/tb/devinfo'
import { devselect } from '@/api/tb/devinfo'
// 加载状态
const loading = ref(false)
const submitting = ref(false)
const deviceLoading = ref(false)
const savingDeviceBind = ref(false)
// 功能岛列表
const islandList = ref<any[]>([])
@@ -309,7 +279,7 @@ const queryForm = reactive({
// 分页
const pagination = reactive({
pageNum: 1,
pageSize: 10,
pageSize: 9,
})
// 抽屉相关
@@ -323,15 +293,12 @@ const formData = reactive({
id: undefined,
islandName: '',
islandCode: '',
plcAddr: undefined as number | undefined,
islandDesc: '',
})
// 绑定设备相关
const bindDeviceDialogVisible = ref(false)
const deviceListForBind = ref<any[]>([])
const deviceTableRef = ref()
const selectedDevices = ref<any[]>([])
const boundDevices = ref<any[]>([])
// 动作列表相关
const deviceList = ref<any[]>([])
// 表单验证规则
const formRules = {
@@ -341,50 +308,59 @@ const formRules = {
islandCode: [
{ required: true, message: '请输入功能岛编码', trigger: 'blur' },
],
plcAddr: [
{ required: true, message: '请输入PLC映射地址', trigger: 'change' },
],
}
// 获取功能岛图标(根据名称自动生成)
const getIslandIcon = (name: string) => {
if (!name) return Box
// 根据名称关键词匹配图标
const nameLower = name.toLowerCase()
if (nameLower.includes('加液') || nameLower.includes('ph') || nameLower.includes('涡旋')) {
return Watermelon
} else if (nameLower.includes('水浴') || nameLower.includes('恒温')) {
return Sunny
} else if (nameLower.includes('震荡')) {
return Connection
} else if (nameLower.includes('超声')) {
return Histogram
} else if (nameLower.includes('离心')) {
return RefreshRight
} else if (nameLower.includes('移液')) {
return Aim
} else if (nameLower.includes('萃取')) {
return Goblet
} else if (nameLower.includes('氮吹')) {
return WindPower
} else if (nameLower.includes('过膜') || nameLower.includes('过滤')) {
return Filter
} else if (nameLower.includes('人工')) {
return User
} else if (nameLower.includes('系统') || nameLower.includes('管理')) {
return Setting
} else if (nameLower.includes('工具')) {
return Tools
} else if (nameLower.includes('数据') || nameLower.includes('分析')) {
return DataAnalysis
} else if (nameLower.includes('监控') || nameLower.includes('显示')) {
return Monitor
} else if (nameLower.includes('处理') || nameLower.includes('计算')) {
return Cpu
} else if (nameLower.includes('网格') || nameLower.includes('布局')) {
return Grid
}
// 默认图标
return Box
// 预设功能岛与唯一图标的映射(避免重复)
const iconRules = [
{ keywords: ['称重', '称重岛', '称量', '重量'], icon: ScaleToOriginal },
{ keywords: ['共沉淀', '共沉淀岛', '沉淀'], icon: CollectionTag },
{ keywords: ['过柱', '过柱岛', '柱'], icon: Histogram },
{ keywords: ['电沉积', '电沉积岛', '沉积'], icon: Cpu },
{ keywords: ['烧白', '烧白岛', '焚烧', '灰化'], icon: Sunrise },
{ keywords: ['定容', '定容岛', '补液', '补容'], icon: Goblet },
{ keywords: ['涡旋'], icon: MagicStick },
{ keywords: ['加液', '加样'], icon: Watermelon },
{ keywords: ['进样'], icon: Upload },
{ keywords: ['分液'], icon: TakeawayBox },
{ keywords: ['浓缩'], icon: DataAnalysis },
{ keywords: ['移上清'], icon: Position },
{ keywords: ['取液'], icon: Download },
{ keywords: ['金属浴', '金属'], icon: Bowl },
{ keywords: ['干燥', '硫酸钠'], icon: Files },
{ keywords: ['温室', '静置'], icon: HomeFilled },
{ keywords: ['ph', '酸碱'], icon: Odometer },
{ keywords: ['出样'], icon: List },
{ keywords: ['水浴', '恒温'], icon: Sunny },
{ keywords: ['震荡'], icon: Operation },
{ keywords: ['超声'], icon: Connection },
{ keywords: ['离心'], icon: RefreshRight },
{ keywords: ['移液'], icon: Aim },
{ keywords: ['萃取'], icon: Dish },
{ keywords: ['氮吹'], icon: WindPower },
{ keywords: ['过膜', '过滤'], icon: Filter },
{ keywords: ['人工'], icon: User },
{ keywords: ['系统', '管理'], icon: Setting },
{ keywords: ['工具'], icon: EditPen },
{ keywords: ['数据', '分析'], icon: Document },
{ keywords: ['监控', '显示'], icon: Monitor },
{ keywords: ['处理', '计算'], icon: Tools },
{ keywords: ['网格', '布局'], icon: Grid },
]
const matched = iconRules.find(rule =>
rule.keywords.some(keyword => nameLower.includes(keyword.toLowerCase()))
)
return matched ? matched.icon : Box
}
// 处理名称输入
@@ -408,16 +384,11 @@ const getStatusText = (status: number | null | undefined) => {
return '未知'
}
// 获取设备序号
// 获取动作序号
const getDeviceIndex = (index: number) => {
return index + 1
}
// 获取已绑定设备序号
const getBoundDeviceIndex = (index: number) => {
return index + 1
}
// 获取功能岛列表
const getIslandList = async () => {
try {
@@ -494,9 +465,9 @@ const resetForm = () => {
formData.id = undefined
formData.islandName = ''
formData.islandCode = ''
formData.plcAddr = undefined
formData.islandDesc = ''
boundDevices.value = []
selectedDevices.value = []
deviceList.value = []
formRef.value?.clearValidate()
}
@@ -519,12 +490,13 @@ const handleEdit = async (item: any) => {
formData.id = data.id ?? item.id
formData.islandName = data.islandName ?? data.name ?? ''
formData.islandCode = data.islandCode ?? data.code ?? ''
formData.plcAddr = data.plcAddr !== undefined && data.plcAddr !== null ? Number(data.plcAddr) : undefined
// 优先使用后端的 desc 字段,兼容其他可能的字段名
formData.islandDesc = data.desc ?? data.islandDesc ?? data.description ?? ''
// 加载已绑定的设备
// 加载该功能岛下的动作列表
if (formData.id) {
await loadBoundDevices(formData.id)
await loadDeviceList(formData.id)
}
isEdit.value = true
@@ -541,39 +513,51 @@ const handleEdit = async (item: any) => {
}
}
// 加载已绑定的设备
const loadBoundDevices = async (islandId: number | string) => {
// 加载该功能岛下的动作列表
const loadDeviceList = async (islandId: number | string) => {
try {
deviceLoading.value = true
const res: any = await devselect({})
const allDevices = res?.data ?? res ?? []
if (Array.isArray(allDevices)) {
boundDevices.value = allDevices.filter((device: any) =>
device.islandId && (device.islandId === islandId || device.islandId === String(islandId))
)
// 过滤出该功能岛下的动作并排除PLC动作
deviceList.value = allDevices.filter((device: any) => {
const devModel = device.devModel || ''
const isNotPlc = devModel.toUpperCase() !== 'PLC'
const isBelongToIsland = device.islandId && (device.islandId === islandId || device.islandId === String(islandId))
return isNotPlc && isBelongToIsland
})
} else {
deviceList.value = []
}
} catch (error) {
console.error('加载已绑定设备失败:', error)
boundDevices.value = []
console.error('加载动作列表失败:', error)
deviceList.value = []
} finally {
deviceLoading.value = false
}
}
// 删除功能岛
const handleDelete = async (item: any) => {
try {
// 检查是否绑定了设备
// 检查是否绑定了动作
const res: any = await devselect({})
const allDevices = res?.data ?? res ?? []
const boundDevicesList = Array.isArray(allDevices)
? allDevices.filter((device: any) =>
device.islandId && (device.islandId === item.id || device.islandId === String(item.id))
)
? allDevices.filter((device: any) => {
const devModel = device.devModel || ''
const isNotPlc = devModel.toUpperCase() !== 'PLC'
const isBelongToIsland = device.islandId && (device.islandId === item.id || device.islandId === String(item.id))
return isNotPlc && isBelongToIsland
})
: []
if (boundDevicesList.length > 0) {
const deviceNames = boundDevicesList.map((d: any) => d.devName || '未命名设备').join('、')
const deviceNames = boundDevicesList.map((d: any) => d.devName || '未命名动作').join('、')
const islandName = item.islandName || item.name || '未命名'
await ElMessageBox.alert(
`${islandName}已绑定${deviceNames},请先完成设备解绑`,
`${islandName}下还有${deviceNames}${boundDevicesList.length}个动作,请先完成动作解绑`,
'提示',
{
confirmButtonText: '确定',
@@ -623,11 +607,11 @@ const handleSubmit = async () => {
const submitData: any = {
islandName: formData.islandName,
islandCode: formData.islandCode,
plcAddr: formData.plcAddr,
desc: formData.islandDesc || '', // 描述字段映射为 desc
}
let res: any
let newIslandId: any = null
if (isEdit.value && formData.id) {
// 编辑 - 需要传入ID其他字段可选(非空则更新)
@@ -635,42 +619,12 @@ const handleSubmit = async () => {
id: formData.id,
...submitData,
})
newIslandId = formData.id
} else {
// 新增
res = await islandInfoadd(submitData)
// 获取新增后的ID
if (res.code === '0' || res.code === 0) {
newIslandId = res.data?.id || res.id
}
}
if (res.code === '0' || res.code === 0) {
// 如果是新增模式且有绑定的设备需要更新设备的islandId
if (!isEdit.value && newIslandId && boundDevices.value.length > 0) {
try {
const bindPromises = boundDevices.value.map((device: any) => {
return devInfoupd({
id: device.id,
devName: device.devName, // 必填字段
islandId: newIslandId,
})
})
const bindResults = await Promise.all(bindPromises)
// 检查是否有失败的请求
const failedResults = bindResults.filter((res: any) => {
return res && (res.code !== '0' && res.code !== 0 && !res.success)
})
if (failedResults.length > 0) {
console.warn('部分设备绑定失败:', failedResults)
// 不阻止功能岛创建成功,只提示警告
}
} catch (error) {
console.error('绑定设备失败:', error)
// 不阻止功能岛创建成功,只记录错误
}
}
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
drawerVisible.value = false
getIslandList()
@@ -687,167 +641,6 @@ const handleSubmit = async () => {
}
}
// 打开绑定设备对话框
const handleBindDevice = async () => {
try {
deviceLoading.value = true
const res: any = await devselect({})
const allDevices = res?.data ?? res ?? []
if (Array.isArray(allDevices)) {
if (isEdit.value && formData.id) {
// 编辑模式:显示未绑定的设备 + 已绑定该功能岛的设备
deviceListForBind.value = allDevices.filter((device: any) =>
!device.islandId ||
device.islandId === formData.id ||
device.islandId === String(formData.id) ||
device.islandId === 0 ||
device.islandId === '0'
)
} else {
// 新增模式:只显示未绑定的设备
deviceListForBind.value = allDevices.filter((device: any) =>
!device.islandId ||
device.islandId === 0 ||
device.islandId === '0'
)
}
} else {
deviceListForBind.value = []
}
bindDeviceDialogVisible.value = true
// 等待DOM更新后设置已选中的设备
await new Promise(resolve => setTimeout(resolve, 100))
if (deviceTableRef.value && isEdit.value && formData.id) {
// 编辑模式:默认选中已绑定的设备
const boundDeviceIds = boundDevices.value.map((d: any) => d.id)
deviceListForBind.value.forEach((device: any) => {
if (boundDeviceIds.includes(device.id)) {
deviceTableRef.value.toggleRowSelection(device, true)
}
})
}
} catch (error) {
console.error('获取设备列表失败:', error)
ElMessage.error('获取设备列表失败')
} finally {
deviceLoading.value = false
}
}
// 设备选择变化
const handleDeviceSelectionChange = (selection: any[]) => {
selectedDevices.value = selection
}
// 保存设备绑定
const handleSaveDeviceBind = async () => {
try {
savingDeviceBind.value = true
// 获取当前选中的设备ID列表
const selectedDeviceIds = selectedDevices.value.map((d: any) => d.id)
if (isEdit.value && formData.id) {
// 编辑模式:立即保存绑定关系
// 获取之前已绑定的设备ID列表
const previousBoundDeviceIds = boundDevices.value.map((d: any) => d.id)
// 需要绑定的设备(新增的)
const devicesToBind = selectedDeviceIds.filter((id: any) => !previousBoundDeviceIds.includes(id))
// 需要解绑的设备(取消选中的)
const devicesToUnbind = previousBoundDeviceIds.filter((id: any) => !selectedDeviceIds.includes(id))
// 执行绑定操作
const bindPromises = devicesToBind.map((deviceId: any) => {
const device = deviceListForBind.value.find((d: any) => d.id === deviceId)
if (device) {
// 包含设备名称等必填字段
return devInfoupd({
id: deviceId,
devName: device.devName, // 必填字段
islandId: formData.id,
})
}
return Promise.resolve()
})
// 执行解绑操作
const unbindPromises = devicesToUnbind.map((deviceId: any) => {
const device = deviceListForBind.value.find((d: any) => d.id === deviceId) ||
boundDevices.value.find((d: any) => d.id === deviceId)
if (device) {
// 包含设备名称等必填字段
return devInfoupd({
id: deviceId,
devName: device.devName, // 必填字段
islandId: 0, // 解绑时设置为0
})
}
return Promise.resolve()
})
// 执行所有操作
const allPromises = [...bindPromises, ...unbindPromises]
if (allPromises.length > 0) {
const results = await Promise.all(allPromises)
// 检查是否有失败的请求
const failedResults = results.filter((res: any) => {
if (!res) return false
// 检查响应数据(可能是直接返回的数据对象)
const data = res.data || res
return data && (data.code !== '0' && data.code !== 0 && data.code !== undefined && !data.success)
})
if (failedResults.length > 0) {
const firstFailed = failedResults[0]
const errorData = firstFailed?.data || firstFailed
throw new Error(errorData?.message || errorData?.msg || '部分设备绑定/解绑失败')
}
}
// 更新已绑定设备列表
await loadBoundDevices(formData.id)
ElMessage.success('保存成功')
} else {
// 新增模式:只更新本地列表,不调用接口(等创建功能岛后再绑定)
boundDevices.value = deviceListForBind.value.filter((device: any) =>
selectedDeviceIds.includes(device.id)
)
ElMessage.success('已选择设备,将在创建功能岛后自动绑定')
}
bindDeviceDialogVisible.value = false
} catch (error: any) {
console.error('保存设备绑定失败:', error)
// 处理不同类型的错误
let errorMessage = '保存设备绑定失败'
if (error?.message) {
errorMessage = error.message
} else if (error?.response?.data?.message) {
errorMessage = error.response.data.message
} else if (error?.response?.data?.msg) {
errorMessage = error.response.data.msg
} else if (typeof error === 'string') {
errorMessage = error
}
ElMessage.error(errorMessage)
} finally {
savingDeviceBind.value = false
}
}
// 关闭绑定设备对话框
const handleBindDialogClose = () => {
bindDeviceDialogVisible.value = false
selectedDevices.value = []
if (deviceTableRef.value) {
deviceTableRef.value.clearSelection()
}
}
// 关闭抽屉
const handleDrawerClose = () => {
@@ -931,6 +724,8 @@ onMounted(() => {
min-height: 95px;
height: 95px;
border: 1px solid rgba(64, 158, 255, 0.1);
animation: slideUpIn 0.4s ease-out forwards;
opacity: 0;
&:hover {
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
@@ -1062,15 +857,15 @@ onMounted(() => {
gap: 10px;
}
.bind-device-wrapper {
:deep(.el-form-item__label) {
white-space: nowrap;
}
.device-list-wrapper {
width: 100%;
}
.bound-devices-table-wrapper {
width: 100%;
}
.no-bound-devices {
.no-devices {
margin-top: 12px;
padding: 12px;
text-align: center;
@@ -1081,10 +876,16 @@ onMounted(() => {
border: 1px dashed #dcdfe6;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
// 功能岛卡片动画:从下往上滑动
@keyframes slideUpIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -1,68 +1,94 @@
<template>
<div class="manage-log-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统信息管理</el-breadcrumb-item>
<el-breadcrumb-item>操作日志管理</el-breadcrumb-item>
</el-breadcrumb>
<el-card class="search-card" shadow="never">
<div class="search-bar">
<el-form :inline="true" :model="queryForm" label-width="90px">
<el-form-item label="日志名称">
<el-input
v-model="queryForm.logName"
placeholder="输入日志名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="日志类型">
<el-input
<el-select
v-model="queryForm.logType"
placeholder="输入日志类型"
placeholder="请选择日志类型"
clearable
style="width: 200px"
/>
style="width: 280px"
>
<el-option
v-for="item in logTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="创建人ID">
<el-input
<el-form-item label="操作人">
<el-select
v-model="queryForm.createId"
placeholder="输入创建人ID"
placeholder="请选择操作人"
clearable
filterable
style="width: 200px"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.userName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="录入时间">
<el-date-picker
v-model="queryForm.logWritetimeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar">
<el-button type="primary" @click="openDrawer('create')">新增日志</el-button>
</div>
</div>
</el-card>
<el-card shadow="never">
<el-table
:data="tableData"
stripe
border
style="width: 100%"
v-loading="loading"
>
<el-table :data="tableData" stripe border style="width: 100%" v-loading="loading">
<el-table-column type="index" label="序号" width="80" />
<el-table-column prop="logName" label="日志名称" min-width="150" />
<el-table-column prop="logType" label="日志类型" min-width="120" />
<el-table-column prop="logContent" label="日志内容" min-width="200" show-overflow-tooltip />
<el-table-column prop="logType" label="日志类型" min-width="170" show-overflow-tooltip>
<template #default="scope">
<span class="log-type-text">{{ scope.row.logType || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="logContent" label="日志内容" min-width="430" show-overflow-tooltip />
<el-table-column prop="createId" label="操作人" min-width="140">
<template #default="scope">
{{ getUserName(scope.row.createId) }}
</template>
</el-table-column>
<el-table-column prop="logWritetime" label="日志记录时间" min-width="180">
<template #default="scope">
{{ formatDateTime(scope.row.logWritetime) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="openDrawer('edit', scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
<el-button type="primary" link @click="openDetailDialog(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pagination.pageNum"
@@ -77,105 +103,74 @@
</div>
</el-card>
<el-drawer
v-model="drawerVisible"
:title="drawerTitle"
direction="rtl"
:size="500"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
class="drawer-form"
>
<el-form-item v-if="isEdit" label="日志ID">
<el-input v-model="form.id" disabled />
</el-form-item>
<el-form-item label="日志名称" prop="logName">
<el-input v-model="form.logName" placeholder="请输入日志名称" />
</el-form-item>
<el-form-item label="日志类型" prop="logType">
<el-input v-model="form.logType" placeholder="请输入日志类型" />
</el-form-item>
<el-form-item label="日志内容" prop="logContent">
<el-input
v-model="form.logContent"
type="textarea"
placeholder="请输入日志内容"
:rows="4"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="日志记录时间" prop="logWritetime">
<el-date-picker
v-model="form.logWritetime"
type="datetime"
placeholder="选择日志记录时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss.SSSZ"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<el-dialog v-model="detailVisible" title="日志详情" width="700px">
<el-descriptions :column="1" border>
<el-descriptions-item label="日志编号">{{ detailData.id ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="日志类型">{{ detailData.logType || '-' }}</el-descriptions-item>
<el-descriptions-item label="日志内容">{{ detailData.logContent || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作人">
{{ getUserName(detailData.createId) }}
</el-descriptions-item>
<el-descriptions-item label="日志记录时间">
{{ formatDateTime(detailData.logWritetime) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(detailData.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(detailData.updateTime) }}
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="drawer-footer">
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm">
{{ isEdit ? '更新' : '提交' }}
</el-button>
</div>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-drawer>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
managelogadd,
managelogbyid,
managelogdel,
manageloglist,
managelogupd,
} from '@/api/system/manage-log'
import { ElMessage } from 'element-plus'
import { managelogbyid, manageloglist } from '@/api/system/manage-log'
import { userselect } from '@/api/system/user'
interface LogItem {
id?: string | number
logName: string
logType: string
logContent: string
logName?: string
logType?: string
logContent?: string
logWritetime?: string
remark?: string
createId?: string
createId?: string | number
createTime?: string
updateTime?: string
updateId?: string
updateId?: string | number
}
interface UserOption {
id: string | number
userName: string
}
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<LogItem[]>([])
const total = ref(0)
const userOptions = ref<UserOption[]>([])
const userMap = ref<Map<string, string>>(new Map())
const detailVisible = ref(false)
const detailData = ref<LogItem>({})
const logTypeOptions = [
'登录日志',
'样品执行sop操作日志',
'基础数据修改日志sop序列修改日志',
]
const queryForm = reactive({
logName: '',
logType: '',
createId: '',
createId: '' as string | number | '',
logWritetimeRange: [] as string[],
})
const pagination = reactive({
@@ -183,31 +178,11 @@ const pagination = reactive({
pageSize: 10,
})
const drawerVisible = ref(false)
const drawerTitle = ref('新增日志')
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<LogItem>({
id: undefined,
logName: '',
logType: '',
logContent: '',
logWritetime: '',
remark: '',
})
const rules: FormRules = {
logName: [{ required: true, message: '请输入日志名称', trigger: 'blur' }],
logType: [{ required: true, message: '请输入日志类型', trigger: 'blur' }],
logContent: [{ required: true, message: '请输入日志内容', trigger: 'blur' }],
logWritetime: [{ required: true, message: '请选择日志记录时间', trigger: 'change' }],
}
const formatDateTime = (dateTime?: string) => {
if (!dateTime) return '-'
try {
const date = new Date(dateTime)
if (Number.isNaN(date.getTime())) return dateTime
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
@@ -220,14 +195,78 @@ const formatDateTime = (dateTime?: string) => {
}
}
const resetForm = () => {
form.id = undefined
form.logName = ''
form.logType = ''
form.logContent = ''
form.logWritetime = ''
form.remark = ''
formRef.value?.clearValidate()
const getUserName = (userId?: string | number) => {
if (userId === undefined || userId === null || userId === '') return '-'
return userMap.value.get(String(userId)) || String(userId)
}
const normalizeDateTimeParam = (value?: string | Date) => {
if (!value) return ''
// 统一为后端 LocalDateTime 可解析格式yyyy-MM-dd HH:mm:ss
// 兼容 Date / ISO / 带毫秒 / 带时区等格式
if (value instanceof Date) {
const year = value.getFullYear()
const month = String(value.getMonth() + 1).padStart(2, '0')
const day = String(value.getDate()).padStart(2, '0')
const hours = String(value.getHours()).padStart(2, '0')
const minutes = String(value.getMinutes()).padStart(2, '0')
const seconds = String(value.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const raw = String(value).trim().replace('T', ' ').replace('Z', '')
// 优先提取标准 yyyy-MM-dd HH:mm:ss自动去掉毫秒/时区尾巴)
const matched = raw.match(/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/)
if (matched?.[0]) return matched[0]
// 兜底:尝试使用 Date 解析
const parsed = new Date(raw)
if (!Number.isNaN(parsed.getTime())) {
const year = parsed.getFullYear()
const month = String(parsed.getMonth() + 1).padStart(2, '0')
const day = String(parsed.getDate()).padStart(2, '0')
const hours = String(parsed.getHours()).padStart(2, '0')
const minutes = String(parsed.getMinutes()).padStart(2, '0')
const seconds = String(parsed.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
return raw
}
const loadUserOptions = async () => {
try {
const res: any = await userselect({})
const data = res?.data ?? res ?? {}
const users = Array.isArray(data)
? data
: Array.isArray(data.data)
? data.data
: Array.isArray(data.records)
? data.records
: Array.isArray(data.list)
? data.list
: []
const options: UserOption[] = users
.filter((item: any) => item?.id !== undefined && item?.id !== null)
.map((item: any) => ({
id: item.id,
userName: item.userName || item.username || `用户${item.id}`,
}))
userOptions.value = options
userMap.value.clear()
options.forEach((item) => {
userMap.value.set(String(item.id), item.userName)
})
} catch (error) {
console.error('load user options error', error)
ElMessage.error('加载操作人下拉失败')
}
}
const loadList = async () => {
@@ -237,16 +276,17 @@ const loadList = async () => {
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
}
// 只添加有值的查询参数
if (queryForm.logName) params.logName = queryForm.logName
if (queryForm.logType) params.logType = queryForm.logType
if (queryForm.createId) params.createId = queryForm.createId
if (queryForm.createId !== '' && queryForm.createId !== null) params.createId = queryForm.createId
if (queryForm.logWritetimeRange?.length === 2) {
params.logWritetimeStart = normalizeDateTimeParam(queryForm.logWritetimeRange[0])
params.logWritetimeEnd = normalizeDateTimeParam(queryForm.logWritetimeRange[1])
}
const res: any = await manageloglist(params)
const data = res?.data ?? res ?? {}
// 兼容多种返回格式:{data:{records,total}} | {records,total} | {data:[]} | []
const recordsFromData =
data.records ||
data.list ||
@@ -260,8 +300,7 @@ const loadList = async () => {
? data
: []
const totalValue =
data.total ?? data.count ?? data.totalCount ?? records.length ?? 0
const totalValue = data.total ?? data.count ?? data.totalCount ?? records.length ?? 0
tableData.value = records
total.value = Number(totalValue) || 0
@@ -279,9 +318,9 @@ const handleSearch = () => {
}
const resetSearch = () => {
queryForm.logName = ''
queryForm.logType = ''
queryForm.createId = ''
queryForm.logWritetimeRange = []
handleSearch()
}
@@ -295,123 +334,27 @@ const handleCurrentChange = (page: number) => {
loadList()
}
const openDrawer = async (mode: 'create' | 'edit', row?: LogItem) => {
resetForm()
isEdit.value = mode === 'edit'
drawerTitle.value = isEdit.value ? '编辑日志' : '新增日志'
drawerVisible.value = true
if (isEdit.value && row?.id != null) {
try {
const res: any = await managelogbyid(row.id)
const data = res?.data || res || {}
form.id = data.id ?? row.id
form.logName = data.logName ?? row.logName ?? ''
form.logType = data.logType ?? row.logType ?? ''
form.logContent = data.logContent ?? row.logContent ?? ''
form.logWritetime = data.logWritetime ?? row.logWritetime ?? ''
form.remark = data.remark ?? row.remark ?? ''
} catch (error) {
ElMessage.error('获取日志详情失败')
drawerVisible.value = false
}
}
}
// 将时间格式转换为ISO 8601格式
const formatToISO = (dateTime?: string) => {
if (!dateTime) return new Date().toISOString()
try {
// 如果已经是ISO格式直接返回
if (dateTime.includes('T') && dateTime.includes('Z')) {
return dateTime
}
// 如果是其他格式转换为ISO格式
const date = new Date(dateTime)
return date.toISOString()
} catch {
return new Date().toISOString()
}
}
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
// 获取当前时间ISO 8601格式
const now = new Date().toISOString()
// 获取用户ID可以从localStorage或其他地方获取这里先设为0
const userId = 0 // 可以根据实际情况从用户store或localStorage获取
// 准备提交数据按照后端Swagger要求补充所有字段
const submitData: any = {
logName: form.logName,
logType: form.logType,
logContent: form.logContent || '',
remark: form.remark || '',
logWritetime: formatToISO(form.logWritetime),
createId: userId,
updateId: userId,
createTime: now,
updateTime: now,
delSign: false, // 新增时delSign为false
}
// 如果是编辑需要包含id
if (isEdit.value && form.id) {
submitData.id = Number(form.id)
} else {
// 新增时id设为0
submitData.id = 0
}
if (isEdit.value) {
await managelogupd(submitData)
ElMessage.success('更新成功')
} else {
await managelogadd(submitData)
ElMessage.success('新增成功')
}
drawerVisible.value = false
loadList()
} catch (error: any) {
console.error('submit log error', error)
const errorMsg = error?.response?.data?.message || error?.message || (isEdit.value ? '更新失败' : '新增失败')
ElMessage.error(errorMsg)
} finally {
submitLoading.value = false
}
})
}
const handleDelete = (row: LogItem) => {
if (!row.id) {
ElMessage.warning('缺少日志ID')
const openDetailDialog = async (row: LogItem) => {
if (!row?.id) {
detailData.value = row
detailVisible.value = true
return
}
ElMessageBox.confirm(`确认删除日志「${row.logName}」吗?`, '提示', {
type: 'warning',
})
.then(async () => {
if (row.id == null) {
ElMessage.warning('缺少日志ID无法删除')
return
}
try {
await managelogdel(row.id)
ElMessage.success('删除成功')
loadList()
} catch (error) {
console.error('delete log error', error)
ElMessage.error('删除失败')
}
})
.catch(() => {})
try {
const res: any = await managelogbyid(row.id)
detailData.value = res?.data ?? res ?? row
} catch (error) {
console.error('load log detail error', error)
detailData.value = row
ElMessage.warning('获取详情失败,已展示当前行数据')
} finally {
detailVisible.value = true
}
}
onMounted(() => {
onMounted(async () => {
await loadUserOptions()
loadList()
})
</script>
@@ -435,27 +378,14 @@ onMounted(() => {
gap: 8px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.drawer-form {
padding-right: 10px;
.log-type-text {
display: inline-block;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,504 @@
<template>
<div class="permission-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统信息管理</el-breadcrumb-item>
<el-breadcrumb-item>权限管理</el-breadcrumb-item>
</el-breadcrumb>
<div class="permission-container">
<el-card class="tree-card" shadow="never">
<template #header>
<div class="tree-header">
<span>权限信息</span>
<el-button v-if="selectedPermissionId" type="text" size="small" @click="handleTreeReset">
显示全部
</el-button>
</div>
</template>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
:default-expand-all="false"
:highlight-current="true"
@node-click="handleTreeNodeClick"
v-loading="treeLoading"
>
<template #default="{ node }">
<span class="tree-node">
<span>{{ node.label }}</span>
</span>
</template>
</el-tree>
</el-card>
<div class="list-container">
<el-card class="search-card" shadow="never">
<div class="search-bar">
<el-form :inline="true" :model="queryForm" label-width="80px">
<el-form-item label="权限名称">
<el-input
v-model="queryForm.permissionName"
placeholder="输入权限名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar">
<el-button type="primary" @click="openDrawer('create')">新增权限</el-button>
</div>
</div>
</el-card>
<el-card shadow="never" class="table-card">
<el-table :data="tableData" stripe border style="width: 100%" v-loading="loading">
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="permissionName" label="权限名称" min-width="180" :formatter="formatCell" />
<el-table-column prop="permissionCode" label="权限标识" min-width="180" :formatter="formatCell" />
<el-table-column prop="parentId" label="上级权限" min-width="180" :formatter="formatParentCell" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="openDrawer('edit', scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
background
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</el-card>
</div>
</div>
<el-drawer v-model="drawerVisible" :title="drawerTitle" direction="rtl" :size="420">
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" class="drawer-form">
<el-form-item v-if="isEdit" label="权限ID">
<el-input v-model="form.id" disabled />
</el-form-item>
<el-form-item label="权限名称" prop="permissionName">
<el-input v-model="form.permissionName" placeholder="请输入权限名称" />
</el-form-item>
<el-form-item label="权限标识" prop="permissionCode">
<el-input v-model="form.permissionCode" placeholder="请输入权限标识" />
</el-form-item>
<el-form-item label="上级权限" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="treeOptions"
:props="treeSelectProps"
check-strictly
placeholder="请选择上级权限"
clearable
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="drawer-footer">
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm">
{{ isEdit ? '更新' : '提交' }}
</el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { permissionadd, permissionbyid, permissiondel, permissionlist, permissionselect, permissionupd } from '@/api/system/permission'
interface PermissionItem {
id?: number | string
permissionName: string
permissionCode: string
parentId?: number | string | null
parentName?: string
children?: PermissionItem[]
}
const loading = ref(false)
const submitLoading = ref(false)
const treeLoading = ref(false)
const tableData = ref<PermissionItem[]>([])
const treeData = ref<PermissionItem[]>([])
const total = ref(0)
const selectedPermissionId = ref<number | string | null>(null)
const treeRef = ref()
const queryForm = reactive({
permissionName: '',
parentId: 0 as number | string,
})
const pagination = reactive({
pageNum: 1,
pageSize: 10,
})
const drawerVisible = ref(false)
const drawerTitle = ref('新增权限')
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<PermissionItem>({
id: undefined,
permissionName: '',
permissionCode: '',
parentId: null,
})
const rules: FormRules = {
permissionName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
permissionCode: [{ required: true, message: '请输入权限标识', trigger: 'blur' }],
}
const treeProps = { children: 'children', label: 'permissionName' }
const treeSelectProps = { value: 'id', label: 'permissionName', children: 'children' }
const resetForm = () => {
form.id = undefined
form.permissionName = ''
form.permissionCode = ''
form.parentId = null
formRef.value?.clearValidate()
}
const formatCell = (_row: any, _column: any, value: any) => {
return value === undefined || value === null || value === '' ? '暂无' : value
}
const flattenTree = (nodes: PermissionItem[] = [], result: PermissionItem[] = []): PermissionItem[] => {
nodes.forEach((node) => {
result.push(node)
if (node.children?.length) flattenTree(node.children, result)
})
return result
}
const buildTree = (list: PermissionItem[]): PermissionItem[] => {
const map = new Map<string | number, PermissionItem>()
const roots: PermissionItem[] = []
list.forEach((item) => {
map.set(item.id as string | number, { ...item, children: [] })
})
list.forEach((item) => {
const node = map.get(item.id as string | number)
if (!node) return
if (item.parentId !== null && item.parentId !== undefined && map.has(item.parentId as string | number)) {
map.get(item.parentId as string | number)!.children!.push(node)
} else {
roots.push(node)
}
})
return roots
}
const normalizeTree = (data: any): PermissionItem[] => {
if (Array.isArray(data)) return data
if (Array.isArray(data?.records)) return data.records
if (Array.isArray(data?.data)) return data.data
if (Array.isArray(data?.list)) return data.list
return []
}
const loadTree = async () => {
treeLoading.value = true
try {
const res: any = await permissionselect({})
const data = res?.data ?? res
const nodes = normalizeTree(data)
treeData.value = nodes.some((item) => Array.isArray(item.children)) ? nodes : buildTree(nodes)
} catch (error) {
console.error('load permission tree error', error)
ElMessage.error('加载权限树失败')
} finally {
treeLoading.value = false
}
}
const handleTreeNodeClick = (data: PermissionItem) => {
selectedPermissionId.value = data.id || null
queryForm.parentId = data.id || 0
pagination.pageNum = 1
loadList()
}
const handleTreeReset = () => {
selectedPermissionId.value = null
queryForm.parentId = 0
treeRef.value?.setCurrentKey(null)
pagination.pageNum = 1
loadList()
}
const loadList = async () => {
loading.value = true
try {
const params: any = {
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
}
if (queryForm.permissionName?.trim()) {
params.permissionName = queryForm.permissionName.trim()
}
params.parentId = queryForm.parentId ?? 0
const res: any = await permissionlist(params)
const data = res?.data ?? res ?? {}
const recordsFromData =
data.records || data.list || data.rows || data.items || (Array.isArray(data.data) ? data.data : undefined)
const records = Array.isArray(recordsFromData)
? recordsFromData
: Array.isArray(data)
? data
: []
const allTreeNodes = flattenTree(treeData.value, [])
const idNameMap = new Map(allTreeNodes.map((item) => [item.id as string | number, item.permissionName]))
tableData.value = records.map((item: PermissionItem) => ({
...item,
parentName: item.parentName || (item.parentId != null ? idNameMap.get(item.parentId as string | number) : '暂无'),
}))
total.value = Number(data.total ?? data.count ?? data.totalCount ?? records.length ?? 0) || 0
} catch (error) {
console.error('load permission list error', error)
tableData.value = []
total.value = 0
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.pageNum = 1
loadList()
}
const resetSearch = () => {
queryForm.permissionName = ''
handleSearch()
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
loadList()
}
const handleCurrentChange = (page: number) => {
pagination.pageNum = page
loadList()
}
const treeOptions = computed(() => {
const filterCurrent = (nodes: PermissionItem[]): any[] => {
return nodes
.filter((node) => node.id !== form.id)
.map((node) => ({
id: node.id,
permissionName: node.permissionName,
children: node.children ? filterCurrent(node.children) : undefined,
}))
}
return filterCurrent(treeData.value)
})
const openDrawer = async (mode: 'create' | 'edit', row?: PermissionItem) => {
resetForm()
isEdit.value = mode === 'edit'
drawerTitle.value = isEdit.value ? '编辑权限' : '新增权限'
drawerVisible.value = true
await loadTree()
if (isEdit.value && row?.id != null) {
try {
const res: any = await permissionbyid(row.id)
const data = res?.data || res || {}
form.id = data.id ?? row.id
form.permissionName = data.permissionName ?? row.permissionName
form.permissionCode = data.permissionCode ?? row.permissionCode
form.parentId = data.parentId ?? row.parentId ?? null
} catch (error) {
ElMessage.error('获取权限详情失败')
drawerVisible.value = false
}
}
}
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
const payload: any = {
...form,
}
if (payload.parentId === undefined || payload.parentId === null || payload.parentId === '') {
payload.parentId = 0
}
if (isEdit.value) {
await permissionupd(payload)
ElMessage.success('更新成功')
} else {
await permissionadd(payload)
ElMessage.success('新增成功')
}
drawerVisible.value = false
await loadTree()
loadList()
} catch (error: any) {
console.error('submit permission error', error)
ElMessage.error(error?.message || error?.msg || (isEdit.value ? '更新失败' : '新增失败'))
} finally {
submitLoading.value = false
}
})
}
const handleDelete = (row: PermissionItem) => {
if (!row.id) {
ElMessage.warning('缺少权限ID')
return
}
ElMessageBox.confirm(`确认删除权限「${row.permissionName}」吗?`, '提示', { type: 'warning' })
.then(async () => {
try {
await permissiondel(row.id as string | number)
ElMessage.success('删除成功')
await loadTree()
loadList()
} catch (error: any) {
console.error('delete permission error', error)
ElMessage.error(error?.message || error?.msg || '删除失败,可能已被角色绑定')
}
})
.catch(() => {})
}
const formatParentCell = (row: PermissionItem) => {
return row.parentName || '暂无'
}
onMounted(() => {
loadTree()
loadList()
})
</script>
<style scoped>
.permission-page {
display: flex;
flex-direction: column;
gap: 12px;
height: calc(100vh - 120px);
}
.permission-container {
display: flex;
gap: 12px;
flex: 1;
overflow: hidden;
}
.tree-card {
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tree-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.tree-card :deep(.el-card__body) {
flex: 1;
overflow: auto;
padding: 12px;
}
.tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
}
.list-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
min-width: 0;
}
.search-card {
padding-bottom: 4px;
flex-shrink: 0;
}
.search-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.table-card :deep(.el-card__body) {
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-card :deep(.el-table) {
flex: 1;
}
.pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
<template>
<div class="position-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统管理</el-breadcrumb-item>
<el-breadcrumb-item>职位管理</el-breadcrumb-item>
</el-breadcrumb>
<el-card class="search-card" shadow="never">
<div class="search-bar">
<el-form :inline="true" :model="queryForm" label-width="80px">

View File

@@ -1,5 +1,10 @@
<template>
<div class="role-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统信息管理</el-breadcrumb-item>
<el-breadcrumb-item>角色管理</el-breadcrumb-item>
</el-breadcrumb>
<el-card class="search-card" shadow="never">
<div class="search-bar">
<el-form :inline="true" :model="queryForm" label-width="80px">
@@ -39,11 +44,17 @@
v-loading="loading"
>
<el-table-column type="index" label="序号" width="180" />
<el-table-column prop="roleName" label="角色名称" min-width="*" />
<el-table-column prop="roleCode" label="角色编码" min-width="*" />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column prop="roleName" label="角色名称" min-width="160" />
<el-table-column prop="roleCode" label="角色编码" min-width="160" />
<el-table-column prop="remark" label="备注" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
{{ row.remark || row.remarks || '暂无' }}
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="openDrawer('edit', scope.row)">编辑</el-button>
<el-button type="success" link @click="openPermissionDialog(scope.row)">配置权限</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
@@ -104,14 +115,45 @@
</div>
</template>
</el-drawer>
<el-dialog
v-model="permissionDialogVisible"
:title="permissionDialogTitle"
width="780px"
destroy-on-close
>
<div class="permission-dialog-tree">
<el-tree
ref="permissionTreeRef"
:data="permissionTreeData"
:props="permissionTreeProps"
node-key="id"
show-checkbox
default-expand-all
:check-strictly="false"
:expand-on-click-node="false"
v-loading="permissionTreeLoading"
/>
</div>
<template #footer>
<div class="drawer-footer">
<el-button @click="permissionDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="permissionSubmitLoading" @click="submitPermissionForm">
保存
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { FormInstance, FormRules, TreeInstance } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { roleadd, rolebyid, roledel, rolelist, roleupd } from '@/api/system/role'
import { permissionselect } from '@/api/system/permission'
import { rolePermissionAdd, rolePermissionClearByRoleId, rolePermissionListByRoleId } from '@/api/system/role-permission'
interface RoleItem {
id?: number | string
@@ -142,6 +184,14 @@ const drawerVisible = ref(false)
const drawerTitle = ref('新增角色')
const isEdit = ref(false)
const permissionDialogVisible = ref(false)
const permissionDialogTitle = ref('权限配置')
const permissionTreeLoading = ref(false)
const permissionSubmitLoading = ref(false)
const permissionTreeRef = ref<TreeInstance>()
const permissionTreeData = ref<any[]>([])
const currentPermissionRoleId = ref<number | string | null>(null)
const formRef = ref<FormInstance>()
const form = reactive<RoleItem>({
id: undefined,
@@ -155,6 +205,11 @@ const rules: FormRules = {
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
}
const permissionTreeProps = {
children: 'children',
label: 'permissionName',
}
const resetForm = () => {
form.id = undefined
form.roleName = ''
@@ -269,6 +324,160 @@ const submitForm = () => {
})
}
const normalizePermissionTree = (data: any): any[] => {
if (Array.isArray(data)) return data
if (Array.isArray(data?.records)) return data.records
if (Array.isArray(data?.data)) return data.data
if (Array.isArray(data?.list)) return data.list
return []
}
const buildPermissionTree = (list: any[]) => {
const map = new Map<string | number, any>()
const roots: any[] = []
list.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
list.forEach((item) => {
const node = map.get(item.id)
if (!node) return
const parentId = item.parentId
if (parentId !== null && parentId !== undefined && map.has(parentId)) {
map.get(parentId).children.push(node)
} else {
roots.push(node)
}
})
return roots
}
const buildPermissionIndex = (nodes: any[]) => {
const nodeMap = new Map<string | number, any>()
const childMap = new Map<string | number, Array<string | number>>()
const traverse = (items: any[]) => {
items.forEach((node) => {
nodeMap.set(node.id, node)
childMap.set(node.id, (node.children || []).map((child: any) => child.id))
if (Array.isArray(node.children) && node.children.length > 0) {
traverse(node.children)
}
})
}
traverse(nodes)
return { nodeMap, childMap }
}
const getDeepestCheckedIds = (checkedIds: Array<string | number>) => {
const { nodeMap } = buildPermissionIndex(permissionTreeData.value)
const checkedSet = new Set(checkedIds)
return checkedIds.filter((id) => {
const node = nodeMap.get(id)
if (!node) return true
const stack = Array.isArray(node.children) ? [...node.children] : []
while (stack.length) {
const current = stack.pop()
if (!current) continue
if (checkedSet.has(current.id)) return false
if (Array.isArray(current.children) && current.children.length > 0) {
stack.push(...current.children)
}
}
return true
})
}
const collectPermissionIdsWithParents = (checkedIds: Array<string | number>) => {
const idSet = new Set<string | number>()
const { nodeMap } = buildPermissionIndex(permissionTreeData.value)
checkedIds.forEach((id) => {
let current = nodeMap.get(id)
while (current) {
idSet.add(current.id)
const parentId = current.parentId
current = parentId !== null && parentId !== undefined ? nodeMap.get(parentId) : undefined
}
})
return Array.from(idSet)
}
const loadPermissionTree = async () => {
permissionTreeLoading.value = true
try {
const res: any = await permissionselect({})
const data = res?.data ?? res
const list = normalizePermissionTree(data)
permissionTreeData.value = list.some((item) => Array.isArray(item.children)) ? list : buildPermissionTree(list)
} catch (error) {
console.error('load permission tree error', error)
ElMessage.error('加载权限树失败')
permissionTreeData.value = []
} finally {
permissionTreeLoading.value = false
}
}
const setCheckedKeys = (keys: Array<string | number>) => {
permissionTreeRef.value?.setCheckedKeys(keys)
}
const openPermissionDialog = async (row: RoleItem) => {
if (!row.id) {
ElMessage.warning('缺少角色ID')
return
}
currentPermissionRoleId.value = row.id
permissionDialogTitle.value = `权限配置-${row.roleName || '未命名角色'}`
permissionDialogVisible.value = true
await loadPermissionTree()
try {
const res: any = await rolePermissionListByRoleId(row.id)
const data = res?.data ?? res ?? {}
const checkedIds = Array.isArray(data)
? data.map((item: any) => item.permissionId ?? item.id).filter((id: any) => id !== undefined && id !== null)
: Array.isArray(data?.data)
? data.data.map((item: any) => item.permissionId ?? item.id).filter((id: any) => id !== undefined && id !== null)
: Array.isArray(data?.records)
? data.records.map((item: any) => item.permissionId ?? item.id).filter((id: any) => id !== undefined && id !== null)
: []
setCheckedKeys(getDeepestCheckedIds(checkedIds))
} catch (error) {
console.error('load role permissions error', error)
setCheckedKeys([])
}
}
const submitPermissionForm = async () => {
if (!currentPermissionRoleId.value) {
ElMessage.warning('缺少角色ID')
return
}
const checkedKeys = permissionTreeRef.value?.getCheckedKeys(false) || []
const leafCheckedKeys = getDeepestCheckedIds(checkedKeys)
const permissionIds = collectPermissionIdsWithParents(leafCheckedKeys)
permissionSubmitLoading.value = true
try {
await rolePermissionClearByRoleId(currentPermissionRoleId.value)
await Promise.all(
permissionIds.map((permissionId: string | number) =>
rolePermissionAdd({ roleId: currentPermissionRoleId.value, permissionId })
),
)
ElMessage.success('权限配置成功')
permissionDialogVisible.value = false
loadList()
} catch (error: any) {
console.error('submit role permission error', error)
ElMessage.error(error?.message || error?.msg || '权限配置失败')
} finally {
permissionSubmitLoading.value = false
}
}
const handleDelete = (row: RoleItem) => {
if (!row.id) {
ElMessage.warning('缺少角色ID')
@@ -325,6 +534,11 @@ onMounted(() => {
justify-content: flex-end;
}
.permission-dialog-tree {
max-height: 520px;
overflow: auto;
}
.drawer-footer {
display: flex;
justify-content: flex-end;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
<template>
<div class="user-role-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统管理</el-breadcrumb-item>
<el-breadcrumb-item>用户角色管理</el-breadcrumb-item>
</el-breadcrumb>
<el-card class="search-card" shadow="never">
<div class="search-bar">
<el-form :inline="true" :model="queryForm" label-width="80px">

View File

@@ -1,5 +1,10 @@
<template>
<div class="user-page">
<el-breadcrumb separator="/" style="margin-bottom: 16px">
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
<el-breadcrumb-item>系统信息管理</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
<el-card class="search-card" shadow="never">
<div class="search-bar">
<el-form :inline="true" :model="queryForm" label-width="80px">
@@ -61,15 +66,6 @@
{{ getDeptName(scope.row.depId) }}
</template>
</el-table-column>
<el-table-column
prop="posId"
label="职位"
min-width="120"
>
<template #default="scope">
{{ getPosName(scope.row.posId) }}
</template>
</el-table-column>
<el-table-column
prop="telephone"
label="联系方式"
@@ -94,12 +90,6 @@
min-width="150"
:formatter="formatCell"
/>
<el-table-column
prop="remark"
label="备注"
min-width="150"
:formatter="formatCell"
/>
<el-table-column label="操作" width="250" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="openDrawer('edit', scope.row)">编辑</el-button>
@@ -178,32 +168,6 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="职位" prop="posId">
<el-select
v-model="form.posId"
filterable
clearable
placeholder="请选择职位"
style="width: 100%"
>
<el-option
v-for="item in positionOptions"
:key="item.id"
:label="item.posiName || item.posiCode"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="drawer-footer">
@@ -219,17 +183,49 @@
<el-dialog
v-model="roleDialogVisible"
title="分配角色"
width="500px"
width="760px"
>
<el-checkbox-group v-model="selectedRoleIds">
<el-checkbox
v-for="role in roleOptions"
:key="role.id"
:label="role.id"
>
{{ role.roleName || role.roleCode }}
</el-checkbox>
</el-checkbox-group>
<div class="role-dialog-search">
<el-form :inline="true" :model="roleQueryForm">
<el-form-item label="角色名称">
<el-input
v-model="roleQueryForm.roleName"
placeholder="输入角色名称"
clearable
style="width: 220px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleRoleSearch">查询</el-button>
<el-button @click="resetRoleSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="roleOptions" border stripe v-loading="assignRoleLoading" style="width: 100%">
<el-table-column width="50" align="center">
<template #default="scope">
<el-checkbox
:model-value="selectedRoleIds.includes(scope.row.id)"
@change="(checked: boolean) => toggleRoleSelection(scope.row.id, checked)"
/>
</template>
</el-table-column>
<el-table-column prop="roleName" label="角色名称" min-width="180" />
<el-table-column prop="roleCode" label="角色编码" min-width="180" />
<el-table-column prop="remark" label="备注" min-width="200" />
</el-table>
<div class="pagination role-pagination">
<el-pagination
v-model:current-page="rolePagination.pageNum"
v-model:page-size="rolePagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="roleTotal"
background
@current-change="handleRoleCurrentChange"
@size-change="handleRoleSizeChange"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="roleDialogVisible = false">取消</el-button>
@@ -248,9 +244,8 @@ import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useradd, userbyid, userdel, userlist, userupd } from '@/api/system/user'
import { departtree } from '@/api/system/department'
import { positionselect } from '@/api/system/position'
import { userRoleAdd, userRoleByUserId, userRoleDel } from '@/api/system/user-role'
import { rleselect } from '@/api/system/role'
import { rolelist } from '@/api/system/role'
interface UserItem {
id?: number | string
@@ -275,16 +270,11 @@ interface DeptItem {
children?: DeptItem[]
}
interface PositionItem {
id?: number | string
posiName?: string
posiCode?: string
}
interface RoleItem {
id?: number | string
roleName?: string
roleCode?: string
remark?: string
}
const loading = ref(false)
@@ -292,15 +282,16 @@ const submitLoading = ref(false)
const tableData = ref<UserItem[]>([])
const total = ref(0)
const deptTreeData = ref<DeptItem[]>([])
const positionOptions = ref<PositionItem[]>([])
const deptMap = ref<Map<number | string, string>>(new Map())
const posMap = ref<Map<number | string, string>>(new Map())
const roleDialogVisible = ref(false)
const assignRoleLoading = ref(false)
const roleOptions = ref<RoleItem[]>([])
const selectedRoleIds = ref<(number | string)[]>([])
const currentAssignUserId = ref<number | string | null>(null)
const originalUserRoles = ref<any[]>([]) // 保存用户原有的角色关联列表
const originalUserRoles = ref<any[]>([])
const roleQueryForm = reactive({ roleName: '' })
const rolePagination = reactive({ pageNum: 1, pageSize: 10 })
const roleTotal = ref(0)
const queryForm = reactive({
userName: '',
@@ -327,7 +318,6 @@ const form = reactive<UserItem>({
telephone: '',
depId: null,
posId: null,
remark: '',
})
const rules: FormRules = {
@@ -353,7 +343,6 @@ const resetForm = () => {
form.telephone = ''
form.depId = null
form.posId = null
form.remark = ''
formRef.value?.clearValidate()
}
@@ -432,12 +421,6 @@ const getDeptName = (depId: number | string | null | undefined) => {
return deptMap.value.get(depId) || '暂无'
}
// 获取职位名称
const getPosName = (posId: number | string | null | undefined) => {
if (!posId) return '暂无'
return posMap.value.get(posId) || '暂无'
}
// 加载部门树
const loadDeptTree = async () => {
try {
@@ -462,36 +445,6 @@ const loadDeptTree = async () => {
}
}
// 加载职位列表
const loadPositionList = async () => {
try {
const res: any = await positionselect()
const data = res?.data ?? res ?? {}
// 兼容多种返回格式
const positions = Array.isArray(data)
? data
: Array.isArray(data.data)
? data.data
: Array.isArray(data.records)
? data.records
: Array.isArray(data.list)
? data.list
: []
positionOptions.value = positions
posMap.value.clear()
positions.forEach((pos: PositionItem) => {
if (pos.id) {
posMap.value.set(pos.id, pos.posiName || pos.posiCode || '')
}
})
console.log('职位列表数据:', positionOptions.value)
} catch (error) {
console.error('load position list error', error)
}
}
// 加载用户列表
const loadList = async () => {
loading.value = true
@@ -585,8 +538,8 @@ const openDrawer = async (mode: 'create' | 'edit', row?: UserItem) => {
drawerTitle.value = isEdit.value ? '编辑用户' : '新增用户'
drawerVisible.value = true
// 加载部门和职位数据
await Promise.all([loadDeptTree(), loadPositionList()])
// 加载部门数据
await loadDeptTree()
if (isEdit.value && row?.id != null) {
try {
@@ -601,7 +554,6 @@ const openDrawer = async (mode: 'create' | 'edit', row?: UserItem) => {
form.telephone = data.telephone ?? row.telephone ?? ''
form.depId = data.depId ?? row.depId ?? null
form.posId = data.posId ?? row.posId ?? null
form.remark = data.remark ?? row.remark ?? ''
// 编辑时不设置密码
form.password = ''
} catch (error) {
@@ -632,6 +584,7 @@ const submitForm = () => {
if (!payload.posId) {
payload.posId = null
}
delete payload.remark
if (isEdit.value) {
await userupd(payload)
@@ -681,13 +634,64 @@ const handleDelete = (row: UserItem) => {
// 加载角色列表
const loadRoleList = async () => {
try {
const res: any = await rleselect({})
const data = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []
roleOptions.value = data
const params: any = {
pageNum: rolePagination.pageNum,
pageSize: rolePagination.pageSize,
}
if (roleQueryForm.roleName && roleQueryForm.roleName.trim()) {
params.roleName = roleQueryForm.roleName.trim()
}
const res: any = await rolelist(params)
const data = res?.data ?? res ?? {}
const recordsFromData =
data.records ||
data.list ||
data.rows ||
data.items ||
(Array.isArray(data.data) ? data.data : undefined)
const records = Array.isArray(recordsFromData)
? recordsFromData
: Array.isArray(data)
? data
: []
roleOptions.value = records
roleTotal.value = Number(data.total ?? data.count ?? data.totalCount ?? records.length ?? 0) || 0
} catch (error) {
console.error('load role list error', error)
ElMessage.error('加载角色列表失败')
roleOptions.value = []
roleTotal.value = 0
}
}
const handleRoleSearch = () => {
rolePagination.pageNum = 1
loadRoleList()
}
const resetRoleSearch = () => {
roleQueryForm.roleName = ''
handleRoleSearch()
}
const handleRoleCurrentChange = (page: number) => {
rolePagination.pageNum = page
loadRoleList()
}
const handleRoleSizeChange = (size: number) => {
rolePagination.pageSize = size
loadRoleList()
}
const toggleRoleSelection = (roleId: string | number | undefined, checked: boolean) => {
if (roleId === undefined || roleId === null) return
const exists = selectedRoleIds.value.includes(roleId)
if (checked && !exists) {
selectedRoleIds.value.push(roleId)
}
if (!checked && exists) {
selectedRoleIds.value = selectedRoleIds.value.filter((id) => id !== roleId)
}
}
@@ -699,19 +703,15 @@ const openRoleDialog = async (row: UserItem) => {
}
currentAssignUserId.value = row.id
selectedRoleIds.value = []
roleQueryForm.roleName = ''
rolePagination.pageNum = 1
roleDialogVisible.value = true
// 确保角色列表已加载
if (roleOptions.value.length === 0) {
await loadRoleList()
}
await loadRoleList()
// 获取用户已有的角色
try {
const res: any = await userRoleByUserId(row.id)
const data = res?.data ?? res ?? {}
// 兼容多种返回格式
const userRoles = Array.isArray(data)
? data
: Array.isArray(data.data)
@@ -721,22 +721,14 @@ const openRoleDialog = async (row: UserItem) => {
: Array.isArray(data.list)
? data.list
: []
// 保存原有的角色关联列表包含ID
originalUserRoles.value = userRoles
// 提取已有角色的ID列表
if (Array.isArray(userRoles) && userRoles.length > 0) {
selectedRoleIds.value = userRoles
.map((item: any) => item.roleId)
.filter((id: any) => id !== undefined && id !== null)
} else {
selectedRoleIds.value = []
originalUserRoles.value = []
}
selectedRoleIds.value = Array.isArray(userRoles)
? userRoles
.map((item: any) => item.roleId)
.filter((id: any) => id !== undefined && id !== null)
: []
} catch (error) {
console.error('load user roles error', error)
// 如果获取失败,不影响弹框打开,只是不预选角色
originalUserRoles.value = []
}
}
@@ -747,46 +739,28 @@ const handleAssignRole = async () => {
ElMessage.warning('缺少用户ID')
return
}
try {
await ElMessageBox.confirm('确认保存当前角色分配吗?', '提示', { type: 'warning' })
} catch {
return
}
assignRoleLoading.value = true
try {
// 获取原有的角色ID列表
const originalRoleIds = originalUserRoles.value
.map((item: any) => item.roleId)
.filter((id: any) => id !== undefined && id !== null)
// 计算需要新增的角色(新选中但原来没有的)
const rolesToAdd = selectedRoleIds.value.filter(
(roleId) => !originalRoleIds.includes(roleId)
)
// 计算需要删除的角色(原来有但现在未选中的)
const rolesToDelete = originalUserRoles.value.filter(
(item: any) => !selectedRoleIds.value.includes(item.roleId)
)
// 执行新增操作
const rolesToAdd = selectedRoleIds.value.filter((roleId) => !originalRoleIds.includes(roleId))
const rolesToDelete = originalUserRoles.value.filter((item: any) => !selectedRoleIds.value.includes(item.roleId))
const addPromises = rolesToAdd.map((roleId) => {
return userRoleAdd({
userId: currentAssignUserId.value,
roleId: roleId,
remark: '',
})
return userRoleAdd({ userId: currentAssignUserId.value, roleId, remark: '' })
})
// 执行删除操作
const deletePromises = rolesToDelete.map((item: any) => {
if (item.id) {
return userRoleDel(item.id)
}
return Promise.resolve()
})
// 等待所有操作完成
const deletePromises = rolesToDelete.map((item: any) => item.id ? userRoleDel(item.id) : Promise.resolve())
await Promise.all([...addPromises, ...deletePromises])
ElMessage.success('分配角色成功')
roleDialogVisible.value = false
loadList()
} catch (error: any) {
console.error('assign role error', error)
const errorMsg = error?.message || error?.msg || '分配角色失败'
@@ -798,7 +772,6 @@ const handleAssignRole = async () => {
onMounted(() => {
loadDeptTree()
loadPositionList()
loadRoleList()
loadList()
})
@@ -835,6 +808,14 @@ onMounted(() => {
justify-content: flex-end;
}
.role-dialog-search {
margin-bottom: 12px;
}
.role-pagination {
margin-top: 12px;
}
.drawer-footer {
display: flex;
justify-content: flex-end;

View File

@@ -1,5 +1,4 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],

View File

@@ -1,18 +1,31 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
},
server: {
host: '0.0.0.0', // 允许外部访问
port: 5173, // 端口号
proxy: {
'/plc': {
target: env.VITE_API_URL,
changeOrigin: true,
},
},
},
}
})