feat: update MES tracking and timesheet reports
This commit is contained in:
BIN
装配任务看板/dist.zip
Normal file
BIN
装配任务看板/dist.zip
Normal file
Binary file not shown.
@@ -15,6 +15,12 @@ const routes = [
|
||||
name: "root",
|
||||
component: () => import("@/views/index.vue"),
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
// 测试任务看板
|
||||
path: "/TestTaskDashboard",
|
||||
name: "TestTaskDashboard",
|
||||
component: () => import("@/views/TestTaskDashboard.vue"),
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
898
装配任务看板/src/views/TestTaskDashboard.vue
Normal file
898
装配任务看板/src/views/TestTaskDashboard.vue
Normal file
@@ -0,0 +1,898 @@
|
||||
<template>
|
||||
<div class="assembly-dashboard"
|
||||
:style="{ width: `${style.width}px`, height: `${style.height}px`, transform: style.transform }">
|
||||
<div class="dashboard-shell">
|
||||
<header class="dashboard-header">
|
||||
<div class="brand"><img class="brand-logo" :src="clientLogoUrl" alt="RENLY" /></div>
|
||||
<h1>质检测试任务看板</h1>
|
||||
<div class="current-time">{{ currentTime }}</div>
|
||||
</header>
|
||||
<main class="dashboard-content">
|
||||
<section class="summary-row">
|
||||
<article v-for="item in summaryCards" :key="item.title" class="panel summary-card" :class="item.className">
|
||||
<div>
|
||||
<div class="summary-title">{{ item.title }}</div>
|
||||
<div v-if="item.metrics" class="summary-metrics">
|
||||
<div v-for="metric in item.metrics" :key="metric.label" class="summary-metric">
|
||||
<span>{{ metric.label }}</span>
|
||||
<b>{{ metric.value }}</b>
|
||||
<small>{{ item.unit }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="summary-body">
|
||||
<img class="summary-icon" :src="item.icon" :alt="item.title" />
|
||||
<div class="summary-value" :class="item.valueClass"><span>{{ item.value }}</span><small
|
||||
v-if="item.unit">{{ item.unit }}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.people" class="people-list">
|
||||
<div v-for="person in item.people" :key="person.name"><span>{{ person.name }}</span><b>{{ person.group
|
||||
}}</b></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<section class="main-grid">
|
||||
<div class="left-column">
|
||||
<article class="panel table-panel">
|
||||
<div class="panel-caption">发货状态列表</div>
|
||||
<div class="scroll-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="idx-th">序号</th>
|
||||
<th v-for="c in taskColumns2" :key="c.key">{{ c.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :style="deliveryScrollStyle">
|
||||
<tr v-for="(r, i) in deliveryDisplayRows" :key="i">
|
||||
<td class="idx-td">{{ r._idx }}</td>
|
||||
<td v-for="c in taskColumns2" :key="c.key">{{ r[c.key] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
<article class="panel table-panel task-panel">
|
||||
<div class="panel-caption">测试装配任务列表</div>
|
||||
<div class="scroll-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="idx-th">序号</th>
|
||||
<th v-for="c in taskColumns" :key="c.key">{{ c.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :style="taskScrollStyle">
|
||||
<tr v-for="(r, i) in taskDisplayRows" :key="i" :class="{ warning: r.urgentQty > 0 }">
|
||||
<td class="idx-td">{{ r._idx }}</td>
|
||||
<td v-for="c in taskColumns" :key="c.key">{{ r[c.key] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<aside class="right-column">
|
||||
<article class="panel table-panel">
|
||||
<div class="panel-caption">在制任务缺件明细</div>
|
||||
<div class="scroll-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="idx-th">序号</th>
|
||||
<th v-for="c in materialColumns" :key="c.key">{{ c.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :style="materialScrollStyle">
|
||||
<tr v-for="(r, i) in materialDisplayRows" :key="i">
|
||||
<td class="idx-td">{{ r._idx }}</td>
|
||||
<td v-for="c in materialColumns" :key="c.key">{{ r[c.key] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
<article class="panel table-panel">
|
||||
<div class="panel-caption">已完工任务</div>
|
||||
<div class="scroll-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="idx-th">序号</th>
|
||||
<th v-for="c in doneColumns" :key="c.key">{{ c.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :style="doneScrollStyle">
|
||||
<tr v-for="(r, i) in doneDisplayRows" :key="i">
|
||||
<td class="idx-td">{{ r._idx }}</td>
|
||||
<td v-for="c in doneColumns" :key="c.key" :class="c.key === 'passRate' ? r.rateClass : ''">{{ r[c.key] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
const CreateData = inject('CreateData')
|
||||
const ExecDatabase = inject('ExecDatabase')
|
||||
defineOptions({ name: 'AssemblyCenterTaskDashboard' })
|
||||
|
||||
const now = ref(new Date('2026-05-06 10:10:10'))
|
||||
let timer = null
|
||||
let resizeHandler = null
|
||||
let scrollTimers = []
|
||||
const style = reactive({ width: 1920, height: 1080, transform: 'scaleY(1) scaleX(1) translate(-50%, -50%)' })
|
||||
const setScale = () => { const s = getScale(); style.transform = `scaleY(${s.y}) scaleX(${s.x}) translate(-50%, -50%)` }
|
||||
const getScale = () => ({ x: window.innerWidth / style.width, y: window.innerHeight / style.height })
|
||||
const pad = v => String(v).padStart(2, '0')
|
||||
const currentTime = computed(() => `${now.value.getFullYear()}-${pad(now.value.getMonth() + 1)}-${pad(now.value.getDate())} ${pad(now.value.getHours())}:${pad(now.value.getMinutes())}:${pad(now.value.getSeconds())}`)
|
||||
const clientLogoUrl = './public/ClientLogo.png'
|
||||
const icon = name => `./public/svg/${name}.svg`
|
||||
const tableData = ref([])
|
||||
const getFirstRow = rows => {
|
||||
if (Array.isArray(rows) && rows.length > 0) {
|
||||
return rows[0]
|
||||
}
|
||||
return {}
|
||||
}
|
||||
onMounted(() => {
|
||||
startClock();
|
||||
setScale();
|
||||
resizeHandler = setScale;
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
searchDashboardData()
|
||||
startAllScroll()
|
||||
setInterval(function(){
|
||||
searchDashboardData()
|
||||
},600000)
|
||||
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (timer) window.clearInterval(timer);
|
||||
if (resizeHandler) window.removeEventListener('resize', resizeHandler)
|
||||
stopAllScroll()
|
||||
})
|
||||
|
||||
// ============ 滚动数据 ============
|
||||
const deliveryRows = ref([])
|
||||
const taskRows = ref([])
|
||||
const materialRows = ref([])
|
||||
const doneRows = ref([])
|
||||
|
||||
// 每个列表的滚动偏移
|
||||
const deliveryOffset = ref(0)
|
||||
const taskOffset = ref(0)
|
||||
const materialOffset = ref(0)
|
||||
const doneOffset = ref(0)
|
||||
|
||||
// 可见行数
|
||||
const VISIBLE_ROWS = {
|
||||
delivery: 8,
|
||||
task: 7,
|
||||
material: 8,
|
||||
done: 7
|
||||
}
|
||||
|
||||
// 生成带序号和循环滚动的显示行
|
||||
const makeDisplayRows = (rows, offset, visibleCount) => {
|
||||
if (!rows || rows.length === 0) return []
|
||||
const len = rows.length
|
||||
const result = []
|
||||
for (let i = 0; i < visibleCount; i++) {
|
||||
const idx = (offset + i) % len
|
||||
result.push({ ...rows[idx], _idx: idx + 1 })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const deliveryDisplayRows = computed(() => makeDisplayRows(deliveryRows.value, deliveryOffset.value, VISIBLE_ROWS.delivery))
|
||||
const taskDisplayRows = computed(() => makeDisplayRows(taskRows.value, taskOffset.value, VISIBLE_ROWS.task))
|
||||
const materialDisplayRows = computed(() => makeDisplayRows(materialRows.value, materialOffset.value, VISIBLE_ROWS.material))
|
||||
const doneDisplayRows = computed(() => makeDisplayRows(doneRows.value, doneOffset.value, VISIBLE_ROWS.done))
|
||||
|
||||
// 滚动样式(平滑过渡)
|
||||
const deliveryScrollStyle = computed(() => ({ transition: 'none' }))
|
||||
const taskScrollStyle = computed(() => ({ transition: 'none' }))
|
||||
const materialScrollStyle = computed(() => ({ transition: 'none' }))
|
||||
const doneScrollStyle = computed(() => ({ transition: 'none' }))
|
||||
|
||||
// 启动单个列表滚动
|
||||
const startScroll = (rowsRef, offsetRef) => {
|
||||
const id = window.setInterval(() => {
|
||||
if (rowsRef.value.length > 0) {
|
||||
offsetRef.value = (offsetRef.value + 1) % rowsRef.value.length
|
||||
}
|
||||
}, 2000)
|
||||
scrollTimers.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
const startAllScroll = () => {
|
||||
startScroll(deliveryRows, deliveryOffset)
|
||||
startScroll(taskRows, taskOffset)
|
||||
startScroll(materialRows, materialOffset)
|
||||
startScroll(doneRows, doneOffset)
|
||||
}
|
||||
|
||||
const stopAllScroll = () => {
|
||||
scrollTimers.forEach(id => window.clearInterval(id))
|
||||
scrollTimers = []
|
||||
}
|
||||
|
||||
// ============ 数据查询 ============
|
||||
const searchDashboardData = () => {
|
||||
searchTestAssemblyTaskData()
|
||||
searchQualityTimelinessData()
|
||||
searchDoneTaskData()
|
||||
searchMaterialshortageData()
|
||||
searchDeliveryData()
|
||||
}
|
||||
const searchTestAssemblyTaskData = () => {
|
||||
const param = [
|
||||
['任务类型', '全部'],
|
||||
['生产订单', ''],
|
||||
['销售订单', ''],
|
||||
['物料名称', ''],
|
||||
['物料编码', '']
|
||||
]
|
||||
queryDashboardData('质量管理_测试装配任务_查询', applyTestAssemblyTaskData, param)
|
||||
}
|
||||
const searchQualityTimelinessData = () => {
|
||||
const param = [
|
||||
['任务类型', '全部'],
|
||||
['生产订单', ''],
|
||||
['销售订单', ''],
|
||||
['物料名称', ''],
|
||||
['物料编码', '']
|
||||
]
|
||||
queryDashboardData('质量管理_测试装配任务_质检及时率', applyQualityTimelinessData, param)
|
||||
}
|
||||
const searchDoneTaskData = () => {
|
||||
const param = [
|
||||
['任务类型', '全部'],
|
||||
['生产订单', ''],
|
||||
['销售订单', ''],
|
||||
['物料名称', ''],
|
||||
['物料编码', '']
|
||||
]
|
||||
queryDashboardData('质量管理_测试装配任务_完成查询', applyDoneTaskData, param)
|
||||
}
|
||||
const searchDeliveryData = () => {
|
||||
queryDashboardData('装配中心任务看板_发货状态查询', applyDeliveryData)
|
||||
}
|
||||
const searchMaterialshortageData = () => {
|
||||
queryDashboardData('装配中心任务看板_在制品缺件查询', applyMaterialshortageData)
|
||||
}
|
||||
const applyTestAssemblyTaskData = rows => {
|
||||
const sortedRows = sortTestAssemblyTaskRows(rows)
|
||||
const executableRows = sortedRows.filter(isInspectionUnfinished)
|
||||
const valueRows = executableRows.filter(item => getTaskType(item) === '阀类')
|
||||
const systemRows = executableRows.filter(item => getTaskType(item) === '系统类')
|
||||
|
||||
summaryCards[0].metrics[0].value = valueRows.length
|
||||
summaryCards[0].metrics[1].value = systemRows.length
|
||||
summaryCards[1].value = executableRows.filter(isReworkTask).length
|
||||
summaryCards[3].value = executableRows.filter(item => getNumber(item.加急总数 || item.加急数量) > 0).length
|
||||
summaryCards[4].value = sortedRows.reduce((sum, item) => sum + getNumber(item.异常未关闭数 || item.测试装配异常未关闭总数), 0)
|
||||
|
||||
applyTaskData(sortedRows)
|
||||
}
|
||||
const applyQualityTimelinessData = rows => {
|
||||
const row = getFirstRow(rows)
|
||||
summaryCards[2].value = `${getNumber(row.质检及时率).toFixed(2)}`
|
||||
}
|
||||
const applyDeliveryData = rows => {
|
||||
deliveryRows.value = rows.map(item => {
|
||||
let requiredDate = ''
|
||||
if (item.要求发货日期) {
|
||||
// 如果是字符串
|
||||
if (typeof item.要求发货日期 === 'string') {
|
||||
requiredDate = item.要求发货日期.split(' ')[0]
|
||||
}
|
||||
// 如果是 Date 对象
|
||||
else if (item.要求发货日期 instanceof Date) {
|
||||
requiredDate = item.要求发货日期.toISOString().split('T')[0]
|
||||
}
|
||||
// 如果是其他格式
|
||||
else {
|
||||
const dateStr = String(item.要求发货日期)
|
||||
requiredDate = dateStr.split(' ')[0]
|
||||
}
|
||||
}
|
||||
return {
|
||||
productionOrder: item.订单编号,
|
||||
salesOrder: item.合同号 || '',
|
||||
materialName: item.物料描述 || '',
|
||||
materialCode: item.物料编号 || '',
|
||||
completeSet: item.齐套 === 1 ? '是' : '否',
|
||||
issueStatus: item.发料状态 === '1' ? '发料齐套' : (item.发料状态 === '2' ? '发料缺件' : ''),
|
||||
urgentQty: item.操作人,
|
||||
assignQty: item.交货数量,
|
||||
taskdate: requiredDate,
|
||||
operator: item.任务状态 === 0 ? '待执行' : (item.发料状态 === 1 ? '可执行' : '已开工'),
|
||||
}
|
||||
})
|
||||
}
|
||||
const applyDoneTaskData = rows => {
|
||||
doneRows.value = rows.map(item => ({
|
||||
productionOrder: item.生产订单?.toString() || item.计划号?.toString() || '',
|
||||
materialName: item.物料名称 || item.零件名称 || '',
|
||||
materialCode: item.物料编码 || item.零件编码 || '',
|
||||
passRate: `${getPassRate(item)}%`,
|
||||
rateClass: getPassRate(item) <= 70 ? 'orange-cell' : 'green-cell'
|
||||
}))
|
||||
}
|
||||
|
||||
const applyTaskData = rows => {
|
||||
taskRows.value = rows.map(item => ({
|
||||
productionOrder: item.生产订单?.toString() || item.订单号?.toString() || item.计划号?.toString() || '',
|
||||
salesOrder: item.销售订单 || item.合同号 || '',
|
||||
materialName: item.物料名称 || item.零件名称 || '',
|
||||
materialCode: item.物料编码 || item.零件编码 || '',
|
||||
taskType: item.任务类型 || (String(item.自制件属性 || item.物料名称 || item.零件名称 || '').includes('系统') ? '系统类' : '阀类'),
|
||||
urgentQty: getNumber(item.加急总数 || item.加急数量),
|
||||
assignQty: getNumber(item.计划数量 || item.计划生产数 || item.分配数),
|
||||
completeQty: getNumber(item.完成数量),
|
||||
inspectQty: getNumber(item.检验数量),
|
||||
expectedInspectionDate: formatDate(item.预计送检日期)
|
||||
}))
|
||||
}
|
||||
const applyMaterialshortageData = rows => {
|
||||
console.log(rows)
|
||||
materialRows.value = rows.map(item => ({
|
||||
wipOrder: item.DocEntry,
|
||||
shortageStatus: item.U_Name,
|
||||
finishTime: item.ItemCode,
|
||||
owner: item.加急数量
|
||||
}))
|
||||
console.log(materialRows.value)
|
||||
}
|
||||
const queryDashboardData = (procedureName, applyData, param = []) => {
|
||||
let data
|
||||
data = CreateData('11', procedureName, param)
|
||||
ExecDatabase(data).then(response => {
|
||||
const rows = getResponseRows(response)
|
||||
tableData.value = rows
|
||||
applyData(rows)
|
||||
})
|
||||
}
|
||||
const getResponseRows = response => {
|
||||
if (response && Array.isArray(response.data)) {
|
||||
return response.data
|
||||
}
|
||||
if (response && response.data && Array.isArray(response.data.result)) {
|
||||
return response.data.result
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getNumber = value => {
|
||||
if (value === null || value === undefined || value === '') return 0
|
||||
const number = Number(String(value).replace(/,/g, ''))
|
||||
return Number.isNaN(number) ? 0 : number
|
||||
}
|
||||
|
||||
const formatDate = value => {
|
||||
if (!value) return ''
|
||||
return String(value).split(' ')[0]
|
||||
}
|
||||
|
||||
const getDateTime = value => {
|
||||
if (!value) return Infinity
|
||||
const time = new Date(String(value).replace(/-/g, '/')).getTime()
|
||||
return Number.isNaN(time) ? Infinity : time
|
||||
}
|
||||
|
||||
const getTaskType = item => item.任务类型 || (String(item.自制件属性 || item.物料名称 || item.零件名称 || '').includes('系统') ? '系统类' : '阀类')
|
||||
|
||||
const isInspectionUnfinished = item => getNumber(item.完成数量) > getNumber(item.检验数量)
|
||||
|
||||
const isReworkTask = item => String(item.检测说明 || '').includes('返工')
|
||||
|
||||
const sortTestAssemblyTaskRows = rows => rows.slice().sort((a, b) => {
|
||||
const unfinishedA = isInspectionUnfinished(a)
|
||||
const unfinishedB = isInspectionUnfinished(b)
|
||||
if (unfinishedA !== unfinishedB) return unfinishedA ? -1 : 1
|
||||
return getDateTime(a.预计送检日期) - getDateTime(b.预计送检日期)
|
||||
})
|
||||
|
||||
const getPassRate = item => {
|
||||
const inspectQty = getNumber(item.检验数量)
|
||||
if (inspectQty <= 0) return 100
|
||||
return Number(((getNumber(item.检验合格数量) * 100) / inspectQty).toFixed(2))
|
||||
}
|
||||
|
||||
const startClock = () => { timer = window.setInterval(() => { now.value = new Date(now.value.getTime() + 1000) }, 1000) }
|
||||
const svgData = svg => 'data:image/svg+xml;utf8,' + encodeURIComponent(svg)
|
||||
const svgIcon = (from, to, body) => svgData(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="${from}"/>
|
||||
<stop offset="1" stop-color="${to}"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="shine" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".35"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-55%" y="-55%" width="210%" height="210%"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
||||
</defs>
|
||||
<rect x="7" y="7" width="58" height="58" rx="11" fill="url(#bg)" filter="url(#glow)"/>
|
||||
<path d="M14 14h44v20c-10-4-29-4-44 1z" fill="url(#shine)"/>
|
||||
<g transform="translate(36 36) scale(1.12) translate(-36 -36)" stroke="#fff" stroke-width="4.1" stroke-linecap="round" stroke-linejoin="round" opacity=".96">${body}</g>
|
||||
</svg>`
|
||||
)
|
||||
const icons = {
|
||||
pending: svgIcon('#37a9ff', '#0862f3', '<rect x="23" y="20" width="26" height="36" rx="4"/><path d="M29 20h14l-3-6h-8l-3 6zM29 34h14M29 43h14"/>'),
|
||||
urgent: svgIcon('#ff7469', '#dc2524', '<path d="M21 55h30M25 50V34a11 11 0 0 1 22 0v16M36 13v8M18 21l6 6M54 21l-6 6M15 38h8M49 38h8"/>'),
|
||||
cart: svgIcon('#28a8ff', '#0a58ff', '<path d="M16 22h7l5 25h27l5-17H28"/><circle cx="32" cy="56" r="2.8"/><circle cx="52" cy="56" r="2.8"/>'),
|
||||
checklist: svgIcon('#49e89e', '#08a86e', '<rect x="21" y="17" width="32" height="42" rx="5"/><path d="M28 35l5 5 12-13M28 49h19"/>'),
|
||||
returnDown: svgIcon('#ffb23f', '#fb7418', '<rect x="21" y="23" width="31" height="32" rx="4"/><path d="M36 14v24M27 31l9 9 9-9"/>'),
|
||||
returnSales: svgIcon('#9867ff', '#6232e6', '<path d="M51 22H28a10 10 0 0 0 0 20h26M28 14l-11 9 11 9"/><rect x="35" y="37" width="18" height="18" rx="2"/>'),
|
||||
finalCheck: svgIcon('#3a95ff', '#0a61e8', '<path d="M36 14l16 7v13c0 11-7 18-16 24-9-6-16-13-16-24V21z"/><path d="M27 37l6 6 13-15"/>'),
|
||||
other: svgIcon('#4bd8ff', '#0a88cc', '<path d="M19 26l17-10 17 10-17 10zM19 26v19l17 10 17-10V26M36 36v19"/>'),
|
||||
shield: svgIcon('#3a9aff', '#0861e8', '<path d="M36 14l16 7v13c0 11-7 18-16 24-9-6-16-13-16-24V21z"/><path d="M28 37l6 6 12-14"/>'),
|
||||
done: svgIcon('#47eaa0', '#0da76b', '<rect x="19" y="19" width="34" height="34" rx="8"/><path d="M27 37l7 7 15-18"/>')
|
||||
}
|
||||
const summaryCards = [
|
||||
{ title: '可执行任务数', unit: '条', className: 'executable-card', metrics: [{ label: '阀类', value: 0 }, { label: '系统类', value: 0 }] },
|
||||
{ title: '待协助任务数', value: 0, unit: '条', icon: icons.pending , valueClass: 'red-text'},
|
||||
{ title: '质检及时率', value: '100.00', icon: icons.finalCheck, valueClass: 'green-text full-value' },
|
||||
{ title: '加急任务数', value: 125, unit: '条', icon: icons.urgent, valueClass: 'red-text' },
|
||||
{ title: '异常未关闭消息数', value: 5, unit: '条', icon: icons.urgent, valueClass: 'red-text', className: 'exception-card' },
|
||||
]
|
||||
const taskColumns = [
|
||||
{ label: '生产订单', key: 'productionOrder', width: '72px' },
|
||||
{ label: '销售订单', key: 'salesOrder', width: '92px' },
|
||||
{ label: '物料名称', key: 'materialName', width: '190px' },
|
||||
{ label: '物料编码', key: 'materialCode', width: '176px' },
|
||||
{ label: '任务类型', key: 'taskType', width: '62px' },
|
||||
{ label: '加急数', key: 'urgentQty', width: '62px' },
|
||||
{ label: '计划数', key: 'assignQty', width: '70px' },
|
||||
{ label: '完成数', key: 'completeQty', width: '70px' },
|
||||
{ label: '检验数', key: 'inspectQty', width: '70px' },
|
||||
{ label: '预计送检', key: 'expectedInspectionDate', width: '88px' },
|
||||
|
||||
]
|
||||
const taskColumns2 = [
|
||||
{ label: '生产订单', key: 'productionOrder', width: '72px' },
|
||||
{ label: '销售订单', key: 'salesOrder', width: '92px' },
|
||||
{ label: '物料名称', key: 'materialName', width: '190px' },
|
||||
{ label: '物料编码', key: 'materialCode', width: '176px' },
|
||||
{ label: '数量', key: 'assignQty', width: '176px' },
|
||||
{ label: '要求发货日期', key: 'taskdate', width: '176px' },
|
||||
{ label: '齐套', key: 'completeSet', width: '52px' },
|
||||
{ label: '发料状态', key: 'issueStatus', width: '88px' },
|
||||
{ label: '操作人', key: 'urgentQty', width: '62px' },
|
||||
{ label: '装配开工', key: 'operator', width: '82px' },
|
||||
]
|
||||
const materialColumns = [
|
||||
{ label: '生产订单', key: 'wipOrder', width: '76px' },
|
||||
{ label: '物料名称', key: 'shortageStatus', width: '105px' },
|
||||
{ label: '物料编码', key: 'finishTime', width: '160px' },
|
||||
{ label: '加急数量', key: 'owner', width: '90px' },
|
||||
]
|
||||
const doneColumns = [
|
||||
{ label: '生产订单', key: 'productionOrder', width: '88px' },
|
||||
{ label: '物料名称', key: 'materialName', width: '150px' },
|
||||
{ label: '物料编码', key: 'materialCode', width: '165px' },
|
||||
{ label: '合格率', key: 'passRate', width: '86px' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.assembly-dashboard {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
transform-origin: 0 0;
|
||||
transition: .3s;
|
||||
overflow: hidden;
|
||||
color: #f3f9ff;
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: #07001f;
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: grid;
|
||||
grid-template-columns: 336px 1fr 442px;
|
||||
align-items: center;
|
||||
height: 95px;
|
||||
border-bottom: 1px solid rgba(21, 162, 240, .7);
|
||||
}
|
||||
|
||||
.brand {
|
||||
width: 300px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 224px;
|
||||
height: 58px;
|
||||
padding: 5px 10px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.28));
|
||||
}
|
||||
.dashboard-header h1 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 55px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .08em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
padding-right: 40px;
|
||||
color: #eef7ff;
|
||||
font-size: 27px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
display: grid;
|
||||
grid-template-rows: 126px 1fr;
|
||||
gap: 17px;
|
||||
height: calc(100% - 95px);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.28fr 1fr 1fr 1fr .82fr;
|
||||
gap: 17px;
|
||||
}
|
||||
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2.05fr 1fr;
|
||||
gap: 17px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
display: grid;
|
||||
grid-template-rows: 1.05fr .9fr;
|
||||
gap: 17px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
display: grid;
|
||||
grid-template-rows: 1.05fr .9fr;
|
||||
gap: 17px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
border: 1px solid rgba(29, 153, 229, .78);
|
||||
background: rgba(3, 16, 48, .38);
|
||||
box-shadow: inset 0 0 18px rgba(27, 130, 214, .24), 0 0 10px rgba(1, 132, 220, .18);
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
background: linear-gradient(90deg, #2bd8ff 0 14px, transparent 14px) left top/14px 2px no-repeat, linear-gradient(180deg, #2bd8ff 0 14px, transparent 14px) left top/2px 14px no-repeat, linear-gradient(270deg, #2bd8ff 0 14px, transparent 14px) right top/14px 2px no-repeat, linear-gradient(180deg, #2bd8ff 0 14px, transparent 14px) right top/2px 14px no-repeat, linear-gradient(90deg, #2bd8ff 0 14px, transparent 14px) left bottom/14px 2px no-repeat, linear-gradient(0deg, #2bd8ff 0 14px, transparent 14px) left bottom/2px 14px no-repeat, linear-gradient(270deg, #2bd8ff 0 14px, transparent 14px) right bottom/14px 2px no-repeat, linear-gradient(0deg, #2bd8ff 0 14px, transparent 14px) right bottom/2px 14px no-repeat;
|
||||
}
|
||||
|
||||
.panel::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 0 12px rgba(43, 216, 255, .12);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
padding: 8px 18px;
|
||||
}
|
||||
|
||||
.summary-card > div:first-child {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.absence-card {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.absence-card > div:first-child {
|
||||
width: auto;
|
||||
padding-right: 22px;
|
||||
border-right: 3px solid rgba(238, 247, 255, 0.92);
|
||||
}
|
||||
|
||||
.absence-card .summary-title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.absence-card .summary-body {
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.absence-card .summary-value {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.absence-card .summary-value span {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.executable-card {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.executable-card .summary-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 21px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.summary-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary-metric {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: 22px 48px;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
padding: 6px 10px 4px;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.summary-metric span {
|
||||
grid-column: 1 / -1;
|
||||
color: #89d4ff;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary-metric b {
|
||||
color: #18c65b;
|
||||
font-size: 50px;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
text-shadow: 0 0 10px rgba(24, 198, 91, .32);
|
||||
}
|
||||
|
||||
.summary-metric small {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.exception-card {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.exception-card .summary-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.exception-card .summary-body {
|
||||
grid-template-columns: 54px 150px;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
.exception-card .summary-icon {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.exception-card .summary-value {
|
||||
grid-template-columns: 105px 28px;
|
||||
}
|
||||
|
||||
.exception-card .summary-value span {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.exception-card .summary-value small {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
margin-bottom: 5px;
|
||||
color: #89d4ff;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.summary-body {
|
||||
display: grid;
|
||||
grid-template-columns: 66px 214px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 26px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 32px;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-value span {
|
||||
font-size: 57px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-value small {
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.full-value span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.red-text span {
|
||||
color: #ff1e2e;
|
||||
}
|
||||
|
||||
.green-text span {
|
||||
color: #18c65b;
|
||||
}
|
||||
|
||||
.orange-text span {
|
||||
color: #ff9c12;
|
||||
}
|
||||
|
||||
.absence-card .summary-value::before {
|
||||
content: '总数:';
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.people-list {
|
||||
min-width: 130px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.people-list div {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-caption {
|
||||
height: 38px;
|
||||
color: #8cd3ff;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.scroll-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 9px 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
height: 48px;
|
||||
border: 1px solid rgba(53, 149, 217, .54);
|
||||
color: #f1f8ff;
|
||||
font-size: 14px;
|
||||
line-height: 1.18;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
background: rgba(4, 24, 57, .44);
|
||||
}
|
||||
|
||||
th {
|
||||
height: 44px;
|
||||
color: #bde9ff;
|
||||
font-weight: 500;
|
||||
background: rgba(12, 68, 119, .28);
|
||||
}
|
||||
|
||||
.idx-th {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.idx-td {
|
||||
width: 44px;
|
||||
color: #89d4ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-panel td {
|
||||
height: 47px;
|
||||
}
|
||||
|
||||
.warning td {
|
||||
color: #ff4949;
|
||||
}
|
||||
|
||||
.green-cell {
|
||||
color: #32ff32;
|
||||
}
|
||||
|
||||
.orange-cell {
|
||||
color: #ff7a1a;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user