Compare commits

...

12 Commits

Author SHA1 Message Date
Lxq
e304c012ce 业务逻辑修改 2026-02-08 17:02:10 +08:00
Lxq
e36faaf94f PLC设备管理 2026-02-02 13:48:55 +08:00
Lxq
0701e9b536 页面优化 2026-01-26 09:29:46 +08:00
Lxq
82764f06d1 PLC设备管理页面修改 2026-01-23 11:26:14 +08:00
Lxq
7e3ff083c7 登录注册管理 2026-01-19 16:15:02 +08:00
Lxq
d95092a4c9 完善 2026-01-19 13:41:14 +08:00
Lxq
36e0644b5d 样品信息管理 2026-01-14 14:04:08 +08:00
Lxq
7c2678429c 页面优化 2026-01-08 14:10:55 +08:00
Lxq
2c25ef613b 页眉修改 2026-01-08 13:05:59 +08:00
Lxq
3c5d854967 标准流程管理+流程创建 2026-01-08 11:52:11 +08:00
Lxq
5f6808b2a4 字典类型管理+字典数据管理 2026-01-05 13:07:30 +08:00
Lxq
bdd50a498b 标准流程管理+流程创建 2026-01-04 17:52:10 +08:00
40 changed files with 10535 additions and 1929 deletions

12
rc_autoplc_backend/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>

6
rc_autoplc_backend/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

54
rc_autoplc_backend/.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="d47bc58e-d36b-422c-92c6-393c467a25d3" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="useMavenConfig" value="true" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProjectId" id="37uy9ngFoDUVUddX8Yg9afHWRsn" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="d47bc58e-d36b-422c-92c6-393c467a25d3" name="Changes" comment="" />
<created>1767767108230</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1767767108230</updated>
<workItem from="1767767109651" duration="29000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

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>北京融创智能仪器管理系统</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

@@ -1,5 +1,51 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { onMounted, onBeforeUnmount } from 'vue'
import { useAuthStore } from '@/stores/auth'
// 清除token和用户信息
const clearAuthInfo = () => {
const authStore = useAuthStore()
// 清除token这会自动同步到持久化存储
authStore.removeToken()
// 清除localStorage中的用户信息
localStorage.removeItem('username')
// 清除Pinia持久化存储的auth数据pinia-plugin-persistedstate默认使用'pinia-{storeName}'格式)
localStorage.removeItem('pinia-auth')
}
// 页面关闭时清除信息
onMounted(() => {
// 监听页面关闭/刷新事件beforeunload在页面关闭或刷新前触发
const handleBeforeUnload = () => {
clearAuthInfo()
}
// 监听页面卸载事件unload在页面卸载时触发
const handleUnload = () => {
clearAuthInfo()
}
// 监听页面隐藏事件pagehide更可靠可以区分刷新和关闭
const handlePageHide = (event: PageTransitionEvent) => {
// 如果页面被缓存bfcache不清除信息
// 只有在真正关闭时才清除
if (!event.persisted) {
clearAuthInfo()
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
window.addEventListener('unload', handleUnload)
window.addEventListener('pagehide', handlePageHide)
// 清理事件监听器
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
window.removeEventListener('unload', handleUnload)
window.removeEventListener('pagehide', handlePageHide)
})
})
</script>
<template>

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

@@ -45,3 +45,25 @@ export function userbyid(id: string | number) {
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

@@ -45,3 +45,27 @@ export function devparamselect(data: any) {
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,108 @@
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设备复位故障
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 { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
const router = createRouter({
history: createWebHistory(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',
@@ -42,6 +60,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 +75,64 @@ 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'),
},
],
},
],
})
// 添加全局路由守卫
// 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">欢迎使用北京融创智能仪器管理系统</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

