619 lines
29 KiB
Markdown
619 lines
29 KiB
Markdown
# 运营支撑模块 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:** Implement 3 operations support modules (洁净室管理, 库位管理, 展馆预约) with backend API + frontend UI, following existing codebase patterns exactly.
|
||
|
||
**Architecture:** FastAPI backend with flat-function CRUD (no service layer), Vue 3 + Element Plus frontend with standard list/tree page templates. All 3 modules share a new API route file `app/api/operations.py` and frontend route group.
|
||
|
||
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2 (backend); Vue 3 / TypeScript / Element Plus / Tailwind CSS 4 (frontend)
|
||
|
||
**Design Spec:** `docs/superpowers/specs/2026-05-15-operations-support-design.md`
|
||
|
||
**Key Reference Files (READ FIRST):**
|
||
- Backend CRUD pattern: `backend/app/crud/item_unit.py`
|
||
- Backend API pattern: `backend/app/api/basic_info.py` (lines 1-100 for simple CRUD)
|
||
- Backend model pattern: `backend/app/models/models.py` (class BizItemUnit, lines 259-275)
|
||
- Frontend standard list: `web-admin/src/views/system/unit/index.vue`
|
||
- Frontend tree template: `web-admin/src/views/system/category/index.vue`
|
||
- Frontend API pattern: `web-admin/src/api/admin.ts` (any CRUD section)
|
||
- Frontend router: `web-admin/src/router/modules/sqxp.ts`
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### Backend (create/modify)
|
||
| File | Action | Purpose |
|
||
|------|--------|---------|
|
||
| `backend/app/models/models.py` | MODIFY | Add 5 model classes at end of file |
|
||
| `backend/app/crud/clean_room.py` | CREATE | CleanRoom + AccessLog CRUD functions |
|
||
| `backend/app/crud/storage_facility.py` | CREATE | StorageFacility tree + Slot CRUD functions |
|
||
| `backend/app/crud/exhibition_appointment.py` | CREATE | ExhibitionAppointment CRUD functions |
|
||
| `backend/app/api/operations.py` | CREATE | All 3 modules API routes |
|
||
| `backend/app/api/__init__.py` | MODIFY | Import + export operations_router |
|
||
| `backend/app/main.py` | MODIFY | Import + register operations_router |
|
||
| `backend/tests/test_operations_api.py` | CREATE | Backend API tests |
|
||
|
||
### Frontend (create/modify)
|
||
| File | Action | Purpose |
|
||
|------|--------|---------|
|
||
| `web-admin/src/api/admin.ts` | MODIFY | Add API functions for all 3 modules |
|
||
| `web-admin/src/router/modules/sqxp.ts` | MODIFY | Add 运营支撑 route group |
|
||
| `web-admin/src/views/operations/clean-room/index.vue` | CREATE | Clean room list page |
|
||
| `web-admin/src/views/operations/storage-facility/index.vue` | CREATE | Storage facility tree page |
|
||
| `web-admin/src/views/operations/exhibition-appointment/index.vue` | CREATE | Exhibition appointment list page |
|
||
|
||
---
|
||
|
||
## Task 1: Backend Models (5 SQLAlchemy models)
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/models/models.py` (append at end, before EOF)
|
||
|
||
- [ ] **Step 1: Add 5 model classes to models.py**
|
||
|
||
Append the following after the last class in `backend/app/models/models.py`:
|
||
|
||
```python
|
||
# =============================================================================
|
||
# V0.4 运营支撑(2.4 洁净室管理 / 库位管理 / 展馆预约)
|
||
# =============================================================================
|
||
|
||
|
||
class BizCleanRoom(Base):
|
||
"""洁净室信息 biz_clean_room"""
|
||
|
||
__tablename__ = "biz_clean_room"
|
||
|
||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||
room_code = Column(String(64), nullable=False, comment="编号")
|
||
room_name = Column(String(200), nullable=False, comment="名称")
|
||
room_grade = Column(String(8), nullable=True, comment="等级 A/B/C/D")
|
||
room_status = Column(String(16), nullable=False, default="zc", comment="zc正常 yc异常")
|
||
area_sqm = Column(Numeric(12, 2), nullable=True, comment="面积m²")
|
||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||
remark = Column(String(512), nullable=True, comment="备注")
|
||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||
|
||
|
||
class BizCleanRoomAccessLog(Base):
|
||
"""洁净室人员进出 biz_clean_room_access_log"""
|
||
|
||
__tablename__ = "biz_clean_room_access_log"
|
||
|
||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||
clean_room_id = Column(String(64), nullable=False, comment="洁净室ID")
|
||
user_id = Column(String(64), nullable=True, comment="人员用户ID")
|
||
person_name = Column(String(100), nullable=True, comment="人员姓名(冗余)")
|
||
dept_name = Column(String(200), nullable=True, comment="部门")
|
||
enter_time = Column(DateTime, nullable=False, comment="进入时间")
|
||
purpose = Column(String(512), nullable=True, comment="目的")
|
||
leave_time = Column(DateTime, nullable=True, comment="离开时间")
|
||
body_temp = Column(String(32), nullable=True, comment="体温")
|
||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||
remark = Column(String(512), nullable=True, comment="备注")
|
||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||
|
||
|
||
class BizStorageFacility(Base):
|
||
"""细胞存储设施树节点 biz_storage_facility"""
|
||
|
||
__tablename__ = "biz_storage_facility"
|
||
|
||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||
parent_id = Column(String(64), nullable=False, default="0", comment="父节点ID,0根")
|
||
node_name = Column(String(200), nullable=False, comment="节点名称(罐/层/盒等)")
|
||
node_type = Column(String(32), nullable=True, comment="facility/layer/box 等扩展")
|
||
is_leaf = Column(SmallInteger, nullable=False, default=0, comment="是否叶子存储位 1是")
|
||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||
remark = Column(String(512), nullable=True, comment="备注")
|
||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||
|
||
|
||
class BizStorageFacilitySlot(Base):
|
||
"""细胞存储设施储存位置(格子)biz_storage_facility_slot"""
|
||
|
||
__tablename__ = "biz_storage_facility_slot"
|
||
|
||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||
facility_node_id = Column(String(64), nullable=False, comment="设施树叶子节点ID")
|
||
slot_code = Column(String(64), nullable=True, comment="小格编码/坐标")
|
||
slot_status = Column(String(16), nullable=True, comment="占用状态等扩展")
|
||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||
remark = Column(String(512), nullable=True, comment="备注")
|
||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||
|
||
|
||
class BizExhibitionAppointment(Base):
|
||
"""展馆预约 biz_exhibition_appointment"""
|
||
|
||
__tablename__ = "biz_exhibition_appointment"
|
||
|
||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||
appointment_no = Column(String(64), nullable=False, comment="预约编号")
|
||
visit_org = Column(String(200), nullable=True, comment="来访单位")
|
||
contact_name = Column(String(100), nullable=True, comment="联系人")
|
||
visit_date = Column(CHAR(8), nullable=True, comment="到访日期YYYYMMDD")
|
||
visitor_count = Column(Integer, nullable=True, comment="人数")
|
||
reception_user_id = Column(String(64), nullable=True, comment="接待人员用户ID")
|
||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||
remark = Column(String(512), nullable=True, comment="备注")
|
||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||
```
|
||
|
||
- [ ] **Step 2: Verify models load without errors**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.models.models import BizCleanRoom, BizCleanRoomAccessLog, BizStorageFacility, BizStorageFacilitySlot, BizExhibitionAppointment; print('All models imported OK')"`
|
||
Expected: "All models imported OK"
|
||
|
||
---
|
||
|
||
## Task 2: Backend CRUD — Exhibition Appointment (simplest, start here)
|
||
|
||
**Files:**
|
||
- Create: `backend/app/crud/exhibition_appointment.py`
|
||
|
||
- [ ] **Step 1: Create CRUD file**
|
||
|
||
Create `backend/app/crud/exhibition_appointment.py` following the `item_unit.py` pattern exactly. Functions: `_row_to_dict`, `list_exhibition_appointments`, `get_exhibition_appointment`, `appointment_no_exists`, `create_exhibition_appointment`, `update_exhibition_appointment`, `soft_delete_exhibition_appointment`, `total_pages`. All queries filter by `del_flag==0` and `tenant_no`. List supports keyword search on `appointment_no`, `visit_org`, `contact_name`. Default sort by `create_time DESC`.
|
||
|
||
- [ ] **Step 2: Verify import**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud import exhibition_appointment; print('OK')"`
|
||
Expected: "OK"
|
||
|
||
---
|
||
|
||
## Task 3: Backend CRUD — Clean Room + Access Log
|
||
|
||
**Files:**
|
||
- Create: `backend/app/crud/clean_room.py`
|
||
|
||
- [ ] **Step 1: Create CRUD file**
|
||
|
||
Create `backend/app/crud/clean_room.py` with two sets of CRUD functions:
|
||
|
||
**Clean Room functions:** `_row_to_dict`, `list_clean_rooms`, `get_clean_room`, `room_code_exists`, `create_clean_room`, `update_clean_room`, `soft_delete_clean_room`, `total_pages`. Keyword search on `room_code`, `room_name`. Filter by `tenant_no` and `del_flag==0`.
|
||
|
||
**Access Log functions:** `_access_log_to_dict`, `list_access_logs` (filter by `clean_room_id`), `get_access_log`, `create_access_log`, `update_access_log`, `soft_delete_access_log`. List supports `clean_room_id` filter + keyword on `person_name`, `dept_name`.
|
||
|
||
- [ ] **Step 2: Verify import**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud import clean_room; print('OK')"`
|
||
Expected: "OK"
|
||
|
||
---
|
||
|
||
## Task 4: Backend CRUD — Storage Facility (tree) + Slot
|
||
|
||
**Files:**
|
||
- Create: `backend/app/crud/storage_facility.py`
|
||
|
||
- [ ] **Step 1: Create CRUD file**
|
||
|
||
Create `backend/app/crud/storage_facility.py` with:
|
||
|
||
**Facility tree functions:** `_row_to_dict`, `list_all_facilities` (returns flat list, filtered by `tenant_no`, `del_flag==0`), `get_facility`, `create_facility`, `update_facility`, `soft_delete_facility` (cascade: soft-delete all children + their slots), `get_children` (find direct children of a node).
|
||
|
||
**Slot functions:** `_slot_to_dict`, `list_slots` (filter by `facility_node_id`), `get_slot`, `create_slot`, `update_slot`, `soft_delete_slot`.
|
||
|
||
**Tree helpers:** `build_tree` — converts flat list to nested tree structure with `children` array. `collect_descendant_ids` — recursively collects all descendant node IDs for cascade delete.
|
||
|
||
- [ ] **Step 2: Verify import**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud import storage_facility; print('OK')"`
|
||
Expected: "OK"
|
||
|
||
---
|
||
|
||
## Task 5: Backend API Routes — operations.py
|
||
|
||
**Files:**
|
||
- Create: `backend/app/api/operations.py`
|
||
|
||
- [ ] **Step 1: Create the API route file**
|
||
|
||
Create `backend/app/api/operations.py` with `router = APIRouter(prefix="/web/admin", tags=["运营支撑"])`. Include:
|
||
|
||
**Clean Room endpoints:**
|
||
- `GET /clean-rooms` — paginated list with keyword
|
||
- `POST /clean-rooms` — create
|
||
- `PUT /clean-rooms/{room_id}` — update
|
||
- `DELETE /clean-rooms/{room_id}` — soft delete
|
||
|
||
**Access Log endpoints:**
|
||
- `GET /clean-room-access-logs` — list by clean_room_id, paginated
|
||
- `POST /clean-room-access-logs` — create
|
||
- `PUT /clean-room-access-logs/{log_id}` — update
|
||
- `DELETE /clean-room-access-logs/{log_id}` — soft delete
|
||
|
||
**Storage Facility endpoints:**
|
||
- `GET /storage-facilities` — flat list (frontend builds tree)
|
||
- `POST /storage-facilities` — create node
|
||
- `PUT /storage-facilities/{node_id}` — update node
|
||
- `DELETE /storage-facilities/{node_id}` — cascade soft delete
|
||
|
||
**Storage Slot endpoints:**
|
||
- `GET /storage-facility-slots` — list by facility_node_id
|
||
- `POST /storage-facility-slots` — create
|
||
- `PUT /storage-facility-slots/{slot_id}` — update
|
||
- `DELETE /storage-facility-slots/{slot_id}` — soft delete
|
||
|
||
**Exhibition Appointment endpoints:**
|
||
- `GET /exhibition-appointments` — paginated list with keyword
|
||
- `POST /exhibition-appointments` — create
|
||
- `PUT /exhibition-appointments/{appointment_id}` — update
|
||
- `DELETE /exhibition-appointments/{appointment_id}` — soft delete
|
||
|
||
All endpoints use `Depends(require_admin_or_super_admin)` and `_get_tenant_no(db, user)`.
|
||
|
||
Each endpoint follows the pattern from `basic_info.py`: Pydantic body classes inline with `ConfigDict(populate_by_name=True)` and `Field(..., alias="camelCase")`. List endpoints return `success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})`.
|
||
|
||
- [ ] **Step 2: Verify import**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.api.operations import router; print(f'Routes: {len(router.routes)}')"`
|
||
Expected: "Routes: 13"
|
||
|
||
---
|
||
|
||
## Task 6: Backend Router Registration
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/api/__init__.py`
|
||
- Modify: `backend/app/main.py`
|
||
|
||
- [ ] **Step 1: Register in __init__.py**
|
||
|
||
Add to `backend/app/api/__init__.py`:
|
||
```python
|
||
from app.api.operations import router as operations_router
|
||
```
|
||
And add `"operations_router"` to `__all__`.
|
||
|
||
- [ ] **Step 2: Register in main.py**
|
||
|
||
Add to `backend/app/main.py` line 13 import section:
|
||
```python
|
||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router
|
||
```
|
||
Add after line 67:
|
||
```python
|
||
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
|
||
```
|
||
|
||
- [ ] **Step 3: Verify app starts**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.main import app; print(f'App routes: {len(app.routes)}')"`
|
||
Expected: A number > previous route count
|
||
|
||
---
|
||
|
||
## Task 7: Backend Tests
|
||
|
||
**Files:**
|
||
- Create: `backend/tests/test_operations_api.py`
|
||
|
||
- [ ] **Step 1: Write test file**
|
||
|
||
Create `backend/tests/test_operations_api.py` with tests for each module:
|
||
|
||
1. **Clean Room tests:** test_create_clean_room, test_list_clean_rooms, test_update_clean_room, test_delete_clean_room
|
||
2. **Access Log tests:** test_create_access_log, test_list_access_logs_by_room
|
||
3. **Storage Facility tests:** test_create_facility_node, test_list_facilities_flat, test_create_child_node, test_cascade_delete
|
||
4. **Slot tests:** test_create_slot, test_list_slots_by_node
|
||
5. **Exhibition Appointment tests:** test_create_appointment, test_list_appointments, test_update_appointment, test_delete_appointment
|
||
|
||
Use `pytest` with FastAPI `TestClient`. Each test creates, reads, updates, deletes and verifies. Tests can use the running dev server or TestClient.
|
||
|
||
- [ ] **Step 2: Run tests**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -m pytest tests/test_operations_api.py -v`
|
||
Expected: All tests PASS
|
||
|
||
---
|
||
|
||
## Task 8: Frontend API Functions
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/api/admin.ts`
|
||
|
||
- [ ] **Step 1: Add API functions for all 3 modules**
|
||
|
||
Append to `web-admin/src/api/admin.ts` three new section blocks:
|
||
|
||
**Clean Room section:**
|
||
```typescript
|
||
// ---------------------------------------------------------------------------
|
||
// 洁净室管理
|
||
// ---------------------------------------------------------------------------
|
||
export function fetchCleanRoomList(params?: { page?: number; pageSize?: number; keyword?: string }) {
|
||
return request.get({ url: '/web/admin/clean-rooms', params })
|
||
}
|
||
export function fetchCreateCleanRoom(data: Record<string, any>) {
|
||
return request.post({ url: '/web/admin/clean-rooms', params: data })
|
||
}
|
||
export function fetchUpdateCleanRoom(id: string, data: Record<string, any>) {
|
||
return request.put({ url: `/web/admin/clean-rooms/${id}`, params: data })
|
||
}
|
||
export function fetchDeleteCleanRoom(id: string) {
|
||
return request.del({ url: `/web/admin/clean-rooms/${id}` })
|
||
}
|
||
// 人员进出
|
||
export function fetchAccessLogList(params?: { page?: number; pageSize?: number; cleanRoomId?: string; keyword?: string }) {
|
||
return request.get({ url: '/web/admin/clean-room-access-logs', params })
|
||
}
|
||
export function fetchCreateAccessLog(data: Record<string, any>) {
|
||
return request.post({ url: '/web/admin/clean-room-access-logs', params: data })
|
||
}
|
||
export function fetchUpdateAccessLog(id: string, data: Record<string, any>) {
|
||
return request.put({ url: `/web/admin/clean-room-access-logs/${id}`, params: data })
|
||
}
|
||
export function fetchDeleteAccessLog(id: string) {
|
||
return request.del({ url: `/web/admin/clean-room-access-logs/${id}` })
|
||
}
|
||
```
|
||
|
||
**Storage Facility section:**
|
||
```typescript
|
||
// ---------------------------------------------------------------------------
|
||
// 库位管理
|
||
// ---------------------------------------------------------------------------
|
||
export function fetchStorageFacilityList(params?: { keyword?: string }) {
|
||
return request.get({ url: '/web/admin/storage-facilities', params })
|
||
}
|
||
export function fetchCreateStorageFacility(data: Record<string, any>) {
|
||
return request.post({ url: '/web/admin/storage-facilities', params: data })
|
||
}
|
||
export function fetchUpdateStorageFacility(id: string, data: Record<string, any>) {
|
||
return request.put({ url: `/web/admin/storage-facilities/${id}`, params: data })
|
||
}
|
||
export function fetchDeleteStorageFacility(id: string) {
|
||
return request.del({ url: `/web/admin/storage-facilities/${id}` })
|
||
}
|
||
// 格子
|
||
export function fetchStorageSlotList(params?: { facilityNodeId?: string }) {
|
||
return request.get({ url: '/web/admin/storage-facility-slots', params })
|
||
}
|
||
export function fetchCreateStorageSlot(data: Record<string, any>) {
|
||
return request.post({ url: '/web/admin/storage-facility-slots', params: data })
|
||
}
|
||
export function fetchUpdateStorageSlot(id: string, data: Record<string, any>) {
|
||
return request.put({ url: `/web/admin/storage-facility-slots/${id}`, params: data })
|
||
}
|
||
export function fetchDeleteStorageSlot(id: string) {
|
||
return request.del({ url: `/web/admin/storage-facility-slots/${id}` })
|
||
}
|
||
```
|
||
|
||
**Exhibition Appointment section:**
|
||
```typescript
|
||
// ---------------------------------------------------------------------------
|
||
// 展馆预约
|
||
// ---------------------------------------------------------------------------
|
||
export function fetchExhibitionAppointmentList(params?: { page?: number; pageSize?: number; keyword?: string }) {
|
||
return request.get({ url: '/web/admin/exhibition-appointments', params })
|
||
}
|
||
export function fetchCreateExhibitionAppointment(data: Record<string, any>) {
|
||
return request.post({ url: '/web/admin/exhibition-appointments', params: data })
|
||
}
|
||
export function fetchUpdateExhibitionAppointment(id: string, data: Record<string, any>) {
|
||
return request.put({ url: `/web/admin/exhibition-appointments/${id}`, params: data })
|
||
}
|
||
export function fetchDeleteExhibitionAppointment(id: string) {
|
||
return request.del({ url: `/web/admin/exhibition-appointments/${id}` })
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify no TypeScript errors**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build:typecheck 2>&1 | tail -5`
|
||
Expected: No errors related to new functions
|
||
|
||
---
|
||
|
||
## Task 9: Frontend Router — Add 运营支撑 Group
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/router/modules/sqxp.ts`
|
||
|
||
- [ ] **Step 1: Add route group**
|
||
|
||
Add before the closing `]` of `sqxpAdminRoutes` (before the last `}` or after the Workflow section):
|
||
|
||
```typescript
|
||
,
|
||
{
|
||
id: '60000000000000000000000000000001',
|
||
name: 'Operations',
|
||
path: '/operations',
|
||
component: '/index/index',
|
||
meta: {
|
||
title: '运营支撑',
|
||
icon: 'operation',
|
||
keepAlive: false,
|
||
roles: ['admin']
|
||
},
|
||
children: [
|
||
{
|
||
id: '60000000000000000000000000000002',
|
||
name: 'CleanRoom',
|
||
path: 'clean-room',
|
||
component: '/operations/clean-room/index',
|
||
meta: { title: '洁净室管理', keepAlive: true }
|
||
},
|
||
{
|
||
id: '60000000000000000000000000000003',
|
||
name: 'StorageFacility',
|
||
path: 'storage-facility',
|
||
component: '/operations/storage-facility/index',
|
||
meta: { title: '库位管理', keepAlive: true }
|
||
},
|
||
{
|
||
id: '60000000000000000000000000000004',
|
||
name: 'ExhibitionAppointment',
|
||
path: 'exhibition-appointment',
|
||
component: '/operations/exhibition-appointment/index',
|
||
meta: { title: '展馆预约', keepAlive: true }
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify routes compile**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build:typecheck 2>&1 | tail -5`
|
||
Expected: No route-related errors
|
||
|
||
---
|
||
|
||
## Task 10: Frontend — Clean Room Page
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/operations/clean-room/index.vue`
|
||
|
||
- [ ] **Step 1: Create the Vue page**
|
||
|
||
Create `web-admin/src/views/operations/clean-room/index.vue` following the `system/unit/index.vue` template exactly:
|
||
|
||
**Page structure:**
|
||
- `page-container` → `header.page-head` → `section.panel`
|
||
- Filter: keyword search
|
||
- Table columns: 编号(roomCode), 名称(roomName), 等级(roomGrade, tag display: A级绿色/B级蓝色/C级黄色/D级灰色), 状态(roomStatus, tag: 正常=success/异常=danger), 面积(areaSqm), 操作
|
||
- Toolbar: 新增按钮, 批量删除按钮
|
||
- Dialog form (label-position="top", form-grid-2):
|
||
- 编号(roomCode, required), 名称(roomName, required), 等级(roomGrade, select: A/B/C/D), 状态(roomStatus, select: 正常zc/异常yc), 面积(areaSqm, number input, suffix "m²"), 备注(remark)
|
||
- 操作列: 查看, 编辑, 人员进出, 删除
|
||
- "人员进出"按钮 → 打开新弹窗显示该洁净室的进出记录列表(带分页), 弹窗内支持新增/编辑/删除进出记录
|
||
- 进出记录表单: 人员姓名(personName), 部门(deptName), 进入时间(enterTime, datetime), 目的(purpose), 离开时间(leaveTime, datetime), 体温(bodyTemp), 备注(remark)
|
||
- View dialog using SystemViewDialog component
|
||
- API calls: `fetchCleanRoomList`, `fetchCreateCleanRoom`, `fetchUpdateCleanRoom`, `fetchDeleteCleanRoom`, `fetchAccessLogList`, `fetchCreateAccessLog`, `fetchUpdateAccessLog`, `fetchDeleteAccessLog`
|
||
|
||
- [ ] **Step 2: Verify page renders**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm dev`, then navigate to `/#/operations/clean-room`
|
||
Expected: Page loads without console errors, shows empty list with filter bar and toolbar
|
||
|
||
---
|
||
|
||
## Task 11: Frontend — Storage Facility Page (Tree)
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/operations/storage-facility/index.vue`
|
||
|
||
- [ ] **Step 1: Create the Vue page**
|
||
|
||
Create `web-admin/src/views/operations/storage-facility/index.vue` following the `system/category/index.vue` tree template:
|
||
|
||
**Page structure:**
|
||
- Left panel: tree structure display using `el-table` with `row-key="id"` and `:tree-props="{ children: 'children' }"`
|
||
- Data fetched as flat list via `fetchStorageFacilityList`, then `buildTree()` converts to nested tree
|
||
- Tree columns: 节点名称(nodeName), 节点类型(nodeType, display as tag: facility=设施/layer=层/box=盒), 是否叶子(isLeaf, icon or tag), 备注(remark)
|
||
- Toolbar: 新增根节点, 展开全部, 折叠全部
|
||
- 操作列: 新增子节点, 编辑, 删除(需确认)
|
||
- Right panel: when a leaf node (`isLeaf===1`) is selected, show its slots (格子) list
|
||
- Slot table: 编码(slotCode), 状态(slotStatus), 操作(编辑/删除)
|
||
- Add slot button
|
||
- Dialog form for facility node (label-position="top", form-grid-2):
|
||
- 父节点(parentId, select from tree list, show full path via `buildTreePathLabelFromFlat`), 节点名称(nodeName, required), 节点类型(nodeType, select: facility/layer/box), 是否叶子(isLeaf, switch), 备注(remark)
|
||
- Dialog form for slot:
|
||
- 编码(slotCode, required), 状态(slotStatus, select), 备注(remark)
|
||
- Tree utilities: import `buildTreePathLabelFromFlat` from `@/utils/tree-path`
|
||
- API calls: `fetchStorageFacilityList`, `fetchCreateStorageFacility`, `fetchUpdateStorageFacility`, `fetchDeleteStorageFacility`, `fetchStorageSlotList`, `fetchCreateStorageSlot`, `fetchUpdateStorageSlot`, `fetchDeleteStorageSlot`
|
||
|
||
- [ ] **Step 2: Verify page renders**
|
||
|
||
Navigate to `/#/operations/storage-facility`
|
||
Expected: Page loads, shows empty tree with toolbar
|
||
|
||
---
|
||
|
||
## Task 12: Frontend — Exhibition Appointment Page
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/operations/exhibition-appointment/index.vue`
|
||
|
||
- [ ] **Step 1: Create the Vue page**
|
||
|
||
Create `web-admin/src/views/operations/exhibition-appointment/index.vue` following the `system/unit/index.vue` standard list template:
|
||
|
||
**Page structure:**
|
||
- Filter: keyword search (搜索预约编号/来访单位/联系人)
|
||
- Table columns: 预约编号(appointmentNo), 来访单位(visitOrg), 联系人(contactName), 到访日期(visitDate), 人数(visitorCount), 接待人员(receptionUserId → display name), 操作
|
||
- Toolbar: 新增按钮, 批量删除按钮
|
||
- Dialog form (label-position="top", form-grid-2):
|
||
- 预约编号(appointmentNo, required), 来访单位(visitOrg, required), 联系人(contactName), 到访日期(visitDate, date picker), 人数(visitorCount, number input), 接待人员(receptionUserId, select from user options), 备注(remark)
|
||
- View dialog using SystemViewDialog component
|
||
- API calls: `fetchExhibitionAppointmentList`, `fetchCreateExhibitionAppointment`, `fetchUpdateExhibitionAppointment`, `fetchDeleteExhibitionAppointment`
|
||
|
||
- [ ] **Step 2: Verify page renders**
|
||
|
||
Navigate to `/#/operations/exhibition-appointment`
|
||
Expected: Page loads without errors, shows empty list
|
||
|
||
---
|
||
|
||
## Task 13: Integration Test & Verification
|
||
|
||
- [ ] **Step 1: Start backend and verify all API endpoints**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -m pytest tests/test_operations_api.py -v`
|
||
Expected: All tests pass
|
||
|
||
- [ ] **Step 2: Start frontend and verify all 3 pages**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm dev`
|
||
Navigate to each page:
|
||
- `/#/operations/clean-room` — verify list, create dialog, access log dialog
|
||
- `/#/operations/storage-facility` — verify tree display, node CRUD, slot CRUD
|
||
- `/#/operations/exhibition-appointment` — verify list, create/edit dialog
|
||
|
||
- [ ] **Step 3: Run frontend typecheck**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build:typecheck`
|
||
Expected: No TypeScript errors
|
||
|
||
- [ ] **Step 4: Run frontend lint**
|
||
|
||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm lint`
|
||
Expected: No lint errors (or only pre-existing ones)
|
||
|
||
---
|
||
|
||
## Self-Review Checklist
|
||
|
||
1. **Spec coverage**: All 5 tables have models ✓, all 3 modules have CRUD ✓, API ✓, frontend ✓
|
||
2. **Placeholder scan**: No TBD/TODO, all steps have actual code or descriptions ✓
|
||
3. **Type consistency**: Model field names match CRUD `_row_to_dict` keys match API aliases ✓
|
||
4. **Convention compliance**: tenant_no filter ✓, soft delete ✓, camelCase API ✓, Fresh Mint colors ✓
|