Преглед изворни кода

docs: add QC inspection item template implementation plan.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu пре 2 месеци
родитељ
комит
bf8e8e219f
1 измењених фајлова са 626 додато и 0 уклоњено
  1. 626 0
      docs/superpowers/plans/2026-08-17-qc-inspection-item-template.md

+ 626 - 0
docs/superpowers/plans/2026-08-17-qc-inspection-item-template.md

@@ -0,0 +1,626 @@
+# 质检检测项目模版 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:** 新增租户级「检测项目模版」主数据(CRUD + 启停),并在质检工单「配置检测」弹窗中支持选用模版覆盖当前批次项目、以及将当前勾选存成新模版。
+
+**Architecture:** 单表 `biz_qc_inspection_item_template` 存模版名与两组逗号分隔项目 ID;后端 CRUD 挂在 `app/api/qc.py`;web-admin 在「质检基础信息」下新增标准列表页;`QcWorkOrderConfigureBatches` 每批次 Tab 增加选用/另存操作条。套用仅改当前批次勾选,不改 configure 落库逻辑。
+
+**Tech Stack:** FastAPI + SQLAlchemy + MySQL 5.7;Vue 3 + Element Plus(web-admin);权限 `useMenuPerm` / `require_menu_code`
+
+**Spec:** `docs/superpowers/specs/2026-08-17-qc-inspection-item-template-design.md`
+
+---
+
+## File map
+
+| 文件 | 职责 |
+|------|------|
+| `docs/sql/alter_biz_qc_inspection_item_template.sql` | 建表 + 菜单 + 角色授权(幂等) |
+| `docs/sql/zkxw.sql` | 基线补充表结构与菜单 INSERT |
+| `待同步到生产的sql脚本` | 追加上线片段 |
+| `backend/app/models/models.py` | `BizQcInspectionItemTemplate` |
+| `backend/app/crud/qc_inspection_item_template.py` | CRUD + `_row_to_dict` + ID 解析/校验 |
+| `backend/tests/test_qc_inspection_item_template_helpers.py` | 纯函数/校验单测 |
+| `backend/app/api/qc.py` | REST 路由 |
+| `web-admin/src/api/qc-inspection-item-template.ts` | 前端 API |
+| `web-admin/src/views/quality-inspection/inspection-item-template/index.vue` | 主数据列表 + 表单弹窗 |
+| `web-admin/src/router/modules/sqxp.ts` | 静态路由 |
+| `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderConfigureBatches.vue` | 选用/存成模版 |
+| `./scripts/update-graph.sh` | 实现后增量更新图谱 |
+
+**常量(全任务统一)**
+
+| 常量 | 值 |
+|------|-----|
+| 表名 | `biz_qc_inspection_item_template` |
+| C 菜单 id | `95300000000000000000000000000001` |
+| F list id | `95300000000000000000000000000002` |
+| F add id | `95300000000000000000000000000003` |
+| F edit id | `95300000000000000000000000000004` |
+| F remove id | `95300000000000000000000000000005` |
+| parent_id | `QCB00000000000000000000000000001` |
+| path | `inspection-item-template` |
+| component | `/quality-inspection/inspection-item-template/index` |
+| order_num(C) | `5` |
+| perms list | `biz:qcInspectionItemTemplate:list` |
+| perms add | `biz:qcInspectionItemTemplate:add` |
+| perms edit | `biz:qcInspectionItemTemplate:edit` |
+| perms remove | `biz:qcInspectionItemTemplate:remove` |
+| Hash | `/#/quality-group/quality-base/inspection-item-template` |
+| API 前缀 | `/web/admin/qc-inspection-item-templates` |
+
+**可用性约定(相对设计的小幅落地):** 启用列表 `GET .../active` 使用 `require_any_menu_code(list, biz:qcWorkOrder:list)`,以便仅有工单权限的操作员可套用;前端「选用」在 `list` **或** `biz:qcWorkOrder:list` 时显示。「存成新模版」仍严格要求 `add`。SQL 授权仍建议给已有质检工单或质检基础信息子菜单的角色一并授予本 C + 全部 F。
+
+---
+
+### Task 1: SQL 建表 + 菜单授权
+
+**Files:**
+- Create: `docs/sql/alter_biz_qc_inspection_item_template.sql`
+- Modify: `待同步到生产的sql脚本`(文末追加本脚本文件名说明)
+
+- [ ] **Step 1: 写幂等迁移脚本**
+
+创建 `docs/sql/alter_biz_qc_inspection_item_template.sql`,内容需包含:
+
+1. `CREATE TABLE IF NOT EXISTS biz_qc_inspection_item_template`(字段与设计 §3 一致;`lab_item_ids` / `third_party_item_ids` VARCHAR(2000) NOT NULL DEFAULT `''`;索引 `idx_qc_iit_tenant_status (tenant_no, del_flag, status)`)
+2. 插入/更新 C 菜单与 4 个 F 按钮(id 用上表常量;C 的 `perms` = list)
+3. 角色授权:对「已拥有 QCB 下任一有效子菜单」或「已拥有质检工单菜单 `70000000000000000000000000000007`」的角色,幂等插入本 C + 全部 F 到 `sys_role_menu`;并补授祖先 `QCB` / 质检 TOP(写法对照 `docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql`,注意避免 MySQL 1093:用临时表或派生表,勿在同一语句中对 `sys_role_menu` 边读边写)
+
+建表 DDL 核心:
+
+```sql
+CREATE TABLE IF NOT EXISTS `biz_qc_inspection_item_template` (
+  `id` varchar(64) NOT NULL COMMENT '主键ID',
+  `template_name` varchar(200) NOT NULL COMMENT '模版名称',
+  `lab_item_ids` varchar(2000) NOT NULL DEFAULT '' COMMENT '实验室项目ID逗号分隔',
+  `third_party_item_ids` varchar(2000) NOT NULL DEFAULT '' COMMENT '第三方项目ID逗号分隔',
+  `status` char(1) NOT NULL DEFAULT '1' COMMENT '1启用 0停用',
+  `tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
+  `order_num` int(11) DEFAULT NULL COMMENT '排序号',
+  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
+  `create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
+  `create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  `update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
+  `update_time` datetime NOT NULL COMMENT '更新时间',
+  `del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_qc_iit_tenant_status` (`tenant_no`,`del_flag`,`status`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='质检检测项目模版';
+```
+
+- [ ] **Step 2: 在待同步清单登记**
+
+在 `待同步到生产的sql脚本` 追加一行指向本 alter 文件。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/sql/alter_biz_qc_inspection_item_template.sql 待同步到生产的sql脚本
+git commit -m "$(cat <<'EOF'
+docs(sql): add QC inspection item template table and menu.
+
+EOF
+)"
+```
+
+---
+
+### Task 2: SQLAlchemy 模型
+
+**Files:**
+- Modify: `backend/app/models/models.py`(紧挨 `BizQcLabTestItem` 之后)
+
+- [ ] **Step 1: 新增模型类**
+
+```python
+class BizQcInspectionItemTemplate(Base):
+    """质检检测项目模版 biz_qc_inspection_item_template"""
+
+    __tablename__ = "biz_qc_inspection_item_template"
+
+    id = Column(String(64), primary_key=True, comment="主键ID")
+    template_name = Column(String(200), nullable=False, comment="模版名称")
+    lab_item_ids = Column(String(2000), nullable=False, default="", comment="实验室项目ID逗号分隔")
+    third_party_item_ids = Column(String(2000), nullable=False, default="", comment="第三方项目ID逗号分隔")
+    status = Column(String(1), nullable=False, default="1", comment="1启用 0停用")
+    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: Commit**
+
+```bash
+git add backend/app/models/models.py
+git commit -m "$(cat <<'EOF'
+feat(backend): add BizQcInspectionItemTemplate model.
+
+EOF
+)"
+```
+
+---
+
+### Task 3: 纯函数单测(TDD)+ CRUD 模块
+
+**Files:**
+- Create: `backend/tests/test_qc_inspection_item_template_helpers.py`
+- Create: `backend/app/crud/qc_inspection_item_template.py`
+
+- [ ] **Step 1: 写失败测试**
+
+```python
+# backend/tests/test_qc_inspection_item_template_helpers.py
+import unittest
+
+from app.crud import qc_inspection_item_template as svc
+
+
+class QcInspectionItemTemplateHelpersTests(unittest.TestCase):
+    def test_parse_ids_splits_and_dedupes(self):
+        self.assertEqual(svc.parse_id_csv("a,b, a,,c"), ["a", "b", "c"])
+
+    def test_join_ids(self):
+        self.assertEqual(svc.join_id_list(["a", "b"]), "a,b")
+        self.assertEqual(svc.join_id_list([]), "")
+
+    def test_filter_active_ids(self):
+        active = {"1", "2"}
+        kept, skipped = svc.filter_active_ids(["1", "9", "2", "9"], active)
+        self.assertEqual(kept, ["1", "2"])
+        self.assertEqual(skipped, 2)
+
+    def test_require_at_least_one_item(self):
+        with self.assertRaises(ValueError):
+            svc.require_nonempty_items([], [])
+        svc.require_nonempty_items(["1"], [])
+```
+
+- [ ] **Step 2: 跑测试确认失败**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_qc_inspection_item_template_helpers.py -v
+```
+
+Expected: FAIL(模块或函数不存在)。
+
+- [ ] **Step 3: 实现 helpers + CRUD**
+
+在 `backend/app/crud/qc_inspection_item_template.py` 实现:
+
+```python
+def parse_id_csv(raw: Optional[str]) -> List[str]:
+    ...
+
+def join_id_list(ids: List[str]) -> str:
+    ...
+
+def filter_active_ids(ids: List[str], active_set: Set[str]) -> Tuple[List[str], int]:
+    """返回 (保留且去重保序的 id 列表, 跳过次数含重复无效)"""
+    ...
+
+def require_nonempty_items(lab_ids: List[str], third_ids: List[str]) -> None:
+    if not lab_ids and not third_ids:
+        raise ValueError("请至少选择一个检测项目")
+```
+
+以及完整 CRUD(对齐 `qc_lab_test_item.py` 风格):
+
+- `_row_to_dict`:返回 `id, templateName, labItemIds: List[str], thirdPartyItemIds: List[str], labItemCount, thirdPartyItemCount, status, orderNum, remark, createTime, updateTime`
+- `template_name_exists(db, name, tenant_no, exclude_id=None)`
+- `list_templates`:keyword 模糊 `template_name`;status;`create_time DESC`;`apply_data_scope`
+- `list_active_templates`:`status=="1"`,`create_time DESC`
+- `get_template` / `create_template` / `update_template` / `soft_delete_template`
+- `create`/`update` 内:`require_nonempty_items`;名称唯一;调用 `_assert_item_ids_exist(db, tenant_no, lab_ids, third_ids)`:查 `BizQcLabTestItem` / `BizQcThirdPartyItem` 的 `del_flag==0` 且租户匹配,缺一则 `ValueError("存在无效的检测项目")`(**允许 status=0 写入模版**,套用时再过滤启用)
+
+- [ ] **Step 4: 跑测试通过**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_qc_inspection_item_template_helpers.py -v
+```
+
+Expected: PASS。
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/crud/qc_inspection_item_template.py backend/tests/test_qc_inspection_item_template_helpers.py
+git commit -m "$(cat <<'EOF'
+feat(backend): add QC inspection item template CRUD helpers.
+
+EOF
+)"
+```
+
+---
+
+### Task 4: FastAPI 路由
+
+**Files:**
+- Modify: `backend/app/api/qc.py`
+
+- [ ] **Step 1: 增加 import 与 Body**
+
+```python
+from app.crud import qc_inspection_item_template as iit_svc
+
+class InspectionItemTemplateBody(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+    template_name: str = Field(..., alias="templateName")
+    lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
+    third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
+    status: Optional[str] = "1"
+    order_num: Optional[int] = Field(None, alias="orderNum")
+    remark: Optional[str] = None
+
+class InspectionItemTemplateUpdateBody(InspectionItemTemplateBody):
+    template_name: Optional[str] = Field(None, alias="templateName")
+```
+
+(若项目 Pydantic 写法与邻近 `LabTestItemBody` 不一致,以 `qc.py` 现有 Body 为准复制改字段。)
+
+- [ ] **Step 2: 注册路由**
+
+| Method | Path | Guard |
+|--------|------|-------|
+| GET | `/qc-inspection-item-templates` | `list` |
+| GET | `/qc-inspection-item-templates/active` | `require_any_menu_code(list, "biz:qcWorkOrder:list")` |
+| GET | `/qc-inspection-item-templates/{id}` | `list` |
+| POST | `/qc-inspection-item-templates` | `add` |
+| PUT | `/qc-inspection-item-templates/{id}` | `edit` |
+| DELETE | `/qc-inspection-item-templates/{id}` | `remove` |
+
+实现模式照抄 lab-test-items:`_get_tenant_no`、`success_response` / `error_response`、`PermissionError`→403、`ValueError`→400。
+
+**注意:** 将 `/active` 写在 `/{id}` **之前**,避免被路径参数吞掉。
+
+- [ ] **Step 3: 冒烟(可选,需本地库已执行 Task1 SQL)**
+
+```bash
+# 启动后端后用已登录 token 调 active;无 token 可跳过本步
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add backend/app/api/qc.py
+git commit -m "$(cat <<'EOF'
+feat(api): expose QC inspection item template endpoints.
+
+EOF
+)"
+```
+
+---
+
+### Task 5: 前端 API 模块
+
+**Files:**
+- Create: `web-admin/src/api/qc-inspection-item-template.ts`
+
+- [ ] **Step 1: 编写 API**
+
+```typescript
+import request from '@/utils/http'
+
+type PageList<T> = {
+  list: T[]
+  total: number
+  page: number
+  pageSize: number
+  totalPages: number
+}
+
+export type QcInspectionItemTemplate = {
+  id: string
+  templateName: string
+  labItemIds: string[]
+  thirdPartyItemIds: string[]
+  labItemCount?: number
+  thirdPartyItemCount?: number
+  status: string
+  orderNum?: number
+  remark?: string
+  createTime?: string
+  updateTime?: string
+}
+
+export type QcInspectionItemTemplatePayload = {
+  templateName: string
+  labItemIds: string[]
+  thirdPartyItemIds: string[]
+  status?: string
+  orderNum?: number
+  remark?: string
+}
+
+export function fetchQcInspectionItemTemplateList(params?: {
+  page?: number
+  pageSize?: number
+  keyword?: string
+  status?: string
+}) {
+  return request.get<PageList<QcInspectionItemTemplate>>({
+    url: '/web/admin/qc-inspection-item-templates',
+    params
+  })
+}
+
+export function fetchQcInspectionItemTemplatesActive() {
+  return request.get<QcInspectionItemTemplate[]>({
+    url: '/web/admin/qc-inspection-item-templates/active'
+  })
+}
+
+export function fetchQcInspectionItemTemplateDetail(id: string) {
+  return request.get<QcInspectionItemTemplate>({
+    url: `/web/admin/qc-inspection-item-templates/${id}`
+  })
+}
+
+export function fetchCreateQcInspectionItemTemplate(data: QcInspectionItemTemplatePayload) {
+  return request.post<QcInspectionItemTemplate>({
+    url: '/web/admin/qc-inspection-item-templates',
+    data
+  })
+}
+
+export function fetchUpdateQcInspectionItemTemplate(
+  id: string,
+  data: Partial<QcInspectionItemTemplatePayload>
+) {
+  return request.put<QcInspectionItemTemplate>({
+    url: `/web/admin/qc-inspection-item-templates/${id}`,
+    data
+  })
+}
+
+export function fetchDeleteQcInspectionItemTemplate(id: string) {
+  return request.del({ url: `/web/admin/qc-inspection-item-templates/${id}` })
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add web-admin/src/api/qc-inspection-item-template.ts
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add QC inspection item template API module.
+
+EOF
+)"
+```
+
+---
+
+### Task 6: 主数据列表页
+
+**Files:**
+- Create: `web-admin/src/views/quality-inspection/inspection-item-template/index.vue`
+
+- [ ] **Step 1: 以环境检测项列表为骨架新建页面**
+
+复制结构自 `web-admin/src/views/quality-inspection/env-detect-item/index.vue`,再按下列差异改:
+
+1. 标题:`检测项目模版列表`(`h2.title-bar-heading`)
+2. **去掉** selection 列与批量删除(设计首版无多选)
+3. 工具栏仅「新增模版」主按钮(`hasMenuPerm('biz:qcInspectionItemTemplate:add')` 时显示)
+4. 筛选:模版名称 keyword、状态
+5. 列:模版名称、实验室项目数、第三方项目数、状态 `el-tag`(启用 success / 停用 info)、创建时间、操作(min-width ≥ 260)
+6. 操作:编辑(edit)、启用/停用(edit,调 update 改 status)、删除(remove);用 `useMenuPerm().hasMenuPerm` 控制显隐
+7. 新增/编辑弹窗:`dialog-form-layout` + `label-position="top"` + `append-to-body` + `modal-class="protocol-dialog-mask"`  
+   - 字段:模版名称;状态(新增默认启用)  
+   - 项目勾选:加载 `fetchQcLabTestItemsActive` + `fetchQcThirdPartyItemsActive`,布局可简化为两组 `el-checkbox-group`(实验室一组;第三方按公司分组),与配置检测语义一致即可  
+   - 校验:名称必填;`labItemIds.length + thirdPartyItemIds.length >= 1`  
+   - footer:Close/Check 图标 + 取消/确定
+8. `defineOptions({ name: 'QualityInspectionInspectionItemTemplate' })`
+9. 列表请求 `fetchQcInspectionItemTemplateList`;默认不展示 orderNum
+
+- [ ] **Step 2: 本地打开页面目视**
+
+Hash:`/#/quality-group/quality-base/inspection-item-template`(路由 Task 7 完成后)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add web-admin/src/views/quality-inspection/inspection-item-template/index.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add inspection item template list page.
+
+EOF
+)"
+```
+
+---
+
+### Task 7: 静态路由注册
+
+**Files:**
+- Modify: `web-admin/src/router/modules/sqxp.ts`(`QualityBase.children`)
+
+- [ ] **Step 1: 在质检项目管理之后插入路由**
+
+定位 `name: 'QualityInspectionLabTestItem'`,在其后插入:
+
+```ts
+          {
+            id: '95300000000000000000000000000001',
+            name: 'QualityInspectionInspectionItemTemplate',
+            path: 'inspection-item-template',
+            component: '/quality-inspection/inspection-item-template/index',
+            meta: {
+              title: '检测项目模版',
+              keepAlive: true,
+              menuCode: 'biz:qcInspectionItemTemplate:list'
+            }
+          },
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add web-admin/src/router/modules/sqxp.ts
+git commit -m "$(cat <<'EOF'
+feat(web-admin): register inspection item template route.
+
+EOF
+)"
+```
+
+---
+
+### Task 8: 配置检测弹窗 — 选用 / 存成模版
+
+**Files:**
+- Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderConfigureBatches.vue`
+
+- [ ] **Step 1: 在每个批次 Tab 的 head-row 与「已选项目」之间插入模版条**
+
+模板片段(放在 `qc-batch-config__head-row` 之后):
+
+```vue
+        <div
+          v-if="canUseTemplate || canSaveTemplate"
+          class="qc-batch-config__template-bar"
+        >
+          <el-select
+            v-if="canUseTemplate"
+            :model-value="selectedTemplateIdByIndex[index] || ''"
+            clearable
+            filterable
+            placeholder="选用检测项目模版"
+            style="min-width: 240px; flex: 1"
+            @change="(val) => onTemplateSelect(batch, index, val)"
+          >
+            <el-option
+              v-for="t in templateOptions"
+              :key="t.id"
+              :label="t.templateName"
+              :value="t.id"
+            />
+          </el-select>
+          <el-button
+            v-if="canSaveTemplate"
+            class="btn-ghost"
+            @click="saveBatchAsTemplate(batch)"
+          >
+            <el-icon><DocumentAdd /></el-icon>存成新模版
+          </el-button>
+        </div>
+```
+
+- [ ] **Step 2: script 逻辑**
+
+1. import:`DocumentAdd`;`fetchQcInspectionItemTemplatesActive`、`fetchCreateQcInspectionItemTemplate`;`useMenuPerm`;`ElMessageBox`(若未自动导入则按项目习惯)
+2. `const { hasMenuPerm } = useMenuPerm()`
+3. `canUseTemplate = computed(() => hasMenuPerm('biz:qcInspectionItemTemplate:list') || hasMenuPerm('biz:qcWorkOrder:list'))`
+4. `canSaveTemplate = computed(() => hasMenuPerm('biz:qcInspectionItemTemplate:add'))`
+5. `templateOptions` ref;`onMounted`/`watch` 在 `canUseTemplate` 时加载 active 列表
+6. `selectedTemplateIdByIndex`:`Record<number, string>`
+7. `onTemplateSelect(batch, index, templateId)`:
+   - 空值:清空选中,return
+   - 找模版;用当前 `labOptions`(均视为启用列表)与 `thirdOptions` 的 id 集合做 `filter`:只保留仍在 options 中的 id
+   - `skipped = 模版原 id 数 - 保留数`
+   - 若保留数为 0:`ElMessage.warning`,清空下拉,不改 batch
+   - 若 `batch.labItemIds.length || batch.thirdPartyItemIds.length`:confirm「将覆盖当前批次已选检测项目,是否继续?」;取消则还原下拉为空
+   - 确认后:`batch.labItemIds = keptLab`;`batch.thirdPartyItemIds = keptThird`;`syncBatchFlags(batch)`;若 skipped>0 再 warning
+8. `saveBatchAsTemplate(batch)`:无项目则 warning;`ElMessageBox.prompt` 输入名称;调 create;成功 toast + `reloadTemplates()`;不必强制改下拉选中
+
+- [ ] **Step 3: 样式**
+
+```scss
+.qc-batch-config__template-bar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  margin-bottom: 12px;
+}
+```
+
+- [ ] **Step 4: 手动验收**
+
+1. 配置检测:选模版 → 当前批次勾选变化;其它 Tab 不变  
+2. 已有勾选时出现确认框  
+3. 存成新模版后可再次选用  
+4. 无 add 权限时无「存成新模版」
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderConfigureBatches.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): apply and save QC item templates in configure dialog.
+
+EOF
+)"
+```
+
+---
+
+### Task 9: 基线 SQL 同步 + 图谱
+
+**Files:**
+- Modify: `docs/sql/zkxw.sql`(追加 CREATE TABLE 与菜单 INSERT,与 alter 一致)
+- Run: `./scripts/update-graph.sh all`(或分别 backend / frontend)
+
+- [ ] **Step 1: 将表结构与菜单写入 `zkxw.sql`**
+
+在合适位置插入 `CREATE TABLE` 与 5 条 `sys_menu` INSERT(id/perms 与常量一致)。不必导出全库;手工补齐即可。
+
+- [ ] **Step 2: 更新 graphify**
+
+```bash
+./scripts/update-graph.sh all
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/sql/zkxw.sql backend/graphify-out web-admin/graphify-out
+git commit -m "$(cat <<'EOF'
+chore: sync baseline SQL and graphify for item templates.
+
+EOF
+)"
+```
+
+---
+
+### Task 10: 验收清单(实现者勾选)
+
+- [ ] 执行 `alter_biz_qc_inspection_item_template.sql` 后菜单「检测项目模版」可见
+- [ ] 列表可新增/编辑/启停/删除模版
+- [ ] 配置检测选用仅覆盖当前批次;有勾选时二次确认
+- [ ] 存成新模版写入主数据且可再选
+- [ ] 无 list/工单权限时不显示选用;无 add 不显示存成
+- [ ] 模版含已删/停用项目时套用跳过并提示;全无效不覆盖
+- [ ] 确认配置后任务生成与改前一致
+- [ ] `pytest tests/test_qc_inspection_item_template_helpers.py -v` 通过
+
+---
+
+## Spec coverage(自评)
+
+| 设计项 | 任务 |
+|--------|------|
+| 表 + 租户字段 | Task 1–2 |
+| 主数据 CRUD/启停/菜单权限 | Task 1, 3–7 |
+| 配置弹窗选用覆盖 + 确认 | Task 8 |
+| 存成新模版 | Task 8 |
+| 失效项目跳过 | Task 3 helpers + Task 8 |
+| 不改 configure 落库 | Task 8 仅改勾选 |
+| zkxw / 待同步 / graphify | Task 1, 9 |
+
+## Placeholder scan
+
+无 TBD;开放细节(启停并入 update、VARCHAR 长度)已在实现中按「update 改 status」「VARCHAR(2000)」落地。