@@ -31,52 +31,82 @@
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>
<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="/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>
<el-icon><component :is="getMenuIcon('职位管理')" /></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>
<el-icon><component :is="getMenuIcon('用户角色管理')" /></el-icon>
<span>用户角色管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="island">
<template #title>
<el-icon><Grid /></el-icon>
<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-sub-menu>
<el-sub-menu index="flow">
<template #title>
<el-icon><component :is="getMenuIcon('流程管理')" /></el-icon>
<span>流程管理</span>
</template>
<el-menu-item index="/plc-device-control">
<el-icon><component :is="getMenuIcon('PLC设备控制')" /></el-icon>
<span>PLC设备控制</span>
</el-menu-item>
<el-menu-item index="/goods-info">
<el-icon><component :is="getMenuIcon('样品管理')" /></el-icon>
<span>样品管理</span>
</el-menu-item>
<el-menu-item index="/flow-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>流程创建</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
@@ -94,7 +124,26 @@
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,
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 +160,38 @@ const activeMenu = computed(() => {
return route.path
})
// 根据菜单名称获取图标
const getMenuIcon = (menuName: string) => {
const iconMap: Record<string, any> = {
// 一级
首页: House,
系统管理: Setting,
业务管理: Grid,
流程管理: List,
// 系统管理
用户管理: User,
角色管理: Avatar,
部门管理: OfficeBuilding,
职位管理: Briefcase,
操作日志管理: Document,
用户角色管理: UserFilled,
// 业务管理
PLC设备管理: Connection,
动作参数管理: Tools,
动作管理: Monitor,
功能岛管理: Grid,
// 流程管理
PLC设备控制: Operation,
样品管理: Box,
'标准流程管理': Files, // 文件图标,适合标准流程管理
'流程创建': EditPen,
}
return iconMap[menuName] || EditPen
}
// 处理下拉菜单命令
const handleCommand = (command: string) => {
if (command === 'logout') {
@@ -122,8 +203,8 @@ const handleCommand = (command: string) => {
authStore.removeToken()
localStorage.removeItem('username')
ElMessage.success('退出成功')
// 跳转到登录页(如果有登录页)
// router.push('/login')
// 跳转到登录页
router.push('/login')
})
.catch(() => {})
}
@@ -132,7 +213,7 @@ const handleCommand = (command: string) => {
<style scoped>
.layout-container {
height: 100vh;
height: 98vh;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,317 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h2>北京融创智能仪器管理系统</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>
@@ -78,13 +71,6 @@
{{ 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
@@ -247,7 +233,6 @@ const total = ref(0)
// 查询表单
const queryForm = reactive({
devName: '',
status: undefined as number | undefined,
})
// 分页
@@ -273,14 +258,75 @@ const formData = reactive({
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' },
],
}
// 获取序号
@@ -288,21 +334,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 +349,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 +393,6 @@ const handleSearch = () => {
// 重置查询
const resetSearch = () => {
queryForm.devName = ''
queryForm.status = undefined
handleSearch()
}
@@ -401,7 +427,8 @@ const resetForm = () => {
formData.company = ''
formData.devDesc = ''
formData.remark = ''
formData.status = 0
formData.status = 0 // 状态默认为0
formData.runModel = 0 // 运行模式默认为0
formRef.value?.clearValidate()
}
@@ -423,6 +450,7 @@ const handleEdit = async (item: any) => {
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,23 +503,18 @@ 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
}

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"
@@ -102,7 +108,7 @@
v-model="drawerVisible"
:title="drawerTitle"
direction="rtl"
size="500px"
size="600px"
:before-close="handleDrawerClose"
>
<el-form
@@ -144,41 +150,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 +196,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 +225,17 @@ import {
Monitor,
Cpu,
DataAnalysis,
Upload,
Download,
MagicStick,
TakeawayBox,
CollectionTag,
Position,
Dish,
Bowl,
HomeFilled,
Odometer,
Sunrise,
} from '@element-plus/icons-vue'
import {
islandInfoadd,
@@ -289,13 +244,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 +263,7 @@ const queryForm = reactive({
// 分页
const pagination = reactive({
pageNum: 1,
pageSize: 10,
pageSize: 9,
})
// 抽屉相关
@@ -326,12 +280,8 @@ const formData = reactive({
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 = {
@@ -347,44 +297,44 @@ const formRules = {
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: MagicStick },
{ keywords: ['加液', '加样'], icon: Watermelon },
{ keywords: ['进样'], icon: Upload },
{ keywords: ['分液'], icon: TakeawayBox },
{ keywords: ['浓缩'], icon: CollectionTag },
{ keywords: ['移上清'], icon: Position },
{ keywords: ['取液'], icon: Dish },
{ keywords: ['金属浴', '金属'], icon: Bowl },
{ keywords: ['干燥', '硫酸钠'], icon: Sunrise },
{ keywords: ['温室', '静置'], icon: HomeFilled },
{ keywords: ['ph', '酸碱'], icon: Odometer },
{ keywords: ['出样'], icon: Download },
{ keywords: ['水浴', '恒温'], icon: Sunny },
{ keywords: ['震荡'], icon: Connection },
{ keywords: ['超声'], icon: Histogram },
{ keywords: ['离心'], icon: RefreshRight },
{ keywords: ['移液'], icon: Aim },
{ keywords: ['萃取'], icon: Goblet },
{ keywords: ['氮吹'], icon: WindPower },
{ keywords: ['过膜', '过滤'], icon: Filter },
{ keywords: ['人工'], icon: User },
{ keywords: ['系统', '管理'], icon: Setting },
{ keywords: ['工具'], icon: Tools },
{ keywords: ['数据', '分析'], icon: DataAnalysis },
{ keywords: ['监控', '显示'], icon: Monitor },
{ keywords: ['处理', '计算'], icon: Cpu },
{ keywords: ['网格', '布局'], icon: Grid },
]
const matched = iconRules.find(rule =>
rule.keywords.some(keyword => nameLower.includes(keyword.toLowerCase()))
)
return matched ? matched.icon : Box
}
// 处理名称输入
@@ -408,16 +358,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 {
@@ -495,8 +440,7 @@ const resetForm = () => {
formData.islandName = ''
formData.islandCode = ''
formData.islandDesc = ''
boundDevices.value = []
selectedDevices.value = []
deviceList.value = []
formRef.value?.clearValidate()
}
@@ -522,9 +466,9 @@ const handleEdit = async (item: any) => {
// 优先使用后端的 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 +485,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: '确定',
@@ -627,7 +583,6 @@ const handleSubmit = async () => {
}
let res: any
let newIslandId: any = null
if (isEdit.value && formData.id) {
// 编辑 - 需要传入ID其他字段可选(非空则更新)
@@ -635,42 +590,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 +612,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 +695,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 +828,11 @@ onMounted(() => {
gap: 10px;
}
.bind-device-wrapper {
.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 +843,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,5 +1,10 @@
<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">

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">

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">

View File

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

View File

@@ -15,4 +15,8 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
host: '0.0.0.0', // 允许外部访问
port: 5173, // 端口号
},
})