初始化2

This commit is contained in:
SunYF
2026-08-28 15:06:01 +08:00
parent b34325a16f
commit a9c3fbeb41
537 changed files with 176215 additions and 17 deletions
@@ -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>
);
}
+128
View File
@@ -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>
);
}