From d2792174b5426fd002f65d13583d0b802056849c Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Tue, 18 Aug 2026 14:40:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/env_detect.py | 2 +- backend/app/crud/env_detect_record.py | 2 +- .../app/crud/qc_inbound_lab_storage_sort.py | 81 +++ backend/app/crud/qc_task.py | 18 +- backend/app/crud/qc_work_order.py | 12 +- backend/app/crud/storage_facility.py | 42 +- .../tests/test_qc_inbound_lab_storage_sort.py | 45 ++ docs/sql/alter-sys-user-wx-mp-openid.sql | 13 - .../alter_biz_qc_inspection_item_template.sql | 322 --------- ...menu_add_production_pre_transit_sample.sql | 166 ----- .../grant_wechat_notify_menu_to_all_roles.sql | 57 -- docs/sql/mobile_menu_oa_start.sql | 39 -- docs/sql/mobile_menu_oa_task_centers.sql | 169 ----- docs/sql/mobile_menu_oa_wechat_notify.sql | 40 -- ...-08-17-qc-inbound-lab-storage-code-sort.md | 614 ++++++++++++++++++ ...7-sample-reception-protocol-fee-trigger.md | 26 + ...7-qc-batch-name-lab-storage-code-design.md | 38 ++ ...qc-inbound-lab-storage-code-sort-design.md | 78 +++ ...e-reception-protocol-fee-trigger-design.md | 18 + .../static/menu-icons/start-approval.png | Bin 3663 -> 9255 bytes web-admin/src/styles/_route-config.scss | 25 +- .../src/utils/production-step-output.test.ts | 23 + web-admin/src/utils/production-step-output.ts | 9 + .../src/utils/qc-inbound-lab-storage-sort.ts | 40 ++ web-admin/src/utils/qc-inspection-batch.ts | 37 +- web-admin/src/utils/qc-mine-task-groups.ts | 14 + web-admin/src/utils/qc-work-order-groups.ts | 8 +- .../qc-work-order-inbound-sample-cards.ts | 33 +- .../operations/storage-facility/index.vue | 3 +- .../views/production/cell-product/index.vue | 12 +- .../DispatchTaskDialog.vue | 17 +- .../production-work-order/StepDetailPanel.vue | 8 +- .../TaskOutputProductDialog.vue | 12 +- .../useProductionWorkOrderTabPage.ts | 7 +- .../tabs/ProductionWorkOrderTabPanel.vue | 4 - .../useTaskStepEditor.ts | 14 +- .../views/production/waste-output/index.vue | 30 - .../inspection-item-template/index.vue | 465 ++++++++++++- .../QcWorkOrderConfigureBatches.vue | 150 +++-- .../QcWorkOrderProtocolGroup.vue | 15 + .../qc-work-order/QcWorkOrderSampleGroup.vue | 20 + .../qc-work-order/QcWorkOrderSingleCard.vue | 34 +- .../qc-work-order/QcWorkOrderTaskSubList.vue | 1 - .../composables/useQcWorkOrderTabPage.ts | 11 +- .../qc-work-order/index.vue | 14 +- .../tabs/QcWorkOrderTabPanel.vue | 4 - .../SampleReceptionCardList.vue | 98 +-- wx-logo.jpeg | Bin 148420 -> 0 bytes 审批-发起审批.svg | 1 - 开启通知.png | Bin 4206 -> 0 bytes 待办中心 (1).png | Bin 4235 -> 0 bytes 洁净服.txt | 10 - 生产物料.txt | 253 -------- 生产设备.txt | 53 -- 质检物料.txt | 75 --- 55 files changed, 1863 insertions(+), 1419 deletions(-) create mode 100644 backend/app/crud/qc_inbound_lab_storage_sort.py create mode 100644 backend/tests/test_qc_inbound_lab_storage_sort.py delete mode 100644 docs/sql/alter-sys-user-wx-mp-openid.sql delete mode 100644 docs/sql/alter_biz_qc_inspection_item_template.sql delete mode 100644 docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql delete mode 100644 docs/sql/grant_wechat_notify_menu_to_all_roles.sql delete mode 100644 docs/sql/mobile_menu_oa_start.sql delete mode 100644 docs/sql/mobile_menu_oa_task_centers.sql delete mode 100644 docs/sql/mobile_menu_oa_wechat_notify.sql create mode 100644 docs/superpowers/plans/2026-08-17-qc-inbound-lab-storage-code-sort.md create mode 100644 docs/superpowers/plans/2026-08-17-sample-reception-protocol-fee-trigger.md create mode 100644 docs/superpowers/specs/2026-08-17-qc-batch-name-lab-storage-code-design.md create mode 100644 docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md create mode 100644 docs/superpowers/specs/2026-08-17-sample-reception-protocol-fee-trigger-design.md create mode 100644 web-admin/src/utils/production-step-output.test.ts create mode 100644 web-admin/src/utils/production-step-output.ts create mode 100644 web-admin/src/utils/qc-inbound-lab-storage-sort.ts delete mode 100644 wx-logo.jpeg delete mode 100644 审批-发起审批.svg delete mode 100644 开启通知.png delete mode 100644 待办中心 (1).png delete mode 100644 洁净服.txt delete mode 100644 生产物料.txt delete mode 100644 生产设备.txt delete mode 100644 质检物料.txt diff --git a/backend/app/api/env_detect.py b/backend/app/api/env_detect.py index 5368d25f..97a54c04 100644 --- a/backend/app/api/env_detect.py +++ b/backend/app/api/env_detect.py @@ -319,7 +319,7 @@ async def list_env_detect_records( query = apply_data_scope(query, BizEnvDetectRecord, user, db, tenant_no) total = query.count() items = ( - query.order_by(BizEnvDetectRecord.create_time.desc()) + query.order_by(BizEnvDetectRecord.detect_time.desc()) .offset((page - 1) * pageSize) .limit(pageSize) .all() diff --git a/backend/app/crud/env_detect_record.py b/backend/app/crud/env_detect_record.py index 94b84095..7a125f76 100644 --- a/backend/app/crud/env_detect_record.py +++ b/backend/app/crud/env_detect_record.py @@ -170,7 +170,7 @@ def list_env_detect_records( page_size = min(max(1, page_size), 100) rows = ( _filtered() - .order_by(BizEnvDetectRecord.create_time.desc()) + .order_by(BizEnvDetectRecord.detect_time.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() diff --git a/backend/app/crud/qc_inbound_lab_storage_sort.py b/backend/app/crud/qc_inbound_lab_storage_sort.py new file mode 100644 index 00000000..76f0a931 --- /dev/null +++ b/backend/app/crud/qc_inbound_lab_storage_sort.py @@ -0,0 +1,81 @@ +"""入库质检列表:按实验室入库编码排序的辅助逻辑。""" +from __future__ import annotations + +from typing import Optional + +from sqlalchemy import and_, func, literal, select +from sqlalchemy.sql import ColumnElement + +from app.core.db_collation import eq_unicode, unicode_ci +from app.models.models import BizQcWorkOrder, BizSampleReception + +_INBOUND_LAB_SORT_SOURCES = frozenset({"cell_storage", "research_donation"}) + + +def uses_inbound_lab_storage_list_sort(reception_source: Optional[str]) -> bool: + return (reception_source or "").strip() in _INBOUND_LAB_SORT_SOURCES + + +def compare_lab_storage_then_create_time_desc( + code_a: Optional[str], + time_a: Optional[str], + code_b: Optional[str], + time_b: Optional[str], +) -> int: + """列表序比较:编码 DESC,同编码时间 DESC。返回负值表示 a 应排在 b 前。""" + ca = (code_a or "").strip() + cb = (code_b or "").strip() + if ca != cb: + # DESC: 较大字符串在前 + return -1 if ca > cb else 1 + ta = (time_a or "").strip() + tb = (time_b or "").strip() + if ta != tb: + return -1 if ta > tb else 1 + return 0 + + +def lab_storage_code_expr_for_work_order() -> ColumnElement: + """相关子查询:优先 reception_id,否则同租户 sample_code 最新一条接收记录的编码。 + + 子查询结果与空串字面量统一 utf8mb4_unicode_ci,避免 ORDER BY COALESCE + 触发 Illegal mix of collations(unicode_ci vs general_ci)。 + """ + by_id = ( + select(unicode_ci(BizSampleReception.lab_storage_code)) + .where( + and_( + eq_unicode(BizSampleReception.id, BizQcWorkOrder.reception_id), + BizSampleReception.del_flag == 0, + ) + ) + .correlate(BizQcWorkOrder) + .scalar_subquery() + ) + by_sample = ( + select(unicode_ci(BizSampleReception.lab_storage_code)) + .where( + and_( + eq_unicode(BizSampleReception.sample_code, BizQcWorkOrder.sample_code), + eq_unicode(BizSampleReception.tenant_no, BizQcWorkOrder.tenant_no), + BizSampleReception.del_flag == 0, + BizQcWorkOrder.sample_code.isnot(None), + unicode_ci(BizQcWorkOrder.sample_code) != "", + ) + ) + .order_by(BizSampleReception.create_time.desc()) + .limit(1) + .correlate(BizQcWorkOrder) + .scalar_subquery() + ) + empty = unicode_ci(literal("")) + return func.coalesce(by_id, by_sample, empty) + + +def inbound_lab_storage_order_by_clauses(): + """供 Query.order_by(*clauses) 使用。""" + code = lab_storage_code_expr_for_work_order() + return ( + code.desc(), + BizQcWorkOrder.create_time.desc(), + ) diff --git a/backend/app/crud/qc_task.py b/backend/app/crud/qc_task.py index 45abb090..1369e23d 100644 --- a/backend/app/crud/qc_task.py +++ b/backend/app/crud/qc_task.py @@ -24,6 +24,10 @@ from app.crud.qc_work_order import ( from app.core.db_collation import eq_unicode, unicode_ci from app.utils.sample_special import apply_special_to_item_from_samples from app.crud import qc_task_assignee as assignee_svc +from app.crud.qc_inbound_lab_storage_sort import ( + inbound_lab_storage_order_by_clauses, + uses_inbound_lab_storage_list_sort, +) from app.models.models import ( BizAttachment, BizCollectionProtocol, @@ -633,13 +637,21 @@ def list_mine_tasks( total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) - rows = ( - _filtered() - .order_by( + q_page = _filtered() + if uses_inbound_lab_storage_list_sort(reception_source): + q_page = q_page.order_by( + *inbound_lab_storage_order_by_clauses(), + BizQcWorkOrder.work_order_no.desc(), + BizQcTask.task_no.asc(), + ) + else: + q_page = q_page.order_by( BizQcTask.create_time.desc(), BizQcWorkOrder.work_order_no.desc(), BizQcTask.task_no.asc(), ) + rows = ( + q_page .offset((page - 1) * page_size) .limit(page_size) .all() diff --git a/backend/app/crud/qc_work_order.py b/backend/app/crud/qc_work_order.py index 1c55ac48..15bdd91e 100644 --- a/backend/app/crud/qc_work_order.py +++ b/backend/app/crud/qc_work_order.py @@ -16,6 +16,10 @@ from app.crud import qc_task as task_svc from app.crud import qc_task_assignee as assignee_svc from app.crud import qc_third_party_item as third_item_svc from app.crud.qc_third_party_company import get_company +from app.crud.qc_inbound_lab_storage_sort import ( + inbound_lab_storage_order_by_clauses, + uses_inbound_lab_storage_list_sort, +) from app.crud.collection_protocol import urls_to_attachment_list from app.utils.sample_special import apply_special_to_item_from_samples from app.crud import sample_reception as sample_reception_svc @@ -1706,9 +1710,13 @@ def list_work_orders( total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) + q_page = _filtered() + if uses_inbound_lab_storage_list_sort(reception_source): + q_page = q_page.order_by(*inbound_lab_storage_order_by_clauses()) + else: + q_page = q_page.order_by(BizQcWorkOrder.create_time.desc()) rows = ( - _filtered() - .order_by(BizQcWorkOrder.create_time.desc()) + q_page .offset((page - 1) * page_size) .limit(page_size) .all() diff --git a/backend/app/crud/storage_facility.py b/backend/app/crud/storage_facility.py index cc1be4b9..80020221 100644 --- a/backend/app/crud/storage_facility.py +++ b/backend/app/crud/storage_facility.py @@ -771,6 +771,38 @@ def _apply_slot_select_query_filters( return filtered.filter(BizStorageFacilitySlot.id.in_(matched_ids)) +def _load_slot_select_include_rows(query, include_slot_ids: Optional[List[str]]): + """按传入顺序加载需强制保留的储物格(编辑态当前选中格)。""" + include_ids = [ + str(x).strip() + for x in (include_slot_ids or []) + if str(x).strip() + ] + if not include_ids: + return [] + rows_by_id = { + row.id: row + for row in query.filter(BizStorageFacilitySlot.id.in_(include_ids)).all() + } + return [rows_by_id[sid] for sid in include_ids if sid in rows_by_id] + + +def _merge_slot_select_rows_with_includes( + page_rows: List[BizStorageFacilitySlot], + include_rows: List[BizStorageFacilitySlot], +) -> List[BizStorageFacilitySlot]: + """将 include 格置顶合并进当前页,避免分页把当前选中格挤出导致下拉显示 id。""" + merged: List[BizStorageFacilitySlot] = [] + seen: set[str] = set() + for row in list(include_rows or []) + list(page_rows or []): + sid = row.id + if not sid or sid in seen: + continue + seen.add(sid) + merged.append(row) + return merged + + def list_slot_select_options( db: Session, *, @@ -789,6 +821,7 @@ def list_slot_select_options( 1, ) need_count = page * page_size + include_rows = _load_slot_select_include_rows(query, include_slot_ids) filtered = _apply_slot_select_base_filters( query, @@ -811,11 +844,13 @@ def list_slot_select_options( ) total = len(matched_ids) page_ids = matched_ids[(page - 1) * page_size : page * page_size] - if not page_ids: - return [], total rows_by_id = { row.id: row - for row in filtered.filter(BizStorageFacilitySlot.id.in_(page_ids)).all() + for row in ( + filtered.filter(BizStorageFacilitySlot.id.in_(page_ids)).all() + if page_ids + else [] + ) } rows = [rows_by_id[sid] for sid in page_ids if sid in rows_by_id] else: @@ -832,6 +867,7 @@ def list_slot_select_options( .all() ) + rows = _merge_slot_select_rows_with_includes(rows, include_rows) if not rows: return [], int(total) diff --git a/backend/tests/test_qc_inbound_lab_storage_sort.py b/backend/tests/test_qc_inbound_lab_storage_sort.py new file mode 100644 index 00000000..9763fe5c --- /dev/null +++ b/backend/tests/test_qc_inbound_lab_storage_sort.py @@ -0,0 +1,45 @@ +"""入库质检列表:实验室入库编码排序语义。""" +import unittest + +from app.crud.qc_inbound_lab_storage_sort import ( + compare_lab_storage_then_create_time_desc, + uses_inbound_lab_storage_list_sort, +) + + +class TestUsesInboundLabStorageListSort(unittest.TestCase): + def test_cell_storage_and_research_donation(self): + self.assertTrue(uses_inbound_lab_storage_list_sort("cell_storage")) + self.assertTrue(uses_inbound_lab_storage_list_sort("research_donation")) + + def test_other_sources_false(self): + self.assertFalse(uses_inbound_lab_storage_list_sort("")) + self.assertFalse(uses_inbound_lab_storage_list_sort(None)) + self.assertFalse(uses_inbound_lab_storage_list_sort("temporary")) + + +class TestCompareLabStorageThenCreateTimeDesc(unittest.TestCase): + def test_code_desc_empty_as_empty_string(self): + # '' < 'B0000001' lexicographically → DESC 时有编码在前 + self.assertEqual( + compare_lab_storage_then_create_time_desc("B0000001", "2026-01-01", "", "2026-12-01"), + -1, + ) + self.assertEqual( + compare_lab_storage_then_create_time_desc("", "2026-12-01", "B0000001", "2026-01-01"), + 1, + ) + + def test_same_code_newer_create_time_first(self): + self.assertEqual( + compare_lab_storage_then_create_time_desc( + "B1", "2026-08-02 10:00:00", "B1", "2026-08-01 10:00:00" + ), + -1, + ) + + def test_higher_code_first(self): + self.assertEqual( + compare_lab_storage_then_create_time_desc("B2", "t", "B1", "t"), + -1, + ) diff --git a/docs/sql/alter-sys-user-wx-mp-openid.sql b/docs/sql/alter-sys-user-wx-mp-openid.sql deleted file mode 100644 index c37296c1..00000000 --- a/docs/sql/alter-sys-user-wx-mp-openid.sql +++ /dev/null @@ -1,13 +0,0 @@ --- 员工微信服务号 openid / unionid 绑定字段(OA 审批通知) --- MySQL 5.7:唯一索引允许多个 NULL - -ALTER TABLE `sys_user` - ADD COLUMN `wx_mp_openid` varchar(64) DEFAULT NULL COMMENT '微信服务号 openid' AFTER `wx_openid`, - ADD COLUMN `wx_unionid` varchar(64) DEFAULT NULL COMMENT '微信开放平台 unionid' AFTER `wx_mp_openid`, - ADD COLUMN `wx_mp_bind_time` datetime DEFAULT NULL COMMENT '服务号绑定时间' AFTER `wx_unionid`; - -ALTER TABLE `sys_user` - ADD UNIQUE INDEX `uk_sys_user_wx_mp_openid` (`wx_mp_openid`); - -ALTER TABLE `sys_user` - ADD INDEX `idx_sys_user_wx_unionid` (`wx_unionid`); diff --git a/docs/sql/alter_biz_qc_inspection_item_template.sql b/docs/sql/alter_biz_qc_inspection_item_template.sql deleted file mode 100644 index 78900eac..00000000 --- a/docs/sql/alter_biz_qc_inspection_item_template.sql +++ /dev/null @@ -1,322 +0,0 @@ --- 质检:检测项目模版(建表 + C 菜单 + 4 F 按钮 + 角色授权) --- 幂等:可重复执行 --- 授权:已有【质检基础信息】QCB 下任一有效子菜单,或已有质检工单菜单的角色 --- → 授予 TOP + QCB + 本 C/F - -SET @tenant_no = CONVERT('zkxw' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @top_id = CONVERT('TOP00000000000000000000000000003' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @qcb_id = CONVERT('QCB00000000000000000000000000001' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @wo_menu = CONVERT('70000000000000000000000000000007' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_c = CONVERT('95300000000000000000000000000001' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_f_list = CONVERT('95300000000000000000000000000002' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_f_add = CONVERT('95300000000000000000000000000003' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_f_edit = CONVERT('95300000000000000000000000000004' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_f_remove = CONVERT('95300000000000000000000000000005' USING utf8mb4) COLLATE utf8mb4_unicode_ci; - --- 0) 建表 -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='质检检测项目模版'; - --- 1) C 菜单 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_c, - '检测项目模版', - @qcb_id, - 'C', - 'inspection-item-template', - '/quality-inspection/inspection-item-template/index', - 'biz:qcInspectionItemTemplate:list', - 1, '1', '1', @tenant_no, 5, - '质检基础信息:检测项目模版主数据', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_c -); - -UPDATE sys_menu -SET - menu_name = '检测项目模版', - parent_id = @qcb_id, - menu_type = 'C', - path = 'inspection-item-template', - component = '/quality-inspection/inspection-item-template/index', - perms = 'biz:qcInspectionItemTemplate:list', - visible = '1', - status = '1', - order_num = 5, - remark = '质检基础信息:检测项目模版主数据', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_c; - --- 2) F 按钮:list -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_f_list, - '检测项目模版查询', - @menu_c, - 'F', - '', - '', - 'biz:qcInspectionItemTemplate:list', - 1, '0', '1', @tenant_no, 1, - '检测项目模版列表查询', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_f_list -); - -UPDATE sys_menu -SET - menu_name = '检测项目模版查询', - parent_id = @menu_c, - menu_type = 'F', - path = '', - component = '', - perms = 'biz:qcInspectionItemTemplate:list', - visible = '0', - status = '1', - order_num = 1, - remark = '检测项目模版列表查询', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_f_list; - --- 2b) F 按钮:add -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_f_add, - '检测项目模版新增', - @menu_c, - 'F', - '', - '', - 'biz:qcInspectionItemTemplate:add', - 1, '0', '1', @tenant_no, 2, - '检测项目模版新增', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_f_add -); - -UPDATE sys_menu -SET - menu_name = '检测项目模版新增', - parent_id = @menu_c, - menu_type = 'F', - path = '', - component = '', - perms = 'biz:qcInspectionItemTemplate:add', - visible = '0', - status = '1', - order_num = 2, - remark = '检测项目模版新增', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_f_add; - --- 2c) F 按钮:edit -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_f_edit, - '检测项目模版修改', - @menu_c, - 'F', - '', - '', - 'biz:qcInspectionItemTemplate:edit', - 1, '0', '1', @tenant_no, 3, - '检测项目模版修改(含启停)', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_f_edit -); - -UPDATE sys_menu -SET - menu_name = '检测项目模版修改', - parent_id = @menu_c, - menu_type = 'F', - path = '', - component = '', - perms = 'biz:qcInspectionItemTemplate:edit', - visible = '0', - status = '1', - order_num = 3, - remark = '检测项目模版修改(含启停)', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_f_edit; - --- 2d) F 按钮:remove -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_f_remove, - '检测项目模版删除', - @menu_c, - 'F', - '', - '', - 'biz:qcInspectionItemTemplate:remove', - 1, '0', '1', @tenant_no, 4, - '检测项目模版删除', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_f_remove -); - -UPDATE sys_menu -SET - menu_name = '检测项目模版删除', - parent_id = @menu_c, - menu_type = 'F', - path = '', - component = '', - perms = 'biz:qcInspectionItemTemplate:remove', - visible = '0', - status = '1', - order_num = 4, - remark = '检测项目模版删除', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_f_remove; - --- 3) 目标角色:已有 QCB 下任一有效子菜单,或已有质检工单菜单 --- MySQL 5.7:UPDATE 目标表不可在同句 FROM/EXISTS 中直接引用自身,须用派生表 - --- 3a) 恢复软删的 TOP / QCB / C / F 授权 -UPDATE sys_role_menu rm -INNER JOIN sys_role r - ON r.id COLLATE utf8mb4_unicode_ci = rm.role_id COLLATE utf8mb4_unicode_ci - AND r.del_flag = 0 - AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no -INNER JOIN ( - SELECT DISTINCT rm2.role_id AS role_id - FROM sys_role_menu rm2 - INNER JOIN sys_menu m2 - ON m2.id COLLATE utf8mb4_unicode_ci = rm2.menu_id COLLATE utf8mb4_unicode_ci - AND m2.del_flag = 0 - AND ( - m2.parent_id COLLATE utf8mb4_unicode_ci = @qcb_id - OR m2.id COLLATE utf8mb4_unicode_ci = @wo_menu - ) - WHERE rm2.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm2.del_flag = 0 -) eligible - ON eligible.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci -SET - rm.del_flag = 0, - rm.update_time = NOW(), - rm.remark = 'grant qc inspection item template to QCB/WO roles' -WHERE rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm.del_flag = 1 - AND rm.menu_id COLLATE utf8mb4_unicode_ci IN ( - @top_id, @qcb_id, @menu_c, - @menu_f_list, @menu_f_add, @menu_f_edit, @menu_f_remove - ); - --- 3b) 插入缺失授权(TOP + QCB + C + 4 F) -INSERT INTO sys_role_menu ( - id, role_id, menu_id, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - UPPER(MD5(CONCAT(src.role_id, '|', src.menu_id))), - src.role_id, - src.menu_id, - @tenant_no, - 0, - 'grant qc inspection item template to QCB/WO roles', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM ( - SELECT - r.id AS role_id, - x.menu_id AS menu_id - FROM sys_role r - INNER JOIN ( - SELECT @top_id AS menu_id - UNION ALL SELECT @qcb_id - UNION ALL SELECT @menu_c - UNION ALL SELECT @menu_f_list - UNION ALL SELECT @menu_f_add - UNION ALL SELECT @menu_f_edit - UNION ALL SELECT @menu_f_remove - ) x - INNER JOIN ( - SELECT DISTINCT rm2.role_id AS role_id - FROM sys_role_menu rm2 - INNER JOIN sys_menu m2 - ON m2.id COLLATE utf8mb4_unicode_ci = rm2.menu_id COLLATE utf8mb4_unicode_ci - AND m2.del_flag = 0 - AND ( - m2.parent_id COLLATE utf8mb4_unicode_ci = @qcb_id - OR m2.id COLLATE utf8mb4_unicode_ci = @wo_menu - ) - WHERE rm2.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm2.del_flag = 0 - ) eligible - ON eligible.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci - INNER JOIN sys_menu m - ON m.id COLLATE utf8mb4_unicode_ci = x.menu_id - AND m.del_flag = 0 - WHERE r.del_flag = 0 - AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no -) src -LEFT JOIN sys_role_menu existing - ON existing.role_id COLLATE utf8mb4_unicode_ci = src.role_id COLLATE utf8mb4_unicode_ci - AND existing.menu_id COLLATE utf8mb4_unicode_ci = src.menu_id COLLATE utf8mb4_unicode_ci - AND existing.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND existing.del_flag = 0 -WHERE existing.id IS NULL; diff --git a/docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql b/docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql deleted file mode 100644 index 84272731..00000000 --- a/docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql +++ /dev/null @@ -1,166 +0,0 @@ --- 生产管理:预产及在途样本(独立页面 + 独立权限码) --- 幂等:可重复执行 --- 授权:已有【生产管理】PRM 下任一有效子菜单的角色 → 授予本 C/F,并补授 PRM 祖先 - -SET @tenant_no = CONVERT('zkxw' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @prm_id = CONVERT('PRM00000000000000000000000000001' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_c = CONVERT('95200000000000000000000000000001' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @menu_f = CONVERT('95200000000000000000000000000002' USING utf8mb4) COLLATE utf8mb4_unicode_ci; - --- 1) C 菜单 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_c, - '预产及在途样本', - @prm_id, - 'C', - 'pre-transit-sample', - '/production/pre-transit-sample/index', - 'production:preTransitSample:list', - 1, '1', '1', @tenant_no, 0, - '生产看板:在途样本与预产协议(协议单维度只读;与质检页代码独立)', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_c -); - -UPDATE sys_menu -SET - menu_name = '预产及在途样本', - parent_id = @prm_id, - menu_type = 'C', - path = 'pre-transit-sample', - component = '/production/pre-transit-sample/index', - perms = 'production:preTransitSample:list', - visible = '1', - status = '1', - order_num = 0, - remark = '生产看板:在途样本与预产协议(协议单维度只读;与质检页代码独立)', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_c; - --- 2) F 按钮 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - @menu_f, - '预产及在途查询', - @menu_c, - 'F', - '', - '', - 'production:preTransitSample:list', - 1, '0', '1', @tenant_no, 1, - '生产侧预产及在途样本列表查询', - NULL, '', NOW(), NULL, NOW(), 0 -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_f -); - -UPDATE sys_menu -SET - menu_name = '预产及在途查询', - parent_id = @menu_c, - menu_type = 'F', - path = '', - component = '', - perms = 'production:preTransitSample:list', - visible = '0', - status = '1', - order_num = 1, - remark = '生产侧预产及在途样本列表查询', - del_flag = 0, - update_time = NOW() -WHERE id COLLATE utf8mb4_unicode_ci = @menu_f; - --- 3) 目标角色:已有 PRM 下任一有效子菜单授权 --- MySQL 5.7:UPDATE 目标表不可在同句 FROM/EXISTS 中直接引用自身,须用派生表 - --- 3a) 恢复软删的 PRM / C / F 授权 -UPDATE sys_role_menu rm -INNER JOIN sys_role r - ON r.id COLLATE utf8mb4_unicode_ci = rm.role_id COLLATE utf8mb4_unicode_ci - AND r.del_flag = 0 - AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no -INNER JOIN ( - SELECT DISTINCT rm2.role_id AS role_id - FROM sys_role_menu rm2 - INNER JOIN sys_menu m2 - ON m2.id COLLATE utf8mb4_unicode_ci = rm2.menu_id COLLATE utf8mb4_unicode_ci - AND m2.del_flag = 0 - AND m2.parent_id COLLATE utf8mb4_unicode_ci = @prm_id - WHERE rm2.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm2.del_flag = 0 -) eligible - ON eligible.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci -SET - rm.del_flag = 0, - rm.update_time = NOW(), - rm.remark = 'grant production pre-transit to PRM roles' -WHERE rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm.del_flag = 1 - AND rm.menu_id COLLATE utf8mb4_unicode_ci IN (@prm_id, @menu_c, @menu_f); - --- 3b) 插入缺失授权(PRM + C + F) -INSERT INTO sys_role_menu ( - id, role_id, menu_id, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - UPPER(MD5(CONCAT(src.role_id, '|', src.menu_id))), - src.role_id, - src.menu_id, - @tenant_no, - 0, - 'grant production pre-transit to PRM roles', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM ( - SELECT - r.id AS role_id, - x.menu_id AS menu_id - FROM sys_role r - INNER JOIN ( - SELECT @prm_id AS menu_id - UNION ALL SELECT @menu_c - UNION ALL SELECT @menu_f - ) x - INNER JOIN ( - SELECT DISTINCT rm2.role_id AS role_id - FROM sys_role_menu rm2 - INNER JOIN sys_menu m2 - ON m2.id COLLATE utf8mb4_unicode_ci = rm2.menu_id COLLATE utf8mb4_unicode_ci - AND m2.del_flag = 0 - AND m2.parent_id COLLATE utf8mb4_unicode_ci = @prm_id - WHERE rm2.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm2.del_flag = 0 - ) eligible - ON eligible.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci - INNER JOIN sys_menu m - ON m.id COLLATE utf8mb4_unicode_ci = x.menu_id - AND m.del_flag = 0 - WHERE r.del_flag = 0 - AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no -) src -LEFT JOIN sys_role_menu existing - ON existing.role_id COLLATE utf8mb4_unicode_ci = src.role_id COLLATE utf8mb4_unicode_ci - AND existing.menu_id COLLATE utf8mb4_unicode_ci = src.menu_id COLLATE utf8mb4_unicode_ci - AND existing.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND existing.del_flag = 0 -WHERE existing.id IS NULL; diff --git a/docs/sql/grant_wechat_notify_menu_to_all_roles.sql b/docs/sql/grant_wechat_notify_menu_to_all_roles.sql deleted file mode 100644 index 5a2216c6..00000000 --- a/docs/sql/grant_wechat_notify_menu_to_all_roles.sql +++ /dev/null @@ -1,57 +0,0 @@ --- 将「开启微信通知」(OA007) 授予全部未删除角色 --- 前置:已执行 docs/sql/mobile_menu_oa_wechat_notify.sql(菜单须已存在) --- 幂等:可重复执行;已有有效授权跳过;软删则恢复 --- 说明: --- 1) 用户变量显式 utf8mb4_unicode_ci,避免 collation 混用报错 1267 --- 2) 主键用 MD5(role_id|menu_id),避免 INSERT…SELECT 中 UUID() 只算一次导致重复主键 - -SET @menu_id = CONVERT('MINI000000000000000000000000OA007' USING utf8mb4) COLLATE utf8mb4_unicode_ci; -SET @tenant_no = CONVERT('zkxw' USING utf8mb4) COLLATE utf8mb4_unicode_ci; - --- 1) 恢复历史上已软删的授权(按 role_id + menu_id) -UPDATE sys_role_menu rm -INNER JOIN sys_role r - ON r.id COLLATE utf8mb4_unicode_ci = rm.role_id COLLATE utf8mb4_unicode_ci - AND r.del_flag = 0 - AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no -SET - rm.del_flag = 0, - rm.update_time = NOW(), - rm.remark = 'grant wechat-notify to all roles' -WHERE rm.menu_id COLLATE utf8mb4_unicode_ci = @menu_id - AND rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm.del_flag = 1; - --- 2) 为尚未关联该菜单的角色新增授权 -INSERT INTO sys_role_menu ( - id, role_id, menu_id, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - UPPER(MD5(CONCAT(r.id, '|', @menu_id))), - r.id, - @menu_id, - @tenant_no, - 0, - 'grant wechat-notify to all roles', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM sys_role r -WHERE r.del_flag = 0 - AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND EXISTS ( - SELECT 1 FROM sys_menu m - WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_id - AND m.del_flag = 0 - ) - AND NOT EXISTS ( - SELECT 1 FROM sys_role_menu rm - WHERE rm.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci - AND rm.menu_id COLLATE utf8mb4_unicode_ci = @menu_id - AND rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no - AND rm.del_flag = 0 - ); diff --git a/docs/sql/mobile_menu_oa_start.sql b/docs/sql/mobile_menu_oa_start.sql deleted file mode 100644 index 398312e4..00000000 --- a/docs/sql/mobile_menu_oa_start.sql +++ /dev/null @@ -1,39 +0,0 @@ --- 小程序「发起审批」菜单种子(挂在「移动端菜单」根下) --- 执行前请确认移动端根存在;角色授权请在 Web「角色管理」勾选本菜单。 - -SET @mini_root = ( - SELECT id FROM sys_menu - WHERE del_flag = 0 AND parent_id = '0' AND menu_name LIKE '%移动端%' - LIMIT 1 -); - -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA001', - '发起审批', - @mini_root, - 'C', - '/pages/oa/start/index', - NULL, - 'mini:oa:start', - 1, - '1', - '1', - 'zkxw', - 10, - '小程序发起审批', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA001' AND del_flag = 0 - ); diff --git a/docs/sql/mobile_menu_oa_task_centers.sql b/docs/sql/mobile_menu_oa_task_centers.sql deleted file mode 100644 index 58bda120..00000000 --- a/docs/sql/mobile_menu_oa_task_centers.sql +++ /dev/null @@ -1,169 +0,0 @@ --- 小程序 OA 五中心菜单种子(挂在「移动端菜单」根下) --- 前置:docs/sql/mobile_menu_oa_start.sql(发起审批 OA001)可已执行 --- 角色授权请在 Web「角色管理」勾选本菜单。 - -SET @mini_root = ( - SELECT id FROM sys_menu - WHERE del_flag = 0 AND parent_id = '0' AND menu_name LIKE '%移动端%' - LIMIT 1 -); - --- 草稿箱 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA002', - '草稿箱', - @mini_root, - 'C', - '/pages/oa/drafts/index', - NULL, - 'mini:oa:drafts', - 1, - '1', - '1', - 'zkxw', - 20, - '小程序草稿箱', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA002' AND del_flag = 0 - ); - --- 待办中心 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA003', - '待办中心', - @mini_root, - 'C', - '/pages/oa/pending/index', - NULL, - 'mini:oa:pending', - 1, - '1', - '1', - 'zkxw', - 30, - '小程序待办中心', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA003' AND del_flag = 0 - ); - --- 已办中心 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA004', - '已办中心', - @mini_root, - 'C', - '/pages/oa/done/index', - NULL, - 'mini:oa:done', - 1, - '1', - '1', - 'zkxw', - 40, - '小程序已办中心', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA004' AND del_flag = 0 - ); - --- 我发起的 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA005', - '我发起的', - @mini_root, - 'C', - '/pages/oa/mine/index', - NULL, - 'mini:oa:mine', - 1, - '1', - '1', - 'zkxw', - 50, - '小程序我发起的', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA005' AND del_flag = 0 - ); - --- 抄送我的 -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA006', - '抄送我的', - @mini_root, - 'C', - '/pages/oa/cc/index', - NULL, - 'mini:oa:cc', - 1, - '1', - '1', - 'zkxw', - 60, - '小程序抄送我的', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA006' AND del_flag = 0 - ); diff --git a/docs/sql/mobile_menu_oa_wechat_notify.sql b/docs/sql/mobile_menu_oa_wechat_notify.sql deleted file mode 100644 index e190e1ed..00000000 --- a/docs/sql/mobile_menu_oa_wechat_notify.sql +++ /dev/null @@ -1,40 +0,0 @@ --- 小程序「开启微信通知」菜单(抄送我的之后) --- 前置:docs/sql/mobile_menu_oa_task_centers.sql(OA006)可已执行 --- 角色授权请在 Web「角色管理」勾选本菜单。 - -SET @mini_root = ( - SELECT id FROM sys_menu - WHERE del_flag = 0 AND parent_id = '0' AND menu_name LIKE '%移动端%' - LIMIT 1 -); - -INSERT INTO sys_menu ( - id, menu_name, parent_id, menu_type, path, component, perms, - is_frame, visible, status, tenant_no, order_num, remark, - create_by, create_dept_id, create_time, update_by, update_time, del_flag -) -SELECT - 'MINI000000000000000000000000OA007', - '开启微信通知', - @mini_root, - 'C', - '/pages/oa/wechat-notify/index', - NULL, - 'mini:oa:wechat-notify', - 1, - '1', - '1', - 'zkxw', - 70, - '小程序开启微信通知(关注+绑定)', - 'admin', - '', - NOW(), - NULL, - NOW(), - 0 -FROM DUAL -WHERE @mini_root IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA007' AND del_flag = 0 - ); diff --git a/docs/superpowers/plans/2026-08-17-qc-inbound-lab-storage-code-sort.md b/docs/superpowers/plans/2026-08-17-qc-inbound-lab-storage-code-sort.md new file mode 100644 index 00000000..83706c7a --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-qc-inbound-lab-storage-code-sort.md @@ -0,0 +1,614 @@ +# 入库质检列表按实验室入库编码排序 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:** 【质检工单】与【我的质检任务】的「细胞存储」「科研捐赠」Tab,按实验室入库编码倒序、同编码按创建时间倒序分页与展示,跨页一致。 + +**Architecture:** 后端在入库两 Tab 的列表查询上用相关子查询解析 `lab_storage_code`(优先 `reception_id`,否则按样本编码),`ORDER BY COALESCE(code,'') DESC, create_time DESC`;前端入库卡片与我的任务聚合排序对齐同一比较规则;出库组内排序不动。 + +**Tech Stack:** FastAPI / SQLAlchemy 2.0 / Vue 3 / TypeScript;设计依据 `docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md`。 + +**Spec:** `docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md` + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| Create: `backend/app/crud/qc_inbound_lab_storage_sort.py` | 判断是否启用编码排序、相关子查询表达式、`order_by` 元组 | +| Modify: `backend/app/crud/qc_work_order.py` | `list_work_orders` 入库两 Tab 使用新 `order_by` | +| Modify: `backend/app/crud/qc_task.py` | `list_mine_tasks` 入库两 Tab 使用新 `order_by` | +| Create: `backend/tests/test_qc_inbound_lab_storage_sort.py` | 纯函数:来源判断 + 排序键比较语义 | +| Create: `web-admin/src/utils/qc-inbound-lab-storage-sort.ts` | 前端统一比较:编码 DESC → 时间 DESC | +| Modify: `web-admin/src/utils/qc-work-order-groups.ts` | 仅入库组内改为时间倒序 | +| Modify: `web-admin/src/utils/qc-work-order-inbound-sample-cards.ts` | 卡片列表主序改为编码/时间 | +| Modify: `web-admin/src/utils/qc-mine-task-groups.ts` | 导出按编码排序的工单条目 | +| Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts` | mine 模式分页前调用排序 | + +--- + +### Task 1: 后端纯函数 — 来源判断与比较语义 + +**Files:** +- Create: `backend/app/crud/qc_inbound_lab_storage_sort.py` +- Create: `backend/tests/test_qc_inbound_lab_storage_sort.py` + +- [ ] **Step 1: Write the failing test** + +```python +# backend/tests/test_qc_inbound_lab_storage_sort.py +"""入库质检列表:实验室入库编码排序语义。""" +import unittest + +from app.crud.qc_inbound_lab_storage_sort import ( + compare_lab_storage_then_create_time_desc, + uses_inbound_lab_storage_list_sort, +) + + +class TestUsesInboundLabStorageListSort(unittest.TestCase): + def test_cell_storage_and_research_donation(self): + self.assertTrue(uses_inbound_lab_storage_list_sort("cell_storage")) + self.assertTrue(uses_inbound_lab_storage_list_sort("research_donation")) + + def test_other_sources_false(self): + self.assertFalse(uses_inbound_lab_storage_list_sort("")) + self.assertFalse(uses_inbound_lab_storage_list_sort(None)) + self.assertFalse(uses_inbound_lab_storage_list_sort("temporary")) + + +class TestCompareLabStorageThenCreateTimeDesc(unittest.TestCase): + def test_code_desc_empty_as_empty_string(self): + # '' < 'B0000001' lexicographically → DESC 时有编码在前 + self.assertEqual( + compare_lab_storage_then_create_time_desc("B0000001", "2026-01-01", "", "2026-12-01"), + -1, + ) + self.assertEqual( + compare_lab_storage_then_create_time_desc("", "2026-12-01", "B0000001", "2026-01-01"), + 1, + ) + + def test_same_code_newer_create_time_first(self): + self.assertEqual( + compare_lab_storage_then_create_time_desc( + "B1", "2026-08-02 10:00:00", "B1", "2026-08-01 10:00:00" + ), + -1, + ) + + def test_higher_code_first(self): + self.assertEqual( + compare_lab_storage_then_create_time_desc("B2", "t", "B1", "t"), + -1, + ) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_qc_inbound_lab_storage_sort.py -v +``` + +Expected: FAIL(模块不存在或导入失败) + +- [ ] **Step 3: Write minimal implementation** + +```python +# backend/app/crud/qc_inbound_lab_storage_sort.py +"""入库质检列表:按实验室入库编码排序的辅助逻辑。""" +from __future__ import annotations + +from typing import Optional + +from sqlalchemy import and_, func, select +from sqlalchemy.sql import ColumnElement + +from app.models.models import BizQcWorkOrder, BizSampleReception + +_INBOUND_LAB_SORT_SOURCES = frozenset({"cell_storage", "research_donation"}) + + +def uses_inbound_lab_storage_list_sort(reception_source: Optional[str]) -> bool: + return (reception_source or "").strip() in _INBOUND_LAB_SORT_SOURCES + + +def compare_lab_storage_then_create_time_desc( + code_a: Optional[str], + time_a: Optional[str], + code_b: Optional[str], + time_b: Optional[str], +) -> int: + """列表序比较:编码 DESC,同编码时间 DESC。返回负值表示 a 应排在 b 前。""" + ca = (code_a or "").strip() + cb = (code_b or "").strip() + if ca != cb: + # DESC: 较大字符串在前 + return -1 if ca > cb else 1 + ta = (time_a or "").strip() + tb = (time_b or "").strip() + if ta != tb: + return -1 if ta > tb else 1 + return 0 + + +def lab_storage_code_expr_for_work_order() -> ColumnElement: + """相关子查询:优先 reception_id,否则同租户 sample_code 最新一条接收记录的编码。""" + by_id = ( + select(BizSampleReception.lab_storage_code) + .where( + and_( + BizSampleReception.id == BizQcWorkOrder.reception_id, + BizSampleReception.del_flag == 0, + ) + ) + .correlate(BizQcWorkOrder) + .scalar_subquery() + ) + by_sample = ( + select(BizSampleReception.lab_storage_code) + .where( + and_( + BizSampleReception.sample_code == BizQcWorkOrder.sample_code, + BizSampleReception.tenant_no == BizQcWorkOrder.tenant_no, + BizSampleReception.del_flag == 0, + BizQcWorkOrder.sample_code.isnot(None), + BizQcWorkOrder.sample_code != "", + ) + ) + .order_by(BizSampleReception.create_time.desc()) + .limit(1) + .correlate(BizQcWorkOrder) + .scalar_subquery() + ) + return func.coalesce(by_id, by_sample, "") + + +def inbound_lab_storage_order_by_clauses(): + """供 Query.order_by(*clauses) 使用。""" + code = lab_storage_code_expr_for_work_order() + return ( + code.desc(), + BizQcWorkOrder.create_time.desc(), + ) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_qc_inbound_lab_storage_sort.py -v +``` + +Expected: PASS + +- [ ] **Step 5: Commit**(仅当用户明确要求提交时执行) + +```bash +git add backend/app/crud/qc_inbound_lab_storage_sort.py backend/tests/test_qc_inbound_lab_storage_sort.py +git commit -m "$(cat <<'EOF' +feat: add inbound QC list sort helpers by lab storage code + +EOF +)" +``` + +--- + +### Task 2: `list_work_orders` 应用入库排序 + +**Files:** +- Modify: `backend/app/crud/qc_work_order.py`(`list_work_orders` 中约 1706–1715 行的 `order_by`) + +- [ ] **Step 1: Wire order_by** + +在 `list_work_orders` 文件顶部附近增加导入: + +```python +from app.crud.qc_inbound_lab_storage_sort import ( + inbound_lab_storage_order_by_clauses, + uses_inbound_lab_storage_list_sort, +) +``` + +将: + +```python + rows = ( + _filtered() + .order_by(BizQcWorkOrder.create_time.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) +``` + +替换为: + +```python + q_page = _filtered() + if uses_inbound_lab_storage_list_sort(reception_source): + q_page = q_page.order_by(*inbound_lab_storage_order_by_clauses()) + else: + q_page = q_page.order_by(BizQcWorkOrder.create_time.desc()) + rows = ( + q_page + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) +``` + +- [ ] **Step 2: Sanity import check** + +```bash +cd backend && source venv/bin/activate && python -c "from app.crud.qc_work_order import list_work_orders; print('ok')" +``` + +Expected: `ok` + +- [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) + +```bash +git add backend/app/crud/qc_work_order.py +git commit -m "$(cat <<'EOF' +feat: order inbound QC work orders by lab storage code then create time + +EOF +)" +``` + +--- + +### Task 3: `list_mine_tasks` 应用入库排序 + +**Files:** +- Modify: `backend/app/crud/qc_task.py`(`list_mine_tasks` 的 `order_by`,约 637–644 行) + +- [ ] **Step 1: Wire order_by** + +增加导入: + +```python +from app.crud.qc_inbound_lab_storage_sort import ( + inbound_lab_storage_order_by_clauses, + uses_inbound_lab_storage_list_sort, +) +``` + +将 `list_mine_tasks` 内: + +```python + rows = ( + _filtered() + .order_by( + BizQcTask.create_time.desc(), + BizQcWorkOrder.work_order_no.desc(), + BizQcTask.task_no.asc(), + ) + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) +``` + +替换为: + +```python + q_page = _filtered() + if uses_inbound_lab_storage_list_sort(reception_source): + q_page = q_page.order_by( + *inbound_lab_storage_order_by_clauses(), + BizQcWorkOrder.work_order_no.desc(), + BizQcTask.task_no.asc(), + ) + else: + q_page = q_page.order_by( + BizQcTask.create_time.desc(), + BizQcWorkOrder.work_order_no.desc(), + BizQcTask.task_no.asc(), + ) + rows = ( + q_page + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) +``` + +注意:确认 `list_mine_tasks` 形参名确为 `reception_source`(与现有签名一致)。 + +- [ ] **Step 2: Sanity import check** + +```bash +cd backend && source venv/bin/activate && python -c "from app.crud.qc_task import list_mine_tasks; print('ok')" +``` + +Expected: `ok` + +- [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) + +```bash +git add backend/app/crud/qc_task.py +git commit -m "$(cat <<'EOF' +feat: order mine QC tasks by lab storage code on inbound tabs + +EOF +)" +``` + +--- + +### Task 4: 前端统一比较工具 + +**Files:** +- Create: `web-admin/src/utils/qc-inbound-lab-storage-sort.ts` + +web-admin 无单测 runner;用纯函数 + 节点内联断言冒烟(或手动对照设计文档)。 + +- [ ] **Step 1: Add compare helpers** + +```typescript +// web-admin/src/utils/qc-inbound-lab-storage-sort.ts + +/** 编码 DESC → 时间 DESC;负值表示 a 排在 b 前(与后端一致) */ +export function compareLabStorageThenCreateTimeDesc( + codeA: string | null | undefined, + timeA: string | null | undefined, + codeB: string | null | undefined, + timeB: string | null | undefined +): number { + const ca = String(codeA || '').trim() + const cb = String(codeB || '').trim() + if (ca !== cb) { + return ca > cb ? -1 : 1 + } + const ta = String(timeA || '').trim() + const tb = String(timeB || '').trim() + if (ta !== tb) { + return ta > tb ? -1 : 1 + } + return 0 +} + +export function resolveGroupLabStorageCode( + rows: { labStorageCode?: string | null }[] +): string { + for (const row of rows) { + const code = String(row.labStorageCode || '').trim() + if (code) return code + } + return '' +} + +export function resolveGroupMaxCreateTime( + rows: { createTime?: string | null }[] +): string { + let max = '' + for (const row of rows) { + const t = String(row.createTime || '') + if (t > max) max = t + } + return max +} +``` + +- [ ] **Step 2: Smoke-check in Node(可选)** + +在仓库根用短脚本确认比较方向(复制函数体或通过构建后的 bundle 非必须;优先在实现后浏览器验)。 + +- [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) + +```bash +git add web-admin/src/utils/qc-inbound-lab-storage-sort.ts +git commit -m "$(cat <<'EOF' +feat: add frontend inbound lab storage sort compare helpers + +EOF +)" +``` + +--- + +### Task 5: 入库组内与样本卡片排序 + +**Files:** +- Modify: `web-admin/src/utils/qc-work-order-groups.ts` +- Modify: `web-admin/src/utils/qc-work-order-inbound-sample-cards.ts` + +- [ ] **Step 1: Inbound-only within-group sort** + +在 `qc-work-order-groups.ts`: + +保留现有 `sortWorkOrdersInGroup`(出库继续用)。新增: + +```typescript +function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) { + return [...rows].sort((a, b) => + compareLabStorageThenCreateTimeDesc( + '', // 同组编码相同,仅比时间;传空避免干扰 + a.createTime, + '', + b.createTime + ) + ) +} +``` + +更直接: + +```typescript +function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) { + return [...rows].sort((a, b) => + String(b.createTime || '').localeCompare(String(a.createTime || '')) + ) +} +``` + +将 `groupInboundQcWorkOrders` 内 `sortWorkOrdersInGroup(workOrders)` 改为 `sortWorkOrdersInInboundGroup(workOrders)`。 + +`groupOutboundQcWorkOrders` 仍调用 `sortWorkOrdersInGroup`。 + +顶部增加(若用 compare 工具则 import;若仅 localeCompare 可省略): + +```typescript +// 无需改 outbound +``` + +- [ ] **Step 2: Card list order in `buildInboundSampleListItems`** + +在 `qc-work-order-inbound-sample-cards.ts` 增加 import: + +```typescript +import { + compareLabStorageThenCreateTimeDesc, + resolveGroupLabStorageCode, + resolveGroupMaxCreateTime +} from '@/utils/qc-inbound-lab-storage-sort' +``` + +将末尾: + +```typescript + const sortedGroups = [...groups].sort((a, b) => { + const headA = a.workOrders[0] + const headB = b.workOrders[0] + if (!headA || !headB) return 0 + const protocolA = resolveInboundProtocolGroupKey(headA) + const protocolB = resolveInboundProtocolGroupKey(headB) + if (protocolA !== protocolB) { + return protocolSortTime(b).localeCompare(protocolSortTime(a)) + } + return (a.sampleCode || '').localeCompare(b.sampleCode || '') + }) +``` + +替换为: + +```typescript + const sortedGroups = [...groups].sort((a, b) => + compareLabStorageThenCreateTimeDesc( + resolveGroupLabStorageCode(a.workOrders), + resolveGroupMaxCreateTime(a.workOrders), + resolveGroupLabStorageCode(b.workOrders), + resolveGroupMaxCreateTime(b.workOrders) + ) + ) +``` + +若 `protocolSortTime` 不再被本文件使用,删除该死函数。 + +同协议胶囊内按 `sampleCode` 的排序(`buildLinksFromVisibleGroups` 等)**保持不变**。 + +- [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) + +```bash +git add web-admin/src/utils/qc-work-order-groups.ts web-admin/src/utils/qc-work-order-inbound-sample-cards.ts +git commit -m "$(cat <<'EOF' +feat: sort inbound QC sample cards by lab storage code then create time + +EOF +)" +``` + +--- + +### Task 6: 我的质检任务分页前排序 + +**Files:** +- Modify: `web-admin/src/utils/qc-mine-task-groups.ts` +- Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts` + +- [ ] **Step 1: Export sorter** + +在 `qc-mine-task-groups.ts` 增加: + +```typescript +import { compareLabStorageThenCreateTimeDesc } from '@/utils/qc-inbound-lab-storage-sort' +import { isInboundSampleQcTab } from '@/utils/qc-work-order-tabs' + +export function sortMineEntriesForInboundLabStorage( + entries: MineWorkOrderEntry[] +): MineWorkOrderEntry[] { + return [...entries].sort((a, b) => + compareLabStorageThenCreateTimeDesc( + a.workOrder.labStorageCode, + a.workOrder.createTime, + b.workOrder.labStorageCode, + b.workOrder.createTime + ) + ) +} +``` + +确认 `isInboundSampleQcTab` 已从 tabs 导出;若已有同类 import,合并到现有 import 块。 + +- [ ] **Step 2: Call before paginate in mine `loadData`** + +在 `useQcWorkOrderTabPage.ts` 的 mine 分支,在 `filterMineEntriesByWorkOrderSearch` 之后、`paginateMineEntries` 之前: + +```typescript + let entries = groupMineTasksByWorkOrder(tasks) + entries = filterMineEntriesByWorkOrderSearch(entries, buildMineWorkOrderSearchFilters()) + if (isInboundSampleQcTab(tabKey.value)) { + entries = sortMineEntriesForInboundLabStorage(entries) + } + const pageResult = paginateMineEntries(entries, pagination.page, pagination.pageSize) +``` + +补充 import:`sortMineEntriesForInboundLabStorage`、`isInboundSampleQcTab`(后者可能已导入)。 + +- [ ] **Step 3: Commit**(仅当用户明确要求提交时执行) + +```bash +git add web-admin/src/utils/qc-mine-task-groups.ts web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTabPage.ts +git commit -m "$(cat <<'EOF' +feat: sort mine inbound QC entries by lab storage code before paging + +EOF +)" +``` + +--- + +### Task 7: 图谱更新与手工验收 + +**Files:** +- 无业务代码;跑 `scripts/update-graph.sh` + +- [ ] **Step 1: Update knowledge graph** + +```bash +./scripts/update-graph.sh all +``` + +- [ ] **Step 2: Manual QA checklist** + +1. 打开 `/#/quality-group/quality-manage/qc-work-order?tab=inbound_cell_storage`:卡片标题(实验室入库编码)整体倒序;同编码时创建时间新的在上;翻页后仍符合该序。 +2. 切换 `inbound_research_donation`:同上。 +3. `/#/quality-group/quality-manage/my-qc-task` 对应两 Tab:同上。 +4. 无编码卡片按空串参与倒序(相对有编码的位置与字符串 DESC 一致)。 +5. 「全部 / 出库 / 临时」Tab 顺序与交互无明显回归;同协议样本胶囊顺序仍按样本编码。 + +- [ ] **Step 3: Mark design status(可选)** + +将 `docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md` 状态改为 `已实现`。 + +--- + +## Spec coverage (self-review) + +| Spec 项 | Task | +|---------|------| +| 仅两页两 Tab | Task 2/3 条件 `uses_inbound_*`;Task 5/6 仅 inbound | +| 编码 DESC + 时间 DESC | Task 1–6 | +| 空串参与倒序 | Task 1 测试 + COALESCE/COALESCE | +| 后端分页 | Task 2 | +| 我的任务 | Task 3 + Task 6 | +| 组内时间倒序、出库不变 | Task 5 | +| 互链不改 | Task 5 明确保留 | +| 无 DB 迁移 | 全程 JOIN/子查询 | + +无 TBD 占位;前后端比较函数命名与语义一致(负值 = a 在前)。 diff --git a/docs/superpowers/plans/2026-08-17-sample-reception-protocol-fee-trigger.md b/docs/superpowers/plans/2026-08-17-sample-reception-protocol-fee-trigger.md new file mode 100644 index 00000000..0336a7e1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-sample-reception-protocol-fee-trigger.md @@ -0,0 +1,26 @@ +# 样本接收协议费用按需展示 Implementation Plan + +> **For agentic workers:** 单文件小改动,可直接按任务执行。 + +**Goal:** 样本接收卡片右上角「协议费用」按钮,点击弹窗查看完整费用。 + +**Architecture:** `SampleReceptionCardList` 标题行右侧按钮 + 单例 `el-dialog` 嵌入 `QcWorkOrderProtocolPaymentPanel`。 + +**Tech Stack:** Vue 3 + Element Plus(web-admin) + +--- + +### Task 1: 弹窗模式 + 右上角按钮 + +**Files:** +- Modify: `web-admin/src/views/quality-inspection/sample-reception/SampleReceptionCardList.vue` + +- [x] 标题行右上角「协议费用」按钮(`margin-left: auto`) +- [x] 点击打开弹窗,展示完整协议费用面板(`embedded`) +- [x] 移除卡片内联展开/收起逻辑 + +### Task 2: 手动验收 + +- [ ] 按钮在卡片右上角 +- [ ] 点击弹出费用明细;关闭后卡片无费用区域 +- [ ] 科研捐献卡片仍无协议费用入口 diff --git a/docs/superpowers/specs/2026-08-17-qc-batch-name-lab-storage-code-design.md b/docs/superpowers/specs/2026-08-17-qc-batch-name-lab-storage-code-design.md new file mode 100644 index 00000000..0a2e7308 --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-qc-batch-name-lab-storage-code-design.md @@ -0,0 +1,38 @@ +# 质检工单配置检测 · 批次名称默认值 — 设计文档 + +> 日期:2026-08-17 +> 状态:已实现 +> 范围:`web-admin` 【质检工单】配置检测 / 调整检测弹窗 + +--- + +## 1. 背景 + +当前新增检测批次时,【批次名称】默认 `YYYYMMDD检测`(冲突加 `-2`、`-3`…)。 +细胞存储 / 科研捐赠质检希望默认使用工单上的**实验室入库编码**(如 `B0000090`、`BC0000015`)。 + +## 2. 已确认规则 + +| 条件 | 新增批次默认「批次名称」 | +|------|--------------------------| +| `receptionSource` 为 `cell_storage` 或 `research_donation`,且有 `labStorageCode` | 一律用该编码;多批次相同;允许用户再改 | +| 同上,但无 `labStorageCode` | 空字符串,用户自填 | +| 其它来源(临时 / 出库等) | 保持 `YYYYMMDD检测`,冲突加 `-2`、`-3`… | + +覆盖点:打开弹窗时的首个默认批次 +「添加批次」。 +不改已保存批次名;不改后端接口。 + +## 3. 实现要点(方案 1) + +1. 在 `qc-inspection-batch.ts` 抽取 `resolveDefaultQcBatchName(...)` 作为唯一默认名规则。 +2. `index.vue` 打开配置弹窗时记住当前工单的 `receptionSource`、`labStorageCode`,并传给 `QcWorkOrderConfigureBatches`;首个默认批次走同一规则。 +3. `QcWorkOrderConfigureBatches` 的「添加批次」调用同一规则(入库类型不做名称去重后缀)。 + +## 5. 批次负责人默认值(补充) + +点击「添加批次」时: + +1. 弹窗内已有批次 → 复制**列表最后一批**的 `assigneeUserIds`(多选原样带上) +2. 弹窗内无批次 → 仍为:当前登录用户在「质检专员」下拉里则默认自己,否则空 + +打开弹窗首个空默认批次(无历史)仍走规则 2。 diff --git a/docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md b/docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md new file mode 100644 index 00000000..6ffc07a2 --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-qc-inbound-lab-storage-code-sort-design.md @@ -0,0 +1,78 @@ +# 入库质检列表按实验室入库编码排序 — 设计文档 + +> 日期:2026-08-17 +> 状态:已实现 +> 范围:【质检工单】【我的质检任务】的「细胞存储」「科研捐赠」Tab + +--- + +## 1. 背景 + +当前入库列表后端按工单 `create_time DESC` 分页;前端卡片再按「协议组最新创建时间 + 样本编码」重排,组内工单另按触发来源权重 + 创建时间升序。用户希望细胞存储 / 科研捐赠统一按**实验室入库编码倒序**,同编码下按**创建时间倒序**,且跨页一致。 + +## 2. 已确认规则 + +| 项 | 约定 | +|----|------| +| 页面 | 仅【质检工单】+【我的质检任务】 | +| Tab | 仅 `inbound_cell_storage`、`inbound_research_donation` | +| 主排序 | `lab_storage_code` / `labStorageCode` 字符串倒序 | +| 次排序 | 工单 `create_time` / `createTime` 倒序 | +| 空编码 | 按空字符串 `''` 直接参与倒序比较 | +| 分页 | 后端按上述规则排序后再分页(跨页一致) | +| 其它 Tab | 「全部 / 出库 / 临时」等不改 | + +## 3. 方案(方案 1) + +后端入库两 Tab 专用 `ORDER BY`(JOIN 样本接收取编码)+ 前端展示排序对齐同一规则。 + +不采用:仅前端页内重排;工单表冗余 `lab_storage_code`。 + +## 4. 后端 + +### 4.1 编码来源 + +`lab_storage_code` 在 `biz_sample_reception`,不在 `biz_qc_work_order`。列表排序通过 **LEFT JOIN** 取得,`COALESCE(lab_storage_code, '')`。 + +JOIN 对齐现有 enrich 语义:优先 `BizQcWorkOrder.reception_id`;无接收 ID 时再按 `sample_code + tenant`(及必要的 `del_flag`)关联,避免生产触发等无 `reception_id` 的工单编码全空导致乱序。 + +### 4.2 质检工单 `list_work_orders` + +- 当 `reception_source ∈ {cell_storage, research_donation}`: + `ORDER BY COALESCE(lab_storage_code, '') DESC, BizQcWorkOrder.create_time DESC` +- 其它来源:保持 `create_time DESC` +- 不改筛选条件与返回字段结构 + +### 4.3 我的质检任务 `list_mine_tasks` + +- 查询已 JOIN 工单;当请求带入库两 Tab 的 `reception_source` 时: + `ORDER BY COALESCE(lab_storage_code, '') DESC, BizQcWorkOrder.create_time DESC`,并以 `work_order_no` / `task_no` 作为稳定次序 +- 其它 Tab:保持现有任务 `create_time DESC` 等规则 +- 前端仍 `fetchAllMineTasks` 拉全量;聚合后按同一规则排序再客户端分页 + +## 5. 前端 + +| 文件 | 改动 | +|------|------| +| `qc-work-order-inbound-sample-cards.ts` | `buildInboundSampleListItems`:卡片序改为编码倒序 → 组内最大 `createTime` 倒序;去掉「协议组再样本编码」主序 | +| `qc-work-order-groups.ts` | **仅入库路径**组内排序改为 `createTime` 倒序(取消触发来源权重);出库 `groupOutboundQcWorkOrders` 保持原组内规则不变 | +| `qc-mine-task-groups.ts` | 入库两 Tab:分页前对工单条目按编码 DESC → 工单创建时间 DESC 排序 | + +**不改**:同协议样本胶囊内按 `sampleCode` 的互链排序;全部 / 出库 / 临时展示逻辑。 + +卡片代表字段: + +- 编码:组内首个非空 `labStorageCode`,否则 `''` +- 时间:组内最大 `createTime`(同编码下新的在前) + +## 6. 验收 + +1. 细胞存储、科研捐赠:两页列表均为「编码倒序,同编码创建时间倒序」;翻页顺序连续。 +2. 无编码按空串参与倒序;同一样本多工单时间新的在上。 +3. 其它 Tab 无回归。 + +## 7. 非目标 + +- 数据库迁移 / 工单表冗余字段 +- 修改「全部」Tab 中入库卡片的全局排序策略 +- 改变同协议互链胶囊的展示顺序规则 diff --git a/docs/superpowers/specs/2026-08-17-sample-reception-protocol-fee-trigger-design.md b/docs/superpowers/specs/2026-08-17-sample-reception-protocol-fee-trigger-design.md new file mode 100644 index 00000000..8ed6a197 --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-sample-reception-protocol-fee-trigger-design.md @@ -0,0 +1,18 @@ +# 样本接收 · 协议费用按需展示 + +## 背景 + +样本接收列表卡片默认展示整块「协议费用」占用空间。改为按需查看。 + +## 决策 + +仅在 `SampleReceptionCardList.vue` 改造: + +1. 「协议费用」按钮放在卡片标题行右上角(`margin-left: auto`)。 +2. 点击后以弹窗展示完整 `QcWorkOrderProtocolPaymentPanel`(非 compact、`embedded`,避免与弹窗标题重复)。 +3. 不改共享组件;质检工单等其它页保持原交互。 + +## 非目标 + +- 不改其它列表的协议费用交互。 +- 不新增后端字段或接口。 diff --git a/mobile-h5/static/menu-icons/start-approval.png b/mobile-h5/static/menu-icons/start-approval.png index 8101e5c29a037dc1085c0655850c299cc1e53ae4..f798a42b2cc519d3184cfb2796f7a84fb2c04b05 100644 GIT binary patch literal 9255 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 I#uq971xNFe#sB~S diff --git a/web-admin/src/styles/_route-config.scss b/web-admin/src/styles/_route-config.scss index 1d553f88..61ea2825 100644 --- a/web-admin/src/styles/_route-config.scss +++ b/web-admin/src/styles/_route-config.scss @@ -237,22 +237,37 @@ } .route-config-step-actions { display: flex; - flex-direction: column; + flex-direction: row; + flex-wrap: wrap; align-items: center; - justify-content: flex-start; - gap: 2px; + justify-content: center; + gap: 0; flex-shrink: 0; + width: 48px; margin-left: auto; padding-top: 2px; .el-tag { margin: 0; + flex: 1 0 100%; + justify-content: center; + height: 18px; + padding: 0 3px; + font-size: 10px; + line-height: 16px; } .el-button.is-link { margin: 0; - padding: 2px; - height: auto; + padding: 0; + min-height: 20px; + min-width: 20px; + width: 20px; + height: 20px; + } + + .el-icon { + font-size: 13px; } } diff --git a/web-admin/src/utils/production-step-output.test.ts b/web-admin/src/utils/production-step-output.test.ts new file mode 100644 index 00000000..89c469fa --- /dev/null +++ b/web-admin/src/utils/production-step-output.test.ts @@ -0,0 +1,23 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { createEmptyStepOutputRow } from './production-step-output' + +describe('createEmptyStepOutputRow', () => { + it('defaults barcode to lab storage code', () => { + const row = createEmptyStepOutputRow('LAB-001') + assert.equal(row.outputBarcode, 'LAB-001') + assert.equal(row.outputTypeId, '') + assert.equal(row.outputName, '') + assert.equal(row.remark, '') + }) + + it('trims lab storage code', () => { + assert.equal(createEmptyStepOutputRow(' LAB-002 ').outputBarcode, 'LAB-002') + }) + + it('uses empty barcode when lab storage code is missing', () => { + assert.equal(createEmptyStepOutputRow('').outputBarcode, '') + assert.equal(createEmptyStepOutputRow(null).outputBarcode, '') + assert.equal(createEmptyStepOutputRow(undefined).outputBarcode, '') + }) +}) diff --git a/web-admin/src/utils/production-step-output.ts b/web-admin/src/utils/production-step-output.ts new file mode 100644 index 00000000..f5e64b0c --- /dev/null +++ b/web-admin/src/utils/production-step-output.ts @@ -0,0 +1,9 @@ +/** 工艺步骤「产出物」新增行:条码默认实验室入库编码。 */ +export function createEmptyStepOutputRow(labStorageCode?: string | null) { + return { + outputTypeId: '', + outputName: '', + outputBarcode: String(labStorageCode || '').trim(), + remark: '' + } +} diff --git a/web-admin/src/utils/qc-inbound-lab-storage-sort.ts b/web-admin/src/utils/qc-inbound-lab-storage-sort.ts new file mode 100644 index 00000000..1a0de93f --- /dev/null +++ b/web-admin/src/utils/qc-inbound-lab-storage-sort.ts @@ -0,0 +1,40 @@ +/** 编码 DESC → 时间 DESC;负值表示 a 排在 b 前(与后端一致) */ +export function compareLabStorageThenCreateTimeDesc( + codeA: string | null | undefined, + timeA: string | null | undefined, + codeB: string | null | undefined, + timeB: string | null | undefined +): number { + const ca = String(codeA || '').trim() + const cb = String(codeB || '').trim() + if (ca !== cb) { + return ca > cb ? -1 : 1 + } + const ta = String(timeA || '').trim() + const tb = String(timeB || '').trim() + if (ta !== tb) { + return ta > tb ? -1 : 1 + } + return 0 +} + +export function resolveGroupLabStorageCode( + rows: { labStorageCode?: string | null }[] +): string { + for (const row of rows) { + const code = String(row.labStorageCode || '').trim() + if (code) return code + } + return '' +} + +export function resolveGroupMaxCreateTime( + rows: { createTime?: string | null }[] +): string { + let max = '' + for (const row of rows) { + const t = String(row.createTime || '') + if (t > max) max = t + } + return max +} diff --git a/web-admin/src/utils/qc-inspection-batch.ts b/web-admin/src/utils/qc-inspection-batch.ts index 36c5219e..bdec8e29 100644 --- a/web-admin/src/utils/qc-inspection-batch.ts +++ b/web-admin/src/utils/qc-inspection-batch.ts @@ -162,16 +162,49 @@ export function resolveBatchSections( return [] } -export function createEmptyInspectionBatch(defaultAssigneeUserIds?: string[]): QcInspectionBatchConfigureItem { +/** 细胞存储 / 科研捐赠:批次名默认用实验室入库编码 */ +const INBOUND_LAB_CODE_BATCH_SOURCES = new Set(['cell_storage', 'research_donation']) + +export type ResolveDefaultQcBatchNameOpts = { + receptionSource?: string | null + labStorageCode?: string | null + /** 已占用名称;仅非入库类型做 YYYYMMDD检测 去重时使用 */ + usedNames?: Iterable +} + +/** + * 配置检测弹窗新增批次的默认名称。 + * - 细胞存储 / 科研捐赠:有实验室入库编码则用该编码(多批次相同);无则空串 + * - 其它类型:YYYYMMDD检测,冲突则 -2、-3… + */ +export function resolveDefaultQcBatchName(opts: ResolveDefaultQcBatchNameOpts = {}): string { + const source = String(opts.receptionSource || '').trim() + if (INBOUND_LAB_CODE_BATCH_SOURCES.has(source)) { + return String(opts.labStorageCode || '').trim() + } const today = new Date() const y = today.getFullYear() const m = String(today.getMonth() + 1).padStart(2, '0') const d = String(today.getDate()).padStart(2, '0') + const base = `${y}${m}${d}检测` + const used = new Set( + [...(opts.usedNames || [])].map((n) => String(n || '').trim()).filter(Boolean) + ) + if (!used.has(base)) return base + let n = 2 + while (used.has(`${base}-${n}`)) n += 1 + return `${base}-${n}` +} + +export function createEmptyInspectionBatch( + defaultAssigneeUserIds?: string[], + nameOpts?: ResolveDefaultQcBatchNameOpts +): QcInspectionBatchConfigureItem { const assigneeUserIds = (defaultAssigneeUserIds || []) .map((id) => String(id || '').trim()) .filter(Boolean) return { - batchName: `${y}${m}${d}检测`, + batchName: resolveDefaultQcBatchName(nameOpts), assigneeUserIds, hasLab: false, hasThirdParty: false, diff --git a/web-admin/src/utils/qc-mine-task-groups.ts b/web-admin/src/utils/qc-mine-task-groups.ts index 87603bcd..91ff54b3 100644 --- a/web-admin/src/utils/qc-mine-task-groups.ts +++ b/web-admin/src/utils/qc-mine-task-groups.ts @@ -18,6 +18,7 @@ import { workOrderMatchesCategory } from '@/utils/qc-work-order-category' import { matchesWorkOrderStatusFilter } from '@/utils/status-tag-registry' +import { compareLabStorageThenCreateTimeDesc } from '@/utils/qc-inbound-lab-storage-sort' export type QcTaskMineFilters = { keyword?: string @@ -173,6 +174,19 @@ export function paginateMineEntries(items: T[], page: number, pageSize: numbe } } +export function sortMineEntriesForInboundLabStorage( + entries: MineWorkOrderEntry[] +): MineWorkOrderEntry[] { + return [...entries].sort((a, b) => + compareLabStorageThenCreateTimeDesc( + a.workOrder.labStorageCode, + a.workOrder.createTime, + b.workOrder.labStorageCode, + b.workOrder.createTime + ) + ) +} + export function buildMineTableRows(pageEntries: MineWorkOrderEntry[]): { rows: QcWorkOrder[] presetTasksByWorkOrderId: Record diff --git a/web-admin/src/utils/qc-work-order-groups.ts b/web-admin/src/utils/qc-work-order-groups.ts index a150f785..9dbf0edb 100644 --- a/web-admin/src/utils/qc-work-order-groups.ts +++ b/web-admin/src/utils/qc-work-order-groups.ts @@ -23,6 +23,12 @@ function sortWorkOrdersInGroup(rows: QcWorkOrder[]) { }) } +function sortWorkOrdersInInboundGroup(rows: QcWorkOrder[]) { + return [...rows].sort((a, b) => + String(b.createTime || '').localeCompare(String(a.createTime || '')) + ) +} + /** 入库 Tab:按样本编码分组(前端分组,分页仍按工单条数) */ export function groupInboundQcWorkOrders(rows: QcWorkOrder[]): QcWorkOrderSampleGroupModel[] { const map = new Map() @@ -33,7 +39,7 @@ export function groupInboundQcWorkOrders(rows: QcWorkOrder[]): QcWorkOrderSample else map.set(key, [row]) } return Array.from(map.entries()).map(([groupKey, workOrders]) => { - const sorted = sortWorkOrdersInGroup(workOrders) + const sorted = sortWorkOrdersInInboundGroup(workOrders) return { groupKey, sampleCode: sorted[0]?.sampleCode || '', diff --git a/web-admin/src/utils/qc-work-order-inbound-sample-cards.ts b/web-admin/src/utils/qc-work-order-inbound-sample-cards.ts index 065b6e8b..40b4392f 100644 --- a/web-admin/src/utils/qc-work-order-inbound-sample-cards.ts +++ b/web-admin/src/utils/qc-work-order-inbound-sample-cards.ts @@ -1,4 +1,9 @@ import type { QcProtocolSiblingSample, QcWorkOrder } from '@/api/qc-work-order' +import { + compareLabStorageThenCreateTimeDesc, + resolveGroupLabStorageCode, + resolveGroupMaxCreateTime +} from '@/utils/qc-inbound-lab-storage-sort' import { groupInboundQcWorkOrders, resolveInboundProtocolGroupKey, @@ -33,15 +38,6 @@ function sampleTypeNameForGroup(group: QcWorkOrderSampleGroupModel) { return (gate?.sampleTypeName || group.workOrders[0]?.sampleTypeName || '').trim() } -function protocolSortTime(group: QcWorkOrderSampleGroupModel): string { - let max = '' - for (const wo of group.workOrders) { - const t = String(wo.createTime || '') - if (t > max) max = t - } - return max -} - function resolveProtocolSiblingSamplesForGroup( group: QcWorkOrderSampleGroupModel ): QcProtocolSiblingSample[] { @@ -140,17 +136,14 @@ export function buildInboundSampleListItems(rows: QcWorkOrder[]): QcInboundSampl } } - const sortedGroups = [...groups].sort((a, b) => { - const headA = a.workOrders[0] - const headB = b.workOrders[0] - if (!headA || !headB) return 0 - const protocolA = resolveInboundProtocolGroupKey(headA) - const protocolB = resolveInboundProtocolGroupKey(headB) - if (protocolA !== protocolB) { - return protocolSortTime(b).localeCompare(protocolSortTime(a)) - } - return (a.sampleCode || '').localeCompare(b.sampleCode || '') - }) + const sortedGroups = [...groups].sort((a, b) => + compareLabStorageThenCreateTimeDesc( + resolveGroupLabStorageCode(a.workOrders), + resolveGroupMaxCreateTime(a.workOrders), + resolveGroupLabStorageCode(b.workOrders), + resolveGroupMaxCreateTime(b.workOrders) + ) + ) return sortedGroups.map((group) => ({ group, diff --git a/web-admin/src/views/operations/storage-facility/index.vue b/web-admin/src/views/operations/storage-facility/index.vue index e5f19aaf..da192f8c 100644 --- a/web-admin/src/views/operations/storage-facility/index.vue +++ b/web-admin/src/views/operations/storage-facility/index.vue @@ -130,7 +130,7 @@ @click="handleBatchOutbound" > - 出库已选 + 批量出库 @@ -2538,6 +2538,7 @@ .shelf-scroll-wrapper { overflow: auto; max-height: 100%; + padding-top: 12px; } .shelf-container { diff --git a/web-admin/src/views/production/cell-product/index.vue b/web-admin/src/views/production/cell-product/index.vue index 7a5d9589..8527fc55 100644 --- a/web-admin/src/views/production/cell-product/index.vue +++ b/web-admin/src/views/production/cell-product/index.vue @@ -1406,6 +1406,14 @@ async function remoteSearchStorageSlots(query: string) { const token = ++storageSlotSearchToken const trimmed = (query ?? '').trim() + // 先记下当前选中格的展示名,避免远程结果分页未带回时 el-select 回退显示 id + const keepStubs = (storageSlotSearchOpts.value.includeSlotIds || []) + .map((id) => String(id || '').trim()) + .filter(Boolean) + .map((id) => { + const hit = storageSlotOptions.value.find((o) => o.id === id) + return { id, name: hit?.name } + }) if (trimmed) storageSlotOptions.value = [] storageSlotOptionsLoading.value = true try { @@ -1414,10 +1422,10 @@ includeSlotIds: storageSlotSearchOpts.value.includeSlotIds }) if (token !== storageSlotSearchToken) return - storageSlotOptions.value = list ?? [] + storageSlotOptions.value = ensureStorageSlotsInOptions(list ?? [], keepStubs) } catch { if (token !== storageSlotSearchToken) return - storageSlotOptions.value = [] + storageSlotOptions.value = ensureStorageSlotsInOptions([], keepStubs) } finally { if (token === storageSlotSearchToken) { storageSlotOptionsLoading.value = false diff --git a/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue b/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue index 2e9a417f..8a6f9d63 100644 --- a/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue +++ b/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue @@ -16,9 +16,6 @@ >
- 工单号:{{ workOrderRow?.workOrderNo }} 样本编码: + 实验室入库编码:{{ headerLabStorageCode }}
{ + const row = props.workOrderRow + if (!row) return '—' + const fromRow = String(row.labStorageCode || '').trim() + if (fromRow) return fromRow + const fromReception = String( + (row.reception as Record | undefined)?.labStorageCode || '' + ).trim() + return fromReception || '—' + }) + function resolveCreatedTaskId(task: Record | null | undefined) { const raw = task?.id ?? task?.taskId ?? task?.data?.id return String(raw || '').trim() diff --git a/web-admin/src/views/production/production-work-order/StepDetailPanel.vue b/web-admin/src/views/production/production-work-order/StepDetailPanel.vue index 5c96132f..977335a3 100644 --- a/web-admin/src/views/production/production-work-order/StepDetailPanel.vue +++ b/web-admin/src/views/production/production-work-order/StepDetailPanel.vue @@ -589,13 +589,13 @@
-
+
diff --git a/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue b/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue index d32df053..753a224e 100644 --- a/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue +++ b/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue @@ -429,6 +429,14 @@ async function remoteSearchStorageSlots(query: string) { const token = ++storageSlotSearchToken const trimmed = (query ?? '').trim() + // 先记下当前选中格的展示名,避免远程结果分页未带回时 el-select 回退显示 id + const keepStubs = (storageSlotSearchOpts.value.includeSlotIds || []) + .map((id) => String(id || '').trim()) + .filter(Boolean) + .map((id) => { + const hit = storageSlotOptions.value.find((o) => o.id === id) + return { id, name: hit?.name } + }) if (trimmed) storageSlotOptions.value = [] storageSlotOptionsLoading.value = true try { @@ -437,10 +445,10 @@ includeSlotIds: storageSlotSearchOpts.value.includeSlotIds }) if (token !== storageSlotSearchToken) return - storageSlotOptions.value = list ?? [] + storageSlotOptions.value = ensureStorageSlotsInOptions(list ?? [], keepStubs) } catch { if (token !== storageSlotSearchToken) return - storageSlotOptions.value = [] + storageSlotOptions.value = ensureStorageSlotsInOptions([], keepStubs) } finally { if (token === storageSlotSearchToken) { storageSlotOptionsLoading.value = false diff --git a/web-admin/src/views/production/production-work-order/composables/useProductionWorkOrderTabPage.ts b/web-admin/src/views/production/production-work-order/composables/useProductionWorkOrderTabPage.ts index 191d5a41..018e1e9d 100644 --- a/web-admin/src/views/production/production-work-order/composables/useProductionWorkOrderTabPage.ts +++ b/web-admin/src/views/production/production-work-order/composables/useProductionWorkOrderTabPage.ts @@ -31,6 +31,9 @@ import { computeWorkOrderPlannedStartTimeRange } from '@/utils/production-labels export type ProductionWorkOrderTabPageMode = 'manage' | 'mine' +/** 默认筛选:待配置 + 进行中(不含已完成 / 已作废) */ +const DEFAULT_WORK_ORDER_STATUS_FILTER = ['dpz', 'jxz'] + export function useProductionWorkOrderTabPage( tabKey: Ref, pageMode: Ref | ProductionWorkOrderTabPageMode @@ -43,7 +46,7 @@ export function useProductionWorkOrderTabPage( const searchForm = reactive({ productionCategory: '', keyword: '', - workOrderStatus: [] as string[], + workOrderStatus: [...DEFAULT_WORK_ORDER_STATUS_FILTER], taskStatus: '', excludeVoidProtocol: true }) @@ -182,7 +185,7 @@ export function useProductionWorkOrderTabPage( function handleReset() { searchForm.productionCategory = '' searchForm.keyword = '' - searchForm.workOrderStatus = [] + searchForm.workOrderStatus = [...DEFAULT_WORK_ORDER_STATUS_FILTER] searchForm.taskStatus = '' searchForm.excludeVoidProtocol = true plannedStartTimeRange.value = null diff --git a/web-admin/src/views/production/production-work-order/tabs/ProductionWorkOrderTabPanel.vue b/web-admin/src/views/production/production-work-order/tabs/ProductionWorkOrderTabPanel.vue index 83aed8c1..c3dbba70 100644 --- a/web-admin/src/views/production/production-work-order/tabs/ProductionWorkOrderTabPanel.vue +++ b/web-admin/src/views/production/production-work-order/tabs/ProductionWorkOrderTabPanel.vue @@ -30,8 +30,6 @@ placeholder="全部" clearable multiple - collapse-tags - collapse-tags-tooltip style="width: 100%" > - - - - - - - - - 停用 - - - - {{ item.itemName }} - - -

暂无启用的实验室项目

-
- -
-
{{ group.companyName || '未命名公司' }}
- - - {{ item.itemName }} - - -
-

暂无启用的第三方项目

-
+ +
+
+
+ 已选项目 + 共 {{ selectedItemCount }} 项 +
+
+
+ 实验室自检 +
+ + {{ item.itemName }} + +
+
+
+ 第三方检测 +
+
+ {{ + group.companyName || '未命名公司' + }} +
+ + {{ item.itemName }} + +
+
+
+
+
+

暂未选择检测项目

+
+ +
+
+
实验室自检项目
+
+ + + {{ item.itemName }} + + +

暂无启用的实验室项目

+
+
+ +
+
第三方检测项目
+
+ +

暂无启用的第三方项目

+
+
+
+
+