# 运营支撑模块 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) { return request.post({ url: '/web/admin/clean-rooms', params: data }) } export function fetchUpdateCleanRoom(id: string, data: Record) { 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) { return request.post({ url: '/web/admin/clean-room-access-logs', params: data }) } export function fetchUpdateAccessLog(id: string, data: Record) { 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) { return request.post({ url: '/web/admin/storage-facilities', params: data }) } export function fetchUpdateStorageFacility(id: string, data: Record) { 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) { return request.post({ url: '/web/admin/storage-facility-slots', params: data }) } export function fetchUpdateStorageSlot(id: string, data: Record) { 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) { return request.post({ url: '/web/admin/exhibition-appointments', params: data }) } export function fetchUpdateExhibitionAppointment(id: string, data: Record) { 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 ✓