Compare commits

...

8 Commits

@ -73,8 +73,8 @@ getPlan: async () => {
getTodoList: async () => {
return await request.get({ url: `/mes/dashboard/getTodoList` })
},
getDeviceOperationalStatus: async () => {
return await request.get({ url: `/iot/device/getDeviceOperationalStatus` })
getDeviceOperationalStatus: async (params?: any) => {
return await request.get({ url: `/iot/device/getDeviceOperationalStatus`, params })
},
getDeviceRepairLineOptions: async () => {
return await request.get({ url: `/mes/dashboard/getDeviceRepairLineOptions` })
@ -85,11 +85,11 @@ getDeviceTypePieOptions: async () => {
getMoldTypeBarOptions: async () => {
return await request.get({ url: `/mes/dashboard/getMoldTypeBarOptions` })
},
getTaskStatistics: async () => {
return await request.get<TaskStatisticsResponse>({ url: `/mes/dashboard/getTaskStatistics` })
getTaskStatistics: async (params?: any) => {
return await request.get<TaskStatisticsResponse>({ url: `/mes/dashboard/getTaskStatistics`, params })
},
getAllTaskList: async () => {
return await request.get<DashboardTaskListResponse>({ url: `/mes/dashboard/getAllTaskList` })
getAllTaskList: async (params?: any) => {
return await request.get<DashboardTaskListResponse>({ url: `/mes/dashboard/getAllTaskList`, params })
}
}

@ -97,6 +97,10 @@ export const DeviceApi = {
return await request.post({ url: `/iot/device/create`, data })
},
// 批量获取设备属性列表
getDeviceAttributeBatchList: async (params: { deviceIds: string; orgId?: number | string }) => {
return await request.get({ url: `/iot/device/device-attribute/batchList`, params })
},
// 修改物联设备
updateDevice: async (data: DeviceVO) => {
return await request.put({ url: `/iot/device/update`, data })

@ -22,7 +22,11 @@ export interface DeviceWarningRecordVO {
// 实时报警记录 API
export const DeviceWarningRecordApi = {
// 获得实时报警记录列表
getList: async () => {
return await request.get({ url: `/iot/device-warinning-record/getList` })
getList: async (params?: any) => {
return await request.get({ url: `/iot/device-warinning-record/getList`, params })
},
// 获得最近7小时产量
getLastSevenHoursCount: async (params?: any) => {
return await request.get({ url: `/iot/device-warinning-record/getLastSevenHoursCount`, params })
}
}

@ -58,9 +58,8 @@ export const EnergyDeviceApi = {
return await request.get({ url: `/mes/energy-device/getList`, params })
},
// 获取最近的能源统计数据
getLastEnergyStatistics: async (params: any) => {
return await request.get({ url: `/mes/energy-device/lastEnergyStatistics`, params })
getLatestSevenDaysStatistics: async (params: any) => {
return await request.get({ url: `/mes/energy-device/latestSevenDaysStatistics`, params })
},
queryDataRecords: async (params: any) => {

@ -43,7 +43,7 @@ export const EnergyTypeApi = {
},
// 获得能耗类型列表
getEnergyTypeList: async () => {
return await request.get({ url: `/mes/energy-type/list`})
getEnergyTypeList: async (params?: any) => {
return await request.get({ url: `/mes/energy-type/list`, params })
},
}

@ -96,18 +96,25 @@ export const PlanApi = {
getPlanByTicketType: async (status: number) => {
return await request.get({ url: `/mes/plan/getByTicketType?status=` + status })
},
// 产线任务看板
getProductPlans: async () => {
return await request.get({ url: `/mes/plan/getProductPlans` })
// 产线任务看板
getProductPlans: async (params?: any) => {
return await request.get({ url: `/mes/plan/getProductPlans`, params })
},
// 周生产趋势
getWeekTrend: async () => {
return await request.get({ url: `/mes/plan/getWeekTrend` })
getWeekTrend: async (params?: any) => {
return await request.get({ url: `/mes/plan/getWeekTrend`, params })
},
getPlanCapacity: async (type: number) => {
return await request.get({ url: `/mes/plan/getPlanCapacity?type=` + type })
getPlanCapacity: async (type: number, orgId?: any) => {
let url = `/mes/plan/getPlanCapacity?type=` + type
if (orgId) {
url += `&orgId=` + orgId
}
return await request.get({ url })
},
getLastDaysRate: async () => {
return await request.get({ url: `/mes/plan/getLastDaysRate` })
getLastDaysRate: async (params?: any) => {
return await request.get({ url: `/mes/plan/getLastDaysRate`, params })
},
getLastSevenDaysCompletedCount: async (params?: any) => {
return await request.get({ url: `/mes/plan/getLastSevenDaysCompletedCount`, params })
}
}

@ -84,7 +84,7 @@ const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
fileType: propTypes.array.def(['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // (MB)
fileSize: propTypes.number.def(100), // (MB)
limit: propTypes.number.def(5), //
autoUpload: propTypes.bool.def(true), //
drag: propTypes.bool.def(false), //
@ -97,6 +97,7 @@ const uploadRef = ref<UploadInstance>()
const uploadList = ref<UploadUserFile[]>([])
const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0)
const nameMap = ref<Record<string, string>>({})
const { uploadUrl, httpRequest } = useUpload()
@ -130,14 +131,41 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
// const handleFileChange = (uploadFile: UploadFile): void => {
// uploadRef.value.data.path = uploadFile.name
// }
//
const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
const getUrlFromRes = (res: any): string => {
if (!res) return ''
if (typeof res === 'string') return res
if (typeof res.data === 'string') return res.data
const data = res.data || res
if (typeof data === 'string') return data
if (data.fileUrl) return data.fileUrl
return ''
}
const getNameFromRes = (res: any, url: string): string => {
const data = res && res.data ? res.data : res
if (data && typeof data === 'object' && data.fileName) return data.fileName
if (!url) return ''
const idx = url.lastIndexOf('/')
return idx !== -1 ? url.substring(idx + 1) : url
}
const handleFileSuccess: UploadProps['onSuccess'] = (res: any, uploadFile): void => {
const url = getUrlFromRes(res)
if (!url) {
message.error('上传返回数据异常')
return
}
const name = getNameFromRes(res, url)
if (url && name) {
nameMap.value[url] = name
}
message.success('上传成功')
//
const index = fileList.value.findIndex((item) => item.response?.data === res.data)
fileList.value.splice(index, 1)
uploadList.value.push({ name: res.data, url: res.data })
if (uploadList.value.length == uploadNumber.value) {
const index = fileList.value.findIndex((item) => item.uid === uploadFile.uid)
if (index > -1) {
fileList.value.splice(index, 1)
}
uploadList.value.push({ name, url })
if (uploadList.value.length === uploadNumber.value) {
fileList.value.push(...uploadList.value)
uploadList.value = []
uploadNumber.value = 0
@ -156,7 +184,10 @@ const excelUploadError: UploadProps['onError'] = (): void => {
const handleRemove = (file: UploadFile) => {
const index = fileList.value.map((f) => f.name).indexOf(file.name)
if (index > -1) {
fileList.value.splice(index, 1)
const removed = fileList.value.splice(index, 1)[0]
if (removed && removed.url && nameMap.value[removed.url]) {
delete nameMap.value[removed.url]
}
emitUpdateModelValue()
}
}
@ -164,6 +195,98 @@ const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
console.log(uploadFile)
}
const getDisplayNameByUrl = (url: string): string => {
const cached = nameMap.value[url]
if (cached) return cached
const idx = url.lastIndexOf('/')
return idx !== -1 ? url.substring(idx + 1) : url
}
const parseModelValueToInfos = (val: string | string[]): { fileName: string; fileUrl: string }[] => {
const result: { fileName: string; fileUrl: string }[] = []
const addFromUrl = (url: string) => {
const trimmed = url.trim()
if (!trimmed) return
const idx = trimmed.lastIndexOf('/')
const name = idx !== -1 ? trimmed.substring(idx + 1) : trimmed
result.push({ fileName: name, fileUrl: trimmed })
}
const handleString = (text: string) => {
const trimmed = text.trim()
if (!trimmed) return
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed)
if (Array.isArray(parsed)) {
parsed.forEach((item) => {
if (item && typeof item === 'object' && item.fileUrl) {
const u = String(item.fileUrl)
const n = item.fileName ? String(item.fileName) : undefined
const idx = u.lastIndexOf('/')
const name = n || (idx !== -1 ? u.substring(idx + 1) : u)
result.push({ fileName: name, fileUrl: u })
}
})
return
}
if (parsed && typeof parsed === 'object' && parsed.fileUrl) {
const u = String(parsed.fileUrl)
const n = parsed.fileName ? String(parsed.fileName) : undefined
const idx = u.lastIndexOf('/')
const name = n || (idx !== -1 ? u.substring(idx + 1) : u)
result.push({ fileName: name, fileUrl: u })
return
}
} catch {}
}
trimmed
.split(',')
.map((s) => s.trim())
.filter((s) => !!s)
.forEach((u) => addFromUrl(u))
}
const handleAny = (v: any) => {
if (!v) return
if (typeof v === 'string') {
handleString(v)
return
}
if (Array.isArray(v)) {
v.forEach((item) => handleAny(item))
return
}
if (typeof v === 'object' && v.fileUrl) {
const u = String(v.fileUrl)
const n = v.fileName ? String(v.fileName) : undefined
const idx = u.lastIndexOf('/')
const name = n || (idx !== -1 ? u.substring(idx + 1) : u)
result.push({ fileName: name, fileUrl: u })
}
}
handleAny(val as any)
return result
}
const buildInfosFromFileList = (): { fileName: string; fileUrl: string }[] => {
const infos: { fileName: string; fileUrl: string }[] = []
fileList.value.forEach((file) => {
if (!file.url) return
const url = file.url as string
const cached = nameMap.value[url]
let name = cached || file.name || ''
if (!name) {
const idx = url.lastIndexOf('/')
name = idx !== -1 ? url.substring(idx + 1) : url
}
infos.push({ fileName: String(name), fileUrl: url })
})
return infos
}
//
watch(
() => props.modelValue,
@ -173,28 +296,31 @@ watch(
return
}
fileList.value = [] //
// 1
if (isString(val)) {
fileList.value.push(
...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
return
}
// 2
fileList.value = []
nameMap.value = {}
const infos = parseModelValueToInfos(val)
if (!infos.length) return
fileList.value.push(
...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
...infos.map((item) => {
nameMap.value[item.fileUrl] = item.fileName
return { name: item.fileName, url: item.fileUrl }
})
)
},
{ immediate: true, deep: true }
)
//
const emitUpdateModelValue = () => {
// 1
let result: string | string[] = fileList.value.map((file) => file.url!)
// 2
const infos = buildInfosFromFileList()
let result: string | string[]
if (props.limit === 1 || isString(props.modelValue)) {
result = result.join(',')
if (!infos.length) {
result = ''
} else {
result = JSON.stringify(infos[0])
}
} else {
result = JSON.stringify(infos)
}
emit('update:modelValue', result)
}

@ -73,7 +73,7 @@ const props = defineProps({
modelValue: propTypes.string.def(''),
drag: propTypes.bool.def(true), // ==> true
disabled: propTypes.bool.def(false), // ==> false
fileSize: propTypes.number.def(5), // ==> 5M
fileSize: propTypes.number.def(100), // ==> 5M
fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // ==> ["image/jpeg", "image/png", "image/gif"]
height: propTypes.string.def('150px'), // ==> 150px
width: propTypes.string.def('150px'), // ==> 150px
@ -115,10 +115,24 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
return imgType.includes(rawFile.type as FileTypes) && imgSize
}
//
const getUrlFromRes = (res: any): string => {
if (!res) return ''
if (typeof res === 'string') return res
if (typeof res.data === 'string') return res.data
const data = res.data || res
if (typeof data === 'string') return data
if (data.fileUrl) return data.fileUrl
return ''
}
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
const url = getUrlFromRes(res)
if (!url) {
message.notifyError('上传返回数据异常')
return
}
message.success('上传成功')
emit('update:modelValue', res.data)
emit('update:modelValue', url)
}
//

@ -77,7 +77,7 @@ const props = defineProps({
drag: propTypes.bool.def(true), // ==> true
disabled: propTypes.bool.def(false), // ==> false
limit: propTypes.number.def(5), // ==> 5
fileSize: propTypes.number.def(5), // ==> 5M
fileSize: propTypes.number.def(100), // ==> 5M
fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // ==> ["image/jpeg", "image/png", "image/gif"]
height: propTypes.string.def('150px'), // ==> 150px
width: propTypes.string.def('150px'), // ==> 150px
@ -89,6 +89,7 @@ const { uploadUrl, httpRequest } = useUpload()
const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0)
const uploadList = ref<UploadUserFile[]>([])
const nameMap = ref<Record<string, string>>({})
/**
* @description 文件上传之前判断
* @param rawFile 上传的文件
@ -118,13 +119,41 @@ interface UploadEmits {
}
const emit = defineEmits<UploadEmits>()
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
const getUrlFromRes = (res: any): string => {
if (!res) return ''
if (typeof res === 'string') return res
if (typeof res.data === 'string') return res.data
const data = res.data || res
if (typeof data === 'string') return data
if (data.fileUrl) return data.fileUrl
return ''
}
const getNameFromRes = (res: any, url: string): string => {
const data = res && res.data ? res.data : res
if (data && typeof data === 'object' && data.fileName) return data.fileName
if (!url) return ''
const idx = url.lastIndexOf('/')
return idx !== -1 ? url.substring(idx + 1) : url
}
const uploadSuccess: UploadProps['onSuccess'] = (res: any, uploadFile): void => {
const url = getUrlFromRes(res)
if (!url) {
message.error('上传返回数据异常')
return
}
const name = getNameFromRes(res, url)
if (url && name) {
nameMap.value[url] = name
}
message.success('上传成功')
//
const index = fileList.value.findIndex((item) => item.response?.data === res.data)
fileList.value.splice(index, 1)
uploadList.value.push({ name: res.data, url: res.data })
if (uploadList.value.length == uploadNumber.value) {
const index = fileList.value.findIndex((item) => item.uid === uploadFile.uid)
if (index > -1) {
fileList.value.splice(index, 1)
}
uploadList.value.push({ name, url })
if (uploadList.value.length === uploadNumber.value) {
fileList.value.push(...uploadList.value)
uploadList.value = []
uploadNumber.value = 0
@ -132,6 +161,13 @@ const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
}
}
const getDisplayNameByUrl = (url: string): string => {
const cached = nameMap.value[url]
if (cached) return cached
const idx = url.lastIndexOf('/')
return idx !== -1 ? url.substring(idx + 1) : url
}
//
watch(
() => props.modelValue,
@ -142,9 +178,8 @@ watch(
}
fileList.value = [] //
fileList.value.push(
...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
const list = Array.isArray(val) ? (val as string[]) : []
fileList.value.push(...list.map((url) => ({ name: getDisplayNameByUrl(url), url })))
},
{ immediate: true, deep: true }
)
@ -155,9 +190,15 @@ const emitUpdateModelValue = () => {
}
//
const handleRemove = (uploadFile: UploadFile) => {
fileList.value = fileList.value.filter(
(item) => item.url !== uploadFile.url || item.name !== uploadFile.name
)
fileList.value = fileList.value.filter((item) => {
if (item.url === uploadFile.url && item.name === uploadFile.name) {
if (item.url && nameMap.value[item.url]) {
delete nameMap.value[item.url]
}
return false
}
return true
})
emit(
'update:modelValue',
fileList.value.map((file) => file.url!)

@ -19,11 +19,8 @@ export const useUpload = () => {
const httpRequest = async (options: UploadRequestOptions) => {
// 模式一:前端上传
if (isClientUpload) {
// 1.1 生成文件名称
const fileName = await generateFileName(options.file)
// 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
return axios
.put(presignedInfo.uploadUrl, options.file, {
headers: {
@ -31,10 +28,16 @@ export const useUpload = () => {
}
})
.then(() => {
// 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, fileName, options.file)
// 通知成功,数据格式保持与后端上传的返回结果一致
return { data: presignedInfo.url }
const fileVo = createFile(presignedInfo, fileName, options.file)
return {
code: 0,
status: 0,
data: {
fileName: fileVo.path,
fileUrl: fileVo.url
},
msg: ''
}
})
} else {
// 模式二:后端上传

@ -76,7 +76,12 @@
alt="封面图"
/>
<div class="dashboard-card-state">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.state" />
<el-tag v-if="item.state === 1" type="success" size="small">
启用
</el-tag>
<el-tag v-else type="info" size="small">
禁用
</el-tag>
</div>
</div>
<div class="dashboard-card-body">
@ -123,29 +128,53 @@
v-model="createDialogVisible"
:title="dialogMode === 'create' ? '新增数据大屏' : '编辑数据大屏'"
width="600px"
draggable
>
<el-form :model="createForm" ref="createFormRef" label-width="80px">
<el-form-item label="名称">
<el-form :model="createForm" ref="createFormRef" label-width="80px" :rules="createFormRules">
<el-form-item label="名称" prop="name">
<el-input v-model="createForm.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="createForm.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="启用状态">
<el-select v-model="createForm.state" placeholder="请选择启用状态" class="!w-240px">
<el-form-item label="大屏类型" prop="type">
<el-select v-model="createForm.type" placeholder="请选择大屏类型" class="!w-240px">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"
v-for="dict in getStrDictOptions('mes_goview_type')"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="图片路径">
<el-input v-model="createForm.indexImage" placeholder="请输入图片路径" />
<el-form-item label="产线" prop="orgId">
<el-tree-select
v-model="createForm.orgId"
:data="organizationTree"
:props="lineTreeProps"
filterable
clearable
class="!w-240px"
placeholder="请选择产线"
@change="handleOrgChange"
/>
</el-form-item>
<el-form-item label="路由路径">
<el-input v-model="createForm.route" placeholder="请输入路由路径" />
<el-form-item label="设备" v-if="createForm.type === '1'">
<el-select
v-model="selectedDeviceIds"
multiple
:multiple-limit="8"
collapse-tags
collapse-tags-tooltip
placeholder="请选择设备必须选择8个"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in deviceList"
:key="item.id"
:label="item.deviceName"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="内容">
<el-input
@ -155,6 +184,16 @@
placeholder="请输入内容"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="createForm.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="启用状态">
<el-switch
v-model="createForm.state"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false"> </el-button>
@ -169,20 +208,27 @@
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import request from '@/config/axios'
import defaultImage from '@/assets/imgs/logo.png'
import { OrganizationApi } from '@/api/mes/organization'
import { handleTree } from '@/utils/tree'
import { DeviceApi } from '@/api/iot/device'
defineOptions({ name: 'DashboardList' })
const { push } = useRouter()
const router = useRouter()
const message = useMessage()
interface DashboardItem {
id: number
name: string
remark: string
state: string
state: number
indexImage?: string
route?: string
content?: string
type?: string
orgId?: number | string
orgName?: string
deviceIds?: string
}
const loading = ref(false)
@ -207,12 +253,32 @@ const editingId = ref<number | null>(null)
const createForm = reactive({
name: '',
remark: '',
state: '',
state: 1,
type: '',
orgId: undefined as number | string | undefined,
orgName: '',
deviceIds: '',
indexImage: '',
route: '',
content: ''
})
const createFormRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
type: [{ required: true, message: '大屏类型不能为空', trigger: 'change' }],
orgId: [{ required: true, message: '产线不能为空', trigger: 'change' }]
})
const organizationTree = ref<any[]>([])
const lineTreeProps = {
label: 'name',
children: 'children',
value: 'id'
}
const deviceList = ref<any[]>([])
const selectedDeviceIds = ref<string[]>([])
const getList = async () => {
loading.value = true
try {
@ -238,21 +304,33 @@ const resetQuery = () => {
}
const handlePreview = (item: DashboardItem) => {
if (!item.route) {
const typeRoute = getRouteByType(item.type)
const route = typeRoute || item.route || ''
if (!route) {
message.error('未配置预览路由')
return
}
const path = item.route.startsWith('/') ? item.route : `/${item.route}`
push(path)
const path = route.startsWith('/') ? route : `/${route}`
const queryParams = new URLSearchParams()
if (item.orgId) queryParams.append('orgId', String(item.orgId))
if (item.type === '1' && item.deviceIds) queryParams.append('deviceIds', item.deviceIds)
const queryString = queryParams.toString()
const url = router.resolve(path + (queryString ? `?${queryString}` : '')).href
window.open(url, '_blank')
}
const resetCreateForm = () => {
createForm.name = ''
createForm.remark = ''
createForm.state = ''
createForm.state = 1
createForm.type = ''
createForm.orgId = undefined
createForm.orgName = ''
createForm.deviceIds = ''
createForm.indexImage = ''
createForm.route = ''
createForm.content = ''
selectedDeviceIds.value = []
}
const openCreateDialog = () => {
@ -267,18 +345,55 @@ const openEditDialog = (item: DashboardItem) => {
editingId.value = item.id
createForm.name = item.name || ''
createForm.remark = item.remark || ''
createForm.state = item.state || ''
createForm.state = item.state
createForm.type = item.type || ''
createForm.orgId = item.orgId || undefined
createForm.orgName = item.orgName || ''
createForm.deviceIds = item.deviceIds || ''
createForm.indexImage = item.indexImage || ''
createForm.route = item.route || ''
createForm.content = item.content || ''
selectedDeviceIds.value = createForm.deviceIds
? createForm.deviceIds.split(',').filter((v) => v)
: []
createDialogVisible.value = true
}
const getRouteByType = (type?: string) => {
if (type === '1') return 'iot/report/dashboardPage/Dashboard1'
if (type === '2') return 'iot/report/dashboardPage/Dashboard8'
return ''
}
const submitDialog = async () => {
if (!createForm.name) {
message.error('名称不能为空')
try {
await createFormRef.value?.validate()
} catch {
return
}
if (createForm.type === '1') {
if (!selectedDeviceIds.value.length) {
message.error('设备不能为空')
return
}
if (selectedDeviceIds.value.length !== 8) {
message.error('设备必须选择8个')
return
}
createForm.deviceIds = selectedDeviceIds.value.join(',')
} else {
createForm.deviceIds = ''
}
if (createForm.orgId) {
const org = findOrgNode(organizationTree.value || [], createForm.orgId)
createForm.orgName = org?.name || ''
} else {
createForm.orgName = ''
}
const route = getRouteByType(createForm.type)
if (route) {
createForm.route = route
}
if (dialogMode.value === 'edit' && !editingId.value) {
message.error('缺少数据编号,无法编辑')
return
@ -320,8 +435,38 @@ const handleDelete = async (item: DashboardItem) => {
} catch {}
}
const loadOrganizationTree = async () => {
const data = await OrganizationApi.getOrganizationList()
organizationTree.value = handleTree(data, 'id', 'parentId')
}
const findOrgNode = (nodes: any[], id: any): any | undefined => {
for (const node of nodes) {
if (String(node.id) === String(id)) return node
const children = Array.isArray(node.children) ? node.children : []
const found = findOrgNode(children, id)
if (found) return found
}
return undefined
}
const handleOrgChange = () => {
if (!createForm.orgId) {
createForm.orgName = ''
return
}
const org = findOrgNode(organizationTree.value || [], createForm.orgId)
createForm.orgName = org?.name || ''
}
const loadDeviceList = async () => {
deviceList.value = await DeviceApi.getDeviceList()
}
onMounted(() => {
getList()
loadOrganizationTree()
loadDeviceList()
})
</script>

@ -47,10 +47,10 @@
</div>
</div>
<div class="header-right">
<div class="back-btn" @click="goBack">
<!-- <div class="back-btn" @click="goBack">
<Icon icon="ep:back" />
<span>返回</span>
</div>
</div> -->
<div class="weather">
<Icon icon="fa-solid:cloud-sun" class="weather-icon" />
<div class="weather-meta">

@ -19,6 +19,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { colors } from '../utils'
import { DashboardApi } from '@/api/dashboard'
@ -32,6 +33,9 @@ interface OverviewItem {
color: string
}
const route = useRoute()
const orgId = route.query.orgId
const overviewItems = ref<OverviewItem[]>([
{ key: 'device', label: '设备数量', value: '0', percent: 0, color: colors.cyan },
{ key: 'running', label: '运行数量', value: '0', percent: 0, color: colors.blue },
@ -62,7 +66,7 @@ const calcPercent = (part: number, total: number) => {
const loadOverview = async () => {
try {
const res: any = await DashboardApi.getDeviceOperationalStatus()
const res: any = await DashboardApi.getDeviceOperationalStatus({ orgId })
const data = (res && typeof res === 'object' ? (res.data ?? res) : {}) as any
const totalDevices = Number(data.totalDevices ?? 0)

@ -21,11 +21,15 @@ v-model="selectedEnergyTypeId" placeholder="请选择" style="width: 120px" size
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
import { EnergyTypeApi, EnergyTypeVO } from '@/api/mes/energytype'
import { EnergyDeviceApi } from '@/api/mes/energydevice'
const route = useRoute()
const orgId = route.query.orgId
const energyTypes = ref<EnergyTypeVO[]>([])
const selectedEnergyTypeId = ref<number | undefined>(undefined)
const chartRef = ref<HTMLElement | null>(null)
@ -33,7 +37,7 @@ let chart: echarts.ECharts | null = null
const getEnergyTypes = async () => {
try {
const res = await EnergyTypeApi.getEnergyTypeList()
const res = await EnergyTypeApi.getEnergyTypeList({ orgId })
const list = (res as any).data || (Array.isArray(res) ? res : [])
energyTypes.value = list
if (list.length > 0) {
@ -50,7 +54,7 @@ const getChartData = async () => {
try {
const res = await EnergyDeviceApi.getLastEnergyStatistics({
deviceTypeId: selectedEnergyTypeId.value,
orgId: 132
orgId: orgId
})
render(res)
} catch (e) {

@ -33,6 +33,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { DashboardApi, TaskStatisticsData } from '@/api/dashboard'
import { colors } from '../utils'
@ -45,6 +46,9 @@ interface EventItem {
color: string
}
const route = useRoute()
const orgId = route.query.orgId
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const mode = ref<'device' | 'mold'>('device')
@ -129,7 +133,7 @@ const applyTaskStatistics = (data: TaskStatisticsData) => {
const loadTaskStatistics = async () => {
try {
const res = await DashboardApi.getTaskStatistics()
const res = await DashboardApi.getTaskStatistics({ orgId })
const payload = (res && typeof res === 'object' && 'data' in res ? (res as any).data : res) as
| TaskStatisticsData
| null

@ -1,84 +1,113 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>Payment method</span>
<div class="date-filter">
<el-date-picker
v-model="pickedDate"
type="date"
format="YYYY MM/DD"
value-format="YYYY-MM-DD"
:clearable="false"
size="small"
/>
<div class="card-header">
<div class="card-title">
<span class="card-title-icon">
<Icon icon="fa-solid:bell" />
</span>
<span>实时报警信息</span>
</div>
<span class="tag">滚动展示</span>
</div>
<div class="panel-body">
<div ref="chartRef" class="chart"></div>
<div class="card-body">
<ul ref="listRef" class="alarm-list">
<li
v-for="(a, index) in alarms"
:key="index"
class="alarm-item"
:class="[a.type, { 'animating-out': index === 0 && isAnimating }]"
:title="a.msg"
>
<span class="alarm-time">[{{ a.time }}]</span>
<span class="alarm-msg">{{ a.msg }}</span>
<span class="alarm-level">{{ a.level }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const pickedDate = ref('2023-08-31')
const render = () => {
if (!chart) return
const x = ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
const y = [60, 120, 165, 140, 185, 150, 190]
chart.setOption({
backgroundColor: 'transparent',
grid: { top: '14%', left: '6%', right: '4%', bottom: '12%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: x, axisLine: style.axisLine, axisLabel: { ...style.axisLabel, fontSize: 10 } },
yAxis: { type: 'value', axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: style.splitLine },
series: [
{
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 6,
lineStyle: { width: 2, color: colors.cyan },
itemStyle: { color: colors.cyan },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(34,211,238,0.38)' },
{ offset: 1, color: 'rgba(34,211,238,0.06)' }
])
},
data: y
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { DeviceWarningRecordApi, DeviceWarningRecordVO } from '@/api/iot/deviceWarningRecord'
const route = useRoute()
const orgId = route.query.orgId
const alarms = ref<any[]>([])
const isAnimating = ref(false)
let timer: ReturnType<typeof setInterval> | null = null
const getAlarms = async () => {
try {
const data = (await DeviceWarningRecordApi.getList({ orgId })) || []
alarms.value = data.map((item: DeviceWarningRecordVO) => {
let timeStr = ''
if (item.createTime) {
const d = new Date(item.createTime)
timeStr = d.toTimeString().slice(0, 8)
}
let type = 'safe'
let levelText = item.alarmLevel
if (item.alarmLevel === '2') {
type = 'danger'
levelText = '严重'
} else if (item.alarmLevel === '1') {
type = 'warn'
levelText = '警告'
} else if (item.alarmLevel === '0') {
type = 'safe'
levelText = '提示'
} else {
if (item.alarmLevel === '严重') type = 'danger'
else if (item.alarmLevel === '警告') type = 'warn'
else if (item.alarmLevel === '提示') type = 'safe'
}
]
})
return {
time: timeStr,
level: levelText,
type,
msg: `${item.deviceName}-${item.ruleName}`
}
})
startAnimation()
} catch (error) {
console.error('Failed to fetch alarms:', error)
}
}
const resize = () => {
chart?.resize()
const startAnimation = () => {
if (timer) clearInterval(timer)
if (alarms.value.length === 0) return
timer = setInterval(() => {
isAnimating.value = true
setTimeout(() => {
const first = alarms.value.shift()
if (first) {
alarms.value.push(first)
}
isAnimating.value = false
}, 360)
}, 3000)
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
window.addEventListener('resize', resize)
getAlarms()
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
if (timer) clearInterval(timer)
})
</script>
<style scoped>
.card {
height: 100%;
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
border-radius: 8px;
border: 1px solid rgba(30,64,175,0.85);
@ -90,7 +119,6 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.card::before,
@ -119,52 +147,110 @@ onUnmounted(() => {
border-top: none;
}
.panel-title {
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
justify-content: space-between;
margin-bottom: 8px;
padding: 10px 12px 4px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 700;
color: #e5f0ff;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 1px solid rgba(56,189,248,0.95);
box-shadow: 0 0 12px rgba(56,189,248,0.45);
.card-title-icon {
color: #22d3ee;
}
.date-filter {
margin-left: auto;
display: inline-flex;
.card-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px 12px;
overflow: hidden;
}
.tag {
border-radius: 999px;
padding: 2px 6px;
font-size: 10px;
border: 1px solid rgba(148,163,184,0.4);
color: #94a3b8;
}
.alarm-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
height: 100%;
overflow: hidden;
}
.alarm-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 8px;
border-radius: 6px;
background: rgba(15,23,42,0.92);
border: 1px solid rgba(30,64,175,0.9);
font-size: 11px;
color: #e5f0ff;
transition: background 0.3s, box-shadow 0.3s;
flex-shrink: 0;
}
.date-filter :deep(.el-input__wrapper) {
background: rgba(2,6,23,0.35);
box-shadow: none;
border: 1px solid rgba(148,163,184,0.35);
.alarm-item.danger {
border-left: 3px solid #ef4444;
box-shadow: 0 0 18px rgba(239,68,68,0.4);
}
.date-filter :deep(.el-input__inner) {
color: rgba(224,242,254,0.95);
font-size: 12px;
font-weight: 700;
.alarm-item.warn {
border-left: 3px solid #f59e0b;
box-shadow: 0 0 14px rgba(245,158,11,0.35);
}
.alarm-item.safe {
border-left: 3px solid #22c55e;
box-shadow: 0 0 12px rgba(34,197,94,0.35);
}
.alarm-time {
width: 64px;
flex: 0 0 auto;
color: #94a3b8;
}
.panel-body {
.alarm-msg {
flex: 1;
min-height: 0;
padding: 10px 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chart {
width: 100%;
height: 100%;
min-height: 160px;
.alarm-level {
flex: 0 0 auto;
border-radius: 999px;
padding: 2px 6px;
border: 1px solid rgba(148,163,184,0.6);
font-size: 10px;
}
.animating-out {
transition: all 0.35s;
transform: translateY(-48px);
opacity: 0;
}
</style>

@ -13,52 +13,76 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
import { DeviceWarningRecordApi } from '@/api/iot/deviceWarningRecord'
const route = useRoute()
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const render = () => {
if (!chart) return
const x = ['9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
const output = [320, 460, 520, 610, 720, 690, 780]
const passRate = [98.5, 99.2, 98.1, 98.9, 99.0, 98.6, 99.3]
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { top: '18%', left: '6%', right: '6%', bottom: '12%', containLabel: true },
xAxis: { type: 'category', data: x, axisLine: style.axisLine, axisLabel: { ...style.axisLabel, fontSize: 10 } },
yAxis: [
{ type: 'value', axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: style.splitLine },
{ type: 'value', min: 96, max: 100, axisLine: { show: false }, axisLabel: { ...style.axisLabel, fontSize: 10 }, splitLine: { show: false } }
],
series: [
{
name: '产量',
type: 'bar',
barWidth: 12,
itemStyle: {
borderRadius: [6, 6, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colors.purple },
{ offset: 1, color: 'rgba(139,92,246,0.10)' }
])
},
data: output
},
{
name: '良率',
type: 'line',
yAxisIndex: 1,
smooth: true,
showSymbol: false,
lineStyle: { width: 2, color: colors.cyan },
data: passRate
}
]
})
const render = async () => {
if (!chart) return
let x: string[] = []
let output: number[] = []
try {
const orgId = route.query.orgId
const res = await DeviceWarningRecordApi.getLastSevenHoursCount({ orgId })
const raw = res && typeof res === 'object' && 'data' in (res as any) ? (res as any).data : res
const list = Array.isArray(raw) ? raw : []
if (list.length > 0) {
x = list.map((item: any) => {
const h = item.hour || item.time || item.key || item.name || ''
if (typeof h === 'string' && h.includes(' ')) {
return h.split(' ')[1]
}
return h
})
output = list.map((item: any) => {
const val = item.count ?? item.value ?? item.output ?? 0
const n = Number(val)
return Number.isFinite(n) ? n : 0
})
}
} catch (e) {
console.error('Fetch production trend failed:', e)
}
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { top: '18%', left: '6%', right: '6%', bottom: '16%', containLabel: true },
xAxis: {
type: 'category',
data: x,
axisLine: style.axisLine,
axisLabel: { ...style.axisLabel, fontSize: 10, rotate: 40 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisLabel: { ...style.axisLabel, fontSize: 10 },
splitLine: style.splitLine
},
series: [
{
name: '产量',
type: 'bar',
barWidth: 12,
itemStyle: {
borderRadius: [6, 6, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colors.purple },
{ offset: 1, color: 'rgba(139,92,246,0.10)' }
])
},
data: output
}
]
})
}
const resize = () => {

@ -57,6 +57,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import type { ElTable } from 'element-plus'
import { DashboardApi, DashboardTaskItem } from '@/api/dashboard'
@ -70,6 +71,10 @@ interface TaskRow {
finishStatusType: '' | 'success' | 'warning' | 'danger' | 'info'
resultStatusType: '' | 'success' | 'warning' | 'danger' | 'info'
}
const route = useRoute()
const orgId = route.query.orgId
const taskRows = ref<TaskRow[]>([])
const tableRef = ref<InstanceType<typeof ElTable> | null>(null)
let scrollTimer: number | undefined
@ -108,7 +113,7 @@ const mapItemToRow = (item: DashboardTaskItem, index: number): TaskRow => {
const loadTaskList = async () => {
try {
const res = await DashboardApi.getAllTaskList()
const res = await DashboardApi.getAllTaskList({ orgId })
const payload = (res && typeof res === 'object' && 'data' in res ? (res as any).data : res) as
| DashboardTaskItem[]
| null

@ -26,6 +26,42 @@
<el-col :span="14" class="col">
<div class="center-shell">
<img class="dashboard-center-image" src="@/assets/imgs/dashboard_img.png" alt="dashboard" />
<!-- 上面4个 -->
<div v-for="(item, index) in topDevices" :key="'top-' + index" class="device-card top-card" :style="{ left: getTopLeft(index) }">
<div class="device-header">
<div class="header-left">
<span class="device-dot"></span>
<span class="device-name" :title="item.deviceName">{{ item.deviceName }}</span>
</div>
<div class="header-right">
<span class="device-id">ID: {{ item.deviceId }}</span>
</div>
</div>
<div class="device-body">
<div v-for="(attr, aIndex) in item.attributes" :key="aIndex" class="device-row">
<span class="label" :title="attr.attributeName">{{ attr.attributeName }}:</span>
<span class="value">{{ attr.addressValue ?? '-' }} {{ attr.dataUnit || '' }}</span>
</div>
</div>
</div>
<!-- 下面4个 -->
<div v-for="(item, index) in bottomDevices" :key="'bottom-' + index" class="device-card bottom-card" :style="{ left: getBottomLeft(index) }">
<div class="device-header">
<div class="header-left">
<span class="device-dot"></span>
<span class="device-name" :title="item.deviceName">{{ item.deviceName }}</span>
</div>
<div class="header-right">
<span class="device-id">ID: {{ item.deviceId }}</span>
</div>
</div>
<div class="device-body">
<div v-for="(attr, aIndex) in item.attributes" :key="aIndex" class="device-row">
<span class="label" :title="attr.attributeName">{{ attr.attributeName }}:</span>
<span class="value">{{ attr.addressValue ?? '-' }} {{ attr.dataUnit || '' }}</span>
</div>
</div>
</div>
</div>
</el-col>
@ -50,6 +86,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { DeviceApi } from '@/api/iot/device'
import DashboardHeader from './components/DashboardHeader.vue'
import DeviceOverview from './components/DeviceOverview.vue'
import PaymentMethod from './components/PaymentMethod.vue'
@ -57,6 +96,67 @@ import EventReminder from './components/EventReminder.vue'
import TaskList from './components/TaskList.vue'
import EnergyMonitor from './components/EnergyMonitor.vue'
import ProductionTrend from './components/ProductionTrend.vue'
const route = useRoute()
const deviceIds = route.query.deviceIds as string
const orgId = route.query.orgId
interface DeviceAttribute {
attributeName: string
addressValue?: number | string | null
dataUnit?: string | null
}
interface DeviceCardData {
deviceName: string
deviceId: number
attributes: DeviceAttribute[]
}
const topDevices = ref<DeviceCardData[]>([])
const bottomDevices = ref<DeviceCardData[]>([])
const getTopLeft = (index: number) => {
//
const positions = ['5%', '30%', '55%', '80%']
return positions[index] || '0'
}
const getBottomLeft = (index: number) => {
const positions = ['5%', '30%', '55%', '80%']
return positions[index] || '0'
}
const loadDeviceAttributes = async () => {
if (!deviceIds) return
try {
const res = await DeviceApi.getDeviceAttributeBatchList({ deviceIds, orgId })
const list = (res && Array.isArray(res) ? res : []) as any[]
// list deviceIds deviceId
// 4 top4 bottom
const cards: DeviceCardData[] = list.map((d: any) => {
return {
deviceName: d.deviceName || 'Device',
deviceId: d.deviceId,
attributes: (d.attributes || []).map((attr: any) => ({
attributeName: attr.attributeName || '-',
addressValue: attr.addressValue,
dataUnit: attr.dataUnit
}))
}
})
topDevices.value = cards.slice(0, 4)
bottomDevices.value = cards.slice(4, 8)
} catch (e) {
console.error(e)
}
}
onMounted(() => {
loadDeviceAttributes()
})
</script>
<style scoped>
@ -186,6 +286,118 @@ main {
border: 1px solid rgba(30,64,175,0.55);
background: rgba(2,6,23,0.18);
padding: 10px;
position: relative; /* 用于定位内部绝对定位的设备卡片 */
}
.device-card {
position: absolute;
width: 18%; /* 约占宽度的1/5留出间隔 */
min-width: 120px;
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(56, 189, 248, 0.4);
border-radius: 6px;
padding: 8px;
color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.top-card {
top: 6%;
}
.bottom-card {
bottom: 5%;
}
.device-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
border-bottom: 1px solid rgba(56, 189, 248, 0.3);
padding-bottom: 4px;
}
.header-left {
display: flex;
align-items: center;
overflow: hidden;
flex: 1;
margin-right: 8px;
}
.header-right {
flex-shrink: 0;
}
.device-dot {
width: 0;
height: 0;
border-style: solid;
border-width: 4px 0 4px 6px;
border-color: transparent transparent transparent #38bdf8;
margin-right: 6px;
flex-shrink: 0;
}
.device-name {
font-size: 14px;
font-weight: bold;
color: #e5f0ff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.device-id {
font-size: 12px;
color: #94a3b8;
}
.device-body {
font-size: 12px;
height: 100px; /* 固定高度,内容不足留白,内容多则滚动 */
overflow-y: auto; /* 启用垂直滚动 */
padding-right: 4px; /* 防止滚动条遮挡内容 */
}
/* 滚动条样式 */
.device-body::-webkit-scrollbar {
width: 4px;
}
.device-body::-webkit-scrollbar-thumb {
background: rgba(56, 189, 248, 0.3);
border-radius: 2px;
}
.device-body::-webkit-scrollbar-track {
background: transparent;
}
.device-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
line-height: 1.4;
}
.device-row .label {
color: #94a3b8;
margin-right: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60%;
}
.device-row .value {
color: #cbd5e1;
text-align: right;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dashboard-center-image {

@ -11,10 +11,10 @@
</div>
</div>
<div class="header-right">
<el-button class="back-btn" type="primary" size="small" plain @click="goBack">
<!-- <el-button class="back-btn" type="primary" size="small" plain @click="goBack">
<Icon icon="fa-solid:arrow-left" class="back-icon" />
<span>返回</span>
</el-button>
</el-button> -->
<span class="chip">
<Icon icon="fa-regular:clock" class="chip-icon" />
<span>{{ timeStr }}</span>

@ -41,10 +41,14 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, animateCount, style } from '../utils'
import { PlanApi } from '@/api/mes/plan'
const route = useRoute()
const orgId = route.query.orgId
type CapacityData = {
orders: number
scheduled: number
@ -77,7 +81,7 @@ const mapCapacity = (raw: any): CapacityData => {
const loadDayCapacity = async () => {
try {
const raw = await PlanApi.getPlanCapacity(1)
const raw = await PlanApi.getPlanCapacity(1, orgId)
const mapped = mapCapacity(raw || {})
day.orders = mapped.orders
day.scheduled = mapped.scheduled

@ -23,11 +23,15 @@ v-model="selectedEnergyTypeId" placeholder="请选择" class="energy-type-select
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
import { EnergyTypeApi, EnergyTypeVO } from '@/api/mes/energytype'
import { EnergyDeviceApi } from '@/api/mes/energydevice'
const route = useRoute()
const orgId = route.query.orgId
const energyTypes = ref<EnergyTypeVO[]>([])
const selectedEnergyTypeId = ref<number | undefined>(undefined)
const chartRef = ref<HTMLElement | null>(null)
@ -35,7 +39,7 @@ let chart: echarts.ECharts | null = null
const getEnergyTypes = async () => {
try {
const res = await EnergyTypeApi.getEnergyTypeList()
const res = await EnergyTypeApi.getEnergyTypeList({ orgId })
const list = (res as any).data || (Array.isArray(res) ? res : [])
energyTypes.value = list
if (list.length > 0 && !selectedEnergyTypeId.value) {
@ -50,9 +54,9 @@ const getEnergyTypes = async () => {
const getChartData = async () => {
if (!selectedEnergyTypeId.value || !chart) return
try {
const res = await EnergyDeviceApi.getLastEnergyStatistics({
const res = await EnergyDeviceApi.getLatestSevenDaysStatistics({
deviceTypeId: selectedEnergyTypeId.value,
orgId: 132
orgId: orgId
})
render(res)
} catch (e) {
@ -106,14 +110,6 @@ const render = (data: any = []) => {
])
},
data: actual
},
{
name: '基准能耗(kWh)',
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: { width: 2, color: colors.green },
data: baseline
}
]
})

@ -41,10 +41,14 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, animateCount, style } from '../utils'
import { PlanApi } from '@/api/mes/plan'
const route = useRoute()
const orgId = route.query.orgId
type CapacityData = {
orders: number
scheduled: number
@ -77,7 +81,7 @@ const mapCapacity = (raw: any): CapacityData => {
const loadMonthCapacity = async () => {
try {
const raw = await PlanApi.getPlanCapacity(2)
const raw = await PlanApi.getPlanCapacity(2, orgId)
const mapped = mapCapacity(raw || {})
month.orders = mapped.orders
month.scheduled = mapped.scheduled

@ -5,23 +5,11 @@
<span class="card-title-icon">
<Icon icon="fa-solid:wave-square" />
</span>
<span>开机率/稼动率趋势</span>
<span>产能完成数</span>
</div>
<div class="ops-header-right">
<div class="tabs">
<button
v-for="line in opsLines"
:key="line.name"
type="button"
class="tab-btn"
:class="{ active: currentLine === line.name }"
@click="updateOps(line.name)"
>
{{ line.name }}
</button>
</div>
<span class="tag">{{ summary }}</span>
</div>
<span class="tag">{{ summary }}</span>
</div>
</div>
<div class="card-body">
<div ref="chartRef" class="chart"></div>
@ -31,23 +19,16 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
import { PlanApi } from '@/api/mes/plan'
const opsDays: string[] = []
for (let i = 7; i > 0; i--) {
const d = new Date()
d.setDate(d.getDate() - i)
opsDays.push(`${d.getMonth() + 1}-${d.getDate()}`)
}
const route = useRoute()
const opsLines = [
{ name: '产线A', run: [85, 87, 84, 89, 88, 86, 85], avail: [78, 82, 80, 84, 83, 81, 79] },
{ name: '产线B', run: [83, 84, 82, 86, 85, 84, 83], avail: [76, 78, 77, 80, 79, 78, 77] },
{ name: '产线C', run: [88, 90, 89, 91, 92, 90, 89], avail: [82, 84, 83, 86, 87, 85, 84] },
{ name: '产线D', run: [80, 82, 81, 83, 84, 82, 81], avail: [72, 74, 73, 76, 77, 75, 74] },
{ name: '产线E', run: [86, 87, 86, 88, 89, 87, 86], avail: [79, 81, 80, 82, 83, 81, 80] },
{ name: '产线F', run: [90, 92, 91, 93, 94, 92, 91], avail: [84, 86, 85, 87, 88, 86, 85] }
const opsDays: string[] = []
const opsLines: { name: string; value: number[] }[] = [
{ name: '全部产线', value: [] }
]
const currentLine = ref(opsLines[0].name)
@ -62,16 +43,48 @@ const avg = (arr: number[]) => {
const updateOps = (lineName: string) => {
currentLine.value = lineName
const item = opsLines.find((x) => x.name === lineName) || opsLines[0]
summary.value = `${item.name} · 开机率均值 ${avg(item.run)}% · 稼动率均值 ${avg(item.avail)}%`
summary.value = `${item.name} · 日均完成 ${avg(item.value)}`
if (!chart) return
chart.setOption({
xAxis: { data: opsDays },
series: [{ data: item.run }, { data: item.avail }]
series: [{ data: item.value }]
})
}
const initChart = () => {
const loadOpsData = async () => {
try {
const orgId = route.query.orgId
const res = await PlanApi.getLastSevenDaysCompletedCount({ orgId })
const raw = res && typeof res === 'object' && 'data' in (res as any) ? (res as any).data : res
const list = Array.isArray(raw) ? raw : []
const days: string[] = []
const values: number[] = []
list.forEach((item: any) => {
const src = item.day || item.date || item.time || item.hour || item.key || item.name || ''
let label = src
if (typeof label === 'string' && label.includes(' ')) {
label = label.split(' ')[0]
}
days.push(label)
const raw = item.totalWangong ?? item.count ?? 0
const n = Number(raw)
values.push(Number.isFinite(n) ? n : 0)
})
opsDays.length = 0
opsDays.push(...days)
opsLines.length = 0
opsLines.push({ name: '全部产线', value: values })
if (opsLines[0]) {
currentLine.value = opsLines[0].name
updateOps(currentLine.value)
}
} catch (e) {
console.error('Failed to load ops trend:', e)
}
}
const initChart = async () => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
chart.setOption({
@ -80,48 +93,26 @@ const initChart = () => {
legend: { top: 0, right: 0, textStyle: style.legendText },
grid: { top: '22%', left: '6%', right: '6%', bottom: '10%', containLabel: true },
xAxis: { type: 'category', data: opsDays, axisLine: style.axisLine, axisLabel: style.axisLabel },
yAxis: [
{
type: 'value',
name: '开机率(%)',
max: 100,
nameTextStyle: { color: '#a8b7d8', fontSize: 12 },
axisLabel: style.axisLabel,
splitLine: style.splitLine
},
{
type: 'value',
name: '稼动率(%)',
max: 100,
nameTextStyle: { color: '#a8b7d8', fontSize: 12 },
axisLabel: style.axisLabel,
splitLine: { show: false }
}
],
yAxis: {
type: 'value',
name: '产能完成数',
nameTextStyle: { color: '#a8b7d8', fontSize: 12 },
axisLabel: style.axisLabel,
splitLine: style.splitLine
},
series: [
{
name: '开机率',
name: '产能完成数',
type: 'line',
smooth: true,
showSymbol: false,
yAxisIndex: 0,
lineStyle: { width: 2, color: colors.blue },
itemStyle: { color: colors.blue },
data: []
},
{
name: '稼动率',
type: 'line',
smooth: true,
showSymbol: false,
yAxisIndex: 1,
lineStyle: { width: 2, color: colors.green },
itemStyle: { color: colors.green },
data: []
}
]
})
updateOps(currentLine.value)
await loadOpsData()
}
const resizeHandler = () => {
@ -129,7 +120,7 @@ const resizeHandler = () => {
}
onMounted(() => {
initChart()
initChart()
window.addEventListener('resize', resizeHandler)
})

@ -17,10 +17,14 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { useChart, colors, style } from '../utils'
import { PlanApi } from '@/api/mes/plan'
const route = useRoute()
const orgId = route.query.orgId
const { init } = useChart('chart-quality')
const last7Days: string[] = []
@ -71,7 +75,7 @@ onMounted(async () => {
let seriesData = passRate.slice()
try {
const data = await PlanApi.getLastDaysRate()
const data = await PlanApi.getLastDaysRate({ orgId })
const list = normalizeQualityData(data)
if (list.length > 0) {
xAxisData = list.map((item) => item.label)

@ -29,8 +29,12 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { DeviceWarningRecordApi, DeviceWarningRecordVO } from '@/api/iot/deviceWarningRecord'
const route = useRoute()
const orgId = route.query.orgId
const alarms = ref<any[]>([])
const isAnimating = ref(false)
@ -38,7 +42,7 @@ let timer: ReturnType<typeof setInterval> | null = null
const getAlarms = async () => {
try {
const data = await DeviceWarningRecordApi.getList() || []
const data = await DeviceWarningRecordApi.getList({ orgId }) || []
console.log('data',data)
alarms.value = data.map((item: DeviceWarningRecordVO) => {

@ -51,8 +51,12 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { PlanApi, PlanVO } from '@/api/mes/plan'
const route = useRoute()
const orgId = route.query.orgId
const colors = {
blue: '#1e90ff',
cyan: '#22d3ee',
@ -109,7 +113,7 @@ onMounted(async () => {
setBodyHeight()
startScroll()
window.addEventListener('resize', setBodyHeight)
tasks.value = await PlanApi.getProductPlans()
tasks.value = await PlanApi.getProductPlans({ orgId })
// console.log(tasks.value)
})

@ -20,10 +20,14 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { colors, style } from '../utils'
import { PlanApi } from '@/api/mes/plan'
const route = useRoute()
const orgId = route.query.orgId
const weekDays = ref()
const weekPlan = ref()
const weekReal = ref()
@ -76,7 +80,7 @@ const resizeHandler = () => {
onMounted( async() => {
window.addEventListener('resize', resizeHandler)
const data = await PlanApi.getWeekTrend()
const data = await PlanApi.getWeekTrend({ orgId })
weekDays.value = data.weekDays
weekPlan.value = data.weekPlan
weekReal.value = data.weekReal

Loading…
Cancel
Save