feat:生产报表对接接口

main
黄伟杰 4 days ago
parent 38050e953e
commit a25021ba87

@ -19,6 +19,11 @@ export const WorkTeamApi = {
return await request.get({ url: `/mes/work-team/page`, params })
},
// 查询生产班组列表
getWorkTeamList: async () => {
return await request.get({ url: `/mes/work-team/list` })
},
// 查询生产班组详情
getWorkTeam: async (id: number) => {
return await request.get({ url: `/mes/work-team/get?id=` + id })

@ -5750,7 +5750,28 @@ export default {
morePlans: 'more',
planNumber: 'Planned quantity',
loadTreeFailed: 'Failed to load device tree',
loadCalendarFailed: 'Failed to load production plans'
loadCalendarFailed: 'Failed to load production plans',
reportDate: 'Report date',
workTeam: 'Work team',
workTeamPlaceholder: 'Select work team',
generateDailyReport: 'Generate daily report',
queryDailyReport: 'Query',
generatedReportList: 'Generated reports',
queryDeviceHint: 'The first checked device is used for the device filter',
fileName: 'File name',
devices: 'Devices',
createTime: 'Generated at',
preview: 'Preview',
download: 'Download',
selectDeviceForReport: 'Select at least one device',
selectWorkTeamForReport: 'Select a work team',
generateSuccess: 'Daily report generated',
generateFailed: 'Failed to generate daily report',
loadWorkTeamsFailed: 'Failed to load work teams',
loadReportListFailed: 'Failed to load daily reports',
reportTimeRange: 'Work team time range',
capacityName: 'Capacity point',
capacityDiff: 'Capacity difference'
},
Detail: {
invalidId: 'Invalid parameter, task ID cannot be empty'

@ -6021,7 +6021,28 @@ export default {
morePlans: '项',
planNumber: '计划数量',
loadTreeFailed: '加载设备树失败',
loadCalendarFailed: '加载生产计划失败'
loadCalendarFailed: '加载生产计划失败',
reportDate: '日报日期',
workTeam: '班次',
workTeamPlaceholder: '请选择班次',
generateDailyReport: '生成日报',
queryDailyReport: '查询',
generatedReportList: '已生成日报',
queryDeviceHint: '设备筛选使用左侧已勾选的第一台设备',
fileName: '文件名称',
devices: '设备',
createTime: '生成时间',
preview: '预览',
download: '下载',
selectDeviceForReport: '请至少勾选一台设备',
selectWorkTeamForReport: '请选择班次',
generateSuccess: '日报生成成功',
generateFailed: '日报生成失败',
loadWorkTeamsFailed: '加载班次失败',
loadReportListFailed: '加载日报列表失败',
reportTimeRange: '班次时间范围',
capacityName: '产能点位',
capacityDiff: '产能差值'
},
Detail: {
invalidId: '参数错误任务单ID不能为空'

@ -1,55 +1,16 @@
<template>
<el-row :gutter="16" class="production-report-layout">
<el-col :span="6" :xs="24">
<ContentWrap class="production-report-tree-panel">
<div class="production-report-tree-toolbar">
<el-input
v-model="keyword"
clearable
:placeholder="t('ProductionReport.Index.treeSearchPlaceholder')"
@input="handleKeywordChange"
/>
<el-button :loading="treeLoading" @click="handleTreeRefresh">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('ProductionReport.Index.refreshTree') }}
<el-col :span="24">
<el-row :gutter="16" class="production-report-content-row">
<el-col :span="13" :xs="24">
<ContentWrap class="production-report-calendar-panel">
<div class="production-report-report-toolbar">
<el-button type="primary" @click="handleOpenGenerateDialog">
<Icon icon="ep:document-add" class="mr-5px" />
{{ t('ProductionReport.Index.generateDailyReport') }}
</el-button>
</div>
<el-tree
ref="treeRef"
v-loading="treeLoading"
class="production-report-tree"
:data="treeData"
:props="treeProps"
:default-expanded-keys="defaultExpandedKeys"
:current-node-key="selectedDeviceId"
node-key="id"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ data }">
<span class="production-report-tree-node">
<Icon
:icon="data.tag === 'equipment' ? 'ep:monitor' : 'ep:office-building'"
class="production-report-tree-node__icon"
/>
<span class="production-report-tree-node__label">{{ data.label }}</span>
<el-tag
v-if="data.tag === 'equipment'"
type="success"
size="small"
effect="plain"
>
{{ t('ProductionReport.Index.deviceTag') }}
</el-tag>
</span>
</template>
</el-tree>
</ContentWrap>
</el-col>
<el-col :span="18" :xs="24">
<ContentWrap class="production-report-calendar-panel">
<el-calendar v-model="calendarDate" class="production-report-calendar">
<template #header>
<div class="production-report-calendar-header">
@ -99,7 +60,11 @@
</template>
<template #date-cell="{ data }">
<div class="production-report-day" :class="{ 'is-other-month': data.type !== 'current-month' }">
<div
class="production-report-day"
:class="{ 'is-other-month': data.type !== 'current-month' }"
@click="handleCalendarDayClick(data.day)"
>
<div class="production-report-day__number">{{ Number(data.day.slice(-2)) }}</div>
<div v-if="getPlansByDay(data.day).length" class="production-report-day__plans">
<el-tooltip
@ -123,16 +88,107 @@
</el-calendar>
<div v-if="calendarLoading" class="production-report-calendar-loading" v-loading="calendarLoading"></div>
</ContentWrap>
</ContentWrap>
</el-col>
<el-col :span="11" :xs="24">
<ContentWrap class="production-report-file-panel">
<div class="production-report-file-panel__header">
<span>{{ t('ProductionReport.Index.generatedReportList') }}</span>
</div>
<el-table v-loading="reportListLoading" :data="reportFileList" :stripe="true" :show-overflow-tooltip="true">
<el-table-column :label="t('ProductionReport.Index.fileName')" prop="fileName" min-width="240" />
<el-table-column :label="t('ProductionReport.Index.reportDate')" width="120">
<template #default="{ row }">{{ formatReportDate(row.reportDate) }}</template>
</el-table-column>
<el-table-column :label="t('ProductionReport.Index.workTeam')" prop="workTeamName" width="120" />
<el-table-column :label="t('ProductionReport.Index.devices')" min-width="180">
<template #default="{ row }">{{ getDeviceNames(row.deviceIds) }}</template>
</el-table-column>
<el-table-column :label="t('ProductionReport.Index.createTime')" width="180">
<template #default="{ row }">{{ formatGeneratedTime(row.createTime) }}</template>
</el-table-column>
<el-table-column :label="t('ProductionReport.Index.tableOperate')" width="90" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="downloadReport(row)">
{{ t('ProductionReport.Index.download') }}
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="reportQueryParams.total"
v-model:page="reportQueryParams.pageNo"
v-model:limit="reportQueryParams.pageSize"
@pagination="loadReportFileList"
/>
</ContentWrap>
</el-col>
</el-row>
</el-col>
</el-row>
<Dialog :title="t('ProductionReport.Index.generateDailyReport')" v-model="generateDialogVisible">
<el-form label-width="120px" v-loading="generateLoading">
<el-form-item :label="t('ProductionReport.Index.reportDate')">
<el-date-picker
v-model="reportDate"
type="date"
value-format="YYYY-MM-DD"
:clearable="false"
class="!w-full"
/>
</el-form-item>
<el-form-item :label="t('ProductionReport.Index.workTeam')">
<el-select
v-model="selectedWorkTeamId"
class="!w-full"
:loading="workTeamLoading"
:placeholder="t('ProductionReport.Index.workTeamPlaceholder')"
>
<el-option
v-for="team in workTeamList"
:key="String(team.id)"
:label="team.teamName"
:value="String(team.id)"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('ProductionReport.Index.devices')">
<el-tree-select
v-model="selectedDeviceIds"
v-loading="treeLoading"
:data="treeData"
:props="treeProps"
:default-expanded-keys="defaultExpandedKeys"
node-key="id"
multiple
show-checkbox
check-strictly
collapse-tags
collapse-tags-tooltip
class="!w-full"
:placeholder="t('ProductionReport.Index.selectDeviceForReport')"
@change="handleDeviceSelectionChange"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" :loading="generateLoading" @click="handleGenerateReport">
{{ t('common.ok') }}
</el-button>
<el-button @click="generateDialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { OrganizationApi, DeviceParameterAnalysisNodeVO } from '@/api/mes/organization'
import { DevicePlanGanttPlanVO, DevicePlanGanttRespVO, PlanApi } from '@/api/mes/plan'
import { handleTree } from '@/utils/tree'
import { WorkTeamApi, WorkTeamVO } from '@/api/mes/workteam'
import { DeviceApi, ProductionDailyReportFileVO } from '@/api/iot/device'
import { downloadByUrl } from '@/utils/filt'
defineOptions({ name: 'ProductionReport' })
@ -142,27 +198,34 @@ type ProductionTreeNode = {
tag?: 'equipment'
deviceId?: string
children?: ProductionTreeNode[]
disabled?: boolean
}
const { t } = useI18n()
const message = useMessage()
const treeRef = ref()
const treeLoading = ref(false)
const keyword = ref('')
const treeData = ref<ProductionTreeNode[]>([])
const treeProps = { children: 'children', label: 'label' }
const treeProps = { value: 'id', children: 'children', label: 'label', disabled: 'disabled' }
const defaultExpandedKeys = ref<string[]>([])
const selectedDevice = ref<ProductionTreeNode>()
const selectedDeviceId = computed(() => selectedDevice.value?.id)
const selectedDeviceIds = ref<string[]>([])
const generateDialogVisible = ref(false)
const reportDate = ref(dayjs().format('YYYY-MM-DD'))
const workTeamLoading = ref(false)
const workTeamList = ref<WorkTeamVO[]>([])
const selectedWorkTeamId = ref<string>()
const generateLoading = ref(false)
const reportListLoading = ref(false)
const reportFileList = ref<ProductionDailyReportFileVO[]>([])
const reportQueryParams = reactive({ pageNo: 1, pageSize: 10, total: 0 })
const calendarDate = ref(new Date())
const calendarLoading = ref(false)
const scheduleList = ref<DevicePlanGanttRespVO[]>([])
const MAX_VISIBLE_PLANS = 3
let keywordTimer: number | undefined
const calendarTitle = computed(() => {
const date = dayjs(calendarDate.value)
return t('ProductionReport.Index.calendarMonthTitle', {
@ -178,102 +241,150 @@ const yearOptions = computed(() => {
return Array.from({ length: 21 }, (_, index) => currentYear - 10 + index)
})
const extractApiOrgs = (res: any): DeviceParameterAnalysisNodeVO[] => {
if (Array.isArray(res)) return res
if (Array.isArray(res?.data)) return res.data
if (Array.isArray(res?.result)) return res.result
if (Array.isArray(res?.list)) return res.list
return []
const buildTreeFromDevices = (devices: any[]): ProductionTreeNode[] => {
const children = devices.map((device) => ({
id: String(device.id),
label: device.deviceName || device.deviceCode || String(device.id),
tag: 'equipment' as const,
deviceId: String(device.id)
}))
return [{
id: 'iot-device-root',
label: t('ProductionReport.Index.deviceTag'),
children,
disabled: true
}]
}
const buildTreeFromApi = (orgs: DeviceParameterAnalysisNodeVO[]): ProductionTreeNode[] => {
const normalizeOrg = (org: DeviceParameterAnalysisNodeVO): DeviceParameterAnalysisNodeVO => ({
...org,
parentId: org.parentId ?? 0,
children: Array.isArray(org.children) ? org.children.map(normalizeOrg) : undefined
})
const loadTree = async () => {
treeLoading.value = true
try {
const data = await DeviceApi.getDeviceList()
const devices = Array.isArray(data) ? data : data?.list || data?.data || []
treeData.value = buildTreeFromDevices(devices)
defaultExpandedKeys.value = ['iot-device-root']
} catch {
message.error(t('ProductionReport.Index.loadTreeFailed'))
} finally {
treeLoading.value = false
}
}
const normalizedOrgs = orgs.map(normalizeOrg)
const isNestedTree = normalizedOrgs.some((org) => Array.isArray(org.children))
const orgTree = isNestedTree
? normalizedOrgs
: (handleTree(normalizedOrgs, 'id', 'parentId', 'children') as DeviceParameterAnalysisNodeVO[])
const toTreeNode = (org: DeviceParameterAnalysisNodeVO): ProductionTreeNode => {
const orgChildren = Array.isArray(org.children) ? org.children.map(toTreeNode) : []
const equipmentChildren = (org.equipments || []).map((equipment) => ({
id: `equipment-${equipment.id}`,
label: equipment.name || String(equipment.id),
tag: 'equipment' as const,
deviceId: String(equipment.id)
}))
const children = [...orgChildren, ...equipmentChildren]
const handleOpenGenerateDialog = async () => {
generateDialogVisible.value = true
if (!treeData.value.length) await loadTree()
}
return {
id: `org-${org.id}`,
label: org.name || String(org.id),
children: children.length ? children : undefined
const handleDeviceSelectionChange = (deviceIds: string[]) => {
const findDevice = (nodes: ProductionTreeNode[]): ProductionTreeNode | undefined => {
for (const node of nodes) {
if (node.tag === 'equipment' && deviceIds.includes(node.id)) return node
const device = node.children && findDevice(node.children)
if (device) return device
}
}
return orgTree.map(toTreeNode)
selectedDevice.value = findDevice(treeData.value)
}
const hasEquipmentInSubtree = (node: ProductionTreeNode): boolean => {
if (node.tag === 'equipment') return true
return (node.children || []).some(hasEquipmentInSubtree)
const loadWorkTeams = async () => {
workTeamLoading.value = true
try {
const data = await WorkTeamApi.getWorkTeamList()
workTeamList.value = Array.isArray(data) ? data : data.list || []
if (!selectedWorkTeamId.value && workTeamList.value.length) {
selectedWorkTeamId.value = String(workTeamList.value[0].id)
}
} catch {
message.error(t('ProductionReport.Index.loadWorkTeamsFailed'))
} finally {
workTeamLoading.value = false
}
}
const buildDefaultExpandedKeys = (nodes: ProductionTreeNode[]): string[] => {
const keys: string[] = []
const walk = (list: ProductionTreeNode[]) => {
list.forEach((node) => {
if (node.tag !== 'equipment' && hasEquipmentInSubtree(node)) keys.push(node.id)
if (node.children?.length) walk(node.children)
const loadReportFileList = async () => {
reportListLoading.value = true
try {
const data = await DeviceApi.getProductionDailyReportPage({
pageNo: reportQueryParams.pageNo,
pageSize: reportQueryParams.pageSize,
reportDate: reportDate.value || undefined
})
reportFileList.value = data.list || []
reportQueryParams.total = data.total || 0
} catch {
reportFileList.value = []
reportQueryParams.total = 0
message.error(t('ProductionReport.Index.loadReportListFailed'))
} finally {
reportListLoading.value = false
}
walk(nodes)
return keys
}
const loadTree = async () => {
treeLoading.value = true
const handleReportQuery = () => {
reportQueryParams.pageNo = 1
loadReportFileList()
}
const handleCalendarDayClick = (day: string) => {
reportDate.value = day
calendarDate.value = dayjs(day).toDate()
handleReportQuery()
}
const handleGenerateReport = async () => {
const deviceIds = selectedDeviceIds.value.filter((id): id is string => Boolean(id))
if (!deviceIds.length) {
message.warning(t('ProductionReport.Index.selectDeviceForReport'))
return
}
if (!selectedWorkTeamId.value) {
message.warning(t('ProductionReport.Index.selectWorkTeamForReport'))
return
}
generateLoading.value = true
try {
const res = await OrganizationApi.deviceParameterAnalysis({
keyword: keyword.value || undefined,
showDevices: 1,
filterNoDeviceLine: 1
await DeviceApi.generateProductionDailyReport({
deviceIds,
reportDate: reportDate.value,
workTeamId: selectedWorkTeamId.value
})
treeData.value = buildTreeFromApi(extractApiOrgs(res))
defaultExpandedKeys.value = buildDefaultExpandedKeys(treeData.value)
await nextTick()
treeRef.value?.setExpandedKeys?.(defaultExpandedKeys.value)
message.success(t('ProductionReport.Index.generateSuccess'))
generateDialogVisible.value = false
handleReportQuery()
} catch {
message.error(t('ProductionReport.Index.loadTreeFailed'))
message.error(t('ProductionReport.Index.generateFailed'))
} finally {
treeLoading.value = false
generateLoading.value = false
}
}
const handleKeywordChange = () => {
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
keywordTimer = undefined
loadTree()
}, 300)
const getDeviceNames = (deviceIds: Array<string | number>) => {
const names = new Map<string, string>()
const walk = (nodes: ProductionTreeNode[]) => nodes.forEach((node) => {
if (node.tag === 'equipment' && node.deviceId) names.set(String(node.deviceId), node.label)
if (node.children) walk(node.children)
})
walk(treeData.value)
return (deviceIds || []).map((id) => names.get(String(id)) || String(id)).join('、') || '-'
}
const handleTreeRefresh = async () => {
if (keywordTimer) {
window.clearTimeout(keywordTimer)
keywordTimer = undefined
const formatReportDate = (value: unknown) => {
const dateParts = Array.isArray(value) ? value : String(value ?? '').split(',')
if (dateParts.length === 3) {
const [year, month, day] = dateParts.map((part) => String(part).trim())
if (year && month && day) return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
}
await loadTree()
return String(value ?? '-')
}
const handleNodeClick = (node: ProductionTreeNode) => {
if (node.tag !== 'equipment') return
selectedDevice.value = node
const formatGeneratedTime = (value: unknown) => {
const date = dayjs(value as string | number | Date)
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-'
}
const downloadReport = (report: ProductionDailyReportFileVO) => {
if (report.fileUrl) downloadByUrl({ url: report.fileUrl, fileName: report.fileName })
}
const getCalendarRange = () => {
@ -368,11 +479,9 @@ watch(
onMounted(() => {
loadTree()
loadWorkTeams().then(loadReportFileList)
})
onBeforeUnmount(() => {
if (keywordTimer) window.clearTimeout(keywordTimer)
})
</script>
<style scoped lang="scss">
@ -380,43 +489,38 @@ onBeforeUnmount(() => {
min-height: calc(100vh - 130px);
}
.production-report-tree-panel,
.production-report-content-row {
align-items: stretch;
}
.production-report-calendar-panel {
min-height: calc(100vh - 130px);
position: relative;
}
.production-report-tree-toolbar {
.production-report-report-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
margin-bottom: 16px;
}
.production-report-tree {
max-height: calc(100vh - 220px);
margin-top: 12px;
overflow: auto;
.production-report-work-team {
width: 180px;
}
.production-report-tree-node {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 6px;
.production-report-file-panel {
min-height: 100%;
}
.production-report-tree-node__icon {
flex: none;
color: var(--el-color-primary);
}
.production-report-tree-node__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.production-report-calendar-panel {
position: relative;
.production-report-file-panel__header {
display: flex;
align-items: baseline;
margin-bottom: 12px;
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: 600;
}
.production-report-calendar-header {
@ -539,8 +643,8 @@ onBeforeUnmount(() => {
}
:deep(.el-calendar-table .el-calendar-day) {
height: 120px;
padding: 8px;
height: 86px;
padding: 6px;
}
:deep(.el-calendar-table td.is-selected) {
@ -548,13 +652,12 @@ onBeforeUnmount(() => {
}
@media (max-width: 768px) {
.production-report-tree-panel,
.production-report-calendar-panel {
min-height: auto;
}
.production-report-calendar-panel {
margin-top: 16px;
margin-bottom: 16px;
}
.production-report-calendar-header {
@ -565,5 +668,10 @@ onBeforeUnmount(() => {
.production-report-calendar-controller {
width: 100%;
}
.production-report-work-team {
width: 100%;
}
}
</style>

Loading…
Cancel
Save