Files
zkxw/docs/superpowers/plans/2026-05-15-operations-support.md
T
2026-05-17 08:36:37 +08:00

29 KiB
Raw Blame History

运营支撑模块 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 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:

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:

  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:

// ---------------------------------------------------------------------------
// 洁净室管理
// ---------------------------------------------------------------------------
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-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 ✓