diff --git a/src/api/iot/device/index.ts b/src/api/iot/device/index.ts index dac795b3..4d5a2cd3 100644 --- a/src/api/iot/device/index.ts +++ b/src/api/iot/device/index.ts @@ -8,6 +8,7 @@ export interface DeviceVO { deviceType: string // 设备类型 status: string // 状态 isConnect?: string | number + operatingStatus?: string | number readTopic: string // 读主题 writeTopic: string // 写主题 gatewayId: number // 网关id @@ -24,6 +25,7 @@ export interface DeviceVO { password: string // 密码 certificate?: string // 证书 secretKey?: string // 秘钥 + collectionTime?: string | number } export interface DeviceConnectParams { diff --git a/src/api/iot/deviceOperationRecord/index.ts b/src/api/iot/deviceOperationRecord/index.ts new file mode 100644 index 00000000..7d601369 --- /dev/null +++ b/src/api/iot/deviceOperationRecord/index.ts @@ -0,0 +1,31 @@ +import request from '@/config/axios' + +export interface DeviceOperationRecordVO { + id: number + deviceCode: string + deviceName: string + totalRunningTime: number + totalStandbyTime: number + totalFaultTime: number + totalWarningTime: number + utilizationRate: string +} + +export interface DeviceOperationRecordPageParams { + pageNo: number + pageSize: number + deviceCode?: string + deviceName?: string + startTime?: string + endTime?: string + ids?: string +} + +export const DeviceOperationRecordApi = { + getDeviceOperationRecordPage: async (params: DeviceOperationRecordPageParams) => { + return await request.get({ url: `/iot/device-operation-record/deviceOperationPage`, params }) + }, + exportDeviceOperationReport: async (params: DeviceOperationRecordPageParams) => { + return await request.download({ url: `/iot/device-operation-record/export-device-operation-report`, params }) + } +} diff --git a/src/api/mold/moldrepair/index.ts b/src/api/mold/moldrepair/index.ts index 018fab09..62f19421 100644 --- a/src/api/mold/moldrepair/index.ts +++ b/src/api/mold/moldrepair/index.ts @@ -52,5 +52,13 @@ export const MoldRepairApi = { getMoldRepairLineListByRepairId: async (repairId) => { return await request.get({ url: `/mes/mold-repair/mold-repair-line/list-by-repair-id?repairId=` + repairId }) + }, + + getRepairListByMoldId: async (params: { moldId: number; startTime?: string; endTime?: string }) => { + return await request.get({ url: `/mes/mold-repair/getRepairListByMoldId`, params }) + }, + + exportRepairExcel: async (params: { moldId: number; startTime?: string; endTime?: string }) => { + return await request.download({ url: `/mes/mold-repair/export-repair-excel`, params }) } } diff --git a/src/api/mold/ticketManagement/index.ts b/src/api/mold/ticketManagement/index.ts index c6a2515d..ebb4137d 100644 --- a/src/api/mold/ticketManagement/index.ts +++ b/src/api/mold/ticketManagement/index.ts @@ -47,5 +47,21 @@ export const TicketManagementApi = { batchUpdateTicketResults: async (data: TicketResultVO[]) => { return await request.put({ url: `/mes/mold-ticket-results/batchUpdate`, data }) + }, + + getInspectionByMoldId: async (params: { moldId: number; startTime?: string; endTime?: string }) => { + return await request.get({ url: `/mes/mold-ticket-management/getInspectionByMoldId`, params }) + }, + + exportInspection: async (params: { moldId: number; startTime?: string; endTime?: string }) => { + return await request.download({ url: `/mes/mold-ticket-management/export-excel-inspection`, params }) + }, + + getMaintenanceByMoldId: async (params: { moldId: number; startTime?: string; endTime?: string }) => { + return await request.get({ url: `/mes/mold-ticket-management/getMaintenanceByMoldId`, params }) + }, + + exportMaintenance: async (params: { moldId: number; startTime?: string; endTime?: string }) => { + return await request.download({ url: `/mes/mold-ticket-management/export-excel-maintenance`, params }) } } diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue index 9c770072..25f8a709 100644 --- a/src/components/UploadFile/src/UploadFile.vue +++ b/src/components/UploadFile/src/UploadFile.vue @@ -234,8 +234,8 @@ const emitUpdateModelValue = () => { .ellipsis-text { display: inline-block; max-width: 100px; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 986b6360..383bfe5c 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -579,6 +579,37 @@ const remainingRouter: AppRouteRecordRaw[] = [ } ] }, + { + path: '/erp', + component: Layout, + name: 'ErpCenter', + meta: { hidden: true }, + children: [ + { + path: 'mold/detail/:id', + name: 'ErpMoldDetail', + meta: { + title: '模具详情', + noCache: true, + hidden: true, + activeMenu: '/erp/mold' + }, + component: () => import('@/views/erp/mold/detail/index.vue') + } + ] + }, + + { + path: '/iot/report/dashboardPage/Dashboard8', + component: () => import('@/views/report/dashboardPage/dashboard8/Dashboard8.vue'), + name: 'IotReportDashboard8', + meta: { + title: '智能制造产线任务总览', + hidden: true, + noTagsView: true, + canTo: true + } + }, { path: '/:pathMatch(.*)*', diff --git a/src/views/Home/Index.vue b/src/views/Home/Index.vue index 6c43dbbd..d53b0433 100644 --- a/src/views/Home/Index.vue +++ b/src/views/Home/Index.vue @@ -103,8 +103,8 @@ v-model="productionOverviewRange" type="daterange" unlink-panels value-format="Y - - + + @@ -778,13 +778,13 @@ getAllApi() } .home-welcome { - min-height: 240px; display: flex; - justify-content: space-between; - align-items: center; - background: linear-gradient(90deg, #3a6bc5 0%, #2a5298 100%); + min-height: 240px; padding: 0 24px; + background: linear-gradient(90deg, #3a6bc5 0%, #2a5298 100%); border-radius: 4px; + justify-content: space-between; + align-items: center; } .home-welcome-left { @@ -792,9 +792,9 @@ getAllApi() } .home-welcome-title { + margin-bottom: 8px; font-size: 32px; font-weight: 500; - margin-bottom: 8px; } .home-welcome-desc { @@ -847,13 +847,13 @@ getAllApi() } .home-welcome-image { - max-height: 180px; width: auto; + max-height: 180px; } .production-overview-row { - margin-top: 16px; display: flex; + margin-top: 16px; align-items: stretch; gap: 48px; } @@ -866,8 +866,9 @@ getAllApi() } .production-overview-group-center { - border-left: 1px solid var(--el-border-color-lighter); padding: 0 48px; + border-right: 1px solid var(--el-border-color-lighter); + border-left: 1px solid var(--el-border-color-lighter); } .production-overview-item { @@ -913,8 +914,8 @@ getAllApi() .stat-card { padding: 12px 16px; - border-radius: 6px; background-color: #f5f7fa; + border-radius: 6px; } .stat-label { @@ -963,18 +964,18 @@ getAllApi() } .progress-card { + display: flex; + height: 180px; padding: 12px 16px; - border-radius: 6px; background-color: #f5f7fa; - height: 190px; - display: flex; + border-radius: 6px; flex-direction: column; } .progress-card-header { + margin-bottom: 8px; font-size: 14px; font-weight: 600; - margin-bottom: 8px; } .progress-card-body { @@ -1008,8 +1009,8 @@ getAllApi() .mini-card { padding: 12px 16px; - border-radius: 6px; background-color: #f5f7fa; + border-radius: 6px; } .mini-label { @@ -1036,11 +1037,11 @@ getAllApi() } .todo-card { + display: flex; + height: 150px; padding: 12px 16px; - border-radius: 6px; background-color: #f5f7fa; - height: 150px; - display: flex; + border-radius: 6px; flex-direction: column; justify-content: space-between; } @@ -1056,7 +1057,7 @@ getAllApi() color: #909399; } -@media (max-width: 768px) { +@media (width <= 768px) { .home-welcome { flex-direction: column; align-items: flex-start; diff --git a/src/views/erp/mold/components/MoldList.vue b/src/views/erp/mold/components/MoldList.vue index 95d02c4a..37512b08 100644 --- a/src/views/erp/mold/components/MoldList.vue +++ b/src/views/erp/mold/components/MoldList.vue @@ -58,218 +58,6 @@ link type="primary" @click="openForm('update', scope.row.id)" - -
- - {{ detailData?.code ?? '' }} - {{ detailData?.name ?? '' }} - - - - {{ detailData?.brandName ?? '' }} - {{ detailData?.moldType ?? '' }} - - - - {{ detailData?.moldSize ?? '' }} - {{ detailData?.useTime ?? '' }} - {{ detailData?.machineName ?? detailData?.machineId ?? '' - }} - {{ formatDetailDate(detailData?.inTime) }} - - - - {{ detailData?.remark ?? '' }} - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- @@ -354,291 +142,10 @@ const recordFormRef = ref() const openRecordForm = (type: string, id?: number, brandId?: number) => { recordFormRef.value.open(type, id, brandId) } +const { push } = useRouter() -const detailVisible = ref(false) -const detailLoading = ref(false) -const detailData = ref<(MoldVO & Record) | null>(null) -const detailActiveTab = ref('check') - -const formatDetailDate = (value: any) => { - if (!value) return '' - return formatDate(new Date(value), 'YYYY-MM-DD') -} - -const formatHistoryTime = (value: any) => { - const raw = value ?? '-' - if (Array.isArray(raw) && raw.length >= 3) { - const [y, m, d, hh, mm, ss] = raw - const pad = (n: any) => String(n).padStart(2, '0') - if (hh !== undefined) return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}` - return `${y}-${pad(m)}-${pad(d)}` - } - return String(raw) -} - -const getResultLabel = (value: any) => { - const v = value === '' || value === null || value === undefined ? undefined : String(value) - if (v === '0') return '待检测' - if (v === '1') return 'OK' - if (v === '2') return 'NG' - return '-' -} - -const getResultTagType = (value: any) => { - const v = value === '' || value === null || value === undefined ? undefined : String(value) - if (v === '1') return 'success' - if (v === '2') return 'danger' - if (v === '0') return 'info' - return 'info' -} - -const inspectionHistory = computed(() => { - const data: any = detailData.value - return data?.inspectionList ?? [] -}) - -const maintainHistory = computed(() => { - const data: any = detailData.value - const list = data?.maintainList ?? [] - return Array.isArray(list) ? list : [] -}) - -type HistoryStepItem = { - key: string - name: string - result: any - method?: any - criteria?: any - images?: string[] - remark?: any - inspectionTime?: any - createTime?: any -} - -type HistoryStepGroup = { key: string; time: string; operator: string; items: HistoryStepItem[] } - -const parseImages = (value: any): string[] => { - if (!value) return [] - if (Array.isArray(value)) return value.map(String).filter(Boolean) - const cleaned = String(value).replace(/[`'\"]/g, '').trim() - return cleaned - .split(',') - .map((v) => v.trim()) - .filter(Boolean) -} - -const buildStepGroups = ( - rows: any[], - options: { timeField: string; nameFieldCandidates: string[]; resultFieldCandidates: string[] } -) => { - const groups = new Map() - - for (const row of rows ?? []) { - const time = formatHistoryTime(row?.[options.timeField] ?? row?.createTime) - const operator = String(row?.creatorName ?? row?.creator ?? '-') - const groupKey = `${row?.managementId ?? ''}__${time}__${operator}` - - const name = - options.nameFieldCandidates - .map((k) => row?.[k]) - .find((v) => v !== undefined && v !== null && String(v).trim() !== '') ?? '-' - - const result = - options.resultFieldCandidates.map((k) => row?.[k]).find((v) => v !== undefined && v !== null) ?? undefined - - const item: HistoryStepItem = { - key: String(row?.id ?? `${groupKey}__${String(name)}`), - name: String(name), - result, - method: row?.inspectionMethod, - criteria: row?.judgmentCriteria, - images: parseImages(row?.images), - remark: row?.remark, - inspectionTime: row?.inspectionTime, - createTime: row?.createTime - } - - if (!groups.has(groupKey)) { - groups.set(groupKey, { key: groupKey, time, operator, items: [item] }) - } else { - groups.get(groupKey)!.items.push(item) - } - } - - return Array.from(groups.values()).sort((a, b) => String(b.time).localeCompare(String(a.time))) -} - -const inspectionStepGroups = computed(() => { - return buildStepGroups(inspectionHistory.value, { - timeField: 'inspectionTime', - nameFieldCandidates: ['inspectionItemName', 'name'], - resultFieldCandidates: ['inspectionResult'] - }) -}) - -const maintainStepGroups = computed(() => { - return buildStepGroups(maintainHistory.value, { - timeField: 'inspectionTime', - nameFieldCandidates: ['maintainItemName', 'inspectionItemName', 'name'], - resultFieldCandidates: ['maintainResult', 'inspectionResult'] - }) -}) - -type RepairHistoryRow = { - repairId?: any - subjectCode?: any - subjectName?: any - subjectContent?: any - subjectStandard?: any - malfunction?: any - repairDes?: any - remark?: any - createTime?: any -} - -const repairHistoryView = computed(() => { - const data: any = detailData.value - const raw = data?.repairList - if (!raw) return [] - if (Array.isArray(raw)) { - return raw as RepairHistoryRow[] - } - if (typeof raw === 'object') { - const result: RepairHistoryRow[] = [] - Object.values(raw as Record).forEach((rows: any) => { - if (Array.isArray(rows)) { - result.push(...(rows as RepairHistoryRow[])) - } - }) - return result - } - return [] -}) - -const openDetail = async (id: number) => { - detailVisible.value = true - detailActiveTab.value = 'check' - detailLoading.value = true - try { - detailData.value = await MoldBrandApi.getMold(id) - } finally { - detailLoading.value = false - } +const openDetail = (id: number) => { + push({ name: 'ErpMoldDetail', params: { id } }) } - - diff --git a/src/views/erp/mold/detail/index.vue b/src/views/erp/mold/detail/index.vue new file mode 100644 index 00000000..01fd7ae2 --- /dev/null +++ b/src/views/erp/mold/detail/index.vue @@ -0,0 +1,830 @@ + + + + diff --git a/src/views/formula/formulaConfig/index.vue b/src/views/formula/formulaConfig/index.vue index 4fbb86ad..78f339b4 100644 --- a/src/views/formula/formulaConfig/index.vue +++ b/src/views/formula/formulaConfig/index.vue @@ -595,23 +595,23 @@ onMounted(() => { .formula-config-panel { width: calc((100% - 96px) / 2); - border: 1px solid var(--el-border-color); - border-radius: 4px; overflow: hidden; background: var(--el-bg-color); + border: 1px solid var(--el-border-color); + border-radius: 4px; } .formula-config-panel__header { display: flex; - justify-content: space-between; padding: 10px 12px; - border-bottom: 1px solid var(--el-border-color); font-weight: 600; + border-bottom: 1px solid var(--el-border-color); + justify-content: space-between; } .formula-config-panel__meta { - color: var(--el-text-color-secondary); font-weight: 400; + color: var(--el-text-color-secondary); } .formula-config-panel__filter { @@ -624,8 +624,8 @@ onMounted(() => { } .formula-config-actions { - width: 96px; display: flex; + width: 96px; flex-direction: column; justify-content: center; align-items: center; diff --git a/src/views/iot/device/DeviceForm.vue b/src/views/iot/device/DeviceForm.vue index c40f6c77..c61a80f3 100644 --- a/src/views/iot/device/DeviceForm.vue +++ b/src/views/iot/device/DeviceForm.vue @@ -168,9 +168,11 @@ const formRules = reactive({ create: { deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }], deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }], + sampleCycle: [{ required: true, message: '采集周期不能为空', trigger: 'blur' }], isEnable: [{ required: true, message: '是否启用不能为空', trigger: 'blur' }] }, update: { + sampleCycle: [{ required: true, message: '采集周期不能为空', trigger: 'blur' }], isEnable: [{ required: true, message: '是否启用不能为空', trigger: 'blur' }] }, setting: { diff --git a/src/views/iot/device/index.vue b/src/views/iot/device/index.vue index 854b40c1..073f642d 100644 --- a/src/views/iot/device/index.vue +++ b/src/views/iot/device/index.vue @@ -70,22 +70,32 @@ ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-to + + + - + - + - + - + + @@ -158,6 +434,7 @@ link :type="isRowConnected(scope.row) ? 'warning' : 'success'" import { getStrDictOptions, DICT_TYPE } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import download from '@/utils/download' +import request from '@/config/axios' import { DeviceApi, DeviceConnectParams, DeviceVO } from '@/api/iot/device' import DeviceForm from './DeviceForm.vue' import DeviceAttributeList from './components/DeviceAttributeList.vue' @@ -194,20 +471,40 @@ const queryParams = reactive({ const queryFormRef = ref() // 搜索的表单 const exportLoading = ref(false) // 导出的加载中 +const getOperatingStatusLabel = (value: string | number | undefined) => { + const text = String(value ?? '').trim() + if (!text) return '离线' + return text +} + +const getOperatingStatusType = (value: string | number | undefined) => { + const text = String(value ?? '').trim() + if (!text) return 'info' + if (text === '运行') return 'success' + if (text === '待机中') return 'info' + if (text === '故障中') return 'danger' + if (text === '报警中') return 'warning' + return 'info' +} + const selectedIds = ref([]) const handleSelectionChange = (rows: any[]) => { selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? [] } /** 查询列表 */ -const getList = async () => { - loading.value = true +const getList = async (showLoading = true) => { + if (showLoading) { + loading.value = true + } try { const data = await DeviceApi.getDevicePage(queryParams) list.value = data.list total.value = data.total } finally { - loading.value = false + if (showLoading) { + loading.value = false + } } } @@ -288,14 +585,30 @@ const handleExport = async () => { } } +interface DevicePointRuleVO { + id: number + identifier: string + fieldName: string + fieldRule: string + defaultValue: string + deviceId: number + createTime?: string | number | Date + ruleAttributeId?: number + ruleOperator?: string + ruleValue?: string | number +} + const attributeDeviceId = ref(undefined) const attributeDeviceName = ref('') +const deviceTabActive = ref('deviceAttribute') + const deviceAttributeTabLabel = computed(() => { - if (!attributeDeviceId.value) { - return '设备属性' - } - return attributeDeviceName.value ? `设备属性:${attributeDeviceName.value}` : '设备属性' + return '设备属性' +}) + +const deviceRuleTabLabel = computed(() => { + return '点位规则' }) const handleShowAttribute = (row: any) => { @@ -303,6 +616,333 @@ const handleShowAttribute = (row: any) => { attributeDeviceName.value = row?.deviceName ?? '' } +const ruleLoading = ref(false) +const ruleList = ref([]) +const ruleTotal = ref(0) +const ruleQueryParams = reactive({ + pageNo: 1, + pageSize: 10, + identifier: undefined as string | undefined, + fieldName: undefined as string | undefined, + fieldRule: undefined as string | undefined, + defaultValue: undefined as string | undefined, + deviceId: undefined as number | undefined, +}) +const ruleQueryFormRef = ref() + +const ruleAttributeOptions = ref([]) + +const getRuleList = async () => { + if (!attributeDeviceId.value) return + ruleLoading.value = true + try { + const params = { + ...ruleQueryParams, + deviceId: attributeDeviceId.value, + } + const data = await request.get({ url: '/iot/device-point-rules/page', params }) + const listData = Array.isArray((data as any)?.list) ? (data as any).list : (Array.isArray(data) ? data : []) + const totalData = (data as any)?.total ?? listData.length + ruleList.value = listData as DevicePointRuleVO[] + ruleTotal.value = totalData + } finally { + ruleLoading.value = false + } +} + +const handleRuleQuery = () => { + if (!attributeDeviceId.value) return + ruleQueryParams.pageNo = 1 + getRuleList() +} + +const resetRuleQuery = () => { + if (!attributeDeviceId.value) return + ruleQueryFormRef.value?.resetFields?.() + handleRuleQuery() +} + +const loadRuleAttributeOptions = async (deviceId: number) => { + try { + const res = await request.get({ url: '/iot/device-contact-model/list', params: { id: deviceId } }) + const data = Array.isArray(res) ? res : (res as any)?.list ?? [] + ruleAttributeOptions.value = data as any[] + } catch { + ruleAttributeOptions.value = [] + } +} + +watch( + () => attributeDeviceId.value, + async (val) => { + ruleQueryParams.deviceId = val ?? undefined + if (!val) { + ruleList.value = [] + ruleTotal.value = 0 + ruleAttributeOptions.value = [] + return + } + ruleQueryParams.pageNo = 1 + await getRuleList() + } +) + +const ruleDialogVisible = ref(false) +const ruleFormLoading = ref(false) +const ruleFormRef = ref() +const ruleForm = reactive>({ + id: undefined, + identifier: '', + fieldName: '', + fieldRule: '', + defaultValue: '', + deviceId: undefined, + ruleAttributeId: undefined, + ruleOperator: undefined, + ruleValue: undefined, +}) + +const extraPointRules = ref< + Array<{ + id?: number + rule?: string + operator?: string + operatorRule?: string | number + }> +>([]) + +const runningRuleOptions = [ + { value: '1', label: '运行' }, + { value: '2', label: '待机中(不运行、没故障)' }, + { value: '3', label: '故障中(故障且待机)' }, + { value: '4', label: '报警中(故障且运行)' }, +] + +const alarmRuleOptions = [{ value: '5', label: '报警' }] + +const currentRuleOptions = computed(() => { + const id = (ruleForm.identifier || '').toString().toUpperCase() + if (id === 'RUNNING') return runningRuleOptions + if (id === 'ALARM') return alarmRuleOptions + return [] +}) + +const isRuleDisabled = (value: string, selfRule?: string | number) => { + if (!value) return false + const v = String(value) + const self = selfRule != null ? String(selfRule) : undefined + if (self === v) return false + if (ruleForm.fieldRule && String(ruleForm.fieldRule) === v && self !== v) return true + if ( + extraPointRules.value.some( + (item) => item.rule != null && String(item.rule) === v && String(item.rule) !== self + ) + ) { + return true + } + return false +} + +const ruleOperatorOptions = computed(() => getStrDictOptions('czsb_rules_conditions')) + +const isRunningIdentifier = computed(() => { + return (ruleForm.identifier || '').toString().toUpperCase() === 'RUNNING' +}) + +const openRuleForm = async (row: DevicePointRuleVO & { pointRulesVOList?: any[] }) => { + const deviceId = row.deviceId || attributeDeviceId.value + if (!deviceId) { + message.error('设备信息缺失,无法加载点位规则') + return + } + + await loadRuleAttributeOptions(deviceId) + + ruleForm.id = row.id + ruleForm.identifier = row.identifier + ruleForm.fieldName = row.fieldName + ruleForm.defaultValue = row.defaultValue + ruleForm.deviceId = row.deviceId + + extraPointRules.value = [] + + const list = Array.isArray(row.pointRulesVOList) ? row.pointRulesVOList : [] + + if (list.length) { + const first = list[0] as any + const firstRule = first.rule ?? row.fieldRule + const firstAttrId = (() => { + if (first && first.code != null) { + const target = ruleAttributeOptions.value.find( + (item) => item.attributeCode === first.code + ) + return target?.id + } + if (first && first.id != null) return first.id + return undefined + })() + ruleForm.fieldRule = firstRule as any + ruleForm.ruleAttributeId = firstAttrId as any + ruleForm.ruleOperator = first.operator as any + ruleForm.ruleValue = first.operatorRule as any + extraPointRules.value = list.slice(1).map((item: any) => ({ + id: (() => { + if (item && item.code != null) { + const target = ruleAttributeOptions.value.find( + (opt) => opt.attributeCode === item.code + ) + return target?.id + } + if (item && item.id != null) return item.id + return undefined + })(), + rule: item.rule, + operator: item.operator, + operatorRule: item.operatorRule, + })) + } else { + ruleForm.fieldRule = row.fieldRule + ruleForm.ruleAttributeId = row.ruleAttributeId + ruleForm.ruleOperator = row.ruleOperator + ruleForm.ruleValue = row.ruleValue as any + } + + const options = currentRuleOptions.value + if (options.length && !options.some((item) => item.value === ruleForm.fieldRule)) { + ruleForm.fieldRule = options[0].value + } + ruleDialogVisible.value = true +} + +const handleRuleSubmit = async () => { + if (!ruleForm.id) return + try { + ruleFormLoading.value = true + const pointRulesVOList: any[] = [] + + if (ruleForm.ruleAttributeId && ruleForm.ruleOperator) { + const attr = ruleAttributeOptions.value.find( + (item) => item.id === ruleForm.ruleAttributeId + ) + if (attr && attr.attributeCode) { + pointRulesVOList.push({ + code: attr.attributeCode, + rule: ruleForm.fieldRule, + operator: ruleForm.ruleOperator, + operatorRule: ruleForm.ruleValue, + }) + } + } + + extraPointRules.value.forEach((item) => { + if (!item.id || !item.operator) return + const attr = ruleAttributeOptions.value.find((opt) => opt.id === item.id) + if (!attr || !attr.attributeCode) return + pointRulesVOList.push({ + code: attr.attributeCode, + rule: item.rule ?? ruleForm.fieldRule, + operator: item.operator, + operatorRule: item.operatorRule, + }) + }) + + const payload = { + id: ruleForm.id, + identifier: ruleForm.identifier, + fieldName: ruleForm.fieldName, + fieldRule: ruleForm.fieldRule, + defaultValue: ruleForm.defaultValue, + deviceId: ruleForm.deviceId ?? attributeDeviceId.value, + pointRulesVOList, + } + + await request.put({ url: '/iot/device-point-rules/update', data: payload }) + message.success('保存成功') + ruleDialogVisible.value = false + await getRuleList() + } finally { + ruleFormLoading.value = false + } +} + +const handleRuleDelete = async (id: number) => { + if (!id) return + try { + await message.delConfirm() + await request.delete({ url: '/iot/device-point-rules/delete?id=' + id }) + message.success(t('common.delSuccess')) + await getRuleList() + } catch { } +} + +const handleAddPointRule = () => { + if (!isRunningIdentifier.value) return + const allOptions = currentRuleOptions.value || [] + const used = new Set() + if (ruleForm.fieldRule) used.add(String(ruleForm.fieldRule)) + extraPointRules.value.forEach((item) => { + if (item.rule != null) used.add(String(item.rule)) + }) + const next = allOptions.find((opt) => !used.has(String(opt.value))) + if (!next) { + message.warning('已没有可用的点位规则可选') + return + } + extraPointRules.value.push({ + rule: next.value as any, + id: undefined, + operator: undefined, + operatorRule: undefined, + }) +} + +const handleRemovePointRule = (index: number) => { + if (index < 0 || index >= extraPointRules.value.length) return + extraPointRules.value.splice(index, 1) +} + +const createRuleDialogVisible = ref(false) +const createRuleFormLoading = ref(false) +const createRuleFormRef = ref() +const createRuleForm = reactive({ + identifier: 'ALARM', + fieldName: '', + defaultValue: '报警', +}) + +const openCreateRuleForm = () => { + if (!attributeDeviceId.value) { + message.error('请先选择设备') + return + } + createRuleForm.identifier = 'ALARM' + createRuleForm.fieldName = '报警' + createRuleForm.defaultValue = '报警' + createRuleDialogVisible.value = true +} + +const handleCreateRuleSubmit = async () => { + if (!attributeDeviceId.value) return + if (!createRuleForm.fieldName) { + message.warning('请输入名称') + return + } + try { + createRuleFormLoading.value = true + const payload = { + identifier: createRuleForm.identifier, + fieldName: createRuleForm.fieldName, + defaultValue: createRuleForm.defaultValue, + deviceId: attributeDeviceId.value, + } + await request.post({ url: '/iot/device-point-rules/create', data: payload }) + message.success('新增成功') + createRuleDialogVisible.value = false + await getRuleList() + } finally { + createRuleFormLoading.value = false + } +} const connectLoadingMap = reactive>({}) const getRowConnectValue = (row: any) => { @@ -345,9 +985,36 @@ const handleToggleConnect = async (row: DeviceVO) => { } } -/** 初始化 **/ +let deviceTimer: number | undefined + +const startDeviceTimer = () => { + if (deviceTimer) return + deviceTimer = window.setInterval(() => { + getList(false) + }, 5000) +} + +const stopDeviceTimer = () => { + if (!deviceTimer) return + window.clearInterval(deviceTimer) + deviceTimer = undefined +} + onMounted(() => { getList() + startDeviceTimer() +}) + +onActivated(() => { + startDeviceTimer() +}) + +onDeactivated(() => { + stopDeviceTimer() +}) + +onBeforeUnmount(() => { + stopDeviceTimer() }) diff --git a/src/views/iot/devicemodel/index.vue b/src/views/iot/devicemodel/index.vue index 1b8cf46c..67fd636e 100644 --- a/src/views/iot/devicemodel/index.vue +++ b/src/views/iot/devicemodel/index.vue @@ -149,6 +149,12 @@ link type="primary" @click="openForm('update', scope.row.id)" +
+ + 新增告警规则 + +
+ - + @@ -189,7 +203,7 @@ link type="primary" @click="openForm('update', scope.row.id)"
-
+
+ + + + + + + + + + + + + + + @@ -546,6 +584,15 @@ const ruleForm = reactive>({ ruleValue: undefined, }) +const createRuleDialogVisible = ref(false) +const createRuleFormLoading = ref(false) +const createRuleFormRef = ref() +const createRuleForm = reactive({ + identifier: 'ALARM', + fieldName: '', + defaultValue: '报警', +}) + const extraPointRules = ref< Array<{ id?: number @@ -563,7 +610,7 @@ const runningRuleOptions = [ ] const alarmRuleOptions = [ - { value: 'ALARM', label: '报警' }, + { value: '5', label: '报警' }, ] const currentRuleOptions = computed(() => { @@ -589,19 +636,32 @@ const isRuleDisabled = (value: string, selfRule?: string | number) => { return false } -const ruleOperatorOptions = [ - { value: 'TRUE', label: '为真' }, - { value: 'FALSE', label: '为假' }, - { value: 'EQ', label: '等于' }, - { value: 'LE', label: '小于等于' }, - { value: 'GE', label: '大于等于' }, -] +const ruleOperatorOptions = computed(() => getStrDictOptions('czsb_rules_conditions')) const isRunningIdentifier = computed(() => { return (ruleForm.identifier || '').toString().toUpperCase() === 'RUNNING' }) -const openRuleForm = (row: DeviceModelRuleVO & { pointRulesVOList?: any[] }) => { +const openCreateRuleForm = () => { + if (!attributeModelId.value) { + message.error('请先选择模型') + return + } + createRuleForm.identifier = 'ALARM' + createRuleForm.fieldName = '报警' + createRuleForm.defaultValue = '报警' + createRuleDialogVisible.value = true +} + +const openRuleForm = async (row: DeviceModelRuleVO & { pointRulesVOList?: any[] }) => { + const modelId = row.modelId || attributeModelId.value + if (!modelId) { + message.error('模型信息缺失,无法加载点位规则') + return + } + + await loadRuleAttributeOptions(modelId) + ruleForm.id = row.id ruleForm.identifier = row.identifier ruleForm.fieldName = row.fieldName @@ -614,12 +674,32 @@ const openRuleForm = (row: DeviceModelRuleVO & { pointRulesVOList?: any[] }) => if (list.length) { const first = list[0] as any - ruleForm.fieldRule = (first.rule ?? row.fieldRule) as any - ruleForm.ruleAttributeId = first.id as any + const firstRule = first.rule ?? row.fieldRule + const firstAttrId = (() => { + if (first && first.code != null) { + const target = ruleAttributeOptions.value.find( + (item) => item.attributeCode === first.code + ) + return target?.id + } + if (first && first.id != null) return first.id + return undefined + })() + ruleForm.fieldRule = firstRule as any + ruleForm.ruleAttributeId = firstAttrId as any ruleForm.ruleOperator = first.operator as any ruleForm.ruleValue = first.operatorRule as any extraPointRules.value = list.slice(1).map((item: any) => ({ - id: item.id, + id: (() => { + if (item && item.code != null) { + const target = ruleAttributeOptions.value.find( + (opt) => opt.attributeCode === item.code + ) + return target?.id + } + if (item && item.id != null) return item.id + return undefined + })(), rule: item.rule, operator: item.operator, operatorRule: item.operatorRule, @@ -645,18 +725,25 @@ const handleRuleSubmit = async () => { const pointRulesVOList: any[] = [] if (ruleForm.ruleAttributeId && ruleForm.ruleOperator) { + const attr = ruleAttributeOptions.value.find( + (item) => item.id === ruleForm.ruleAttributeId + ) + if (attr && attr.attributeCode) { pointRulesVOList.push({ - id: ruleForm.ruleAttributeId, + code: attr.attributeCode, rule: ruleForm.fieldRule, operator: ruleForm.ruleOperator, operatorRule: ruleForm.ruleValue, }) + } } extraPointRules.value.forEach((item) => { if (!item.id || !item.operator) return + const attr = ruleAttributeOptions.value.find((opt) => opt.id === item.id) + if (!attr || !attr.attributeCode) return pointRulesVOList.push({ - id: item.id, + code: attr.attributeCode, rule: item.rule ?? ruleForm.fieldRule, operator: item.operator, operatorRule: item.operatorRule, @@ -667,7 +754,6 @@ const handleRuleSubmit = async () => { id: ruleForm.id, identifier: ruleForm.identifier, fieldName: ruleForm.fieldName, - fieldRule: ruleForm.fieldRule, defaultValue: ruleForm.defaultValue, modelId: ruleForm.modelId, pointRulesVOList, @@ -682,6 +768,16 @@ const handleRuleSubmit = async () => { } } +const handleRuleDelete = async (id: number) => { + if (!id) return + try { + await message.delConfirm() + await request.delete({ url: '/iot/device-model-rules/delete?id=' + id }) + message.success(t('common.delSuccess')) + await getRuleList() + } catch { } +} + const handleAddPointRule = () => { if (!isRunningIdentifier.value) return const allOptions = currentRuleOptions.value || [] @@ -703,6 +799,29 @@ const handleAddPointRule = () => { }) } +const handleCreateRuleSubmit = async () => { + if (!attributeModelId.value) return + if (!createRuleForm.fieldName) { + message.warning('请输入名称') + return + } + try { + createRuleFormLoading.value = true + const payload = { + identifier: createRuleForm.identifier, + fieldName: createRuleForm.fieldName, + defaultValue: createRuleForm.defaultValue, + modelId: attributeModelId.value, + } + await request.post({ url: '/iot/device-model-rules/create', data: payload }) + message.success('新增成功') + createRuleDialogVisible.value = false + await getRuleList() + } finally { + createRuleFormLoading.value = false + } +} + const handleRemovePointRule = (index: number) => { if (index < 0 || index >= extraPointRules.value.length) return extraPointRules.value.splice(index, 1) diff --git a/src/views/iot/runReport/index.vue b/src/views/iot/runReport/index.vue new file mode 100644 index 00000000..0eafdc64 --- /dev/null +++ b/src/views/iot/runReport/index.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/views/mes/deviceledger/index.vue b/src/views/mes/deviceledger/index.vue index 0d8338f4..4bc3189f 100644 --- a/src/views/mes/deviceledger/index.vue +++ b/src/views/mes/deviceledger/index.vue @@ -265,7 +265,7 @@ v-else direction="vertical" class="device-ledger-history-steps"
保养时间 - {{ String(formatHistoryTime(item.taskTime)).split(' ')[0] }} + {{ String(formatHistoryTime(item.taskTime))}}
@@ -533,7 +533,7 @@ type HistoryStepItem = { criteria?: any images?: string[] remark?: any - inspectionTime?: any + taskTime?: any createTime?: any } type HistoryStepGroup = { key: string; time: string; operator: string; items: HistoryStepItem[] } @@ -542,7 +542,7 @@ const buildStepGroups = (rows: any[], options: { timeField: string; nameFieldCan const groups = new Map() for (const row of rows ?? []) { - const time = formatHistoryTime(row?.[options.timeField] ?? row?.createTime) + const time = formatHistoryTime(row?.taskTime ?? row?.[options.timeField] ?? row?.createTime) const operator = String(row?.operator ?? row?.creatorName ?? row?.creator ?? '-') const groupKey = `${row?.managementId ?? ''}__${time}__${operator}` @@ -1121,12 +1121,12 @@ onMounted(async () => { .device-ledger-history-item { display: flex; - flex-direction: column; - gap: 8px; padding: 10px; + background: var(--el-fill-color-blank); border: 1px solid var(--el-border-color-lighter); border-radius: 8px; - background: var(--el-fill-color-blank); + flex-direction: column; + gap: 8px; } .device-ledger-history-item-head { @@ -1169,19 +1169,19 @@ onMounted(async () => { .device-ledger-history-item-image { width: 76px; height: 76px; - border-radius: 6px; overflow: hidden; + border-radius: 6px; } .device-ledger-history-image-error { + display: flex; width: 100%; height: 100%; - display: flex; - align-items: center; - justify-content: center; font-size: 12px; color: var(--el-text-color-secondary); background: var(--el-fill-color); + align-items: center; + justify-content: center; } .device-ledger-repair-collapse { @@ -1201,8 +1201,8 @@ onMounted(async () => { } .device-ledger-repair-meta { - color: var(--el-text-color-secondary); font-size: 12px; + color: var(--el-text-color-secondary); } .device-ledger-history-item-text { diff --git a/src/views/mes/energydevicecheck/index.vue b/src/views/mes/energydevicecheck/index.vue index 09e1781d..d3681adb 100644 --- a/src/views/mes/energydevicecheck/index.vue +++ b/src/views/mes/energydevicecheck/index.vue @@ -37,7 +37,7 @@ v-model="queryParams.timeRange" type="datetimerange" value-format="YYYY-MM-DD HH diff --git a/src/views/mes/zjschema/ZjSchemaForm.vue b/src/views/mes/zjschema/ZjSchemaForm.vue index 2ad687e6..79b6421a 100644 --- a/src/views/mes/zjschema/ZjSchemaForm.vue +++ b/src/views/mes/zjschema/ZjSchemaForm.vue @@ -415,23 +415,23 @@ const resetForm = () => { .formula-config-panel { width: calc((100% - 96px) / 2); - border: 1px solid var(--el-border-color); - border-radius: 4px; overflow: hidden; background: var(--el-bg-color); + border: 1px solid var(--el-border-color); + border-radius: 4px; } .formula-config-panel__header { display: flex; - justify-content: space-between; padding: 10px 12px; - border-bottom: 1px solid var(--el-border-color); font-weight: 600; + border-bottom: 1px solid var(--el-border-color); + justify-content: space-between; } .formula-config-panel__meta { - color: var(--el-text-color-secondary); font-weight: 400; + color: var(--el-text-color-secondary); } .formula-config-panel__filter { @@ -444,8 +444,8 @@ const resetForm = () => { } .formula-config-actions { - width: 96px; display: flex; + width: 96px; flex-direction: column; justify-content: center; align-items: center; diff --git a/src/views/report/dashboardList/index.vue b/src/views/report/dashboardList/index.vue new file mode 100644 index 00000000..b97115e6 --- /dev/null +++ b/src/views/report/dashboardList/index.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/Dashboard8.vue b/src/views/report/dashboardPage/dashboard8/Dashboard8.vue new file mode 100644 index 00000000..5c1e3c66 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/Dashboard8.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/DashboardHeader.vue b/src/views/report/dashboardPage/dashboard8/components/DashboardHeader.vue new file mode 100644 index 00000000..7bde15b7 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/DashboardHeader.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/DayCapacity.vue b/src/views/report/dashboardPage/dashboard8/components/DayCapacity.vue new file mode 100644 index 00000000..f8a0d240 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/DayCapacity.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/EnergyTrend.vue b/src/views/report/dashboardPage/dashboard8/components/EnergyTrend.vue new file mode 100644 index 00000000..156e192a --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/EnergyTrend.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/MonthCapacity.vue b/src/views/report/dashboardPage/dashboard8/components/MonthCapacity.vue new file mode 100644 index 00000000..82c10cb8 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/MonthCapacity.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/OpsTrend.vue b/src/views/report/dashboardPage/dashboard8/components/OpsTrend.vue new file mode 100644 index 00000000..277c305b --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/OpsTrend.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/QualityTrend.vue b/src/views/report/dashboardPage/dashboard8/components/QualityTrend.vue new file mode 100644 index 00000000..8f732241 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/QualityTrend.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/RealAlarm.vue b/src/views/report/dashboardPage/dashboard8/components/RealAlarm.vue new file mode 100644 index 00000000..c41113eb --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/RealAlarm.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/TaskBoard.vue b/src/views/report/dashboardPage/dashboard8/components/TaskBoard.vue new file mode 100644 index 00000000..d8048d39 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/TaskBoard.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/TodayOps.vue b/src/views/report/dashboardPage/dashboard8/components/TodayOps.vue new file mode 100644 index 00000000..b8e098ab --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/TodayOps.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/components/WeekTrend.vue b/src/views/report/dashboardPage/dashboard8/components/WeekTrend.vue new file mode 100644 index 00000000..7bcd1f72 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/components/WeekTrend.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/dashboard8.html b/src/views/report/dashboardPage/dashboard8/dashboard8.html new file mode 100644 index 00000000..b5fd6196 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/dashboard8.html @@ -0,0 +1,1243 @@ + + + + + + 智能制造产线可视化看板 + + + + + +
+
+ +
+
+
+
+ +
+
+
智能制造产线任务总览
+
INTELLIGENT MANUFACTURING REAL-TIME DASHBOARD
+
+
+
+ + + + + + + 当前在线产线 10 + +
+
+
+ +
+
+ +
+ +
+
+
+ + 产线任务看板 +
+
+ 实时刷新 · 滚动展示 + 已完成 + 进度偏低 +
+
+
+
+ + + + + + + + + + + + +
产线名称排产单产品名称计划数量完工数量完工率
+
+
+
+ + +
+
+
+ + 日产能达成情况 +
+ 当日维度 +
+
+
+
+
+
+
+
+
+
排产单数量
+
+
+
+
+
排产数量
+
+
+
+
+
待生产数量
+
+
+
+
+
产能达成率
+
+
目标 ≥ 80%
+
+
+
+
+
+
+ + +
+
+
+ + 月产能达成情况 +
+ 当月累计 +
+
+
+
+
+
+
+
+
+
排产单数量
+
+
+
+
+
排产数量
+
+
+
+
+
待生产数量
+
+
+
+
+
产能达成率
+
+
目标 ≥ 85%
+
+
+
+
+
+
+
+ + +
+ +
+
+
+ + 周生产趋势 +
+
+ 产量 + 计划 +
+
+
+
+
+
+ + +
+
+
+ + 开机率/稼动率趋势 +
+
+
+ 历史七天(不含当天) +
+
+
+
+
+
+ + +
+
+
+ + 今日开机率/稼动率 +
+ 产线A +
+
+
+
+
+
+ + +
+ +
+
+
+ + 成品检合格率趋势图 +
+ 按天统计(全产线) +
+
+
+
+
+ + +
+
+
+ + 实时报警信息 +
+ 滚动展示 +
+
+
    +
    +
    + + +
    +
    +
    + + 能耗周趋势 +
    + 本周能耗对比 +
    +
    +
    +
    +
    +
    +
    +
    + + + + + diff --git a/src/views/report/dashboardPage/dashboard8/utils.ts b/src/views/report/dashboardPage/dashboard8/utils.ts new file mode 100644 index 00000000..2ba468b1 --- /dev/null +++ b/src/views/report/dashboardPage/dashboard8/utils.ts @@ -0,0 +1,57 @@ +import { onUnmounted } from 'vue' +import * as echarts from 'echarts' + +export const colors = { + blue: '#1e90ff', + cyan: '#22d3ee', + green: '#22c55e', + purple: '#8b5cf6', + warn: '#f59e0b', + danger: '#ef4444' +} + +export const style = { + axisLine: { lineStyle: { color: 'rgba(148,163,184,0.45)' } }, + axisLabel: { color: '#a8b7d8', fontSize: 12 }, + splitLine: { lineStyle: { color: 'rgba(30,64,175,0.55)', type: 'dashed' } }, + legendText: { color: '#e5f0ff', fontSize: 12 } +} + +export function animateCount(el: HTMLElement | null, target: number, suffix: string, duration: number) { + if (!el) return + const start = 0 + const t0 = performance.now() + const step = (t: number) => { + const p = Math.min(1, (t - t0) / duration) + const v = start + (target - start) * p + const out = Number.isInteger(target) ? Math.round(v) : Math.round(v * 10) / 10 + el.textContent = suffix ? `${out}${suffix}` : `${out}` + if (p < 1) requestAnimationFrame(step) + } + requestAnimationFrame(step) +} + +export function useChart(domId: string) { + let chart: echarts.ECharts | null = null + + const init = () => { + const el = document.getElementById(domId) + if (!el) return null + chart = echarts.init(el, 'dark', { renderer: 'canvas' }) + return chart + } + + const resize = () => { + chart?.resize() + } + + onUnmounted(() => { + chart?.dispose() + }) + + return { + init, + resize, + instance: () => chart + } +}