fix(pl): 按车辆类型统一派车日历卡片配色以区分自有三方和待派车。

This commit is contained in:
wuzilong committed 2026-11-01 13:42:54 +08:00
1 parent cb0344498a
commit 043e2ef661
11 files changed
+268 -27

No files matched your search

+6 -3
View File
@@ -64,21 +64,24 @@ def product_options(q: Optional[str] = None, page: int = Query(1, ge=1),
def create_record(body: CreateBody, db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user)):
tenant = get_tenant_no(db, user.to_tenant_user_dict())
return svc._row_to_dict(svc.create_record(db, tenant, user, body))
row = svc.create_record(db, tenant, user, body)
return svc._row_to_dict(row)
@router.get("/{record_id}")
def get_record(record_id: str, db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user)):
tenant = get_tenant_no(db, user.to_tenant_user_dict())
return svc._row_to_dict(svc.get_record(db, tenant, user, record_id))
row = svc.get_record(db, tenant, user, record_id)
return svc.serialize_records(db, tenant, user, [row])[0]
@router.put("/{record_id}", dependencies=[Depends(require_manage)])
def update_record(record_id: str, body: UpdateBody, db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user)):
tenant = get_tenant_no(db, user.to_tenant_user_dict())
return svc._row_to_dict(svc.update_record(db, tenant, user, record_id, body))
row = svc.update_record(db, tenant, user, record_id, body)
return svc._row_to_dict(row)
@router.delete("/{record_id}", dependencies=[Depends(require_manage)])
+1 -1
View File
@@ -46,4 +46,4 @@ def get_record(record_id: str, db: Session = Depends(get_db),
row = svc.get_record(db, tenant, user, record_id)
if row.calendar_kind != "site":
raise HTTPException(status_code=404, detail="派车记录不存在或无权访问")
return svc._row_to_dict(row)
return svc.serialize_records(db, tenant, user, [row])[0]
@@ -28,11 +28,14 @@ def _calendar_kind(kind: Optional[str]) -> str:
return value
def _row_to_dict(row: PlDispatchCalendar) -> dict:
def _row_to_dict(row: PlDispatchCalendar, vehicle_type: Optional[str] = None) -> dict:
if vehicle_type is None:
vehicle_type = getattr(row, "_response_vehicle_type", "unknown")
return {
"id": row.id,
"dispatchDate": row.dispatch_date,
"vehicleId": row.vehicle_id,
"vehicleType": vehicle_type,
"licensePlate": row.license_plate,
"productId": row.product_id,
"productName": row.product_name,
@@ -53,6 +56,23 @@ def _query(db: Session, tenant_no: str, user: DbUserContext, model=PlDispatchCal
return apply_pl_scope(query, model, user, db, tenant_no)
def serialize_records(db: Session, tenant_no: str, user: DbUserContext, rows: list[PlDispatchCalendar]) -> list[dict]:
"""仅批量读取当前页可见车辆类型;历史失效关联保留记录并回退中性色。"""
vehicle_ids = {row.vehicle_id for row in rows if row.vehicle_id != PENDING_VEHICLE_ID}
vehicle_types = {}
if vehicle_ids:
vehicles = _query(db, tenant_no, user, PlVehicle).filter(PlVehicle.id.in_(vehicle_ids)).all()
vehicle_types = {
vehicle.id: vehicle.vehicle_type if vehicle.vehicle_type in ("self", "carrier") else "unknown"
for vehicle in vehicles
}
return [
_row_to_dict(row, "pending" if row.vehicle_id == PENDING_VEHICLE_ID
else vehicle_types.get(row.vehicle_id, "unknown"))
for row in rows
]
def _month_bounds(month: str):
if not re.fullmatch(r"[0-9]{4}-[0-9]{2}", month or ""):
raise HTTPException(status_code=422, detail="月份须为 YYYY-MM")
@@ -76,7 +96,7 @@ def list_records(db: Session, tenant_no: str, user: DbUserContext, *, month: str
rows = query.order_by(PlDispatchCalendar.create_time.desc(), PlDispatchCalendar.id.desc()).offset(
(page - 1) * page_size
).limit(page_size).all()
return {"list": [_row_to_dict(row) for row in rows], "total": total}
return {"list": serialize_records(db, tenant_no, user, rows), "total": total}
def get_record(db: Session, tenant_no: str, user: DbUserContext, record_id: str,
@@ -91,14 +111,16 @@ def get_record(db: Session, tenant_no: str, user: DbUserContext, record_id: str,
return row
def _resolve_snapshots(db: Session, tenant_no: str, user: DbUserContext, body: CreateBody) -> dict:
def _resolve_snapshots(db: Session, tenant_no: str, user: DbUserContext, body: CreateBody) -> tuple[dict, str]:
if body.vehicle_id == PENDING_VEHICLE_ID:
license_plate = "待派车"
vehicle_type = "pending"
else:
vehicle = _query(db, tenant_no, user, PlVehicle).filter(PlVehicle.id == body.vehicle_id).first()
if vehicle is None:
raise HTTPException(status_code=422, detail="车辆不存在、已删除或无权访问,请重新选择")
license_plate = vehicle.license_plate
vehicle_type = vehicle.vehicle_type if vehicle.vehicle_type in ("self", "carrier") else "unknown"
product = (
_query(db, tenant_no, user, CrmProduct)
.filter(CrmProduct.id == body.product_id, CrmProduct.status == "on")
@@ -106,7 +128,7 @@ def _resolve_snapshots(db: Session, tenant_no: str, user: DbUserContext, body: C
)
if product is None:
raise HTTPException(status_code=422, detail="产品不存在、已下架、已删除或无权访问,请重新选择")
return {"license_plate": license_plate, "product_name": product.product_name}
return {"license_plate": license_plate, "product_name": product.product_name}, vehicle_type
def _site_document_type(origin: str, destination: str) -> Optional[str]:
@@ -176,13 +198,15 @@ def _retry_deadlock(action):
@_retry_deadlock
def create_record(db: Session, tenant_no: str, user: DbUserContext, body: CreateBody) -> PlDispatchCalendar:
snapshots = _resolve_snapshots(db, tenant_no, user, body)
snapshots, vehicle_type = _resolve_snapshots(db, tenant_no, user, body)
row = PlDispatchCalendar(
id=generate_uuid(), tenant_no=tenant_no, create_by=user.id,
create_dept_id=user.dept_id or "", update_by=user.id,
site_sync_enabled=1 if body.calendar_kind in ("acid", "alkali", "other") else 0,
**body.model_dump(), **snapshots,
)
# 仅供保存响应使用的非持久化属性,复用提交前已校验的车辆类型。
row._response_vehicle_type = vehicle_type
try:
db.add(row)
db.flush()
@@ -203,13 +227,15 @@ def update_record(db: Session, tenant_no: str, user: DbUserContext, record_id: s
_ensure_manual_record(row)
if body.calendar_kind != (row.calendar_kind or "acid"):
raise HTTPException(status_code=422, detail="不能把派车计划改到另一本日历")
values = {**body.model_dump(), **_resolve_snapshots(db, tenant_no, user, body)}
snapshots, vehicle_type = _resolve_snapshots(db, tenant_no, user, body)
values = {**body.model_dump(), **snapshots}
# 兼容未提交新字段的旧客户端;显式空值仍可清空。
if "document_type" not in body.model_fields_set:
values.pop("document_type", None)
for key, value in values.items():
setattr(row, key, value)
row.update_by = user.id
row._response_vehicle_type = vehicle_type
_sync_site_record(db, tenant_no, user, row)
db.commit()
except Exception:
+105 -1
View File
@@ -73,7 +73,7 @@ def test_crud_duplicate_trips_snapshots_and_soft_delete(db):
assert result['licensePlate'] == '鄂E67890' and result['productName'] == '盐酸'
assert result['origin'] == '武汉'
assert result['calendarKind'] == 'acid'
assert set(result) == {'id', 'dispatchDate', 'vehicleId', 'licensePlate', 'productId', 'productName', 'origin', 'destination', 'quantity', 'calendarKind', 'documentType', 'sourceRecordId', 'remark', 'createTime', 'updateTime'}
assert set(result) == {'id', 'dispatchDate', 'vehicleId', 'vehicleType', 'licensePlate', 'productId', 'productName', 'origin', 'destination', 'quantity', 'calendarKind', 'documentType', 'sourceRecordId', 'remark', 'createTime', 'updateTime'}
assert svc.delete_record(db, 't1', user(), first.id) == 1
assert db.get(model, first.id).del_flag == 1
assert svc.list_records(db, 't1', user(), month='2026-10')['total'] == 1
@@ -460,3 +460,107 @@ def test_pending_vehicle_still_validates_product(db, product_id):
assert exc.value.status_code == 422
assert '产品' in exc.value.detail
assert db.query(model).count() == 0
@pytest.mark.parametrize('kind', ['acid', 'alkali', 'other', 'site'])
def test_vehicle_type_classification_is_scoped_and_current(db, monkeypatch, kind):
model, _, _, svc = modules()
db.add(PlVehicle(id='legacy', license_plate='待派车', vehicle_type='legacy',
tenant_no='t1', create_by='u1'))
ids = ['v1', 'v2', '__pending_vehicle__', 'v3', 'v4', 'missing', 'legacy']
for vehicle_id in ids:
db.add(model(id='type-' + vehicle_id, dispatch_date=date(2026, 10, 26),
vehicle_id=vehicle_id, license_plate='待派车', product_id='p1',
product_name='液碱', origin='', destination='', calendar_kind=kind,
tenant_no='t1', create_by='u1'))
db.commit()
def types(caller):
return {r['vehicleId']: r.get('vehicleType') for r in
svc.list_records(db, 't1', caller, month='2026-10', calendar_kind=kind)['list']}
expected = dict(zip(ids, ['self', 'carrier', 'pending', 'unknown', 'unknown', 'unknown', 'unknown']))
assert types(user()) == expected
import app.core.data_scope as scope
monkeypatch.setattr(scope, '_resolve_user_role_scopes', lambda *a: [{'data_scope': '4', 'details': []}])
assert types(user(role='admin')) == {**expected, 'v2': 'unknown'}
db.get(PlVehicle, 'v1').vehicle_type = 'carrier'
db.commit()
assert types(user())['v1'] == 'carrier'
def test_vehicle_types_use_one_query_for_current_page(db):
from sqlalchemy import event
model, _, _, svc = modules()
for i in range(25):
db.add(model(id=f'batch-{i:02}', dispatch_date=date(2026, 10, 26),
vehicle_id='v1' if i < 20 else 'v2', license_plate='车牌',
product_id='p1', product_name='液碱', origin='', destination='',
tenant_no='t1', create_by='u1', create_time=datetime(2026, 10, 26, 0, i)))
db.commit()
statements = []
def capture(conn, cursor, statement, parameters, context, executemany):
if 'FROM pl_vehicle' in statement:
statements.append((statement, parameters))
event.listen(db.get_bind(), 'before_cursor_execute', capture)
try:
page = svc.list_records(db, 't1', user(), month='2026-10', page_size=5)
assert [r.get('vehicleType') for r in page['list']] == ['carrier'] * 5
assert len(statements) == 1
assert 'v2' in statements[0][1] and 'v1' not in statements[0][1]
statements.clear()
assert svc.list_records(db, 't1', user(), month='2026-09')['list'] == []
assert not statements
finally:
event.remove(db.get_bind(), 'before_cursor_execute', capture)
def test_single_record_routes_return_vehicle_type(db):
from app.api.pl import dispatch_calendar as api, site_dispatch_calendar as site_api
_, Create, Update, _ = modules()
created = api.create_record(Create(**payload()), db, user())
assert created.get('vehicleType') == 'self'
assert api.get_record(created['id'], db, user()).get('vehicleType') == 'self'
updated = api.update_record(created['id'], Update(**payload(vehicleId='v2')), db, user())
assert updated.get('vehicleType') == 'carrier'
pending = api.create_record(Create(**payload(vehicleId='__pending_vehicle__', calendarKind='site')), db, user())
assert pending.get('vehicleType') == 'pending'
assert site_api.get_record(pending['id'], db, user()).get('vehicleType') == 'pending'
with pytest.raises(ValidationError):
Create(**payload(vehicleType='self'))
@pytest.mark.parametrize('action', ['create', 'update'])
@pytest.mark.parametrize('vehicle_id,expected', [
('v1', 'self'), ('v2', 'carrier'), ('__pending_vehicle__', 'pending'), ('legacy', 'unknown'),
])
def test_save_response_never_queries_after_commit(db, action, vehicle_id, expected):
from sqlalchemy import event
from app.api.pl import dispatch_calendar as api
_, Create, Update, _ = modules()
db.add(PlVehicle(id='legacy', license_plate='旧车辆', vehicle_type='legacy',
tenant_no='t1', create_by='u1'))
db.commit()
# 与应用 SessionLocal 一致;提交后失联也必须返回已成功保存的数据。
with sessionmaker(bind=db.get_bind(), expire_on_commit=False)() as session:
existing = api.create_record(Create(**payload()), session, user()) if action == 'update' else None
committed = False
def after_commit(session):
nonlocal committed
committed = True
def reject_after_commit(conn, cursor, statement, parameters, context, executemany):
assert not committed, '保存成功后不得再查询数据库生成响应'
event.listen(session, 'after_commit', after_commit)
event.listen(db.get_bind(), 'before_cursor_execute', reject_after_commit)
try:
body = payload(vehicleId=vehicle_id, origin='山峡')
result = (api.create_record(Create(**body), session, user()) if action == 'create'
else api.update_record(existing['id'], Update(**body), session, user()))
assert committed
assert result['vehicleType'] == expected
assert result['vehicleId'] == vehicle_id
assert result['id']
finally:
event.remove(session, 'after_commit', after_commit)
event.remove(db.get_bind(), 'before_cursor_execute', reject_after_commit)
+7 -1
View File
@@ -13,6 +13,7 @@ from app.core.auth import get_current_user
from app.core.database import get_db
from app.models.models import SysMenu, SysRole, SysRoleMenu, SysUserRole, SysRoleDataScopeDetail
from app.models.pl_dispatch_calendar import PlDispatchCalendar
from app.models.pl_models import PlVehicle
PATH = '/api/transport/site-dispatch-calendar'
ORIGINAL = '/api/transport/dispatch-calendar'
@@ -25,11 +26,13 @@ def setup():
from app.api.pl.site_dispatch_calendar import router
from app.api.pl.dispatch_calendar import router as original_router
engine = create_engine('sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool)
for model in (PlDispatchCalendar, SysMenu, SysRole, SysRoleMenu, SysUserRole, SysRoleDataScopeDetail):
for model in (PlDispatchCalendar, PlVehicle, SysMenu, SysRole, SysRoleMenu, SysUserRole, SysRoleDataScopeDetail):
model.__table__.create(engine)
factory = sessionmaker(bind=engine)
with factory() as db:
db.add_all([
PlVehicle(id='v1', vehicle_type='self', license_plate='鄂E12345', tenant_no='t1', create_by='u1'),
PlVehicle(id='v2', vehicle_type='carrier', license_plate='鄂E67890', tenant_no='t1', create_by='u2'),
SysRole(id='r1', role_name='基础角色', tenant_no='t1', data_scope='4'),
SysRole(id='r2', role_name='现场人员', tenant_no='t1', data_scope='4'),
SysUserRole(id='ur1', user_id='u1', role_id='r1', tenant_no='t1'),
@@ -97,7 +100,9 @@ def test_fixed_kind_tenant_scope_and_details(setup):
'tenant_no': 't2', 'vehicleId': 'v1'})
assert response.status_code == 200
assert [r['id'] for r in response.json()['list']] == ['site-own']
assert response.json()['list'][0]['vehicleType'] == 'self'
assert client.get(PATH + '/site-own').json()['calendarKind'] == 'site'
assert client.get(PATH + '/site-own').json()['vehicleType'] == 'self'
for rid in ('acid', 'alkali', 'other', 'site-t2', 'site-other', 'site-deleted', 'missing', 'vehicles', 'products'):
assert client.get(PATH + '/' + rid).status_code == 404
assert client.get(PATH, params={'month': '2026-10', 'vehicleId': 'v2'}).json()['total'] == 0
@@ -107,6 +112,7 @@ def test_fixed_kind_tenant_scope_and_details(setup):
# 菜单授权来自第二角色,全部数据范围来自第一角色,均按并集生效。
assert client.get(PATH, params={'month': '2026-10'}).json()['total'] == 2
assert client.get(PATH + '/site-other').status_code == 200
assert client.get(PATH + '/site-other').json()['vehicleType'] == 'carrier'
assert client.get(PATH + '/site-t2').status_code == 404
page1 = client.get(PATH, params={'month': '2026-10', 'pageSize': 1, 'page': 1}).json()
page2 = client.get(PATH, params={'month': '2026-10', 'pageSize': 1, 'page': 2}).json()
@@ -0,0 +1,42 @@
# 派车日历车辆配色 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task.
**Goal:** 实现工单 #5 已确认的两端日卡车辆类型配色。
**Architecture:** 后端批量解析只读车辆类型,前端共用日卡按该类型着色。保留租户和数据范围、现有写入及日历交互;不新增数据库列。
**Tech Stack:** FastAPI / SQLAlchemy / Vue 3 / TypeScript / SCSS。
## Task 1:类型返回与共用日卡
修改 `backend/app/services/pl_dispatch_calendar_service.py`、`backend/app/api/pl/dispatch_calendar.py`、`backend/app/api/pl/site_dispatch_calendar.py`、`web-admin/src/api/pl/dispatch-calendar.ts`、`web-admin/src/views/pl/dispatch-calendar/manual-model.ts`、`ManualDayContent.vue`;测试沿用 `backend/tests/test_pl_dispatch_calendar.py` 和 `web-admin/scripts/manual-dispatch-calendar.test.ts`。
- [x] 先添加 SQLite 回归:自有/三方/待派车列表返回 `vehicleType`,无权限、删除和跨租户车辆返回 unknown;断言分页多行只执行一次车辆查询。运行并确认新增用例因缺字段失败。
- [x] `_row_to_dict(row, vehicle_type=None)` 返回规范枚举;保存时复用提交前类型,列表/详情通过批量序列化显式传类型。使用 `_query(db, tenant_no, user, PlVehicle).filter(PlVehicle.id.in_(ids))` 解析当前页 ID。列表与详情复用批量解析;创建、编辑响应使用非持久化属性,提交后不查询。
- [x] 前端增加可选只读 `vehicleType?: 'self' | 'carrier' | 'pending' | 'unknown'`。模型函数先判断 `vehicleId === '__pending_vehicle__'`,再白名单校验车辆类型,其他为 unknown。
- [x] 单车卡及多趟分组绑定 `vehicle-${类型}`;背景使用设计中的四组色值,移动自有车辆覆盖为 `#e9f9f2`。保留产品汇总配色。
- [x] 运行后端日历三个专项文件与前端日历模型测试;规格审查通过,代码质量审查另见下方交付记录。
## Task 2:验证与交付记录
- [x] 浏览器渲染实际组件,用合成数据检查 Web、375px、414px 的三类单车/多趟卡、未知类型与点击行为,保存本地截图。
- [x] 执行目标文件 ESLint、Stylelint 和 `pnpm build:typecheck`,记录基线问题,不改无关代码。
- [x] 复核工单最新正文与评论、运行 `git diff --check`,记录最终验证及限制;仅在本地任务分支保存实现,不推送、合并或关闭工单。
## 2026-11-01 验证与交付记录
- 用户已明确确认 Figma 18:2 配色并要求实施。分支 `codex/gitea-issue-5`,基线 `cb0344498abc4c796bc12ee20f25e9097a4076f1`。
- 工单正文/全部评论在交付前重新读取,正文未变,0评论,无图片,远端仍 open。
- 后端:使用主工作目录现有 Python 3.9 venv,工作目录为本任务 backend;`DATABASE_URL` 指向本机不可连接的测试地址,测试fixture仅使用SQLite。运行 `python -m pytest tests/test_pl_dispatch_calendar.py tests/test_site_dispatch_calendar.py tests/test_pl_dispatch_calendar_site_sync.py -q`:126 passed,12条既有依赖警告。
- 前端:`pnpm exec tsx --test scripts/manual-dispatch-calendar.test.ts scripts/mobile-dispatch-calendar.test.ts scripts/pl-dispatch-calendar.test.ts scripts/calendar-copy.test.ts`:38 passed。
- 对本次四个前端修改文件执行 ESLint;对 ManualDayContent.vue 执行 Stylelint,均通过。Prettier目标检查通过,`git diff --check`通过。
- `pnpm build` 最终通过(39.22秒),存在既有组件重名与产物体积提示。
- `pnpm build:typecheck` 未通过:首次隔离目录缺自动导入声明;生成声明后单独执行 `pnpm exec vue-tsc --noEmit`,基线与修改后均505条错误,输出1351行逐行一致,本次无新增。不能称全量类型检查通过。
- pnpm复用同仓库现有node_modules软链,命令附 `pnpm_config_verify_deps_before_run=false`,防止新版pnpm自动重装共享依赖;未安装/更新依赖。
- 浏览器:Vite在内存编译实际 ManualDayContent.vue,以合成数据在Chrome验证Web 1440px、移动375px/414px × 单车/多趟,共6组通过。核对三种类型及未知回退背景、选择事件、移动触达高度>=44、无横向溢出;实际截图已查看,与已批准配色一致。
- 本地证据位于 `.local/agent/issue5/`:`verify-colors.mjs`、六张截图、`backend-final.log`、`vite-final.log`、`typecheck-baseline.log`、`typecheck-final.log`;这些临时证据被Git忽略。
- 保存回归修正:先以8组commit后禁SQL用例确认6失败/2通过,再复用提交前车辆类型,修正后全部通过。列表仍实时查询类型,保存不新增提交后数据库依赖。
- 独立规格审查和最终代码质量审查均通过,无剩余阻塞;审查者独立执行git diff --check通过。
- 验证限制:未连接真实业务库,未执行微信真机或完整业务环境端到端验收。未修改数据库schema、未推送、未合并、未关闭工单。
@@ -0,0 +1,24 @@
# 工单 #5:派车日历车辆类型配色
来源:https://gitea.aichengyan.com/cykj/shanxia2.0/issues/5 。2026-11-01 已重新读取正文及全部评论(0 条,无图片)。
用户已确认按 Figma v1 实施:https://www.figma.com/design/ImDcikQnCX5GiJQLwUHwDC/shanxia2.0?node-id=18-2 。
## 范围与视觉依据
复用 P08/P27 现有派车日历和 `web-admin/src/views/pl/dispatch-calendar/ManualDayContent.vue`;小程序入口实际使用移动 H5,复用同一日卡组件。酸、碱、其他、现场手工日历沿用现有结构、汇总、复制、打印和点击行为;实际派车信息页不在本次手工日卡范围内。
- 自有车辆:Web 沿用日卡浅绿 `#e8f3ec`,移动端采用三墙浅绿 `#e9f9f2`。
- 三方车辆:沿用日历浅蓝 `#e7f0f8`。
- 待派车:采用 Fresh Mint 浅红 `#fee2e2`。
- 同车多趟的整个分组采用相同车辆类型背景;产品汇总保留产品配色。
- 无法识别类型的历史车辆使用中性 `#f1f5f9`,不推断为三方车辆。
## 数据与实现
后端从当前租户、未删除且满足数据范围的车辆档案批量读取 `vehicle_type`,作为只读 `vehicleType` 返回。取值 `self` / `carrier` / `pending` / `unknown`;待派车以 `__pending_vehicle__` 专用 ID 判断,不依赖显示车牌名称。列表分页不增加逐行查询,详情和保存响应保持同一字段语义。不增加数据库字段,不改变写入、同步或权限逻辑。
前端只读字段不进入保存表单。日卡及同车多趟分组按车辆类型选择背景,不再按产品名选择卡片背景。旧响应缺字段时保留待派车 ID 识别,其余中性回退。
## 验收
覆盖三种类型、未知/已删除/跨租户/无权限车辆、同车多趟、待派车优先级;检查移动端 375/414px 和 Web 日卡的背景、内容及点击。后端用 SQLite 隔离测试,禁止连接业务库。执行受影响前端测试、检查和构建;独立审查后记录实际结果。未授权推送、合并或关单。
@@ -4,6 +4,7 @@ import {
loadCalendarPages,
manualCalendarDays,
manualCopyText,
manualVehicleType,
validateManualDraft
} from '../src/views/pl/dispatch-calendar/manual-model'
@@ -257,3 +258,20 @@ test('现场同车重复记录逐条计车,同名不同产品ID保持独立且
].join('\n')
)
})
test('车辆分类以待派车ID优先,其余仅接受类型白名单,不从车牌或产品推断', () => {
for (const vehicleType of ['self', 'carrier', 'pending', 'unknown'] as const) {
assert.equal(manualVehicleType({ ...row('1'), vehicleType }), vehicleType)
assert.equal(
manualVehicleType({ ...row('1'), vehicleId: '__pending_vehicle__', vehicleType }),
'pending'
)
}
for (const vehicleType of [undefined, null, '', 'legacy', 'SELF']) {
assert.equal(manualVehicleType({ vehicleId: 'v1', vehicleType }), 'unknown')
}
const misleadingPlate = { ...row('1'), licensePlate: '待派车' }
const otherProduct = { ...row('1'), productName: '盐酸' }
assert.equal(manualVehicleType(misleadingPlate), 'unknown')
assert.equal(manualVehicleType(otherProduct), 'unknown')
})
@@ -2,6 +2,8 @@ import request, { pickBxData } from '@/utils/http/bx'
export type DispatchCalendarKind = 'acid' | 'alkali' | 'other' | 'site'
export type DispatchCalendarVehicleType = 'self' | 'carrier' | 'pending' | 'unknown'
export interface DispatchCalendarBody {
dispatchDate: string
vehicleId: string
@@ -16,6 +18,7 @@ export interface DispatchCalendarBody {
export interface DispatchCalendarRecord extends DispatchCalendarBody {
id: string
sourceRecordId?: string | null
readonly vehicleType?: DispatchCalendarVehicleType
licensePlate: string
productName: string
createTime: string | null
@@ -23,7 +23,7 @@
v-if="group.rows.length === 1"
type="button"
class="item"
:class="`tone-${productTone(group.rows[0].productName)}`"
:class="`vehicle-${manualVehicleType(group.rows[0])}`"
data-testid="manual-record"
@click="emit('select', group.rows[0])"
>
@@ -38,7 +38,7 @@
<span class="route">{{ group.rows[0].origin }} → {{ group.rows[0].destination }}</span>
<span v-if="group.rows[0].remark" class="remark">{{ group.rows[0].remark }}</span>
</component>
<div v-else class="pair">
<div v-else class="pair" :class="`vehicle-${manualVehicleType(group.rows[0])}`">
<div
><span class="plate">{{ group.plate }}</span> {{ group.rows.length }}车</div
>
@@ -70,7 +70,7 @@
import { Cell as VanCell } from 'vant'
import 'vant/es/cell/style'
import type { DispatchCalendarRecord } from '@/api/pl/dispatch-calendar'
import { manualQuantityLabel, type manualCalendarDays } from './manual-model'
import { manualQuantityLabel, manualVehicleType, type manualCalendarDays } from './manual-model'
import { productTone } from './model'
defineProps<{ day: ReturnType<typeof manualCalendarDays>[number]; mobile?: boolean }>()
@@ -158,23 +158,22 @@
.item {
display: block;
background: #e8f3ec;
}
.item.tone-sand {
background: #ece8e0;
.vehicle-self {
background: var(--manual-vehicle-self-bg, #e8f3ec);
}
.item.tone-rose {
background: #f3e4e8;
.vehicle-carrier {
background: #e7f0f8;
}
.item.tone-amber {
background: #efe6d0;
.vehicle-pending {
background: #fee2e2;
}
.item.tone-unknown {
background: #fff4d6;
.vehicle-unknown {
background: #f1f5f9;
}
.plate {
@@ -205,7 +204,6 @@
flex-direction: column;
gap: 3px;
padding-bottom: 6px;
background: var(--manual-day-primary-bg, #e7f0f8);
}
.pair-trip {
@@ -224,7 +222,7 @@
.is-mobile {
--manual-day-primary: #0a9f6d;
--manual-day-primary-text: #087f57;
--manual-day-primary-bg: #e9f9f2;
--manual-vehicle-self-bg: #e9f9f2;
}
.is-mobile :deep(.van-cell__value) {
@@ -2,10 +2,27 @@ import type {
CalendarPage,
CalendarPageQuery,
DispatchCalendarBody,
DispatchCalendarRecord
DispatchCalendarRecord,
DispatchCalendarVehicleType
} from '@/api/pl/dispatch-calendar'
import { dateKey, monthCells } from './model'
/** 待派车使用专用ID;旧响应和未知类型保持中性色,不从车牌快照推断。 */
export function manualVehicleType(row: {
vehicleId: string
vehicleType?: string | null
}): DispatchCalendarVehicleType {
if (row.vehicleId === '__pending_vehicle__') return 'pending'
switch (row.vehicleType) {
case 'self':
case 'carrier':
case 'pending':
return row.vehicleType
default:
return 'unknown'
}
}
/** 完整分页:中途缺页或重复页报错,不能把部分月份当作完整数据。 */
export async function loadCalendarPages<T extends { id: string }>(
fetchPage: (query: CalendarPageQuery) => Promise<CalendarPage<T>>,