初始化2
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { Card, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import type { DashboardData, AlarmItem } from '../types';
|
||||
|
||||
const typeLabel: Record<AlarmItem['type'], string> = {
|
||||
equipment: '设备故障',
|
||||
material: '物料短缺',
|
||||
};
|
||||
|
||||
export function AlarmBoard({ data }: { data: DashboardData }) {
|
||||
const rows = data.alarms.map((a) => ({
|
||||
key: a.id,
|
||||
level: a.level,
|
||||
type: a.type,
|
||||
target: a.target,
|
||||
message: a.message,
|
||||
time: a.time,
|
||||
status: a.status,
|
||||
}));
|
||||
|
||||
const activeCount = data.alarms.filter((a) => a.status === 'active').length;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
width: 90,
|
||||
render: (level: AlarmItem['level']) => (
|
||||
<Tag color={level === 'critical' ? 'red' : 'orange'}>
|
||||
{level === 'critical' ? '严重' : '警告'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 110,
|
||||
render: (type: AlarmItem['type']) => typeLabel[type] ?? type,
|
||||
},
|
||||
{ title: '对象', dataIndex: 'target', width: 140 },
|
||||
{ title: '描述', dataIndex: 'message' },
|
||||
{ title: '时间', dataIndex: 'time', width: 90 },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (status: AlarmItem['status']) => (
|
||||
<Tag color={status === 'active' ? 'red' : 'green'}>
|
||||
{status === 'active' ? '处理中' : '已解决'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card title={`异常报警(当前告警 ${activeCount} 条)`}>
|
||||
{rows.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||
<Typography.Text type="tertiary">当前无异常报警</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<Table columns={columns} dataSource={rows} pagination={false} rowKey="key" />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Card, Tag, Tooltip } from '@douyinfe/semi-ui';
|
||||
import type { DashboardData, Station, ToolStatus } from '../types';
|
||||
|
||||
const toolColor: Record<ToolStatus, string> = {
|
||||
online: '#00d68f',
|
||||
busy: '#36a3f7',
|
||||
alarm: '#ff5252',
|
||||
offline: '#8fa3bf',
|
||||
};
|
||||
|
||||
const toolText: Record<ToolStatus, string> = {
|
||||
online: '在线',
|
||||
busy: '作业中',
|
||||
alarm: '报警',
|
||||
offline: '离线',
|
||||
};
|
||||
|
||||
const stationColor: Record<Station['status'], string> = {
|
||||
running: '#00d68f',
|
||||
idle: '#36a3f7',
|
||||
alarm: '#ff5252',
|
||||
offline: '#8fa3bf',
|
||||
};
|
||||
|
||||
/**
|
||||
* 平面产线模型:12 个工位,每个工位含「扫码枪 + 拧紧枪」两台设备。
|
||||
* 本装配线仅这两类设备,无其他设备。
|
||||
*/
|
||||
export function LineModel({ data }: { data: DashboardData }) {
|
||||
const stations = data.equipment;
|
||||
|
||||
return (
|
||||
<Card title="产线平面模型(12 工位 · 扫码枪 + 拧紧枪)">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 14,
|
||||
justifyContent: stations.length ? 'flex-start' : 'center',
|
||||
}}
|
||||
>
|
||||
{stations.length === 0 && (
|
||||
<Tag color="grey">暂无工位数据</Tag>
|
||||
)}
|
||||
{stations.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
style={{
|
||||
width: 148,
|
||||
border: `2px solid ${stationColor[s.status]}`,
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<strong>{s.name}</strong>
|
||||
<Tag color={s.status === 'alarm' ? 'red' : s.status === 'offline' ? 'grey' : 'green'}>
|
||||
{statusText(s.status)}
|
||||
</Tag>
|
||||
</div>
|
||||
<Tooltip content={`扫码枪 · 累计 ${s.scanGun.processedCount} 件`}>
|
||||
<DeviceLine label="扫码枪" status={s.scanGun.status} />
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={`拧紧枪 · 累计 ${s.tighteningGun.processedCount} 件${
|
||||
s.tighteningGun.currentSn ? ` · ${s.tighteningGun.currentSn}` : ''
|
||||
}`}
|
||||
>
|
||||
<DeviceLine label="拧紧枪" status={s.tighteningGun.status} />
|
||||
</Tooltip>
|
||||
{s.currentSn && (
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#8fa3bf' }}>
|
||||
当前工件:{s.currentSn}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20, display: 'flex', gap: 20, flexWrap: 'wrap' }}>
|
||||
<Legend color="#00d68f" label="在线 / 运行" />
|
||||
<Legend color="#36a3f7" label="作业中 / 空闲" />
|
||||
<Legend color="#ff5252" label="报警" />
|
||||
<Legend color="#8fa3bf" label="离线" />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceLine({ label, status }: { label: string; status: ToolStatus }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
|
||||
<span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 5, background: toolColor[status] }} />
|
||||
<span style={{ fontSize: 13 }}>{label}</span>
|
||||
<span style={{ fontSize: 12, color: '#8fa3bf' }}>{toolText[status]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusText(s: Station['status']): string {
|
||||
switch (s) {
|
||||
case 'running':
|
||||
return '运行';
|
||||
case 'idle':
|
||||
return '空闲';
|
||||
case 'alarm':
|
||||
return '报警';
|
||||
default:
|
||||
return '离线';
|
||||
}
|
||||
}
|
||||
|
||||
function Legend({ color, label }: { color: string; label: string }) {
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: '#8fa3bf' }}>
|
||||
<span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 5, background: color }} />
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Card, Progress, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import type { DashboardData } from '../types';
|
||||
import { EChart } from '../components/EChart';
|
||||
|
||||
const gridLabel = {
|
||||
color: '#8fa3bf',
|
||||
fontSize: 13,
|
||||
};
|
||||
|
||||
export function ProductionBoard({ data }: { data: DashboardData }) {
|
||||
const { production, progress, trends } = data;
|
||||
const st = production.stationSummary;
|
||||
const prog =
|
||||
progress.totalQty > 0 ? (progress.completedQty / progress.totalQty) * 100 : 0;
|
||||
|
||||
const ringOption: EChartsOption = {
|
||||
tooltip: {
|
||||
formatter: '{b}: {c}({d}%)',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['62%', '82%'],
|
||||
silent: true,
|
||||
label: { show: true, position: 'center', formatter: '{value}%'.replace('{value}', String(production.qualifiedRate)), fontSize: 30, fontWeight: 'bold', color: '#00d68f' },
|
||||
data: [
|
||||
{ value: production.qualityOk, name: '合格', itemStyle: { color: '#00d68f' } },
|
||||
{ value: production.qualityNg, name: '不合格', itemStyle: { color: '#ff5252' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const trendOption: EChartsOption = {
|
||||
grid: { left: 40, right: 16, top: 28, bottom: 24 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { textStyle: gridLabel, top: 0, data: ['当日产量', '合格率'] },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: trends.production.map((t) => t.time),
|
||||
axisLine: { lineStyle: { color: '#335' } },
|
||||
axisLabel: gridLabel,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '产量',
|
||||
splitLine: { lineStyle: { color: '#1f2a3f' } },
|
||||
axisLabel: gridLabel,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '合格率',
|
||||
min: 80,
|
||||
max: 100,
|
||||
splitLine: { show: false },
|
||||
axisLabel: gridLabel,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '当日产量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trends.production.map((t) => t.output),
|
||||
itemStyle: { color: '#36a3f7' },
|
||||
areaStyle: { color: 'rgba(54,163,247,0.2)' },
|
||||
},
|
||||
{
|
||||
name: '合格率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
smooth: true,
|
||||
data: trends.production.map((t) => t.qualifiedRate),
|
||||
itemStyle: { color: '#00d68f' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
|
||||
<Card title="产线运行状态" style={{ gridColumn: 'span 2' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: 12,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Cell value={st.running} label="运行中" color="#00d68f" />
|
||||
<Cell value={st.idle} label="空闲" color="#36a3f7" />
|
||||
<Cell value={st.alarm} label="报警" color="#ff5252" />
|
||||
<Cell value={st.offline} label="离线" color="#8fa3bf" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="当日产量">
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||||
<span style={{ fontSize: 36, fontWeight: 700, color: '#36a3f7' }}>
|
||||
{production.outputToday}
|
||||
</span>
|
||||
<span style={{ color: '#8fa3bf' }}>/ {production.targetToday}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Typography.Text type="tertiary">日计划达成率</Typography.Text>
|
||||
<Progress
|
||||
percent={
|
||||
production.targetToday
|
||||
? Math.round((production.outputToday / production.targetToday) * 100)
|
||||
: 0
|
||||
}
|
||||
showInfo
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="合格率">
|
||||
<EChart option={ringOption} height={170} />
|
||||
</Card>
|
||||
|
||||
<Card title="工单进度">
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Typography.Text strong>{progress.orderNo}</Typography.Text>
|
||||
<Tag size="small" style={{ marginLeft: 8 }} color="green">
|
||||
{progress.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<Typography.Text type="tertiary">{progress.productName}</Typography.Text>
|
||||
<div style={{ margin: '10px 0' }}>
|
||||
<Typography.Text>
|
||||
完成 {progress.completedQty} / {progress.totalQty}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Progress percent={Math.round(prog)} showInfo stroke="#5cabff" />
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Text type="secondary">当前工序:{progress.currentProcess}</Typography.Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="产量与合格率趋势" style={{ gridColumn: 'span 4' }}>
|
||||
<EChart option={trendOption} height={280} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Cell({ value, label, color }: { value: number; label: string; color: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 32, fontWeight: 600, color }}>{value}</div>
|
||||
<div style={{ color: '#8fa3bf', fontSize: 13 }}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Card, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import type { DashboardData } from '../types';
|
||||
|
||||
interface MovementRow {
|
||||
id: string;
|
||||
time: string;
|
||||
dir: string;
|
||||
material: string;
|
||||
qty: string;
|
||||
operator: string;
|
||||
}
|
||||
|
||||
export function WarehouseBoard({ data }: { data: DashboardData }) {
|
||||
const { warehouse: w } = data;
|
||||
const rows: MovementRow[] = w.movements.map((m) => ({
|
||||
id: m.id,
|
||||
time: m.time,
|
||||
dir: m.type === 'in' ? '入库' : '出库',
|
||||
material: m.material,
|
||||
qty: `${m.qty}`,
|
||||
operator: m.operator,
|
||||
}));
|
||||
|
||||
const columns = [
|
||||
{ title: '时间', dataIndex: 'time' },
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'dir',
|
||||
render: (dir: string) => (
|
||||
<Tag color={dir === '入库' ? 'green' : 'blue'}>{dir}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '物料名称', dataIndex: 'material' },
|
||||
{ title: '数量', dataIndex: 'qty' },
|
||||
{ title: '操作人', dataIndex: 'operator' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
|
||||
<Card title="库存总量" style={{ gridColumn: 'span 2' }}>
|
||||
<BigNumber value={w.totalStock} unit="件" color="#36a3f7" />
|
||||
</Card>
|
||||
<Card title="物料种类">
|
||||
<BigNumber value={w.materialTypes} unit="种" color="#00d68f" />
|
||||
</Card>
|
||||
<Card title="当日出入库">
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
<TextStat color="#00d68f" value={w.inboundToday} label="入库" />
|
||||
<TextStat color="#36a3f7" value={w.outboundToday} label="出库" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="出入库动态(实时)" style={{ gridColumn: 'span 4' }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
empty={<Typography.Text type="tertiary">暂无出入库记录</Typography.Text>}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextStat({ value, label, color }: { value: number; label: string; color: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 28, fontWeight: 600, color }}>{value}</div>
|
||||
<div style={{ color: '#8fa3bf', fontSize: 13 }}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BigNumber({
|
||||
value,
|
||||
unit,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
unit: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
||||
<span style={{ fontSize: 36, fontWeight: 700, color }}>{value}</span>
|
||||
<span style={{ color: '#8fa3bf' }}>{unit}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user