merge: bring storage release review pending/reviewed tabs into OA deploy branch
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
f6efca8fdf
19 files changed
+10224
-8438
No files matched your search
@@ -0,0 +1,365 @@
|
||||
# 入库放行审核双 Tab(待审核 / 已审核)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:** 将入库放行审核列表拆成「待审核 / 已审核」两个 Tab,服务端按 `reviewPhase` 过滤并正确分页,默认进入待审核。
|
||||
|
||||
**Architecture:** 列表 API 增加可选参数 `reviewPhase=pending|reviewed`;CRUD `list_reviews` 按 `review_conclusion` 是否为空过滤。前端对齐样本接收的 `panel-pill-tabs`,切 Tab 时重置页码与勾选、保留筛选,请求始终带对应 `reviewPhase`。审核/入库弹窗与业务规则不改。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy(backend)、Vue 3 + Element Plus(web-admin)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-07-16-storage-release-review-tabs-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `backend/app/crud/storage_release_review.py` | `normalize_review_phase` + `list_reviews` 阶段过滤 |
|
||||
| `backend/app/api/storage_release_review.py` | 列表接口增加 `reviewPhase` Query |
|
||||
| `backend/tests/test_storage_release_review_phase.py` | **新建** 阶段参数规范化单测 |
|
||||
| `web-admin/src/api/storage-release-review.ts` | 列表 params 增加 `reviewPhase` |
|
||||
| `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | pill Tabs、切 Tab、请求带 phase、去掉无效「待审核」选项 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Backend — `reviewPhase` 规范化与列表过滤
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/crud/storage_release_review.py`
|
||||
- Create: `backend/tests/test_storage_release_review_phase.py`
|
||||
- Modify: `backend/app/api/storage_release_review.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测(规范化函数)**
|
||||
|
||||
创建 `backend/tests/test_storage_release_review_phase.py`:
|
||||
|
||||
```python
|
||||
from app.crud.storage_release_review import normalize_review_phase
|
||||
|
||||
|
||||
def test_normalize_review_phase_accepts_pending_and_reviewed():
|
||||
assert normalize_review_phase("pending") == "pending"
|
||||
assert normalize_review_phase("reviewed") == "reviewed"
|
||||
assert normalize_review_phase(" PENDING ") == "pending"
|
||||
assert normalize_review_phase("Reviewed") == "reviewed"
|
||||
|
||||
|
||||
def test_normalize_review_phase_rejects_unknown():
|
||||
assert normalize_review_phase(None) is None
|
||||
assert normalize_review_phase("") is None
|
||||
assert normalize_review_phase("all") is None
|
||||
assert normalize_review_phase("tg") is None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑单测确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_phase.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(`normalize_review_phase` 未定义或导入失败)
|
||||
|
||||
- [ ] **Step 3: 实现 `normalize_review_phase` 并在 `list_reviews` 中应用过滤**
|
||||
|
||||
在 `backend/app/crud/storage_release_review.py` 中,于 `list_reviews` 之前增加:
|
||||
|
||||
```python
|
||||
def normalize_review_phase(review_phase: Optional[str]) -> Optional[str]:
|
||||
"""Return 'pending' | 'reviewed' or None when absent/invalid."""
|
||||
p = (review_phase or "").strip().lower()
|
||||
if p in ("pending", "reviewed"):
|
||||
return p
|
||||
return None
|
||||
```
|
||||
|
||||
修改 `list_reviews` 签名,增加 `review_phase: Optional[str] = None`(放在 `keyword` 与 `user_info` 之间):
|
||||
|
||||
```python
|
||||
def list_reviews(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
reception_source: Optional[str] = None,
|
||||
review_conclusion: Optional[str] = None,
|
||||
storage_status: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
review_phase: Optional[str] = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
```
|
||||
|
||||
在现有 `reception_source` 过滤之后、`review_conclusion` 过滤之前插入:
|
||||
|
||||
```python
|
||||
phase = normalize_review_phase(review_phase)
|
||||
if phase == "pending":
|
||||
q0 = q0.filter(
|
||||
or_(
|
||||
BizStorageReleaseReview.review_conclusion.is_(None),
|
||||
BizStorageReleaseReview.review_conclusion == "",
|
||||
)
|
||||
)
|
||||
elif phase == "reviewed":
|
||||
q0 = q0.filter(
|
||||
BizStorageReleaseReview.review_conclusion.isnot(None),
|
||||
BizStorageReleaseReview.review_conclusion != "",
|
||||
)
|
||||
```
|
||||
|
||||
说明:`or_` 已在文件顶部从 `sqlalchemy` 导入,无需新增 import。
|
||||
|
||||
- [ ] **Step 4: API 增加 Query 并传入 CRUD**
|
||||
|
||||
修改 `backend/app/api/storage_release_review.py` 的 `list_storage_release_reviews`:
|
||||
|
||||
```python
|
||||
@router.get("/storage-release-reviews")
|
||||
async def list_storage_release_reviews(
|
||||
page: int = Query(1, ge=1),
|
||||
pageSize: int = Query(10, ge=1, le=100),
|
||||
keyword: Optional[str] = None,
|
||||
receptionSource: Optional[str] = None,
|
||||
reviewConclusion: Optional[str] = None,
|
||||
storageStatus: Optional[str] = None,
|
||||
reviewPhase: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(require_menu_code("biz:storageReleaseReview:list")),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
result = srr_svc.list_reviews(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
page=page,
|
||||
page_size=pageSize,
|
||||
reception_source=receptionSource,
|
||||
review_conclusion=reviewConclusion,
|
||||
storage_status=storageStatus,
|
||||
keyword=keyword,
|
||||
review_phase=reviewPhase,
|
||||
user_info=user,
|
||||
)
|
||||
return success_response(result)
|
||||
except PermissionError as e:
|
||||
return error_response(403, str(e))
|
||||
except Exception as e:
|
||||
return error_response(400, str(e))
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑单测确认通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_phase.py -v
|
||||
```
|
||||
|
||||
Expected: PASS(2 passed)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/crud/storage_release_review.py \
|
||||
backend/app/api/storage_release_review.py \
|
||||
backend/tests/test_storage_release_review_phase.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add reviewPhase filter for storage release review list
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Frontend API 类型
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/storage-release-review.ts`
|
||||
|
||||
- [ ] **Step 1: 列表 params 增加 `reviewPhase`**
|
||||
|
||||
将 `fetchStorageReleaseReviewList` 的 params 类型改为:
|
||||
|
||||
```typescript
|
||||
export function fetchStorageReleaseReviewList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
receptionSource?: string
|
||||
reviewConclusion?: string
|
||||
storageStatus?: string
|
||||
keyword?: string
|
||||
reviewPhase?: 'pending' | 'reviewed'
|
||||
}) {
|
||||
return request.get<PageList<StorageReleaseReview>>({
|
||||
url: '/web/admin/storage-release-reviews',
|
||||
params
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/api/storage-release-review.ts
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add reviewPhase param to storage release review list API
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 列表页双 Tab UI 与请求
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/quality-inspection/storage-release-review/index.vue`
|
||||
- Reference: `web-admin/src/views/quality-inspection/sample-reception/index.vue`(pill tabs 结构)
|
||||
|
||||
- [ ] **Step 1: 在 `panel-top` 之后插入 pill Tabs**
|
||||
|
||||
在 `</div>`(panel-top 结束)与 `<div class="filter-block">` 之间插入:
|
||||
|
||||
```vue
|
||||
<div class="panel-pill-tabs panel-pill-tabs--lg">
|
||||
<div class="panel-pill-tabs__track" role="tablist">
|
||||
<button
|
||||
v-for="tab in STATUS_TABS"
|
||||
:key="tab.name"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="panel-pill-tabs__btn"
|
||||
:class="{ 'is-active': activeTab === tab.name }"
|
||||
:aria-selected="activeTab === tab.name"
|
||||
@click="handleTabChange(tab.name)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 去掉「审核结论」下拉中的无效「待审核」选项**
|
||||
|
||||
删除:
|
||||
|
||||
```vue
|
||||
<el-option label="待审核" value="" />
|
||||
```
|
||||
|
||||
保留 `clearable` + placeholder「全部」+ `reviewConclusionOptions`(通过/不通过)。
|
||||
|
||||
- [ ] **Step 3: 增加 Tab 状态与切换逻辑**
|
||||
|
||||
在 `<script setup>` 中、`searchForm` 之前增加:
|
||||
|
||||
```typescript
|
||||
type ReviewTab = 'pending' | 'reviewed'
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ name: 'pending' as const, label: '待审核' },
|
||||
{ name: 'reviewed' as const, label: '已审核' }
|
||||
]
|
||||
|
||||
const activeTab = ref<ReviewTab>('pending')
|
||||
|
||||
function handleTabChange(name: ReviewTab) {
|
||||
if (activeTab.value === name) return
|
||||
activeTab.value = name
|
||||
pagination.page = 1
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}
|
||||
```
|
||||
|
||||
注意:`handleTabChange` 使用了 `pagination` / `selectedRows` / `loadData`,须放在这些声明之后;或先声明 `activeTab`/`STATUS_TABS`,把 `handleTabChange` 放在 `loadData` 之后。推荐顺序:`STATUS_TABS` + `activeTab` → `searchForm` / `pagination` / `selectedRows` → `loadData` → `handleTabChange`。
|
||||
|
||||
- [ ] **Step 4: `loadData` 请求带上 `reviewPhase`**
|
||||
|
||||
```typescript
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchStorageReleaseReviewList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
receptionSource: searchForm.receptionSource || undefined,
|
||||
reviewConclusion: searchForm.reviewConclusion || undefined,
|
||||
storageStatus: searchForm.storageStatus || undefined,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
reviewPhase: activeTab.value
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`handleReset` **不要**重置 `activeTab`(仅清空筛选);重置后仍在当前 Tab 刷新。
|
||||
|
||||
- [ ] **Step 5: 本地手测清单**
|
||||
|
||||
启动后端与 `web-admin`(`pnpm dev`),打开 `/#/market/cell-storage/storage-release-review`:
|
||||
|
||||
1. 默认「待审核」高亮;列表行审核结论均为「待审核」
|
||||
2. 切到「已审核」:仅通过/不通过;用「审核结论」再筛有效
|
||||
3. 切 Tab:页码回 1、勾选清空、筛选保留
|
||||
4. 待审核提交审核后该行消失;已审核可再改结论;入库按钮行为不变
|
||||
|
||||
- [ ] **Step 6: 更新图谱(前端 + 后端)**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh all
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 将 design spec 状态改为已确认**
|
||||
|
||||
编辑 `docs/superpowers/specs/2026-07-16-storage-release-review-tabs-design.md` 头部:
|
||||
|
||||
```markdown
|
||||
> 状态:已确认
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/quality-inspection/storage-release-review/index.vue \
|
||||
docs/superpowers/specs/2026-07-16-storage-release-review-tabs-design.md \
|
||||
backend/graphify-out web-admin/graphify-out
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: split storage release review list into pending and reviewed tabs
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
(若图谱无变更可从 `git add` 中去掉 graphify-out 路径。)
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Checklist
|
||||
|
||||
| Spec 要求 | Task |
|
||||
|-----------|------|
|
||||
| 双 Tab 待审核/已审核 | Task 3 |
|
||||
| 默认待审核 | Task 3 `activeTab = 'pending'` |
|
||||
| 已审核含 tg+btg,可再筛结论 | Task 1 phase + Task 3 筛选 |
|
||||
| 已审核仍可改结论 | 不改弹窗;操作列保持「审核」 |
|
||||
| 两 Tab 筛选项相同 | Task 3(仅去掉无效待审核 option) |
|
||||
| `reviewPhase` API + 缺省兼容 | Task 1 |
|
||||
| 非法 phase 忽略 | Task 1 `normalize_review_phase` |
|
||||
| 切 Tab 重置页码/勾选、保留筛选 | Task 3 `handleTabChange` |
|
||||
| 审核后刷新当前 Tab | 现有 `onReviewSubmitted` → `loadData` |
|
||||
| 不做角标/不改弹窗/不改菜单 | 范围外,无任务 |
|
||||
|
||||
---
|
||||
|
||||
## 完成定义
|
||||
|
||||
- [ ] `pytest tests/test_storage_release_review_phase.py` 通过
|
||||
- [ ] 页面双 Tab 行为符合 spec §6 验收要点
|
||||
- [ ] 相关 commit 已完成(若执行时用户要求提交)
|
||||
@@ -0,0 +1,143 @@
|
||||
# 入库放行审核 · 待审核 / 已审核双 Tab
|
||||
|
||||
> 日期:2026-07-16
|
||||
> 状态:已确认
|
||||
> 模块:细胞存储 / 质检 → 入库放行审核
|
||||
> 关联:`docs/superpowers/specs/2026-05-23-storage-release-review-design.md`
|
||||
> 界面模式:P22(审核列表)+ 样本接收式 `panel-pill-tabs`
|
||||
> 参考页:`web-admin/src/views/quality-inspection/sample-reception/index.vue`
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 现状
|
||||
|
||||
「入库放行审核」列表将所有记录(待审核 + 已审核)放在同一表格中。筛选区虽有「审核结论」,但选项「待审核」绑定空字符串,请求时被转成 `undefined`,后端不会按空结论过滤,实际无法单独查看待办。
|
||||
|
||||
### 目标
|
||||
|
||||
将列表拆成两个独立 Tab:
|
||||
|
||||
1. **待审核**:`review_conclusion` 为空的记录(审核员日常主工作台)
|
||||
2. **已审核**:已有结论(通过 `tg` + 不通过 `btg`),可用「审核结论」再筛
|
||||
|
||||
默认进入「待审核」。分页与总数按当前 Tab 正确计算。
|
||||
|
||||
### 范围外
|
||||
|
||||
- 不改审核弹窗、查看弹窗、入库业务规则
|
||||
- 不新增菜单、不拆独立路由页
|
||||
- 不做 Tab 角标数量(后续可单独立项)
|
||||
|
||||
## 2. 需求确认记录
|
||||
|
||||
| 维度 | 决定 |
|
||||
|------|------|
|
||||
| Tab 结构 | 两个:`待审核` \| `已审核`(已审核含通过 + 不通过) |
|
||||
| 已审核改结论 | 仍可点「审核」修改(与现网一致) |
|
||||
| 筛选区 | 两 Tab 筛选项完全相同;已审核可用「审核结论」再筛 |
|
||||
| 默认 Tab | 进入页面默认「待审核」 |
|
||||
| 实现路径 | 前端双 Tab + 列表 API 增加 `reviewPhase`(方案 1) |
|
||||
|
||||
## 3. UI 与交互
|
||||
|
||||
### 3.1 布局顺序
|
||||
|
||||
与样本接收一致:
|
||||
|
||||
`panel-top` 标题 → **pill Tabs**(`panel-pill-tabs`)→ 筛选区 → 工具栏 → 表格 + 分页
|
||||
|
||||
### 3.2 Tabs
|
||||
|
||||
| key | 文案 | 默认 |
|
||||
|-----|------|------|
|
||||
| `pending` | 待审核 | ✓ |
|
||||
| `reviewed` | 已审核 | |
|
||||
|
||||
### 3.3 切换 Tab
|
||||
|
||||
- 页码回到 1
|
||||
- 清空表格勾选
|
||||
- **保留**当前筛选条件
|
||||
- 重新请求列表(带对应 `reviewPhase`)
|
||||
|
||||
### 3.4 筛选区(两 Tab 相同)
|
||||
|
||||
字段:关键词、协议类别、审核结论、入库状态。
|
||||
|
||||
「审核结论」下拉:
|
||||
|
||||
- 去掉无效的「待审核」选项(空值曾被当成全部)
|
||||
- 保留:清空 = 当前 Tab 内「全部」+ 通过 / 不通过
|
||||
- 待审核 Tab 下若选通过/不通过,与阶段叠加,结果为空属预期
|
||||
|
||||
### 3.5 工具栏与操作列(两 Tab 相同)
|
||||
|
||||
- 工具栏:已选 N 项、批量审核、批量入库
|
||||
- 行操作:查看、审核;通过且待入库时显示「样本入库」
|
||||
- 已审核 Tab 仍可「审核」改结论
|
||||
|
||||
### 3.6 审核提交后
|
||||
|
||||
- 刷新**当前** Tab 列表,不强制跳 Tab
|
||||
- 从待审核提交后,该行自然从待审核消失;需到已审核查看
|
||||
|
||||
## 4. API 与数据流
|
||||
|
||||
### 4.1 接口
|
||||
|
||||
沿用 `GET /web/admin/storage-release-reviews`,新增可选查询参数:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `reviewPhase` | `pending` \| `reviewed` | Tab 阶段;**缺省**时不按阶段过滤(与现网兼容) |
|
||||
|
||||
其余参数不变:`page`、`pageSize`、`keyword`、`receptionSource`、`reviewConclusion`、`storageStatus`。
|
||||
|
||||
### 4.2 过滤语义
|
||||
|
||||
在租户 / 软删 / 数据权限 / 其它筛选项之上叠加:
|
||||
|
||||
| reviewPhase | SQL 语义 |
|
||||
|-------------|----------|
|
||||
| `pending` | `review_conclusion IS NULL OR review_conclusion = ''` |
|
||||
| `reviewed` | `review_conclusion IS NOT NULL AND review_conclusion != ''` |
|
||||
|
||||
再叠加:`keyword`、`receptionSource`、`reviewConclusion`(仅 `tg`/`btg`)、`storageStatus`。
|
||||
|
||||
非法 `reviewPhase`:忽略该参数,按无阶段处理(避免误伤调用方)。
|
||||
|
||||
### 4.3 前端请求约定
|
||||
|
||||
- 待审核 Tab:始终传 `reviewPhase=pending`
|
||||
- 已审核 Tab:始终传 `reviewPhase=reviewed`
|
||||
- `reviewConclusion` 仅在有明确值(通过/不通过)时传入;不再用空字符串表示「待审核」
|
||||
|
||||
### 4.4 不变接口
|
||||
|
||||
详情、单条/批量审核、单条/批量入库接口与行为不变。
|
||||
|
||||
## 5. 改动清单
|
||||
|
||||
| 层 | 文件 | 改动 |
|
||||
|----|------|------|
|
||||
| Backend API | `backend/app/api/storage_release_review.py` | 增加 `reviewPhase` Query,传入 CRUD |
|
||||
| Backend CRUD | `backend/app/crud/storage_release_review.py` | `list_reviews` 按阶段过滤 |
|
||||
| Frontend API | `web-admin/src/api/storage-release-review.ts` | 列表参数增加 `reviewPhase` |
|
||||
| Frontend 页 | `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | pill Tabs、切 Tab 刷新、下拉去掉无效「待审核」、请求带 `reviewPhase` |
|
||||
|
||||
不改:`StorageReleaseReviewDialog.vue`、`StorageReleaseViewDialog.vue`、菜单/路由、表结构。
|
||||
|
||||
## 6. 验收要点
|
||||
|
||||
1. 进入页面默认「待审核」,仅无结论记录;分页总数正确
|
||||
2. 「已审核」仅有通过/不通过;可用「审核结论」再筛
|
||||
3. 两 Tab 筛选字段一致;切 Tab 保留筛选、重置页码与勾选
|
||||
4. 待审核提交审核后,该行从待审核消失,出现在已审核
|
||||
5. 已审核仍可「审核」改结论;通过且待入库仍可入库 / 批量入库
|
||||
6. 不传 `reviewPhase` 时列表行为与改前兼容
|
||||
|
||||
## 7. 非目标
|
||||
|
||||
- Tab 角标数量
|
||||
- 独立菜单或独立列表页
|
||||
- 修改审核/入库业务规则与弹窗结构
|
||||
Reference in new issue
Block a user