feat: 在制品列表信息层级重构 + SLA预警色 + 点击跳转
1. 信息层级重构:
主信息: [状态Tag] 任务名 | 负责人: 张三 ⏰3h
附加信息: 物料名 SN:25022 ...A1B2C3 08-12 10:00
新增后端 material_name 字段, 前端卡片式布局
2. SLA滞留预警色:
<12h → 绿色(正常) | 12-24h → 橙色(警告) | >24h → 红色加粗(危险)
3. 交互: 点击卡片新窗口打开对应产品扫码详情页
This commit is contained in:
@ -30,6 +30,7 @@ class WipTask(BaseModel):
|
|||||||
assignee: str
|
assignee: str
|
||||||
product_sn: str
|
product_sn: str
|
||||||
external_serial: str | None # 业务序列号
|
external_serial: str | None # 业务序列号
|
||||||
|
material_name: str # 物料名称
|
||||||
status: str
|
status: str
|
||||||
received_at: str
|
received_at: str
|
||||||
duration_hours: float
|
duration_hours: float
|
||||||
@ -132,10 +133,10 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]:
|
|||||||
from app.core.time_utils import get_beijing_time, BEIJING_TZ
|
from app.core.time_utils import get_beijing_time, BEIJING_TZ
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Task, Product.serial_number, Product.external_serial)
|
select(Task, Product.serial_number, Product.external_serial, Product.material_name)
|
||||||
.join(Product, Task.product_id == Product.id)
|
.join(Product, Task.product_id == Product.id)
|
||||||
.where(Task.status.in_([TASK_STATUS_PENDING, TASK_STATUS_WIP]))
|
.where(Task.status.in_([TASK_STATUS_PENDING, TASK_STATUS_WIP]))
|
||||||
.limit(limit * 2) # 多取一些,后面 Python 统一排序
|
.limit(limit * 2)
|
||||||
)
|
)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
rows = result.all()
|
rows = result.all()
|
||||||
@ -148,7 +149,7 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]:
|
|||||||
|
|
||||||
now = get_beijing_time()
|
now = get_beijing_time()
|
||||||
wip_list: list[WipTask] = []
|
wip_list: list[WipTask] = []
|
||||||
for task, product_sn, ext_sn in rows:
|
for task, product_sn, ext_sn, mat_name in rows:
|
||||||
start = task.received_at or task.created_at
|
start = task.received_at or task.created_at
|
||||||
if start:
|
if start:
|
||||||
if start.tzinfo is None:
|
if start.tzinfo is None:
|
||||||
@ -167,6 +168,7 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]:
|
|||||||
assignee=name_map.get(task.assignee_id or "", task.assignee_id or "未分配"),
|
assignee=name_map.get(task.assignee_id or "", task.assignee_id or "未分配"),
|
||||||
product_sn=product_sn or "",
|
product_sn=product_sn or "",
|
||||||
external_serial=ext_sn or None,
|
external_serial=ext_sn or None,
|
||||||
|
material_name=mat_name or "",
|
||||||
status=task.status,
|
status=task.status,
|
||||||
received_at=recv_str,
|
received_at=recv_str,
|
||||||
duration_hours=hours,
|
duration_hours=hours,
|
||||||
|
|||||||
@ -64,12 +64,11 @@ function ProgressBar({ a, b, c, total, labels, d }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 滞留时间 ─────────────────────────────────────────────
|
// ─── 滞留时间 SLA 预警颜色 ────────────────────────────────
|
||||||
function durationColor(h: number) {
|
function durationColor(h: number) {
|
||||||
if (h >= 48) return "text-red-600 bg-red-50";
|
if (h >= 24) return "text-red-600 bg-red-100 font-bold"; // 超24h 危险
|
||||||
if (h >= 24) return "text-orange-600 bg-orange-50";
|
if (h >= 12) return "text-orange-600 bg-orange-50"; // 12-24h 警告
|
||||||
if (h >= 8) return "text-amber-600 bg-amber-50";
|
return "text-emerald-600 bg-emerald-50"; // <12h 正常
|
||||||
return "text-gray-500 bg-gray-50";
|
|
||||||
}
|
}
|
||||||
function durationLabel(h: number) {
|
function durationLabel(h: number) {
|
||||||
if (h >= 48) return `${Math.round(h / 24)}天`;
|
if (h >= 48) return `${Math.round(h / 24)}天`;
|
||||||
@ -79,29 +78,44 @@ function durationLabel(h: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WipRow({ t }: { t: WipTask }) {
|
function WipRow({ t }: { t: WipTask }) {
|
||||||
|
const nav = useNavigate();
|
||||||
|
const handleClick = () => {
|
||||||
|
if (t.product_sn) {
|
||||||
|
// 跳转到扫码详情页
|
||||||
|
const base = window.location.origin;
|
||||||
|
window.open(`${base}/scan?sn=${t.product_sn}`, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const sn = t.external_serial || "未录入";
|
||||||
|
const trace = t.product_sn ? t.product_sn.slice(-6) : "";
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 border-b border-gray-50 py-2.5 last:border-0">
|
<div
|
||||||
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold ${
|
onClick={handleClick}
|
||||||
t.status === "WIP" ? "bg-blue-100 text-blue-700" : "bg-amber-100 text-amber-700"
|
className="cursor-pointer rounded-lg border border-gray-100 bg-white px-4 py-3 transition-shadow hover:border-blue-200 hover:shadow-md"
|
||||||
}`}>
|
>
|
||||||
{t.status === "WIP" ? "进行中" : "待接收"}
|
{/* 主信息行:状态 + 任务名 + 负责人 */}
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
<div className="min-w-0 flex-1">
|
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold ${
|
||||||
<div className="flex items-baseline gap-2">
|
t.status === "WIP" ? "bg-blue-100 text-blue-700" : "bg-amber-100 text-amber-700"
|
||||||
<span className="text-sm font-medium text-gray-700 truncate">{t.task_name}</span>
|
}`}>
|
||||||
<span className="text-xs text-gray-500 shrink-0">
|
{t.status === "WIP" ? "进行中" : "待接收"}
|
||||||
{t.external_serial || t.product_sn}
|
</span>
|
||||||
</span>
|
<span className="text-sm font-semibold text-gray-800 truncate">{t.task_name}</span>
|
||||||
<span className="text-xs text-gray-400 shrink-0">{t.assignee}</span>
|
<span className="text-xs text-gray-500">|</span>
|
||||||
</div>
|
<span className="text-xs text-gray-500 shrink-0">负责人: {t.assignee}</span>
|
||||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-gray-400">
|
{/* 滞留时间 */}
|
||||||
{t.received_at && <span>接收: {t.received_at}</span>}
|
<span className={`ml-auto shrink-0 rounded-md px-2 py-0.5 text-xs font-bold ${durationColor(t.duration_hours)}`}>
|
||||||
</div>
|
<Clock className="mr-0.5 inline h-3 w-3" />
|
||||||
|
{durationLabel(t.duration_hours)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* 附加信息行:物料名 + 序列号 + 身份证后6位 + 接收时间 */}
|
||||||
|
<div className="mt-1.5 flex items-center gap-2 text-[11px] text-gray-400">
|
||||||
|
{t.material_name && <span className="font-medium text-gray-500">{t.material_name}</span>}
|
||||||
|
<span>SN: {sn}</span>
|
||||||
|
<span className="font-mono text-gray-300">…{trace}</span>
|
||||||
|
{t.received_at && <span className="ml-auto">{t.received_at}</span>}
|
||||||
</div>
|
</div>
|
||||||
<span className={`shrink-0 rounded-md px-2 py-1 text-xs font-bold ${durationColor(t.duration_hours)}`}>
|
|
||||||
<Clock className="mr-0.5 inline h-3 w-3" />
|
|
||||||
{durationLabel(t.duration_hours)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export interface WipTask {
|
|||||||
assignee: string;
|
assignee: string;
|
||||||
product_sn: string;
|
product_sn: string;
|
||||||
external_serial: string | null;
|
external_serial: string | null;
|
||||||
|
material_name: string;
|
||||||
status: string;
|
status: string;
|
||||||
received_at: string;
|
received_at: string;
|
||||||
duration_hours: number;
|
duration_hours: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user