PLC设备管理

This commit is contained in:
Lxq
2026-02-02 13:48:55 +08:00
parent 0701e9b536
commit e36faaf94f
10 changed files with 2308 additions and 1151 deletions

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

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

@@ -85,6 +85,11 @@ const router = createRouter({
name: 'step-info',
component: () => import('../views/stepinfo/index.vue'),
},
{
path: '/plc-device-control',
name: 'plc-device-control',
component: () => import('../views/plcdevicecontrol/index.vue'),
},
],
},
],

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

@@ -100,7 +100,25 @@ request.interceptors.response.use(
// 其他错误码
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)
}
@@ -114,12 +132,26 @@ request.interceptors.response.use(
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

@@ -88,6 +88,10 @@
<el-icon><List /></el-icon>
<span>流程管理</span>
</template>
<el-menu-item index="/plc-device-control">
<el-icon><Connection /></el-icon>
<span>PLC设备控制</span>
</el-menu-item>
<el-menu-item index="/goods-info">
<el-icon><Filter /></el-icon>
<span>样品管理</span>
@@ -172,7 +176,7 @@ const handleCommand = (command: string) => {
<style scoped>
.layout-container {
height: 100vh;
height: 98vh;
display: flex;
flex-direction: column;
}

View File

@@ -550,59 +550,123 @@ const getIslandName = (islandId: number | null | undefined) => {
return islandMap.value[islandId] || `功能岛${islandId}`
}
// 获取设备列表
// 获取设备列表只查询非PLC设备每页显示10条
const getDeviceList = async () => {
try {
loading.value = true
const params: any = {
// 构建查询参数排除PLC设备
const baseParams: any = {
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
}
// 设备名称模糊查询
if (queryForm.devName) {
params.devName = queryForm.devName
baseParams.devName = queryForm.devName
}
// 设备状态精确查询
if (queryForm.status !== undefined && queryForm.status !== null) {
params.status = queryForm.status
baseParams.status = queryForm.status
}
// 所属功能岛精确查询
if (queryForm.islandId !== undefined && queryForm.islandId !== null) {
params.islandId = queryForm.islandId
baseParams.islandId = queryForm.islandId
}
// 排除 devModel 为 PLC 的设备
params.devModelNot = 'PLC'
const res: any = await devInfolist(params)
// 尝试使用 devModelNot 参数排除PLC设备
baseParams.devModelNot = 'PLC'
const data = res?.data ?? res ?? {}
// 收集所有非PLC设备直到凑够pageSize条
let allFilteredRecords: any[] = []
let currentPage = pagination.pageNum
let backendTotal = 0
let backendSupportsDevModelNot = false
// 兼容多种返回格式
const recordsFromData =
data.records ||
data.list ||
data.rows ||
data.items ||
(Array.isArray(data.data) ? data.data : undefined)
while (allFilteredRecords.length < pagination.pageSize) {
const params = {
...baseParams,
pageNum: currentPage,
pageSize: pagination.pageSize,
}
const res: any = await devInfolist(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 records = Array.isArray(recordsFromData)
? recordsFromData
: Array.isArray(data)
? data
: []
// 过滤掉 devModel 为 PLC 的设备(前端过滤作为备用)
const filteredRecords = records.filter((item: any) => {
const devModel = item.devModel || ''
return devModel.toUpperCase() !== 'PLC'
})
// 过滤掉 devModel 为 PLC 的设备
const filteredRecords = records.filter((item: any) => {
const devModel = item.devModel || ''
return devModel.toUpperCase() !== 'PLC'
})
const totalValue =
data.total ?? data.count ?? data.totalCount ?? filteredRecords.length ?? 0
// 第一次请求时,判断后端是否支持 devModelNot
if (currentPage === pagination.pageNum) {
backendTotal = data.total ?? data.count ?? data.totalCount ?? 0
backendSupportsDevModelNot = records.length === filteredRecords.length
}
deviceList.value = filteredRecords
total.value = Number(totalValue) || 0
// 添加到总列表
allFilteredRecords = [...allFilteredRecords, ...filteredRecords]
// 如果已经获取了足够的数据,或者没有更多数据了,退出循环
if (allFilteredRecords.length >= pagination.pageSize || records.length === 0) {
break
}
// 如果已经请求了所有数据,退出循环
if (currentPage * pagination.pageSize >= backendTotal && backendTotal > 0) {
break
}
currentPage++
}
// 只取pageSize条数据
deviceList.value = allFilteredRecords.slice(0, pagination.pageSize)
// 计算total
if (backendSupportsDevModelNot) {
// 后端支持 devModelNot直接使用后端返回的total
total.value = Number(backendTotal) || allFilteredRecords.length
} else {
// 后端不支持 devModelNot估算total
// 如果第一次请求有数据根据PLC比例估算
if (allFilteredRecords.length > 0 && backendTotal > 0) {
// 计算已请求的数据中PLC的比例
const totalRequested = (currentPage - pagination.pageNum + 1) * pagination.pageSize
const plcRatio = totalRequested > 0
? (totalRequested - allFilteredRecords.length) / totalRequested
: 0
const estimatedNonPlcTotal = Math.max(
Math.floor(backendTotal * (1 - plcRatio)),
allFilteredRecords.length
)
total.value = estimatedNonPlcTotal
} else {
total.value = allFilteredRecords.length
}
}
// 如果当前页没有非PLC设备且不是第一页跳转到上一页
if (deviceList.value.length === 0 && pagination.pageNum > 1) {
pagination.pageNum = pagination.pageNum - 1
await getDeviceList()
return
}
} catch (error) {
console.error('获取设备列表失败:', error)
ElMessage.error('获取设备列表失败')

View File

@@ -17,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>
@@ -154,7 +142,7 @@
clearable
/>
</el-form-item>
<el-form-item label="设备型号">
<el-form-item label="设备型号" prop="devModel">
<el-input
v-model="formData.devModel"
disabled
@@ -245,7 +233,6 @@ const total = ref(0)
// 查询表单
const queryForm = reactive({
devName: '',
status: undefined as number | undefined,
})
// 分页
@@ -272,13 +259,74 @@ const formData = reactive({
devDesc: '',
remark: '',
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' },
],
}
// 获取序号
@@ -301,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)
@@ -349,7 +393,6 @@ const handleSearch = () => {
// 重置查询
const resetSearch = () => {
queryForm.devName = ''
queryForm.status = undefined
handleSearch()
}
@@ -384,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()
}
@@ -406,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设备'
@@ -458,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