docs: add implementation plan for plan deliveryTimes column
Break the approved design into TDD tasks across filters, schema, export, and the four plan-family list pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
cb57a71d2d
commit
de7d5194ad
1 file changed
+735
@@ -0,0 +1,735 @@
|
||||
# 运输计划列表「送货时间」列 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在运输计划族四页列表「计划日期」后展示去重升序的送货段日期(`deliveryTimes`),并支持列头区间过滤(任一送货段命中)。
|
||||
|
||||
**Architecture:** 在 `build_plan_column_filters` 增加对 `pl_plan_delivery.delivery_time` 的 EXISTS 过滤;列表装配时从已加载送货段聚合 `deliveryTimes: string[]`;四页 Vue 增加合并列 + 列头漏斗;计划维度导出在「计划日期」后增加「送货时间」(` / ` 拼接)。不使用、不改写 `pl_plan.delivery_time`。
|
||||
|
||||
**Tech Stack:** FastAPI · SQLAlchemy · Pydantic v2 · Vue 3 · Element Plus · pytest
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-12-pl-plan-delivery-times-column-design.md`
|
||||
|
||||
**提交约定:** 默认每个 Task 末尾 commit;若用户要求暂不提交,可攒到全部完成后再提交。
|
||||
|
||||
---
|
||||
|
||||
## 文件地图
|
||||
|
||||
| 职责 | 路径 |
|
||||
|------|------|
|
||||
| 过滤 EXISTS | Modify `backend/app/services/pl/plan_list_filters.py` |
|
||||
| 过滤单测 | Modify `backend/tests/test_pl_plan_list_filters.py` |
|
||||
| 聚合 helper + attach | Modify `backend/app/services/pl_plan_stop_service.py` |
|
||||
| 聚合单测 | Create `backend/tests/test_pl_delivery_times_aggregate.py` |
|
||||
| Schema | Modify `backend/app/schemas/pl/plan.py`(`PlanWithProducts`、`PlanContractGapRead`) |
|
||||
| List/Export/Gaps API | Modify `backend/app/api/pl/plan.py` |
|
||||
| Gaps service 过滤+字段 | Modify `backend/app/services/pl_plan_contract_gap.py` |
|
||||
| FE Plan 类型 | Modify `web-admin/src/api/pl/index.ts` |
|
||||
| FE 列定义 | Modify 四页 `columnFilterDefs.ts` |
|
||||
| FE 四页列表 | Modify `plan` / `plan-archive` / `plan-cert` / `plan-contract` 的 `index.vue` |
|
||||
|
||||
**不做:** DB 迁移、小程序、归档段次展开导出加聚合列、改顶部日期 pills。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 过滤 EXISTS + 单测
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/pl/plan_list_filters.py`
|
||||
- Modify: `backend/tests/test_pl_plan_list_filters.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测**
|
||||
|
||||
在 `backend/tests/test_pl_plan_list_filters.py` 末尾追加:
|
||||
|
||||
```python
|
||||
def test_delivery_time_range_exists_requires_db():
|
||||
filters = build_plan_column_filters(
|
||||
db=None,
|
||||
tenant_no="shanxia2.0",
|
||||
delivery_time_from=date(2026, 9, 12),
|
||||
delivery_time_to=date(2026, 9, 14),
|
||||
)
|
||||
assert filters == []
|
||||
|
||||
|
||||
def test_delivery_time_range_exists_sql(monkeypatch):
|
||||
class _FakeQuery:
|
||||
def __init__(self):
|
||||
self.filters = []
|
||||
|
||||
def filter(self, *args):
|
||||
self.filters.extend(args)
|
||||
return self
|
||||
|
||||
def exists(self):
|
||||
return ("EXISTS", tuple(self.filters))
|
||||
|
||||
class _FakeDb:
|
||||
def query(self, *_args, **_kwargs):
|
||||
return _FakeQuery()
|
||||
|
||||
filters = build_plan_column_filters(
|
||||
db=_FakeDb(),
|
||||
tenant_no="shanxia2.0",
|
||||
delivery_time_from=date(2026, 9, 12),
|
||||
delivery_time_to="2026-09-14",
|
||||
)
|
||||
assert len(filters) == 1
|
||||
kind, clauses = filters[0]
|
||||
assert kind == "EXISTS"
|
||||
compiled = " ".join(_compile(c) for c in clauses)
|
||||
assert "pl_plan_delivery" in compiled or "delivery_time" in compiled
|
||||
assert ">=" in compiled
|
||||
assert "<=" in compiled
|
||||
```
|
||||
|
||||
说明:若实现选用 SQLAlchemy `exists()` 表达式而非假 query 元组,将 `test_delivery_time_range_exists_sql` 改为对真实 `exists` clause 做 `_compile`,断言含 `exists`、`delivery_time`、`>=`、`<=`、`del_flag`(与现有 product_name exists 测法一致即可)。**以能编译出 EXISTS 子查询且不引用 `pl_plan.delivery_time` 为准。**
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py::test_delivery_time_range_exists_requires_db tests/test_pl_plan_list_filters.py::test_delivery_time_range_exists_sql -v
|
||||
```
|
||||
|
||||
Expected: FAIL(未知参数或断言失败)
|
||||
|
||||
- [ ] **Step 3: 实现过滤**
|
||||
|
||||
在 `plan_list_filters.py`:
|
||||
|
||||
1. Import `PlPlanDelivery`:
|
||||
```python
|
||||
from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanDelivery
|
||||
```
|
||||
|
||||
2. 增加 helper(推荐真实 exists,便于编译断言):
|
||||
|
||||
```python
|
||||
def plan_delivery_time_exists(
|
||||
db: Session,
|
||||
tenant_no: str,
|
||||
*,
|
||||
d_from: Optional[date] = None,
|
||||
d_to: Optional[date] = None,
|
||||
):
|
||||
"""任一未删除送货段的送货日历日落在 [from, to] 内。"""
|
||||
q = db.query(PlPlanDelivery.id).filter(
|
||||
PlPlanDelivery.plan_id == PlPlan.id,
|
||||
PlPlanDelivery.tenant_no == tenant_no,
|
||||
PlPlanDelivery.del_flag == 0,
|
||||
PlPlanDelivery.delivery_time.isnot(None),
|
||||
)
|
||||
if d_from is not None:
|
||||
q = q.filter(func.date(PlPlanDelivery.delivery_time) >= d_from)
|
||||
if d_to is not None:
|
||||
q = q.filter(func.date(PlPlanDelivery.delivery_time) <= d_to)
|
||||
return q.exists()
|
||||
```
|
||||
|
||||
3. `build_plan_column_filters` 增加参数并在 `plan_date` 过滤后追加:
|
||||
|
||||
```python
|
||||
delivery_time_from: DateLike = None,
|
||||
delivery_time_to: DateLike = None,
|
||||
```
|
||||
|
||||
```python
|
||||
dt_from = _parse_date(delivery_time_from)
|
||||
dt_to = _parse_date(delivery_time_to)
|
||||
if (dt_from is not None or dt_to is not None) and db is not None:
|
||||
filters.append(
|
||||
plan_delivery_time_exists(
|
||||
db, tenant_no, d_from=dt_from, d_to=dt_to
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
若采用真实 `exists()`,把 Step 1 的 fake-db 测试改成:
|
||||
|
||||
```python
|
||||
def test_delivery_time_range_exists_sql():
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
db = MagicMock()
|
||||
q = MagicMock()
|
||||
db.query.return_value = q
|
||||
q.filter.return_value = q
|
||||
q.exists.return_value = "DELIVERY_TIME_EXISTS"
|
||||
|
||||
filters = build_plan_column_filters(
|
||||
db=db,
|
||||
tenant_no="shanxia2.0",
|
||||
delivery_time_from=date(2026, 9, 12),
|
||||
delivery_time_to="2026-09-14",
|
||||
)
|
||||
assert filters == ["DELIVERY_TIME_EXISTS"]
|
||||
assert db.query.called
|
||||
# filter 调用中应包含 date 比较;至少确认 exists 被调用
|
||||
assert q.exists.called
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/pl/plan_list_filters.py backend/tests/test_pl_plan_list_filters.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(pl): filter plans by delivery-stop date range
|
||||
|
||||
Add EXISTS on pl_plan_delivery.delivery_time for list column filters.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `deliveryTimes` 聚合 helper + 单测
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/pl_plan_stop_service.py`
|
||||
- Create: `backend/tests/test_pl_delivery_times_aggregate.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测**
|
||||
|
||||
创建 `backend/tests/test_pl_delivery_times_aggregate.py`:
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
|
||||
from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings
|
||||
|
||||
|
||||
class _D:
|
||||
def __init__(self, delivery_time):
|
||||
self.delivery_time = delivery_time
|
||||
|
||||
|
||||
def test_collect_unique_sorted_yyyy_mm_dd():
|
||||
rows = [
|
||||
_D(datetime(2026, 9, 14, 10, 0, 0)),
|
||||
_D(datetime(2026, 9, 12, 8, 0, 0)),
|
||||
_D(date(2026, 9, 12)),
|
||||
_D(None),
|
||||
_D(""),
|
||||
_D("2026-09-13"),
|
||||
]
|
||||
assert collect_unique_delivery_date_strings(rows) == [
|
||||
"2026-09-12",
|
||||
"2026-09-13",
|
||||
"2026-09-14",
|
||||
]
|
||||
|
||||
|
||||
def test_collect_empty():
|
||||
assert collect_unique_delivery_date_strings([]) == []
|
||||
assert collect_unique_delivery_date_strings([_D(None)]) == []
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_delivery_times_aggregate.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(ImportError / 函数不存在)
|
||||
|
||||
- [ ] **Step 3: 实现 helper**
|
||||
|
||||
在 `pl_plan_stop_service.py` 合适位置(`attach_plan_stops_to_item` 附近)增加:
|
||||
|
||||
```python
|
||||
def collect_unique_delivery_date_strings(deliveries) -> list[str]:
|
||||
"""送货段 delivery_time → 去重升序 YYYY-MM-DD。"""
|
||||
days: set[date] = set()
|
||||
for row in deliveries or []:
|
||||
raw = getattr(row, "delivery_time", None)
|
||||
if raw is None or raw == "":
|
||||
continue
|
||||
if isinstance(raw, datetime):
|
||||
days.add(raw.date())
|
||||
elif isinstance(raw, date):
|
||||
days.add(raw)
|
||||
else:
|
||||
text = str(raw).strip()
|
||||
if not text:
|
||||
continue
|
||||
try:
|
||||
days.add(datetime.strptime(text[:10], "%Y-%m-%d").date())
|
||||
except ValueError:
|
||||
continue
|
||||
return [d.isoformat() for d in sorted(days)]
|
||||
```
|
||||
|
||||
确保文件顶部已有 `date` / `datetime` import(已有则勿重复)。
|
||||
|
||||
- [ ] **Step 4: 跑测通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_delivery_times_aggregate.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/pl_plan_stop_service.py backend/tests/test_pl_delivery_times_aggregate.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(pl): aggregate unique delivery dates from stop rows
|
||||
|
||||
Provide YYYY-MM-DD dedupe helper for plan list deliveryTimes.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Schema + attach 写入 `deliveryTimes`
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/schemas/pl/plan.py`
|
||||
- Modify: `backend/app/services/pl_plan_stop_service.py`(`attach_plan_stops_to_item`)
|
||||
- Modify: `backend/app/services/pl_plan_contract_gap.py`(`_build_gap_item` 或等价构造函数)
|
||||
|
||||
- [ ] **Step 1: Schema 增加字段**
|
||||
|
||||
在 `PlanWithProducts`(约 `delivery_customer_summary` 后)增加:
|
||||
|
||||
```python
|
||||
delivery_times: list[str] = Field(
|
||||
default_factory=list,
|
||||
alias="deliveryTimes",
|
||||
description="送货段送货时间去重升序 YYYY-MM-DD(列表展示)",
|
||||
)
|
||||
```
|
||||
|
||||
在 `PlanContractGapRead` 增加同样字段(`deliveries` 字段附近):
|
||||
|
||||
```python
|
||||
delivery_times: list[str] = Field(
|
||||
default_factory=list,
|
||||
alias="deliveryTimes",
|
||||
description="送货段送货时间去重升序 YYYY-MM-DD",
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: attach 时写入**
|
||||
|
||||
`attach_plan_stops_to_item` 末尾:
|
||||
|
||||
```python
|
||||
plan_item.delivery_times = collect_unique_delivery_date_strings(delivery_reads)
|
||||
```
|
||||
|
||||
(若 Pydantic model 用 alias,赋值用 `delivery_times` 属性名。)
|
||||
|
||||
- [ ] **Step 3: 合同缺口项写入**
|
||||
|
||||
在构造 `PlanContractGapRead(...)` 处增加:
|
||||
|
||||
```python
|
||||
deliveryTimes=collect_unique_delivery_date_strings(deliveries),
|
||||
```
|
||||
|
||||
或 `delivery_times=...`(与该处其它字段传参风格一致)。从 `pl_plan_stop_service` import `collect_unique_delivery_date_strings`。
|
||||
|
||||
- [ ] **Step 4: 快速冒烟**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && python -c "
|
||||
from app.schemas.pl.plan import PlanWithProducts, PlanContractGapRead
|
||||
from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings, attach_plan_stops_to_item
|
||||
print('ok', collect_unique_delivery_date_strings([]))
|
||||
"
|
||||
```
|
||||
|
||||
Expected: 打印 `ok []`,无 ImportError
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/schemas/pl/plan.py backend/app/services/pl_plan_stop_service.py backend/app/services/pl_plan_contract_gap.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(pl): expose deliveryTimes on plan list and contract-gap schemas
|
||||
|
||||
Populate from delivery stops during attach/gap row build.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: API 透传过滤参数 + 计划导出列
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/pl/plan.py`
|
||||
- Modify: `backend/app/services/pl_plan_contract_gap.py`(`list_plan_contract_gaps` 签名与过滤)
|
||||
|
||||
- [ ] **Step 1: `_filters_from_params` 透传**
|
||||
|
||||
```python
|
||||
delivery_time_from=_param_get(params, "deliveryTimeFrom", "delivery_time_from"),
|
||||
delivery_time_to=_param_get(params, "deliveryTimeTo", "delivery_time_to"),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `list_plans` Query + `_merge_query`**
|
||||
|
||||
在 `plan_date_to` Query 旁增加:
|
||||
|
||||
```python
|
||||
delivery_time_from: Optional[date] = Query(
|
||||
default=None, alias="deliveryTimeFrom", description="送货时间开始"
|
||||
),
|
||||
delivery_time_to: Optional[date] = Query(
|
||||
default=None, alias="deliveryTimeTo", description="送货时间结束"
|
||||
),
|
||||
```
|
||||
|
||||
并在 `_merge_query(...)` 调用处传入 `deliveryTimeFrom=delivery_time_from`、`deliveryTimeTo=delivery_time_to`。
|
||||
|
||||
证书列表若走同一 `list_plans`,无需另改。
|
||||
|
||||
- [ ] **Step 3: Export body + 计划维度导出填充**
|
||||
|
||||
`PlanExportBody` 增加(可依赖 `extra="allow"`,但显式字段更清晰):
|
||||
|
||||
```python
|
||||
delivery_time_from: Optional[date] = Field(default=None, alias="deliveryTimeFrom")
|
||||
delivery_time_to: Optional[date] = Field(default=None, alias="deliveryTimeTo")
|
||||
```
|
||||
|
||||
`PLAN_EXPORT_COLUMNS` 在计划日期后插入:
|
||||
|
||||
```python
|
||||
ExportColumn(title="计划日期", field="planDate"),
|
||||
ExportColumn(title="送货时间", field="deliveryTimes"),
|
||||
```
|
||||
|
||||
非 archive 导出循环中,在 `model_validate(plan)` 后批量加载 stops(页内已有 `load_plan_stops_grouped` 用法):
|
||||
|
||||
```python
|
||||
plan_ids = [p.id for p in plans]
|
||||
stops_by_plan = load_plan_stops_grouped(db, tenant_no, plan_ids)
|
||||
items = []
|
||||
for plan in plans:
|
||||
data = PlanWithProducts.model_validate(plan).model_dump(by_alias=True)
|
||||
if data.get("planDate") is not None:
|
||||
data["planDate"] = str(data["planDate"])[:10]
|
||||
_, delivery_reads = stops_by_plan.get(plan.id, (None, []))
|
||||
times = collect_unique_delivery_date_strings(delivery_reads)
|
||||
data["deliveryTimes"] = " / ".join(times) if times else ""
|
||||
if data.get("erpEntered") is not None:
|
||||
data["erpEntered"] = "是" if int(data["erpEntered"]) else "否"
|
||||
items.append(data)
|
||||
```
|
||||
|
||||
从 `pl_plan_stop_service` import `collect_unique_delivery_date_strings`(若尚未导入)。**不要**改 `PLAN_ARCHIVE_EXPORT_COLUMNS`(段次展开已有「段次时间」)。
|
||||
|
||||
- [ ] **Step 4: 合同缺口 list/export 透传**
|
||||
|
||||
`list_contract_gaps` / `export_contract_gaps`:Query/Body + 调用 `list_plan_contract_gaps(..., delivery_time_from=..., delivery_time_to=...)`。
|
||||
|
||||
在 `list_plan_contract_gaps` 内,于 `plan_date` 过滤附近增加:
|
||||
|
||||
```python
|
||||
from app.services.pl.plan_list_filters import plan_delivery_time_exists, _parse_date
|
||||
|
||||
dt_from = _parse_date(delivery_time_from)
|
||||
dt_to = _parse_date(delivery_time_to)
|
||||
if dt_from is not None or dt_to is not None:
|
||||
query = query.filter(
|
||||
plan_delivery_time_exists(db, tenant_no, d_from=dt_from, d_to=dt_to)
|
||||
)
|
||||
```
|
||||
|
||||
(若 `_parse_date` 不想导出,可在 gap 服务内复用 `build_plan_column_filters` 只取 delivery 条件,或把 `_parse_date` 升为模块公开函数;优先直接调用 `build_plan_column_filters` 把返回的 filters `query.filter(*filters)`,但注意不要重复已有 plan_no 等条件——**仅追加 delivery 相关 EXISTS**:调用 `plan_delivery_time_exists` 最干净。)
|
||||
|
||||
`CONTRACT_GAP_EXPORT_COLUMNS` 在计划日期后加「送货时间」;export 行把 `deliveryTimes` list 转 `" / ".join(...)`。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/api/pl/plan.py backend/app/services/pl_plan_contract_gap.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(pl): wire deliveryTimeFrom/To through plan list and export
|
||||
|
||||
Contract-gap list/export share the same stop-date EXISTS filter.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 前端类型 + 四页 `columnFilterDefs`
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/pl/index.ts`
|
||||
- Modify: `web-admin/src/views/pl/plan/columnFilterDefs.ts`
|
||||
- Modify: `web-admin/src/views/pl/plan-archive/columnFilterDefs.ts`
|
||||
- Modify: `web-admin/src/views/pl/plan-cert/columnFilterDefs.ts`
|
||||
- Modify: `web-admin/src/views/pl/plan-contract/columnFilterDefs.ts`
|
||||
|
||||
- [ ] **Step 1: Plan / Gap 类型**
|
||||
|
||||
在 `Plan` interface(`planDate` 附近)增加:
|
||||
|
||||
```typescript
|
||||
deliveryTimes?: string[]
|
||||
```
|
||||
|
||||
若合同缺口有独立 interface(搜 `PlanContractGap` / gap 列表类型),同样加 `deliveryTimes?: string[]`。
|
||||
|
||||
- [ ] **Step 2: 四页 columnFilterDefs**
|
||||
|
||||
在各文件的 `planDate` 定义后插入:
|
||||
|
||||
```typescript
|
||||
{
|
||||
prop: 'deliveryTimes',
|
||||
label: '送货时间',
|
||||
type: 'datetime',
|
||||
queryKeyFrom: 'deliveryTimeFrom',
|
||||
queryKeyTo: 'deliveryTimeTo'
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/api/pl/index.ts \
|
||||
web-admin/src/views/pl/plan/columnFilterDefs.ts \
|
||||
web-admin/src/views/pl/plan-archive/columnFilterDefs.ts \
|
||||
web-admin/src/views/pl/plan-cert/columnFilterDefs.ts \
|
||||
web-admin/src/views/pl/plan-contract/columnFilterDefs.ts
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add deliveryTimes column filter defs for plan lists
|
||||
|
||||
Align query keys with deliveryTimeFrom/To server filters.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 主列表 `plan/index.vue` UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/pl/plan/index.vue`
|
||||
|
||||
- [ ] **Step 1: 合并列与列设置**
|
||||
|
||||
`PLAN_MERGE_COLUMN_PROPS` 在 `'planDate'` 后插入 `'deliveryTimes'`。
|
||||
|
||||
列设置数组 `{ prop: 'planDate', label: '计划日期' }` 后插入:
|
||||
|
||||
```typescript
|
||||
{ prop: 'deliveryTimes', label: '送货时间' },
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 表格列(计划日期列后)**
|
||||
|
||||
```vue
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('deliveryTimes')"
|
||||
prop="deliveryTimes"
|
||||
label="送货时间"
|
||||
min-width="130"
|
||||
align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">送货时间</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="deliveryTimes"
|
||||
label="送货时间"
|
||||
type="datetime"
|
||||
:is-active="isFilterActive('deliveryTimes')"
|
||||
:model-value="filters.deliveryTimes || null"
|
||||
@update:model-value="(v) => setFilterState('deliveryTimes', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
v-if="(row.plan.deliveryTimes || []).length"
|
||||
class="pl-delivery-times-cell"
|
||||
>
|
||||
<div
|
||||
v-for="(t, i) in row.plan.deliveryTimes"
|
||||
:key="`${row.plan.id}-dt-${i}`"
|
||||
>
|
||||
{{ t }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
若项目无 `.text-muted`,用现有 muted 类或内联 `color: var(--mint-text-muted)`。
|
||||
|
||||
- [ ] **Step 3: `getDisplayStatCellValue`**
|
||||
|
||||
```typescript
|
||||
if (prop === 'deliveryTimes') {
|
||||
return (plan.deliveryTimes || []).join('\n') || '—'
|
||||
}
|
||||
```
|
||||
|
||||
(合并键用字符串化结果,保证同计划多行一致。)
|
||||
|
||||
- [ ] **Step 4: 样式**
|
||||
|
||||
scoped 中:
|
||||
|
||||
```scss
|
||||
.pl-delivery-times-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 确认 pills 只清 planDate**
|
||||
|
||||
核对 `handleDateShortcut` 仅 `delete filters.planDate`,**不要**删除 `filters.deliveryTimes`。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/plan/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): show deliveryTimes column on transport plan list
|
||||
|
||||
Multi-line YYYY-MM-DD with header range filter after planDate.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 归档 / 证书列表 UI(与主列表同构)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/pl/plan-archive/index.vue`
|
||||
- Modify: `web-admin/src/views/pl/plan-cert/index.vue`
|
||||
|
||||
- [ ] **Step 1: 两页均做与 Task 6 相同改动**
|
||||
|
||||
- `PLAN_MERGE_COLUMN_PROPS` 插入 `deliveryTimes`
|
||||
- 列设置插入
|
||||
- `el-table-column`(数据源仍为 `row.plan.deliveryTimes`)
|
||||
- `getDisplayStatCellValue`(或等价)处理 `deliveryTimes`
|
||||
- 同样 cell 样式类
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/plan-archive/index.vue web-admin/src/views/pl/plan-cert/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add deliveryTimes column on plan archive and cert lists
|
||||
|
||||
Match main plan list display and column-header filter behavior.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 合同完善列表 UI + 导出字段
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/pl/plan-contract/index.vue`
|
||||
|
||||
- [ ] **Step 1: 合并与列**
|
||||
|
||||
`PLAN_MERGE_COLUMN_PROPS`:`['planDate', 'deliveryTimes', 'planType', 'followStatus']`(或在 `planDate` 后插入)。
|
||||
|
||||
表格在计划日期后增加列;单元格取 `asGapItem(row).deliveryTimes`(与该页 `planDate` 取值方式一致)。
|
||||
|
||||
列头过滤 defs 已在 Task 5;确保 `useServerColumnFilters` 使用的 defs 含 `deliveryTimes`,且 buildParams 会带上 `deliveryTimeFrom/To`。
|
||||
|
||||
- [ ] **Step 2: 查看弹窗(可选一致性)**
|
||||
|
||||
若补充信息 KV 有「计划日期」,在其后加「送货时间」多行或 ` / ` 拼接;无则跳过(YAGNI)。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/plan-contract/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add deliveryTimes column on plan contract-gap list
|
||||
|
||||
Filter and display deduped delivery dates beside planDate.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 回归验证 + 图谱更新
|
||||
|
||||
- [ ] **Step 1: 后端测试**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py tests/test_pl_delivery_times_aggregate.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: 手测清单(对照 spec §4)**
|
||||
|
||||
1. `/#/pl/plan`:计划日期后有送货时间;多段同日去重;多日多行 `YYYY-MM-DD`
|
||||
2. 列头送货时间区间:任一命中;顶部 pills 仍只动计划日期,不清送货时间过滤
|
||||
3. 归档 / 证书 / 合同完善行为一致
|
||||
4. 计划列表导出有「送货时间」,多值 ` / ` 分隔
|
||||
5. 无送货时间显示 `—`,送货时间过滤不误命中
|
||||
|
||||
- [ ] **Step 3: 更新知识图谱**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh backend
|
||||
./scripts/update-graph.sh frontend
|
||||
```
|
||||
|
||||
或按仓库约定:`graphify update .`(若脚本封装不同,以 `AGENTS.md` 为准)。
|
||||
|
||||
- [ ] **Step 4: Commit 图谱(若有 diff)**
|
||||
|
||||
```bash
|
||||
git add backend/graphify-out web-admin/graphify-out graphify-out 2>/dev/null || true
|
||||
git status -sb
|
||||
# 仅当有图谱文件变更时:
|
||||
git commit -m "$(cat <<'EOF'
|
||||
chore: update graphify after deliveryTimes list column
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review(对照 spec)
|
||||
|
||||
| Spec 项 | Task |
|
||||
|---------|------|
|
||||
| EXISTS 任一命中过滤 | Task 1、4 |
|
||||
| `deliveryTimes[]` 去重升序 YYYY-MM-DD | Task 2、3 |
|
||||
| 不用 `pl_plan.delivery_time` | Task 1/2 数据源仅 delivery 表/段 |
|
||||
| 四页 UI + 合并列 + 列头漏斗 | Task 5–8 |
|
||||
| pills 只筛计划日期 | Task 6 Step 5 |
|
||||
| 计划维度导出 + 段次导出不加聚合列 | Task 4 |
|
||||
| 合同缺口同源 | Task 3、4、8 |
|
||||
| 无 DB 迁移 | 全计划无 DDL |
|
||||
|
||||
无 TBD 占位;命名统一 `deliveryTimes` / `deliveryTimeFrom` / `deliveryTimeTo` / `collect_unique_delivery_date_strings`。
|
||||
Reference in new issue
Block a user