29 KiB
运营支撑模块 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:
# =============================================================================
# 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 keywordPOST /clean-rooms— createPUT /clean-rooms/{room_id}— updateDELETE /clean-rooms/{room_id}— soft delete
Access Log endpoints:
GET /clean-room-access-logs— list by clean_room_id, paginatedPOST /clean-room-access-logs— createPUT /clean-room-access-logs/{log_id}— updateDELETE /clean-room-access-logs/{log_id}— soft delete
Storage Facility endpoints:
GET /storage-facilities— flat list (frontend builds tree)POST /storage-facilities— create nodePUT /storage-facilities/{node_id}— update nodeDELETE /storage-facilities/{node_id}— cascade soft delete
Storage Slot endpoints:
GET /storage-facility-slots— list by facility_node_idPOST /storage-facility-slots— createPUT /storage-facility-slots/{slot_id}— updateDELETE /storage-facility-slots/{slot_id}— soft delete
Exhibition Appointment endpoints:
GET /exhibition-appointments— paginated list with keywordPOST /exhibition-appointments— createPUT /exhibition-appointments/{appointment_id}— updateDELETE /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:
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:
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:
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:
- Clean Room tests: test_create_clean_room, test_list_clean_rooms, test_update_clean_room, test_delete_clean_room
- Access Log tests: test_create_access_log, test_list_access_logs_by_room
- Storage Facility tests: test_create_facility_node, test_list_facilities_flat, test_create_child_node, test_cascade_delete
- Slot tests: test_create_slot, test_list_slots_by_node
- 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:
// ---------------------------------------------------------------------------
// 洁净室管理
// ---------------------------------------------------------------------------
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:
// ---------------------------------------------------------------------------
// 库位管理
// ---------------------------------------------------------------------------
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:
// ---------------------------------------------------------------------------
// 展馆预约
// ---------------------------------------------------------------------------
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):
,
{
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-tablewithrow-key="id"and:tree-props="{ children: 'children' }"- Data fetched as flat list via
fetchStorageFacilityList, thenbuildTree()converts to nested tree - Tree columns: 节点名称(nodeName), 节点类型(nodeType, display as tag: facility=设施/layer=层/box=盒), 是否叶子(isLeaf, icon or tag), 备注(remark)
- Toolbar: 新增根节点, 展开全部, 折叠全部
- 操作列: 新增子节点, 编辑, 删除(需确认)
- Data fetched as flat list via
-
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)
- 父节点(parentId, select from tree list, show full path via
-
Dialog form for slot:
- 编码(slotCode, required), 状态(slotStatus, select), 备注(remark)
-
Tree utilities: import
buildTreePathLabelFromFlatfrom@/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
- Spec coverage: All 5 tables have models ✓, all 3 modules have CRUD ✓, API ✓, frontend ✓
- Placeholder scan: No TBD/TODO, all steps have actual code or descriptions ✓
- Type consistency: Model field names match CRUD
_row_to_dictkeys match API aliases ✓ - Convention compliance: tenant_no filter ✓, soft delete ✓, camelCase API ✓, Fresh Mint colors ✓