feat(analytics): 规格型号带物料名 + 设备字典(/options 数据契约扩展)

- 后端新增 SpecModelOption/DeviceOption schema
- get_analytics_options 返回规格型号的物料名(同型号取首个非空名)+ 关联设备列表
- 前端同步 SpecModelOption/DeviceOption 类型
This commit is contained in:
2026-08-14 14:30:35 +08:00
parent 7d48daca0c
commit 807df56c3a
2 changed files with 92 additions and 10 deletions

View File

@ -70,9 +70,22 @@ class AssigneeOption(BaseModel):
name: str
class SpecModelOption(BaseModel):
spec_model: str
material_name: str
class DeviceOption(BaseModel):
product_sn: str
external_serial: str | None
material_name: str
spec_model: str
class AnalyticsOptions(BaseModel):
assignees: list[AssigneeOption]
spec_models: list[str]
spec_models: list[SpecModelOption]
devices: list[DeviceOption]
class DeviceRecord(BaseModel):
@ -478,11 +491,12 @@ async def get_analytics_options(
) -> AnalyticsOptions:
"""返回筛选栏选项,支持动态联动:
- 传入 spec_models只返回碰过这些型号的人
- 传入 assignee_ids只返回这些人处理过的型号"""
- 传入 assignee_ids只返回这些人处理过的型号
- devices根据筛选条件返回关联设备无筛选则返回最近流转的设备"""
from app.models.task import Task
from app.models.product import Product
# 负责人:从 tasks 去重(可选按 spec_models 精确过滤)
# ── 负责人:从 tasks 去重(可选按 spec_models 过滤) ──
if spec_models:
assignee_stmt = (
select(Task.assignee_id)
@ -512,23 +526,78 @@ async def get_analytics_options(
for aid in distinct_assignees
]
# 规格型号:从 products 去重(可选按 assignee_ids 过滤
# ── 规格型号 + 物料名(可选按 assignee_ids 过滤;同型号取首个非空物料名) ──
if assignee_ids:
spec_stmt = (
select(Product.spec_model)
select(Product.spec_model, Product.material_name)
.join(Task, Task.product_id == Product.id)
.where(
Product.spec_model.isnot(None),
func.trim(Product.spec_model) != "",
Task.assignee_id.in_(assignee_ids),
)
.distinct()
)
else:
spec_stmt = (
select(Product.spec_model)
select(Product.spec_model, Product.material_name)
.where(Product.spec_model.isnot(None), func.trim(Product.spec_model) != "")
.distinct()
)
spec_rows = (await db.execute(spec_stmt.distinct())).all()
spec_models = sorted({r[0] for r in spec_rows if r[0]})
spec_rows = (await db.execute(spec_stmt)).all()
spec_map: dict[str, str] = {}
for row in spec_rows:
sm = row[0]
mn = (row[1] or "").strip()
if sm not in spec_map:
spec_map[sm] = mn
elif mn and not spec_map[sm]:
spec_map[sm] = mn
spec_model_opts = [
SpecModelOption(spec_model=sm, material_name=spec_map[sm])
for sm in sorted(spec_map.keys())
]
return AnalyticsOptions(assignees=assignees, spec_models=spec_models)
# ── 设备字典(可选按 assignee_ids / spec_models 过滤;无筛选返回最近流转 100 台) ──
if assignee_ids or spec_models:
device_stmt = select(
Product.serial_number, Product.external_serial,
Product.material_name, Product.spec_model,
)
if assignee_ids:
device_stmt = device_stmt.join(
Task, Task.product_id == Product.id
).where(Task.assignee_id.in_(assignee_ids))
if spec_models:
device_stmt = device_stmt.where(Product.spec_model.in_(spec_models))
device_stmt = device_stmt.distinct()
else:
latest_subq = (
select(
Task.product_id,
func.max(func.coalesce(Task.received_at, Task.created_at)).label("latest"),
)
.group_by(Task.product_id)
.subquery()
)
device_stmt = (
select(
Product.serial_number, Product.external_serial,
Product.material_name, Product.spec_model,
)
.join(latest_subq, latest_subq.c.product_id == Product.id)
.order_by(latest_subq.c.latest.desc())
.limit(100)
)
device_rows = (await db.execute(device_stmt)).all()
devices = [
DeviceOption(
product_sn=r[0] or "",
external_serial=r[1] or None,
material_name=r[2] or "",
spec_model=r[3] or "",
)
for r in device_rows
]
return AnalyticsOptions(assignees=assignees, spec_models=spec_model_opts, devices=devices)

View File

@ -71,9 +71,22 @@ export interface AssigneeOption {
name: string;
}
export interface SpecModelOption {
spec_model: string;
material_name: string;
}
export interface DeviceOption {
product_sn: string;
external_serial: string | null;
material_name: string;
spec_model: string;
}
export interface AnalyticsOptions {
assignees: AssigneeOption[];
spec_models: string[];
spec_models: SpecModelOption[];
devices: DeviceOption[];
}
// ─── 查询参数 ──────────────────────────────────────────────